server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警
This commit is contained in:
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user