package api import ( "net/http" "os" "path/filepath" "strings" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "openvpn-manager/internal/config" "openvpn-manager/internal/middleware" "openvpn-manager/internal/model" "openvpn-manager/internal/service" ) type Server struct { Cfg *config.Config Svc *service.Service } func NewServer(cfg *config.Config, svc *service.Service) *Server { return &Server{Cfg: cfg, Svc: svc} } func (s *Server) Router(distDir string) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) r.Use(cors.New(cors.Config{ AllowAllOrigins: true, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Authorization", "Content-Type"}, MaxAge: 12 * time.Hour, })) // 公共 r.POST("/api/login", s.login) r.GET("/api/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) }) auth := r.Group("/api", middleware.JWTAuth(s.Cfg.JWTSecret)) { auth.GET("/me", s.me) auth.POST("/logout", s.logout) auth.GET("/dashboard", s.dashboard) auth.GET("/instances", s.listInstances) auth.POST("/instances", s.createInstance) auth.GET("/instances/:id", s.getInstance) auth.PUT("/instances/:id", s.updateInstance) auth.DELETE("/instances/:id", s.deleteInstance) auth.POST("/instances/:id/start", s.startInstance) auth.POST("/instances/:id/stop", s.stopInstance) auth.GET("/instances/:id/online", s.onlineClients) auth.GET("/instances/:id/users", s.listUsers) auth.POST("/instances/:id/users", s.createUser) auth.PUT("/instances/:id/users/:uid", s.updateUser) auth.POST("/instances/:id/users/:uid/revoke", s.revokeUser) auth.DELETE("/instances/:id/users/:uid", s.deleteUser) auth.GET("/instances/:id/users/:uid/ovpn", s.downloadOVPN) auth.GET("/certs", s.listCerts) auth.GET("/connlogs", s.listConnLogs) auth.GET("/backups", s.listBackups) auth.POST("/backups", s.createBackup) auth.POST("/backups/:id/restore", s.restoreBackup) auth.DELETE("/backups/:id", s.deleteBackup) auth.GET("/audits", s.listAudits) // Admin 账号管理(self) auth.POST("/me/password", s.changeMyPassword) // Admin 账号管理(列表/创建/重置密码/启停/删除 - 仅 admin role) auth.GET("/admins", s.listAdmins) auth.POST("/admins", s.createAdmin) auth.POST("/admins/:id/password", s.resetAdminPassword) auth.POST("/admins/:id/status", s.setAdminStatus) auth.DELETE("/admins/:id", s.deleteAdmin) } // 静态前端 if distDir != "" { if _, err := os.Stat(distDir); err == nil { r.NoRoute(func(c *gin.Context) { path := filepath.Join(distDir, c.Request.URL.Path) if !fileExists(path) || strings.HasSuffix(c.Request.URL.Path, "/") { c.File(filepath.Join(distDir, "index.html")) return } c.File(path) }) } } return r } func fileExists(p string) bool { fi, err := os.Stat(p) return err == nil && !fi.IsDir() } // ---------- handlers ---------- func (s *Server) login(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } // 从 admin 表验证;若表为空则用环境变量默认账号首次 seed admin, err := s.Svc.Store.GetAdminByUsername(req.Username) if err != nil { // 兼容:如果 db.json 还没有 admins(首次启动),允许用 env 默认账号登录, // 登录成功后由 SeedDefaultIfEmpty 自动 seed 到 db.json if s.Svc.Store.AdminCount() == 0 && req.Username == s.Cfg.AdminUser && req.Password == s.Cfg.AdminPass { if err := s.Svc.SeedDefaultAdminIfEmpty(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "seed admin: " + err.Error()}) return } admin, err = s.Svc.Store.GetAdminByUsername(req.Username) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } } if admin.Status != "active" { c.JSON(http.StatusForbidden, gin.H{"error": "账号已被禁用"}) return } if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } // 记录登录时间 now := time.Now() admin.LastLoginAt = &now admin.LastLoginIP = c.ClientIP() _ = s.Svc.Store.UpsertAdmin(*admin) // 颁发 token tok, err := middleware.IssueToken(s.Cfg.JWTSecret, admin.ID, admin.Username, admin.Role, 12*time.Hour) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "issue token failed"}) return } c.JSON(200, gin.H{ "token": tok, "username": admin.Username, "role": admin.Role, "must_change_password": admin.MustChangePassword, }) } func (s *Server) logout(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) } func (s *Server) me(c *gin.Context) { userID, _ := c.Get("user_id") uid, _ := userID.(string) username, _ := c.Get("user") role, _ := c.Get("role") must := false if uid != "" { if a, err := s.Svc.Store.GetAdmin(uid); err == nil { must = a.MustChangePassword } } c.JSON(200, gin.H{ "user_id": uid, "username": username, "role": role, "must_change_password": must, }) } // dashboard 汇总统计 func (s *Server) dashboard(c *gin.Context) { instances := s.Svc.Store.ListInstances() users := s.Svc.Store.ListUsers("") certs, _ := s.Svc.CertInfos() expiring := 0 for _, ct := range certs { if ct.DaysLeft <= 30 { expiring++ } } online := 0 for _, in := range instances { cl, _ := s.Svc.ListOnline(in.ID) online += len(cl) } whitelistN := 0 overview := []map[string]any{} for _, in := range instances { if in.AccessMode == model.AccessWhitelist { whitelistN++ } overview = append(overview, map[string]any{ "name": in.Name, "access_mode": in.AccessMode, "allow_networks": in.AllowNetworks, }) } c.JSON(200, gin.H{ "instances": len(instances), "running": countByStatus(instances, "running"), "whitelist_instances": whitelistN, "users": len(users), "active_users": countEnabled(users), "online": online, "expiring_certs": expiring, "recent_audits": s.Svc.Store.ListAudits(20), "recent_conn_logs": s.Svc.Store.ListConnLogs("", 20), "access_overview": overview, }) } // ---- instances ---- func (s *Server) listInstances(c *gin.Context) { c.JSON(200, s.Svc.Store.ListInstances()) } func (s *Server) getInstance(c *gin.Context) { in, err := s.Svc.Store.GetInstance(c.Param("id")) if err != nil { c.JSON(404, gin.H{"error": err.Error()}) return } c.JSON(200, in) } func (s *Server) createInstance(c *gin.Context) { var in model.Instance if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": "bad request"}) return } out, err := s.Svc.CreateInstance(in) if err != nil { s.Svc.AuditForAPI(c, "create_instance", in.Name, err.Error(), "failed") c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "create_instance", in.Name, "port="+itoa(out.Port), "ok") c.JSON(200, out) } func (s *Server) updateInstance(c *gin.Context) { var in model.Instance if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": "bad request"}) return } in.ID = c.Param("id") if err := s.Svc.UpdateInstance(in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "update_instance", in.Name, "", "ok") c.JSON(200, in) } func (s *Server) deleteInstance(c *gin.Context) { id := c.Param("id") if err := s.Svc.DeleteInstance(id); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "delete_instance", id, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) startInstance(c *gin.Context) { id := c.Param("id") if err := s.Svc.StartInstance(id); err != nil { s.Svc.AuditForAPI(c, "start_instance", id, err.Error(), "failed") c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "start_instance", id, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) stopInstance(c *gin.Context) { id := c.Param("id") if err := s.Svc.StopInstance(id); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "stop_instance", id, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) onlineClients(c *gin.Context) { cl, err := s.Svc.ListOnline(c.Param("id")) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, cl) } // ---- users ---- func (s *Server) listUsers(c *gin.Context) { c.JSON(200, s.Svc.Store.ListUsers(c.Param("id"))) } func (s *Server) createUser(c *gin.Context) { var u model.VPNUser if err := c.ShouldBindJSON(&u); err != nil { c.JSON(400, gin.H{"error": "bad request"}) return } u.InstanceID = c.Param("id") out, err := s.Svc.CreateUser(u) if err != nil { s.Svc.AuditForAPI(c, "create_user", u.Username, err.Error(), "failed") c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "create_user", u.Username, "instance="+u.InstanceID, "ok") c.JSON(200, out) } func (s *Server) updateUser(c *gin.Context) { var u model.VPNUser if err := c.ShouldBindJSON(&u); err != nil { c.JSON(400, gin.H{"error": "bad request"}) return } u.ID = c.Param("uid") u.InstanceID = c.Param("id") if err := s.Svc.UpdateUser(u); err != nil { s.Svc.AuditForAPI(c, "update_user", u.Username, err.Error(), "failed") c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "update_user", u.Username, "", "ok") c.JSON(200, u) } func (s *Server) revokeUser(c *gin.Context) { uid := c.Param("uid") if err := s.Svc.RevokeUser(uid); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "revoke_user", uid, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) deleteUser(c *gin.Context) { uid := c.Param("uid") if err := s.Svc.DeleteUser(uid); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "delete_user", uid, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) downloadOVPN(c *gin.Context) { host := c.Query("host") if host == "" { host = c.Request.Host } p, err := s.Svc.GenerateOVPN(c.Param("uid"), host) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.Header("Content-Disposition", "attachment; filename="+filepath.Base(p)) c.File(p) } // ---- certs ---- func (s *Server) listCerts(c *gin.Context) { certs, err := s.Svc.CertInfos() if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, certs) } // ---- conn logs ---- func (s *Server) listConnLogs(c *gin.Context) { c.JSON(200, s.Svc.Store.ListConnLogs(c.Query("instance"), 200)) } // ---- backups ---- func (s *Server) listBackups(c *gin.Context) { c.JSON(200, s.Svc.Store.ListBackups()) } func (s *Server) createBackup(c *gin.Context) { var req struct{ Note string `json:"note"` } _ = c.ShouldBindJSON(&req) b, err := s.Svc.Backup(req.Note) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "create_backup", b.ID, "", "ok") c.JSON(200, b) } func (s *Server) restoreBackup(c *gin.Context) { id := c.Param("id") if err := s.Svc.Restore(id); err != nil { s.Svc.AuditForAPI(c, "restore_backup", id, err.Error(), "failed") c.JSON(400, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "restore_backup", id, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) deleteBackup(c *gin.Context) { id := c.Param("id") if err := s.Svc.DeleteBackup(id); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"ok": true}) } // ---- audits ---- func (s *Server) listAudits(c *gin.Context) { c.JSON(200, s.Svc.Store.ListAudits(500)) } // ---- helpers ---- func countByStatus(in []model.Instance, status string) int { n := 0 for _, x := range in { if x.Status == status { n++ } } return n } func countEnabled(u []model.VPNUser) int { n := 0 for _, x := range u { if x.Enabled { n++ } } return n } func itoa(n int) string { if n == 0 { return "0" } neg := false if n < 0 { neg = true n = -n } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } if neg { i-- buf[i] = '-' } return string(buf[i:]) } // ---------- Admin 账号管理 ---------- func (s *Server) changeMyPassword(c *gin.Context) { var req struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } userID, _ := c.Get("user_id") uid, _ := userID.(string) if err := s.Svc.ChangePassword(uid, req.OldPassword, req.NewPassword); err != nil { s.Svc.AuditForAPI(c, "change_password", uid, err.Error(), "failed") c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "change_password", uid, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) listAdmins(c *gin.Context) { c.JSON(200, s.Svc.Store.ListAdmins()) } func (s *Server) createAdmin(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` Role string `json:"role"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } a, err := s.Svc.CreateAdmin(req.Username, req.Password, req.Role) if err != nil { s.Svc.AuditForAPI(c, "create_admin", req.Username, err.Error(), "failed") c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "create_admin", req.Username, "role="+a.Role, "ok") c.JSON(200, a) } func (s *Server) resetAdminPassword(c *gin.Context) { id := c.Param("id") var req struct { NewPassword string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } if err := s.Svc.ResetPassword(id, req.NewPassword); err != nil { s.Svc.AuditForAPI(c, "reset_password", id, err.Error(), "failed") c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "reset_password", id, "", "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) setAdminStatus(c *gin.Context) { id := c.Param("id") var req struct { Status string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } if err := s.Svc.SetAdminStatus(id, req.Status); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "set_admin_status", id, req.Status, "ok") c.JSON(200, gin.H{"ok": true}) } func (s *Server) deleteAdmin(c *gin.Context) { id := c.Param("id") userID, _ := c.Get("user_id") uid, _ := userID.(string) if err := s.Svc.DeleteAdmin(uid, id); err != nil { s.Svc.AuditForAPI(c, "delete_admin", id, err.Error(), "failed") c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } s.Svc.AuditForAPI(c, "delete_admin", id, "", "ok") c.JSON(200, gin.H{"ok": true}) }