Add Go VPN admin panel with Docker Compose and Postgres 17.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/orohi/vpn-panel/internal/auth"
|
||||
"github.com/orohi/vpn-panel/internal/db"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
DB *sql.DB
|
||||
Auth *auth.Manager
|
||||
Templates *template.Template
|
||||
}
|
||||
|
||||
type pageData map[string]any
|
||||
|
||||
func New(database *sql.DB, authMgr *auth.Manager, templatesDir string) (*Server, error) {
|
||||
pattern := filepath.Join(templatesDir, "*.html")
|
||||
tmpl, err := template.ParseGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Server{DB: database, Auth: authMgr, Templates: tmpl}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Routes() http.Handler {
|
||||
r := mux.NewRouter()
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||
|
||||
r.HandleFunc("/", s.home).Methods(http.MethodGet)
|
||||
r.HandleFunc("/login", s.loginGet).Methods(http.MethodGet)
|
||||
r.HandleFunc("/login", s.loginPost).Methods(http.MethodPost)
|
||||
r.HandleFunc("/logout", s.logout).Methods(http.MethodPost, http.MethodGet)
|
||||
|
||||
r.HandleFunc("/admin", s.Auth.RequireAuth(s.dashboard)).Methods(http.MethodGet)
|
||||
r.HandleFunc("/admin/protocols", s.Auth.RequireAuth(s.protocols)).Methods(http.MethodGet)
|
||||
r.HandleFunc("/admin/protocols/{id}/toggle", s.Auth.RequireAuth(s.toggleProtocol)).Methods(http.MethodPost)
|
||||
|
||||
r.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}).Methods(http.MethodGet)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *Server) render(w http.ResponseWriter, name string, data pageData) {
|
||||
if data == nil {
|
||||
data = pageData{}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.Templates.ExecuteTemplate(w, name, data); err != nil {
|
||||
log.Printf("template %s: %v", name, err)
|
||||
http.Error(w, "template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) home(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Auth.IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.render(w, "home.html", pageData{
|
||||
"Title": "VPN Panel",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) loginGet(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Auth.IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.render(w, "login.html", pageData{
|
||||
"Title": "Вход",
|
||||
"Error": "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) loginPost(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
email := r.FormValue("email")
|
||||
password := r.FormValue("password")
|
||||
|
||||
user, err := db.GetUserByEmail(s.DB, email)
|
||||
if err != nil || user == nil || !auth.CheckPassword(user.PasswordHash, password) {
|
||||
s.render(w, "login.html", pageData{
|
||||
"Title": "Вход",
|
||||
"Error": "Неверный email или пароль",
|
||||
"Email": email,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Auth.Login(w, r, user.ID.String(), user.Email, user.Name); err != nil {
|
||||
http.Error(w, "session error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
_ = s.Auth.Logout(w, r)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := db.GetStats(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
protocols, err := db.ListProtocols(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.render(w, "dashboard.html", pageData{
|
||||
"Title": "Админка",
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Stats": stats,
|
||||
"Protocols": protocols,
|
||||
"Active": "dashboard",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) protocols(w http.ResponseWriter, r *http.Request) {
|
||||
protocols, err := db.ListProtocols(s.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.render(w, "protocols.html", pageData{
|
||||
"Title": "Протоколы",
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Protocols": protocols,
|
||||
"Active": "protocols",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) toggleProtocol(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := db.ToggleProtocol(s.DB, id); err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/protocols", http.StatusSeeOther)
|
||||
}
|
||||
Reference in New Issue
Block a user