package api import ( "crypto/rand" "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/go-ldap/ldap/v3" "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/store" ) type pendingMFA struct { secret string backups []string expires time.Time } var ( pendingMFAMu sync.Mutex pendingMFAs = map[uint]pendingMFA{} ) func requireRole(roles ...string) gin.HandlerFunc { return func(c *gin.Context) { role := c.GetString("role") for _, allowed := range roles { if role == allowed { c.Next() return } } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "权限不足"}) } } func (s *Server) authenticateUser(username, password string) (*model.User, error) { var user model.User err := store.DB.Where("username = ?", username).First(&user).Error if err == nil && user.Source != "ldap" { if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil { return &user, nil } return nil, fmt.Errorf("invalid password") } if user.Source == "ldap" || err != nil { return s.authenticateLDAP(username, password) } return nil, err } func ldapSettings() (*model.LDAPConfig, error) { var cfg model.LDAPConfig if err := store.DB.First(&cfg).Error; err != nil || !cfg.Enabled { return nil, fmt.Errorf("LDAP 未启用") } plain, err := store.DecryptSecret(cfg.BindPassword) if err != nil { return nil, fmt.Errorf("LDAP 配置密钥无效") } cfg.BindPassword = plain if cfg.UsernameAttribute == "" { cfg.UsernameAttribute = "uid" } if cfg.UserFilter == "" { cfg.UserFilter = "(objectClass=inetOrgPerson)" } return &cfg, nil } func ldapConnect(cfg *model.LDAPConfig) (*ldap.Conn, error) { url := cfg.URL if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") { url = "ldap://" + url } conn, err := ldap.DialURL(url) if err != nil { return nil, err } if cfg.BindDN != "" { if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil { conn.Close() return nil, err } } return conn, nil } func (s *Server) authenticateLDAP(username, password string) (*model.User, error) { cfg, err := ldapSettings() if err != nil || password == "" { return nil, fmt.Errorf("LDAP 认证不可用") } conn, err := ldapConnect(cfg) if err != nil { return nil, err } defer conn.Close() filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UsernameAttribute, ldap.EscapeFilter(username)) result, err := conn.Search(ldap.NewSearchRequest(cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 5, false, filter, []string{"dn", cfg.UsernameAttribute, "cn", "mail"}, nil)) if err != nil || len(result.Entries) != 1 { return nil, fmt.Errorf("LDAP 用户不存在") } entry := result.Entries[0] if err := conn.Bind(entry.DN, password); err != nil { return nil, fmt.Errorf("LDAP 密码错误") } var user model.User err = store.DB.Where("username = ?", username).First(&user).Error if err != nil { user = model.User{Username: username, Email: entry.GetAttributeValue("mail"), Source: "ldap", Role: "viewer", Enabled: true} if err := store.DB.Create(&user).Error; err != nil { return nil, err } } return &user, nil } func verifyMFA(user *model.User, code string) error { var device model.MFADevice if err := store.DB.Where("user_id = ? AND enabled = ?", user.ID, true).First(&device).Error; err != nil { return nil } secret, err := store.DecryptSecret(device.Secret) if err != nil { return fmt.Errorf("MFA 配置无效") } if totp.Validate(strings.TrimSpace(code), secret) { return nil } return fmt.Errorf("需要有效的 MFA 验证码") } func (s *Server) mfaStatus(c *gin.Context) { var device model.MFADevice err := store.DB.Where("user_id = ?", c.GetUint("user_id")).First(&device).Error c.JSON(200, gin.H{"enabled": err == nil && device.Enabled}) } func backupCodes() []string { codes := make([]string, 10) for i := range codes { raw := make([]byte, 5) _, _ = rand.Read(raw) codes[i] = fmt.Sprintf("%X", raw) } return codes } func (s *Server) mfaSetup(c *gin.Context) { var user model.User if err := store.DB.First(&user, c.GetUint("user_id")).Error; err != nil { c.JSON(404, gin.H{"error": "用户不存在"}) return } key, err := totp.Generate(totp.GenerateOpts{Issuer: "OPSX VPN", AccountName: user.Username}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } pendingMFAMu.Lock() pendingMFAs[user.ID] = pendingMFA{secret: key.Secret(), backups: backupCodes(), expires: time.Now().Add(15 * time.Minute)} pendingMFAMu.Unlock() c.JSON(200, gin.H{"otpauth_url": key.URL(), "secret": key.Secret(), "expires_in": 900}) } func (s *Server) mfaEnable(c *gin.Context) { var req struct { Code string `json:"code"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } userID := c.GetUint("user_id") pendingMFAMu.Lock() pending, ok := pendingMFAs[userID] delete(pendingMFAs, userID) pendingMFAMu.Unlock() if !ok || time.Now().After(pending.expires) || !totp.Validate(req.Code, pending.secret) { c.JSON(400, gin.H{"error": "验证码无效或设置已过期"}) return } secret, _ := store.EncryptSecret(pending.secret) raw, _ := json.Marshal(pending.backups) backups, _ := store.EncryptSecret(string(raw)) device := model.MFADevice{UserID: userID, Secret: secret, BackupCodes: backups, Enabled: true} store.DB.Where("user_id = ?", userID).Assign(device).FirstOrCreate(&device) store.Audit(c.GetString("username"), "enable_mfa", c.GetString("username"), "") c.JSON(200, gin.H{"ok": true, "backup_codes": pending.backups}) } func (s *Server) mfaDisable(c *gin.Context) { store.DB.Where("user_id = ?", c.GetUint("user_id")).Delete(&model.MFADevice{}) store.Audit(c.GetString("username"), "disable_mfa", c.GetString("username"), "") c.JSON(200, gin.H{"ok": true}) } func (s *Server) listUsers(c *gin.Context) { var users []model.User store.DB.Order("id").Find(&users) c.JSON(200, users) } func (s *Server) createUser(c *gin.Context) { var req struct { Username, Password, Email, Role string Enabled bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || req.Username == "" || len(req.Password) < 8 { c.JSON(400, gin.H{"error": "用户名及至少 8 位的密码必填"}) return } if req.Role == "" { req.Role = "viewer" } if req.Role != "admin" && req.Role != "operator" && req.Role != "viewer" { c.JSON(400, gin.H{"error": "角色无效"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user := model.User{Username: req.Username, Password: string(hash), Email: req.Email, Role: req.Role, Source: "local", Enabled: req.Enabled} if err := store.DB.Create(&user).Error; err != nil { c.JSON(400, gin.H{"error": "用户创建失败:" + err.Error()}) return } store.Audit(c.GetString("username"), "create_user", user.Username, "") c.JSON(200, user) } func (s *Server) updateUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.User if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "用户不存在"}) return } var req struct { Email, Role, Password string Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } if req.Email != "" { user.Email = req.Email } if req.Role != "" { user.Role = req.Role } if req.Enabled != nil { user.Enabled = *req.Enabled } if req.Password != "" { if len(req.Password) < 8 { c.JSON(400, gin.H{"error": "密码至少 8 位"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) } store.DB.Save(&user) c.JSON(200, user) } func (s *Server) deleteUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if uint(id) == c.GetUint("user_id") { c.JSON(400, gin.H{"error": "不能删除当前用户"}) return } store.DB.Delete(&model.User{}, id) c.JSON(200, gin.H{"ok": true}) } func (s *Server) getLDAPConfig(c *gin.Context) { var cfg model.LDAPConfig if store.DB.First(&cfg).Error != nil { c.JSON(200, gin.H{"enabled": false}) return } c.JSON(200, cfg) } func (s *Server) saveLDAPConfig(c *gin.Context) { var req struct { URL string `json:"url"` BindDN string `json:"bind_dn"` BindPassword string `json:"bind_password"` BaseDN string `json:"base_dn"` UserFilter string `json:"user_filter"` UsernameAttribute string `json:"username_attribute"` Enabled bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || (req.Enabled && (req.URL == "" || req.BaseDN == "")) { c.JSON(400, gin.H{"error": "LDAP 地址与 Base DN 必填"}) return } var cfg model.LDAPConfig store.DB.First(&cfg) cfg.URL, cfg.BindDN, cfg.BaseDN, cfg.UserFilter, cfg.UsernameAttribute, cfg.Enabled = req.URL, req.BindDN, req.BaseDN, req.UserFilter, req.UsernameAttribute, req.Enabled if req.BindPassword != "" { cfg.BindPassword, _ = store.EncryptSecret(req.BindPassword) } store.DB.Save(&cfg) c.JSON(200, gin.H{"ok": true}) } func (s *Server) testLDAP(c *gin.Context) { cfg, err := ldapSettings() if err == nil { conn, connErr := ldapConnect(cfg) if connErr == nil { conn.Close() c.JSON(200, gin.H{"ok": true}) return } err = connErr } c.JSON(400, gin.H{"error": "LDAP 连接失败: " + err.Error()}) } func (s *Server) listVPNUsers(c *gin.Context) { var users []model.VPNUser q := store.DB.Order("id desc") if id := c.Query("tunnel_id"); id != "" { q = q.Where("tunnel_id = ?", id) } q.Find(&users) c.JSON(200, users) } func (s *Server) createVPNUser(c *gin.Context) { var req struct { TunnelID uint `json:"tunnel_id"` Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Source string `json:"source"` LDAPDN string `json:"ldap_dn"` Password string `json:"password"` Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || req.Username == "" { c.JSON(400, gin.H{"error": "隧道和用户名必填"}) return } if req.Source == "" { req.Source = "local" } if req.Source != "local" && req.Source != "ldap" { c.JSON(400, gin.H{"error": "来源必须为 local 或 ldap"}) return } user := model.VPNUser{TunnelID: req.TunnelID, Username: req.Username, DisplayName: req.DisplayName, Email: req.Email, Source: req.Source, LDAPDN: req.LDAPDN, Enabled: true} if req.Enabled != nil { user.Enabled = *req.Enabled } if user.Source == "local" { if len(req.Password) < 8 { c.JSON(400, gin.H{"error": "本地拨号用户密码必填"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) } else if user.LDAPDN == "" { c.JSON(400, gin.H{"error": "LDAP 拨号用户必须填写 LDAP DN"}) return } if err := store.DB.Create(&user).Error; err != nil { c.JSON(400, gin.H{"error": "创建失败:" + err.Error()}) return } c.JSON(200, user) } func (s *Server) updateVPNUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } var req struct { DisplayName, Email, Password string Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } user.DisplayName, user.Email = req.DisplayName, req.Email if req.Enabled != nil { user.Enabled = *req.Enabled } if req.Password != "" { hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) } store.DB.Save(&user) c.JSON(200, user) } func (s *Server) deleteVPNUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) store.DB.Delete(&model.VPNUser{}, id) c.JSON(200, gin.H{"ok": true}) }