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} }