package handlers import ( "context" "errors" "net/http" "os" "strings" "time" "unicode/utf8" "yh_web/server/config" "yh_web/server/models" "yh_web/server/utils" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" ) const siteJWTExpire = 30 * 24 * time.Hour // SiteClaims 前台 JWT(仅弹幕等轻量场景,勿与后台 Claims 混用) type SiteClaims struct { UserID string `json:"user_id"` Username string `json:"username"` jwt.RegisteredClaims } func siteJWTSigningKey() []byte { s := strings.TrimSpace(os.Getenv("SITE_JWT_SECRET")) if s == "" { s = "yh_web_site_dm_jwt_change_in_production" } return []byte(s) } // ParseSiteClaims 解析前台 JWT,失败返回 nil,false func ParseSiteClaims(tokenStr string) (*SiteClaims, bool) { tokenStr = strings.TrimSpace(tokenStr) if len(tokenStr) > 7 && strings.EqualFold(tokenStr[:7], "bearer ") { tokenStr = strings.TrimSpace(tokenStr[7:]) } if tokenStr == "" { return nil, false } var claims SiteClaims t, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) { return siteJWTSigningKey(), nil }) if err != nil || !t.Valid { return nil, false } return &claims, true } // SiteDanmakuTokenValid 弹幕发送权限:有效的前台 JWT func SiteDanmakuTokenValid(tokenStr string) bool { _, ok := ParseSiteClaims(tokenStr) return ok } type siteRegisterInput struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } type siteLoginInput struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } func siteUsersColl() *mongo.Collection { return config.GetDB(config.DBName).Collection("site_users") } // WebSiteRegister POST /api/web/site/register — 仅用于前台直播弹幕账号 func WebSiteRegister(c *gin.Context) { if config.MongoClient == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用"}) return } var input siteRegisterInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"}) return } u := strings.TrimSpace(input.Username) if utf8.RuneCountInString(u) < 2 || utf8.RuneCountInString(u) > 32 { c.JSON(http.StatusBadRequest, gin.H{"error": "用户名为 2~32 个字符"}) return } if len(input.Password) < 6 { c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少 6 位"}) return } ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() coll := siteUsersColl() var existing models.SiteUser err := coll.FindOne(ctx, bson.M{"username": u}).Decode(&existing) if err == nil { c.JSON(http.StatusConflict, gin.H{"error": "该用户名已被注册"}) return } if !errors.Is(err, mongo.ErrNoDocuments) { c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) return } doc := models.SiteUser{ Username: u, PasswordHash: utils.HashPassword(input.Password), CreatedAt: time.Now().UTC(), } res, err := coll.InsertOne(ctx, doc) if err != nil { if mongo.IsDuplicateKeyError(err) { c.JSON(http.StatusConflict, gin.H{"error": "该用户名已被注册"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) return } id, _ := res.InsertedID.(bson.ObjectID) token, err := issueSiteToken(id.Hex(), u) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "签发登录凭证失败"}) return } c.JSON(http.StatusOK, gin.H{"token": token, "username": u}) } // WebSiteLogin POST /api/web/site/login func WebSiteLogin(c *gin.Context) { if config.MongoClient == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用"}) return } var input siteLoginInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"}) return } u := strings.TrimSpace(input.Username) ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() var user models.SiteUser err := siteUsersColl().FindOne(ctx, bson.M{"username": u}).Decode(&user) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } if utils.HashPassword(input.Password) != user.PasswordHash { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } token, err := issueSiteToken(user.ID.Hex(), user.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "签发登录凭证失败"}) return } c.JSON(http.StatusOK, gin.H{"token": token, "username": user.Username}) } func issueSiteToken(userID, username string) (string, error) { claims := SiteClaims{ UserID: userID, Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(siteJWTExpire)), IssuedAt: jwt.NewNumericDate(time.Now()), Subject: userID, }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return t.SignedString(siteJWTSigningKey()) }