236 lines
7.3 KiB
Go
236 lines
7.3 KiB
Go
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}
|
||
}
|