package handler import ( "net/http" "strings" "time" "cyberstrike-ai/internal/audit" "cyberstrike-ai/internal/config" "cyberstrike-ai/internal/security" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // AuthHandler handles authentication-related endpoints. type AuthHandler struct { manager *security.AuthManager config *config.Config configPath string logger *zap.Logger audit *audit.Service } // SetAudit wires platform audit logging. func (h *AuthHandler) SetAudit(s *audit.Service) { h.audit = s } // NewAuthHandler creates a new AuthHandler. func NewAuthHandler(manager *security.AuthManager, cfg *config.Config, configPath string, logger *zap.Logger) *AuthHandler { return &AuthHandler{ manager: manager, config: cfg, configPath: configPath, logger: logger, } } type loginRequest struct { Username string `json:"username"` Password string `json:"password" binding:"required"` } type changePasswordRequest struct { OldPassword string `json:"oldPassword"` NewPassword string `json:"newPassword"` } // Login verifies password and returns a session token. func (h *AuthHandler) Login(c *gin.Context) { var req loginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "密码不能为空"}) return } token, expiresAt, err := h.manager.Authenticate(req.Username, req.Password) if err != nil { if h.audit != nil { h.audit.Record(c, audit.Entry{ Level: "warn", Category: "auth", Action: "login", Result: "failure", Message: "登录失败:密码错误", }) } c.JSON(http.StatusUnauthorized, gin.H{"error": "密码错误"}) return } session, _ := h.manager.ValidateToken(token) if h.audit != nil { h.audit.Record(c, audit.Entry{ Category: "auth", Action: "login", Result: "success", SessionHint: audit.HintFromToken(token), Message: "登录成功", Detail: map[string]interface{}{ "expires_at": expiresAt.UTC().Format(time.RFC3339), }, }) } c.JSON(http.StatusOK, gin.H{ "token": token, "expires_at": expiresAt.UTC().Format(time.RFC3339), "session_duration_hr": h.manager.SessionDurationHours(), "user": gin.H{ "id": session.UserID, "username": session.Username, "display_name": session.DisplayName, }, "roles": session.Roles, "permissions": permissionKeys(session.Permissions), "scope": session.Scope, }) } // Logout revokes the current session token. func (h *AuthHandler) Logout(c *gin.Context) { token := c.GetString(security.ContextAuthTokenKey) if token == "" { authHeader := c.GetHeader("Authorization") if len(authHeader) > 7 && strings.EqualFold(authHeader[:7], "Bearer ") { token = strings.TrimSpace(authHeader[7:]) } else { token = strings.TrimSpace(authHeader) } } h.manager.RevokeToken(token) if h.audit != nil { h.audit.Record(c, audit.Entry{ Category: "auth", Action: "logout", Result: "success", Message: "退出登录", }) } c.JSON(http.StatusOK, gin.H{"message": "已退出登录"}) } // ChangePassword updates the login password. func (h *AuthHandler) ChangePassword(c *gin.Context) { var req changePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"}) return } oldPassword := strings.TrimSpace(req.OldPassword) newPassword := strings.TrimSpace(req.NewPassword) if oldPassword == "" || newPassword == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "当前密码和新密码均不能为空"}) return } if len(newPassword) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "新密码长度至少需要 8 位"}) return } if oldPassword == newPassword { c.JSON(http.StatusBadRequest, gin.H{"error": "新密码不能与旧密码相同"}) return } session, _ := security.CurrentSession(c) if session.Username == "" { session.Username = "admin" } if !h.manager.CheckUserPassword(session.Username, oldPassword) { if h.audit != nil { h.audit.Record(c, audit.Entry{ Level: "warn", Category: "auth", Action: "change_password", Result: "failure", Message: "修改密码失败:当前密码不正确", }) } c.JSON(http.StatusBadRequest, gin.H{"error": "当前密码不正确"}) return } if session.UserID == "" || session.UserID == "admin" { if err := config.PersistAuthPassword(h.configPath, newPassword); err != nil { if h.logger != nil { h.logger.Error("保存新密码失败", zap.Error(err)) } c.JSON(http.StatusInternalServerError, gin.H{"error": "保存新密码失败,请重试"}) return } if err := h.manager.UpdateConfig(newPassword, h.config.Auth.SessionDurationHours); err != nil { if h.logger != nil { h.logger.Error("更新认证配置失败", zap.Error(err)) } c.JSON(http.StatusInternalServerError, gin.H{"error": "更新认证配置失败"}) return } h.config.Auth.Password = newPassword h.config.Auth.GeneratedPassword = "" h.config.Auth.GeneratedPasswordPersisted = false h.config.Auth.GeneratedPasswordPersistErr = "" } else if err := h.manager.UpdateUserPassword(session.UserID, newPassword); err != nil { if h.logger != nil { h.logger.Error("更新用户密码失败", zap.Error(err)) } c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户密码失败"}) return } if h.logger != nil { h.logger.Info("登录密码已更新,所有会话已失效") } if h.audit != nil { h.audit.Record(c, audit.Entry{ Category: "auth", Action: "change_password", Result: "success", Message: "登录密码已修改", }) } c.JSON(http.StatusOK, gin.H{"message": "密码已更新,请使用新密码重新登录"}) } // Validate returns the current session status. func (h *AuthHandler) Validate(c *gin.Context) { token := c.GetString(security.ContextAuthTokenKey) if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "会话无效"}) return } session, ok := h.manager.ValidateToken(token) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "会话已过期"}) return } c.JSON(http.StatusOK, gin.H{ "token": session.Token, "expires_at": session.ExpiresAt.UTC().Format(time.RFC3339), "user": gin.H{ "id": session.UserID, "username": session.Username, "display_name": session.DisplayName, }, "roles": session.Roles, "permissions": permissionKeys(session.Permissions), "scope": session.Scope, }) } func permissionKeys(perms map[string]bool) []string { keys := make([]string, 0, len(perms)) for key, ok := range perms { if ok { keys = append(keys, key) } } return keys }