package api import ( "encoding/base64" "errors" "net/http" "github.com/gin-gonic/gin" "ws_usernode/internal/auth" "ws_usernode/internal/config" "ws_usernode/internal/service" ) // AuthHandler 认证接口:图形验证码、OTP 双通道登录、管理员登录、密码重置、会话。 type AuthHandler struct { svc *service.AuthService cfg *config.Config } // Captcha GET /auth/captcha —— 获取图形验证码(id + base64 PNG)。 func (h *AuthHandler) Captcha(c *gin.Context) { id, png, err := h.svc.NewCaptcha() if err != nil { fail(c, http.StatusInternalServerError, "验证码生成失败") return } ok(c, gin.H{ "captcha_id": id, "image": "data:image/png;base64," + base64.StdEncoding.EncodeToString(png), }) } // OTPSendRequest 外部用户请求 OTP。 type OTPSendRequest struct { Username string `json:"username" binding:"required"` // 含或不含 ext_ 前缀 CaptchaID string `json:"captcha_id" binding:"required"` CaptchaCode string `json:"captcha_code" binding:"required"` } // OTPSend POST /auth/otp/send —— 图形验证码前置,生成 OTP 并发邮件(失败不阻断)。 func (h *AuthHandler) OTPSend(c *gin.Context) { var req OTPSendRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } err := h.svc.UserOTPSend(c.Request.Context(), req.Username, req.CaptchaID, req.CaptchaCode, c.ClientIP()) if err != nil { switch { case errors.Is(err, service.ErrCaptchaFailed): fail(c, http.StatusBadRequest, "图形验证码错误") case errors.Is(err, service.ErrUserUnavailable): fail(c, http.StatusNotFound, "用户不存在或不可用") case errors.Is(err, auth.ErrCooldown): fail(c, http.StatusTooManyRequests, "发送冷却中,请稍后重试") default: fail(c, http.StatusInternalServerError, err.Error()) } return } ok(c, gin.H{"status": "sent"}) } // OTPLoginRequest 外部用户 OTP 登录。 type OTPLoginRequest struct { Username string `json:"username" binding:"required"` Code string `json:"code" binding:"required"` // 6 位 OTP } // OTPLogin POST /auth/otp/login —— OTP 校验并建立 cookie 会话。 func (h *AuthHandler) OTPLogin(c *gin.Context) { var req OTPLoginRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } sid, err := h.svc.UserOTPLogin(c.Request.Context(), req.Username, req.Code, c.ClientIP(), c.Request.UserAgent()) if err != nil { switch { case errors.Is(err, service.ErrUserUnavailable): fail(c, http.StatusNotFound, "用户不存在或不可用") case errors.Is(err, auth.ErrInvalidCode): fail(c, http.StatusUnauthorized, "验证码错误或已过期") case errors.Is(err, auth.ErrTooManyFails): fail(c, http.StatusTooManyRequests, "失败次数过多,请稍后再试") default: fail(c, http.StatusInternalServerError, err.Error()) } return } setSessionCookie(c, sid, h.cfg.Server.SessionTTL, h.cfg.App.Env == "production") ok(c, gin.H{"session": "created"}) } // AdminLoginRequest 管理员登录。 type AdminLoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // AdminLogin POST /auth/admin/login —— 管理员用户名+口令登录(含失败限速)。 func (h *AuthHandler) AdminLogin(c *gin.Context) { var req AdminLoginRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } sid, err := h.svc.AdminLogin(c.Request.Context(), req.Username, req.Password, c.ClientIP(), c.Request.UserAgent()) if err != nil { switch { case errors.Is(err, service.ErrRateLimited): fail(c, http.StatusTooManyRequests, "尝试次数过多,请稍后再试") case errors.Is(err, service.ErrBadCredentials): fail(c, http.StatusUnauthorized, "用户名或密码错误") default: fail(c, http.StatusInternalServerError, err.Error()) } return } setSessionCookie(c, sid, h.cfg.Server.SessionTTL, h.cfg.App.Env == "production") ok(c, gin.H{"session": "created"}) } // AdminForgotRequest 管理员忘记密码。 type AdminForgotRequest struct { Username string `json:"username" binding:"required"` } // AdminForgot POST /auth/admin/forgot —— 发送密码重置邮件(用户不存在也返回成功,防枚举)。 func (h *AuthHandler) AdminForgot(c *gin.Context) { var req AdminForgotRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } if err := h.svc.AdminForgot(c.Request.Context(), req.Username, c.ClientIP()); err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } ok(c, gin.H{"status": "sent"}) } // AdminResetRequest 通过令牌重置密码。 type AdminResetRequest struct { Token string `json:"token" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } // AdminReset POST /auth/admin/reset —— 校验令牌并重置密码。 func (h *AuthHandler) AdminReset(c *gin.Context) { var req AdminResetRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } err := h.svc.AdminReset(c.Request.Context(), req.Token, req.NewPassword, c.ClientIP()) if err != nil { switch { case errors.Is(err, auth.ErrResetTokenInvalid): fail(c, http.StatusBadRequest, "重置令牌无效或已过期") case errors.Is(err, service.ErrWeakPassword): fail(c, http.StatusBadRequest, err.Error()) default: fail(c, http.StatusInternalServerError, err.Error()) } return } ok(c, gin.H{"status": "reset"}) } // Logout POST /auth/logout —— 登出(会话删除 + cookie 清除)。 func (h *AuthHandler) Logout(c *gin.Context) { sess := sessionFrom(c) if sess != nil { _ = h.svc.Logout(c.Request.Context(), sess.ID) } setSessionCookie(c, "", 0, h.cfg.App.Env == "production") ok(c, gin.H{"status": "logged_out"}) } // Me GET /auth/me —— 当前会话主体信息。 func (h *AuthHandler) Me(c *gin.Context) { sess := sessionFrom(c) if sess == nil { fail(c, http.StatusUnauthorized, "未登录") return } info, err := h.svc.Me(c.Request.Context(), sess.ID) if err != nil { fail(c, http.StatusUnauthorized, "会话失效或已过期") return } ok(c, info) }