feat: 添加Yggdrasil密码重置功能,更新依赖和配置

This commit is contained in:
lafay
2025-11-30 18:56:56 +08:00
parent a4b6c5011e
commit 4188ee1555
18 changed files with 683 additions and 95 deletions

View File

@@ -37,6 +37,9 @@ func RegisterRoutes(router *gin.Engine) {
// 更换邮箱
userGroup.POST("/change-email", ChangeEmail)
// Yggdrasil密码相关
userGroup.POST("/yggdrasil-password/reset", ResetYggdrasilPassword) // 重置Yggdrasil密码并返回新密码
}
// 材质路由

View File

@@ -5,6 +5,7 @@ import (
"carrotskin/internal/service"
"carrotskin/internal/types"
"carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/logger"
"carrotskin/pkg/redis"
"carrotskin/pkg/storage"
@@ -413,3 +414,49 @@ func ChangeEmail(c *gin.Context) {
UpdatedAt: user.UpdatedAt,
}))
}
// ResetYggdrasilPassword 重置Yggdrasil密码
// @Summary 重置Yggdrasil密码
// @Description 重置当前用户的Yggdrasil密码并返回新密码
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response "重置成功"
// @Failure 401 {object} model.ErrorResponse "未授权"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/user/yggdrasil-password/reset [post]
func ResetYggdrasilPassword(c *gin.Context) {
loggerInstance := logger.MustGetLogger()
db := database.MustGetDB()
// 从上下文获取用户ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
model.CodeUnauthorized,
"未授权",
nil,
))
return
}
userId := userID.(int64)
// 重置Yggdrasil密码
newPassword, err := service.ResetYggdrasilPassword(db, userId)
if err != nil {
loggerInstance.Error("[ERROR] 重置Yggdrasil密码失败", zap.Error(err), zap.Int64("userId", userId))
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(
model.CodeServerError,
"重置Yggdrasil密码失败",
nil,
))
return
}
loggerInstance.Info("[INFO] Yggdrasil密码重置成功", zap.Int64("userId", userId))
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"password": newPassword,
}))
}

View File

@@ -2,22 +2,24 @@ package model
import (
"time"
"gorm.io/datatypes"
)
// User 用户模型
type User struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"`
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties string `gorm:"column:properties;type:jsonb" json:"properties"` // JSON字符串存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex" json:"username"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 密码不返回给前端
Email string `gorm:"column:email;type:varchar(255);not null;uniqueIndex" json:"email"`
Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" json:"avatar"`
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user'" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
}
// TableName 指定表名

View File

@@ -14,3 +14,9 @@ func GetYggdrasilPasswordById(Id int64) (string, error) {
}
return yggdrasil.Password, nil
}
// ResetYggdrasilPassword 重置Yggdrasil密码
func ResetYggdrasilPassword(userId int64, newPassword string) error {
db := database.MustGetDB()
return db.Model(&model.Yggdrasil{}).Where("id = ?", userId).Update("password", newPassword).Error
}

View File

@@ -129,13 +129,19 @@ func GenerateCaptchaData(ctx context.Context, redisClient *redis.Client) (string
redisDataJSON,
expireTime,
); err != nil {
return "", "", "", 0, fmt.Errorf("存储验证码到Redis失败: %w", err)
return "", "", "", 0, fmt.Errorf("存储验证码到redis失败: %w", err)
}
return mBase64, tBase64, captchaID, y - 10, nil
}
// VerifyCaptchaData 验证用户验证码
func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, id string) (bool, error) {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return true, nil
}
redisKey := redisKeyPrefix + id
// 从Redis获取验证信息使用注入的客户端
@@ -144,11 +150,11 @@ func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, i
if redisClient.Nil(err) { // 使用封装客户端的Nil错误
return false, errors.New("验证码已过期或无效")
}
return false, fmt.Errorf("Redis查询失败: %w", err)
return false, fmt.Errorf("redis查询失败: %w", err)
}
var redisData RedisData
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil {
return false, fmt.Errorf("解析Redis数据失败: %w", err)
return false, fmt.Errorf("解析redis数据失败: %w", err)
}
tx := redisData.Tx
ty := redisData.Ty

View File

@@ -4,9 +4,10 @@ import (
"carrotskin/internal/model"
"carrotskin/pkg/redis"
"encoding/base64"
"go.uber.org/zap"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)

View File

@@ -20,10 +20,10 @@ func TestSerializeUser_NilUser(t *testing.T) {
func TestSerializeUser_ActualCall(t *testing.T) {
logger := zaptest.NewLogger(t)
user := &model.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Properties: "{}",
ID: 1,
Username: "testuser",
Email: "test@example.com",
// Properties 使用 datatypes.JSON测试中可以为空
}
result := SerializeUser(logger, user, "test-uuid-123")

View File

@@ -50,6 +50,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
Role: "user",
Status: 1,
Points: 0, // 初始积分可以从配置读取
// Properties 字段使用 datatypes.JSON默认为 nil数据库会存储 NULL
}
if err := repository.CreateUser(user); err != nil {

View File

@@ -7,18 +7,19 @@ import (
"math/big"
"time"
"carrotskin/pkg/config"
"carrotskin/pkg/email"
"carrotskin/pkg/redis"
)
const (
// 验证码类型
VerificationTypeRegister = "register"
VerificationTypeRegister = "register"
VerificationTypeResetPassword = "reset_password"
VerificationTypeChangeEmail = "change_email"
// 验证码配置
CodeLength = 6 // 验证码长度
CodeLength = 6 // 验证码长度
CodeExpiration = 10 * time.Minute // 验证码有效期
CodeRateLimit = 1 * time.Minute // 发送频率限制
)
@@ -39,6 +40,12 @@ func GenerateVerificationCode() (string, error) {
// SendVerificationCode 发送验证码
func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailService *email.Service, email, codeType string) error {
// 测试环境下直接跳过,不存储也不发送
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return nil
}
// 检查发送频率限制
rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email)
exists, err := redisClient.Exists(ctx, rateLimitKey)
@@ -78,8 +85,14 @@ func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailS
// VerifyCode 验证验证码
func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, codeType string) error {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return nil
}
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码
storedCode, err := redisClient.Get(ctx, codeKey)
if err != nil {

View File

@@ -8,11 +8,12 @@ import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
"net"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -78,6 +79,33 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
return passwordStore, nil
}
// ResetYggdrasilPassword 重置并返回新的Yggdrasil密码
func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) {
// 生成新的16位随机密码
newPassword := model.GenerateRandomPassword(16)
// 检查Yggdrasil记录是否存在
_, err := repository.GetYggdrasilPasswordById(userId)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userId,
Password: newPassword,
}
if err := db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return newPassword, nil
}
// 如果存在,更新密码
if err := repository.ResetYggdrasilPassword(userId, newPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
return newPassword, nil
}
// JoinServer 记录玩家加入服务器的会话信息
func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error {
// 输入验证