Merge pull request 'feat: 添加 PostgreSQL 服务并挂载数据库迁移脚本' #101
194
backend/internal/api/auth.go
Normal file
194
backend/internal/api/auth.go
Normal file
@@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user