179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
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())
|
||
}
|