Files
EveryPublish/server/internal/api/handlers/auth.go
T

236 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"everypublish/server/internal/api/response"
"everypublish/server/internal/auth"
"everypublish/server/internal/cache"
"everypublish/server/internal/config"
"everypublish/server/internal/models"
)
// AuthHandler 认证处理器
type AuthHandler struct {
DB *gorm.DB
Rds *cache.Redis
Cfg *config.Config
}
type registerReq struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required,min=8"`
Nickname string `json:"nickname"`
}
type loginReq struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
TotpCode string `json:"totpCode"`
}
type refreshReq struct {
RefreshToken string `json:"refreshToken" binding:"required"`
}
type logoutReq struct {
RefreshToken string `json:"refreshToken"`
}
// Register 注册:创建用户 + 默认工作空间 + owner 成员
func (h *AuthHandler) Register(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法:"+err.Error())
return
}
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
if !strings.Contains(req.Email, "@") {
response.Fail(c, http.StatusBadRequest, 1001, "邮箱格式不正确")
return
}
if req.Nickname == "" {
req.Nickname = strings.Split(req.Email, "@")[0]
}
var count int64
h.DB.Model(&models.User{}).Where("email = ?", req.Email).Count(&count)
if count > 0 {
response.Fail(c, http.StatusConflict, 2001, "邮箱已注册")
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "服务内部错误")
return
}
err = h.DB.Transaction(func(tx *gorm.DB) error {
user := models.User{Email: req.Email, PasswordHash: hash, Nickname: req.Nickname, Role: "user", Status: "active"}
if err := tx.Create(&user).Error; err != nil {
return err
}
ws := models.Workspace{Name: req.Nickname + "的工作空间", OwnerID: user.ID, Plan: "free", Status: "active"}
if err := tx.Create(&ws).Error; err != nil {
return err
}
member := models.Member{WorkspaceID: ws.ID, UserID: user.ID, Role: "owner"}
if err := tx.Create(&member).Error; err != nil {
return err
}
c.Set("regUID", user.ID)
c.Set("regWSID", ws.ID)
return nil
})
if err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "注册失败")
return
}
uid, wsid := c.GetUint64("regUID"), c.GetUint64("regWSID")
access, refresh, err := h.issuePair(uid, wsid, "user", "owner")
if err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "签发令牌失败")
return
}
var user models.User
h.DB.First(&user, uid)
response.OK(c, gin.H{
"accessToken": access,
"refreshToken": refresh,
"user": userDTO(user),
"workspace": gin.H{"id": wsid, "name": req.Nickname + "的工作空间"},
})
}
// Login 登录(2FA 默认关;开启后需 totpCode)
func (h *AuthHandler) Login(c *gin.Context) {
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
return
}
var user models.User
if err := h.DB.Where("email = ?", strings.ToLower(strings.TrimSpace(req.Email))).First(&user).Error; err != nil {
response.Fail(c, http.StatusUnauthorized, 2002, "邮箱或密码错误")
return
}
ok, err := auth.VerifyPassword(req.Password, user.PasswordHash)
if err != nil || !ok {
response.Fail(c, http.StatusUnauthorized, 2002, "邮箱或密码错误")
return
}
if user.TOTPEnabled {
// 二期接入 TOTP 校验;一期默认关闭
response.Fail(c, http.StatusForbidden, 1006, "需要两步验证码")
return
}
if user.Status != "active" {
response.Fail(c, http.StatusForbidden, 2003, "账号已停用")
return
}
var member models.Member
if err := h.DB.Where("user_id = ?", user.ID).Order("id asc").First(&member).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "工作空间数据缺失")
return
}
access, refresh, err := h.issuePair(user.ID, member.WorkspaceID, user.Role, member.Role)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "签发令牌失败")
return
}
c.Set("wsid", member.WorkspaceID)
response.Audit(c, "auth.login", "user:"+itoa(user.ID), gin.H{"email": user.Email})
response.OK(c, gin.H{
"accessToken": access,
"refreshToken": refresh,
"user": userDTO(user),
"workspace": gin.H{"id": member.WorkspaceID},
})
}
// Refresh 刷新令牌轮换:旧 jti 吊销,签发新对
func (h *AuthHandler) Refresh(c *gin.Context) {
var req refreshReq
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
return
}
claims, err := auth.ParseRefresh(h.Cfg.JWTSecret, req.RefreshToken)
if err != nil {
response.Fail(c, http.StatusUnauthorized, 2003, "刷新令牌无效或已过期")
return
}
valid, err := h.Rds.ExistsRefresh(claims.UID, claims.JTI)
if err != nil || !valid {
response.Fail(c, http.StatusUnauthorized, 2003, "刷新令牌已失效")
return
}
if err = h.Rds.DeleteRefresh(claims.UID, claims.JTI); err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "服务内部错误")
return
}
var user models.User
if err = h.DB.First(&user, claims.UID).Error; err != nil {
response.Fail(c, http.StatusUnauthorized, 2003, "用户不存在")
return
}
var member models.Member
if err = h.DB.Where("user_id = ?", user.ID).Order("id asc").First(&member).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "工作空间数据缺失")
return
}
access, refresh, err := h.issuePair(user.ID, member.WorkspaceID, user.Role, member.Role)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "签发令牌失败")
return
}
response.OK(c, gin.H{"accessToken": access, "refreshToken": refresh})
}
// Logout 登出:吊销刷新令牌
func (h *AuthHandler) Logout(c *gin.Context) {
var req logoutReq
_ = c.ShouldBindJSON(&req)
if req.RefreshToken != "" {
if claims, err := auth.ParseRefresh(h.Cfg.JWTSecret, req.RefreshToken); err == nil {
_ = h.Rds.DeleteRefresh(claims.UID, claims.JTI)
}
}
response.Audit(c, "auth.logout", "user:"+itoa(c.GetUint64("uid")), nil)
response.OK(c, nil)
}
// Me 当前用户信息
func (h *AuthHandler) Me(c *gin.Context) {
var user models.User
if err := h.DB.First(&user, c.GetUint64("uid")).Error; err != nil {
response.Fail(c, http.StatusUnauthorized, 1002, "用户不存在")
return
}
response.OK(c, gin.H{"user": userDTO(user), "workspaceId": c.GetUint64("wsid"), "memberRole": c.GetString("mrole")})
}
// issuePair 签发令牌对并把 refresh jti 登记到 Redis
func (h *AuthHandler) issuePair(uid, wsid uint64, role, mrole string) (string, string, error) {
access, err := auth.IssueAccess(h.Cfg.JWTSecret, uid, wsid, role, mrole)
if err != nil {
return "", "", err
}
jti := uuid.NewString()
refresh, err := auth.IssueRefresh(h.Cfg.JWTSecret, uid, jti)
if err != nil {
return "", "", err
}
if err = h.Rds.SaveRefresh(uid, jti, auth.RefreshTTL); err != nil {
return "", "", err
}
return access, refresh, nil
}
func userDTO(u models.User) gin.H {
return gin.H{"id": u.ID, "email": u.Email, "nickname": u.Nickname, "role": u.Role, "totpEnabled": u.TOTPEnabled}
}