package auth import ( "errors" "net/http" "strings" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" ) type Handler struct { service *Service } type SendSMSRequest struct { Phone string `json:"phone" binding:"required"` CaptchaID string `json:"captcha_id" binding:"required"` CaptchaCode string `json:"captcha_code" binding:"required"` } type LoginRequest struct { Phone string `json:"phone" binding:"required"` Code string `json:"code" binding:"required,len=6"` } type RefreshRequest struct { RefreshToken string `json:"refresh_token" binding:"required"` } type PasswordLoginRequest struct { Phone string `json:"phone" binding:"required"` Password string `json:"password" binding:"required,min=8,max=20"` } type SetPasswordRequest struct { Password string `json:"password" binding:"required,min=8,max=20"` OldPassword string `json:"old_password"` } type RegisterRequest struct { Phone string `json:"phone" binding:"required"` Code string `json:"code" binding:"required,len=6"` Password string `json:"password" binding:"required,min=8,max=20"` } type ResetPasswordRequest struct { Phone string `json:"phone" binding:"required"` Code string `json:"code" binding:"required,len=6"` NewPassword string `json:"new_password" binding:"required,min=8,max=20"` } func NewHandler(service *Service) *Handler { return &Handler{service: service} } func (h *Handler) Captcha(c *gin.Context) { item, err := h.service.Captcha(c.Request.Context()) if err != nil { writeAuthError(c, err) return } response.OK(c, item) } // SendSMS 发送短信验证码 // @Summary 发送短信验证码 // @Description 发送登录验证码到指定手机号 // @Tags 认证 // @Accept json // @Produce json // @Param request body SendSMSRequest true "手机号" // @Success 200 {object} response.Body "发送成功" // @Failure 400 {object} response.Body "请求参数错误" // @Failure 500 {object} response.Body "服务器错误" // @Router /auth/sms/send [post] func (h *Handler) SendSMS(c *gin.Context) { var req SendSMSRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "手机号和图形验证码不能为空") return } if err := h.service.SendSMSCode(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.CaptchaID), strings.TrimSpace(req.CaptchaCode)); err != nil { writeAuthError(c, err) return } response.OK(c, gin.H{"phone": PublicPhone(req.Phone), "expires_in": 300}) } // Login 短信验证码登录 // @Summary 短信验证码登录 // @Description 使用手机号和验证码登录 // @Tags 认证 // @Accept json // @Produce json // @Param request body LoginRequest true "登录信息" // @Success 200 {object} response.Body "登录成功,返回用户信息和 token" // @Failure 400 {object} response.Body "请求参数错误" // @Failure 401 {object} response.Body "验证码错误" // @Failure 500 {object} response.Body "服务器错误" // @Router /auth/sms/login [post] func (h *Handler) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "手机号和验证码不能为空") return } result, err := h.service.LoginWithSMS(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code)) if err != nil { writeAuthError(c, err) return } setAccessCookie(c, result.Tokens.AccessToken) response.OK(c, result) } // Refresh 刷新访问令牌 // @Summary 刷新访问令牌 // @Description 使用 refresh_token 获取新的 access_token // @Tags 认证 // @Accept json // @Produce json // @Param request body RefreshRequest true "刷新令牌" // @Success 200 {object} response.Body "刷新成功,返回新的 token" // @Failure 400 {object} response.Body "请求参数错误" // @Failure 401 {object} response.Body "refresh_token 无效" // @Router /auth/refresh [post] func (h *Handler) Refresh(c *gin.Context) { var req RefreshRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "refresh_token 不能为空") return } tokens, err := h.service.RefreshToken(c.Request.Context(), req.RefreshToken) if err != nil { response.Unauthorized(c, "刷新令牌无效或已过期") return } setAccessCookie(c, tokens.AccessToken) response.OK(c, tokens) } // Logout 登出 // @Summary 登出 // @Description 用户登出(客户端需清除本地 token) // @Tags 认证 // @Accept json // @Produce json // @Success 200 {object} response.Body "登出成功" // @Security BearerAuth // @Router /auth/logout [post] func (h *Handler) Logout(c *gin.Context) { userID, ok := currentUserID(c) if !ok { response.Unauthorized(c, "缺少用户上下文") return } if err := h.service.Logout(c.Request.Context(), userID); err != nil { writeAuthError(c, err) return } clearAccessCookie(c) response.OK(c, gin.H{"logged_out": true}) } func (h *Handler) PasswordLogin(c *gin.Context) { var req PasswordLoginRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "手机号和密码不能为空,密码长度 8-20 位") return } result, err := h.service.LoginWithPassword(c.Request.Context(), strings.TrimSpace(req.Phone), req.Password, c.ClientIP()) if err != nil { writeAuthError(c, err) return } setAccessCookie(c, result.Tokens.AccessToken) response.OK(c, result) } func (h *Handler) Register(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "手机号、验证码和密码不能为空,密码长度 8-20 位") return } result, err := h.service.RegisterWithPassword(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code), req.Password) if err != nil { writeAuthError(c, err) return } setAccessCookie(c, result.Tokens.AccessToken) response.OK(c, result) } func (h *Handler) SetPassword(c *gin.Context) { var req SetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "密码不能为空,长度 8-20 位") return } userID, ok := currentUserID(c) if !ok { response.Unauthorized(c, "缺少用户上下文") return } if err := h.service.SetPassword(c.Request.Context(), userID, req.Password, req.OldPassword); err != nil { writeAuthError(c, err) return } response.OK(c, gin.H{"updated": true}) } func (h *Handler) ResetPassword(c *gin.Context) { var req ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "手机号、验证码和新密码不能为空") return } if err := h.service.ResetPassword(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code), req.NewPassword); err != nil { writeAuthError(c, err) return } response.OK(c, gin.H{"reseted": true}) } func writeAuthError(c *gin.Context, err error) { response.RecordError(c, err) switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "数据库或 Redis 未连接") case errors.Is(err, ErrInvalidPhone): response.BadRequest(c, "手机号格式不正确") case errors.Is(err, ErrCaptchaInvalid): response.BadRequest(c, "图形验证码错误或已过期") case errors.Is(err, ErrCodeRateLimited): response.Error(c, http.StatusTooManyRequests, "rate_limited", "验证码发送过于频繁") case errors.Is(err, ErrSMSSendFailed): response.Error(c, http.StatusBadGateway, "sms_send_failed", "验证码发送失败,请稍后重试") case errors.Is(err, ErrCodeInvalid): response.BadRequest(c, "验证码错误或已过期") case errors.Is(err, ErrUserDisabled): response.Error(c, http.StatusForbidden, "user_disabled", "用户已被冻结") case errors.Is(err, ErrInvalidPassword): response.BadRequest(c, "手机号或密码错误") case errors.Is(err, ErrInvalidOldPassword): response.BadRequest(c, "原密码错误") case errors.Is(err, ErrPasswordNotSet): response.BadRequest(c, "未设置密码,请使用短信验证码登录后设置密码") case errors.Is(err, ErrPasswordTooWeak): response.BadRequest(c, "密码长度应为 8-20 位") case errors.Is(err, ErrLoginLocked): response.Error(c, http.StatusTooManyRequests, "login_locked", "登录失败次数过多,请 15 分钟后再试") case errors.Is(err, ErrUserAlreadyExists): response.BadRequest(c, "该手机号已注册,请直接登录或使用短信验证码登录") default: response.Error(c, http.StatusInternalServerError, "internal_error", "服务暂时不可用") } } func currentUserID(c *gin.Context) (uint64, bool) { val, exists := c.Get("user_id") if !exists { return 0, false } userID, ok := val.(uint64) return userID, ok } func setAccessCookie(c *gin.Context, token string) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie("hfb_user_access", token, 2*60*60, "/api", "", requestUsesHTTPS(c), true) } func clearAccessCookie(c *gin.Context) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie("hfb_user_access", "", -1, "/api", "", requestUsesHTTPS(c), true) } func requestUsesHTTPS(c *gin.Context) bool { return c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") }