server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent bf25d8beac
commit cc8845dcec
44 changed files with 4995 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
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}
}