diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go new file mode 100644 index 0000000..3b886c3 --- /dev/null +++ b/backend/internal/api/auth.go @@ -0,0 +1,194 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/hhs/camtalk/internal/auth" + apperr "github.com/hhs/camtalk/internal/errors" +) + +// 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) { + authGroup := rg.Group("/auth") + { + authGroup.POST("/register", h.Register) + authGroup.POST("/login", h.Login) + 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) { + 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 { + handleAuthError(c, err) + return + } + + c.JSON(http.StatusCreated, resp) +} + +// Login POST /api/auth/login — 用户登录。 +func (h *AuthHandler) Login(c *gin.Context) { + 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 { + handleAuthError(c, err) + return + } + + c.JSON(http.StatusOK, resp) +} + +// Refresh POST /api/auth/refresh — 刷新令牌。 +func (h *AuthHandler) Refresh(c *gin.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 { + handleAuthError(c, err) + return + } + + c.JSON(http.StatusOK, resp) +} + +// Logout POST /api/auth/logout — 登出(需要认证)。 +func (h *AuthHandler) Logout(c *gin.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 { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": apperr.CodeInternalError, + "message": "failed to logout", + }) + return + } + + 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", + }) + } +}