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" "github.com/orohi/vpn-panel/internal/secretbox" ) type Server struct { DB *sql.DB Auth *auth.Manager Templates *template.Template SecretKey []byte } type pageData map[string]any func New(database *sql.DB, authMgr *auth.Manager, appSecret, templatesDir string) (*Server, error) { pattern := filepath.Join(templatesDir, "*.html") tmpl, err := template.New("").Funcs(template.FuncMap{ "statusClass": func(s string) string { switch s { case "online": return "on" case "installing": return "warn" case "error": return "err" case "offline": return "off" default: return "off" } }, }).ParseGlob(pattern) if err != nil { return nil, err } return &Server{ DB: database, Auth: authMgr, Templates: tmpl, SecretKey: secretbox.DeriveKey(appSecret), }, 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("/admin/nodes", s.Auth.RequireAuth(s.nodesList)).Methods(http.MethodGet) r.HandleFunc("/admin/nodes/new", s.Auth.RequireAuth(s.nodesNewGet)).Methods(http.MethodGet) r.HandleFunc("/admin/nodes/new", s.Auth.RequireAuth(s.nodesCreate)).Methods(http.MethodPost) r.HandleFunc("/admin/nodes/{id}", s.Auth.RequireAuth(s.nodeView)).Methods(http.MethodGet) r.HandleFunc("/admin/nodes/{id}/install", s.Auth.RequireAuth(s.nodeInstall)).Methods(http.MethodPost) r.HandleFunc("/admin/nodes/{id}/check", s.Auth.RequireAuth(s.nodeCheck)).Methods(http.MethodPost) r.HandleFunc("/admin/nodes/{id}/delete", s.Auth.RequireAuth(s.nodeDelete)).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 } nodes, _ := db.ListNodes(s.DB) s.render(w, "dashboard.html", pageData{ "Title": "Админка", "UserName": s.Auth.CurrentName(r), "Stats": stats, "Protocols": protocols, "Nodes": nodes, "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) }