package api import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/hhs/camtalk/internal/auth" apperr "github.com/hhs/camtalk/internal/errors" "github.com/hhs/camtalk/internal/ratelimit" "github.com/hhs/camtalk/internal/trace" ) // AuthHandler 提供认证相关的 REST 端点。 type AuthHandler struct { authService auth.Service tokenMgr *auth.TokenManager } // NewAuthHandler 创建 AuthHandler。 func NewAuthHandler(authService auth.Service, tokenMgr *auth.TokenManager) *AuthHandler { return &AuthHandler{ authService: authService, tokenMgr: tokenMgr, } } // RegisterRoutes 注册认证相关路由到给定的路由组。 func (h *AuthHandler) RegisterRoutes(rg *gin.RouterGroup, limiter ratelimit.Limiter) { authGroup := rg.Group("/auth") { // 注册和登录端点添加限流中间件(按 IP 限流) if limiter != nil { authGroup.POST("/register", ratelimit.Middleware(limiter, func(c *gin.Context) string { return c.ClientIP() + ":register" }), h.Register) authGroup.POST("/login", ratelimit.Middleware(limiter, func(c *gin.Context) string { return c.ClientIP() + ":login" }), h.Login) } else { authGroup.POST("/register", h.Register) authGroup.POST("/login", h.Login) } // refresh 和 logout 不限流 authGroup.POST("/refresh", h.Refresh) authGroup.POST("/logout", auth.AuthMiddleware(h.tokenMgr), h.Logout) } } // Register POST /api/auth/register — 用户注册。 func (h *AuthHandler) Register(c *gin.Context) { log := trace.FromContext(c.Request.Context()) clientIP := c.ClientIP() var req auth.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "invalid request body", }) return } if msg := validateCredentials(req.Username, req.Password); msg != "" { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": msg, }) return } resp, err := h.authService.Register(c.Request.Context(), req) if err != nil { log.Warnw("register failed", "username", req.Username, "client_ip", clientIP, "error", err) handleAuthError(c, err) return } log.Infow("register success", "username", req.Username, "client_ip", clientIP) c.JSON(http.StatusCreated, resp) } // Login POST /api/auth/login — 用户登录。 func (h *AuthHandler) Login(c *gin.Context) { log := trace.FromContext(c.Request.Context()) clientIP := c.ClientIP() var req auth.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "invalid request body", }) return } if msg := validateCredentials(req.Username, req.Password); msg != "" { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": msg, }) return } resp, err := h.authService.Login(c.Request.Context(), req) if err != nil { log.Warnw("login failed", "username", req.Username, "client_ip", clientIP, "error", err) handleAuthError(c, err) return } log.Infow("login success", "username", req.Username, "client_ip", clientIP) c.JSON(http.StatusOK, resp) } // Refresh POST /api/auth/refresh — 刷新令牌。 func (h *AuthHandler) Refresh(c *gin.Context) { log := trace.FromContext(c.Request.Context()) var req auth.RefreshRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "invalid request body", }) return } if req.RefreshToken == "" { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "refresh_token is required", }) return } resp, err := h.authService.Refresh(c.Request.Context(), req) if err != nil { log.Warnw("token refresh failed", "client_ip", c.ClientIP(), "error", err) handleAuthError(c, err) return } log.Infow("token refresh success", "client_ip", c.ClientIP()) c.JSON(http.StatusOK, resp) } // Logout POST /api/auth/logout — 登出(需要认证)。 func (h *AuthHandler) Logout(c *gin.Context) { log := trace.FromContext(c.Request.Context()) userID := c.GetString(auth.ContextKeyUserID) var req struct { RefreshToken string `json:"refresh_token"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "invalid request body", }) return } if req.RefreshToken == "" { c.JSON(http.StatusBadRequest, gin.H{ "code": apperr.CodeInvalidInput, "message": "refresh_token is required", }) return } if err := h.authService.Logout(c.Request.Context(), userID, req.RefreshToken); err != nil { log.Errorw("logout failed", "user_id", userID, "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "code": apperr.CodeInternalError, "message": "failed to logout", }) return } log.Infow("logout success", "user_id", userID) c.JSON(http.StatusOK, gin.H{ "message": "logged out successfully", }) } // validateCredentials 校验用户名和密码格式。 // 返回空字符串表示校验通过,否则返回错误描述。 func validateCredentials(username, password string) string { if len(username) < 3 || len(username) > 64 { return "username must be 3-64 characters" } if len(password) < 8 || len(password) > 72 { return "password must be 8-72 characters" } return "" } // handleAuthError 将 auth 层错误映射为 HTTP 响应。 func handleAuthError(c *gin.Context, err error) { switch { case errors.Is(err, auth.ErrUsernameTaken): c.JSON(http.StatusConflict, gin.H{ "code": apperr.CodeUsernameTaken, "message": "username already taken", }) case errors.Is(err, auth.ErrInvalidCredentials): c.JSON(http.StatusUnauthorized, gin.H{ "code": apperr.CodeInvalidCredentials, "message": "invalid username or password", }) case errors.Is(err, auth.ErrRefreshTokenUsed): c.JSON(http.StatusUnauthorized, gin.H{ "code": apperr.CodeInvalidToken, "message": "refresh token has been used or expired", }) default: c.JSON(http.StatusInternalServerError, gin.H{ "code": apperr.CodeInternalError, "message": "internal server error", }) } }