tss-rocks/backend/internal/handler/auth.go

253 lines
6 KiB
Go

package handler
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"required,oneof=admin editor contributor"`
}
type LoginRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Password string `json:"password" binding:"required"`
}
type AuthResponse struct {
Token string `json:"token"`
}
func (h *Handler) Register(c *gin.Context) {
// 检查是否启用注册功能
if !h.cfg.Auth.Registration.Enabled {
message := h.cfg.Auth.Registration.Message
if message == "" {
message = "Registration is currently disabled"
}
c.JSON(http.StatusForbidden, gin.H{
"error": gin.H{
"code": "REGISTRATION_DISABLED",
"message": message,
},
})
return
}
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": gin.H{
"code": "INVALID_REQUEST",
"message": err.Error(),
},
})
return
}
user, err := h.service.CreateUser(c.Request.Context(), req.Username, req.Email, req.Password, req.Role)
if err != nil {
log.Error().Err(err).Msg("Failed to create user")
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "CREATE_USER_FAILED",
"message": "Failed to create user",
},
})
return
}
// Get user roles
roles, err := h.service.GetUserRoles(c.Request.Context(), user.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to get user roles")
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "GET_ROLES_FAILED",
"message": "Failed to get user roles",
},
})
return
}
// Extract role names for JWT
roleNames := make([]string, len(roles))
for i, r := range roles {
roleNames[i] = r.Name
}
// Generate JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": int64(user.ID), // 将用户 ID 转换为 int64
"roles": roleNames,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenString, err := token.SignedString([]byte(h.cfg.JWT.Secret))
if err != nil {
log.Error().Err(err).Msg("Failed to generate token")
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "GENERATE_TOKEN_FAILED",
"message": "Failed to generate token",
},
})
return
}
c.JSON(http.StatusCreated, AuthResponse{Token: tokenString})
}
func (h *Handler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": gin.H{
"code": "INVALID_REQUEST",
"message": err.Error(),
},
})
return
}
user, err := h.service.GetUserByUsername(c.Request.Context(), req.Username)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "INVALID_CREDENTIALS",
"message": "Invalid username or password",
},
})
return
}
// 验证密码
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "INVALID_CREDENTIALS",
"message": "Invalid username or password",
},
})
return
}
// Get user roles
roles, err := h.service.GetUserRoles(c.Request.Context(), user.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to get user roles")
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "GET_ROLES_FAILED",
"message": "Failed to get user roles",
},
})
return
}
// Extract role names for JWT
roleNames := make([]string, len(roles))
for i, r := range roles {
roleNames[i] = r.Name
}
// Generate JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": int64(user.ID), // 将用户 ID 转换为 int64
"roles": roleNames,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenString, err := token.SignedString([]byte(h.cfg.JWT.Secret))
if err != nil {
log.Error().Err(err).Msg("Failed to generate token")
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "GENERATE_TOKEN_FAILED",
"message": "Failed to generate token",
},
})
return
}
c.JSON(http.StatusOK, AuthResponse{Token: tokenString})
}
func (h *Handler) Logout(c *gin.Context) {
// 获取当前用户ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "UNAUTHORIZED",
"message": "User not authenticated",
},
})
return
}
// 获取 token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "UNAUTHORIZED",
"message": "Authorization header is required",
},
})
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "UNAUTHORIZED",
"message": "Authorization header format must be Bearer {token}",
},
})
return
}
// 解析 token 以获取过期时间
token, err := jwt.Parse(parts[1], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(h.cfg.JWT.Secret), nil
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{
"code": "INVALID_TOKEN",
"message": "Invalid token",
},
})
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// 将 token 添加到黑名单
h.service.GetTokenBlacklist().AddToBlacklist(parts[1], claims)
}
// 记录日志
log.Info().
Interface("user_id", userID).
Msg("User logged out")
c.JSON(http.StatusOK, gin.H{
"message": "Successfully logged out",
})
}