feat: 添加种子数据初始化功能,重构多个处理程序以简化错误响应和用户验证

This commit is contained in:
lan
2025-12-02 10:33:19 +08:00
parent bdd2be5dc5
commit 10fdcd916b
30 changed files with 1291 additions and 1778 deletions

105
internal/service/helpers.go Normal file
View File

@@ -0,0 +1,105 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"errors"
"fmt"
"gorm.io/gorm"
)
// 通用错误
var (
ErrProfileNotFound = errors.New("档案不存在")
ErrProfileNoPermission = errors.New("无权操作此档案")
ErrTextureNotFound = errors.New("材质不存在")
ErrTextureNoPermission = errors.New("无权操作此材质")
ErrUserNotFound = errors.New("用户不存在")
)
// NormalizePagination 规范化分页参数
func NormalizePagination(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
return page, pageSize
}
// GetProfileWithPermissionCheck 获取档案并验证权限
// 返回档案,如果不存在或无权限则返回相应错误
func GetProfileWithPermissionCheck(uuid string, userID int64) (*model.Profile, error) {
profile, err := repository.FindProfileByUUID(uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrProfileNotFound
}
return nil, fmt.Errorf("查询档案失败: %w", err)
}
if profile.UserID != userID {
return nil, ErrProfileNoPermission
}
return profile, nil
}
// GetTextureWithPermissionCheck 获取材质并验证权限
// 返回材质,如果不存在或无权限则返回相应错误
func GetTextureWithPermissionCheck(textureID, userID int64) (*model.Texture, error) {
texture, err := repository.FindTextureByID(textureID)
if err != nil {
return nil, err
}
if texture == nil {
return nil, ErrTextureNotFound
}
if texture.UploaderID != userID {
return nil, ErrTextureNoPermission
}
return texture, nil
}
// EnsureTextureExists 确保材质存在
func EnsureTextureExists(textureID int64) (*model.Texture, error) {
texture, err := repository.FindTextureByID(textureID)
if err != nil {
return nil, err
}
if texture == nil {
return nil, ErrTextureNotFound
}
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return texture, nil
}
// EnsureUserExists 确保用户存在
func EnsureUserExists(userID int64) (*model.User, error) {
user, err := repository.FindUserByID(userID)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
// WrapError 包装错误,添加上下文信息
func WrapError(err error, message string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", message, err)
}

View File

@@ -9,6 +9,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"gorm.io/gorm"
@@ -16,53 +17,47 @@ import (
// CreateProfile 创建档案
func CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error) {
// 1. 验证用户存在
user, err := repository.FindUserByID(userID)
// 验证用户存在
user, err := EnsureUserExists(userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("用户不存在")
}
return nil, fmt.Errorf("查询用户失败: %w", err)
return nil, err
}
if user.Status != 1 {
return nil, fmt.Errorf("用户状态异常")
}
// 2. 检查角色名是否已存在
// 检查角色名是否已存在
existingName, err := repository.FindProfileByName(name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询角色名失败: %w", err)
return nil, WrapError(err, "查询角色名失败")
}
if existingName != nil {
return nil, fmt.Errorf("角色名已被使用")
}
// 3. 生成UUID
// 生成UUID和RSA密钥
profileUUID := uuid.New().String()
// 4. 生成RSA密钥对
privateKey, err := generateRSAPrivateKey()
if err != nil {
return nil, fmt.Errorf("生成RSA密钥失败: %w", err)
return nil, WrapError(err, "生成RSA密钥失败")
}
// 5. 创建档案
// 创建档案
profile := &model.Profile{
UUID: profileUUID,
UserID: userID,
Name: name,
RSAPrivateKey: privateKey,
IsActive: true, // 新创建的档案默认为活跃状态
IsActive: true,
}
if err := repository.CreateProfile(profile); err != nil {
return nil, fmt.Errorf("创建档案失败: %w", err)
return nil, WrapError(err, "创建档案失败")
}
// 6. 将用户的其他档案设置为非活跃
// 设置活跃状态
if err := repository.SetActiveProfile(profileUUID, userID); err != nil {
return nil, fmt.Errorf("设置活跃状态失败: %w", err)
return nil, WrapError(err, "设置活跃状态失败")
}
return profile, nil
@@ -73,9 +68,9 @@ func GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error) {
profile, err := repository.FindProfileByUUID(uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("档案不存在")
return nil, ErrProfileNotFound
}
return nil, fmt.Errorf("查询档案失败: %w", err)
return nil, WrapError(err, "查询档案失败")
}
return profile, nil
}
@@ -84,32 +79,24 @@ func GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error) {
func GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error) {
profiles, err := repository.FindProfilesByUserID(userID)
if err != nil {
return nil, fmt.Errorf("查询档案列表失败: %w", err)
return nil, WrapError(err, "查询档案列表失败")
}
return profiles, nil
}
// UpdateProfile 更新档案
func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error) {
// 1. 查询档案
profile, err := repository.FindProfileByUUID(uuid)
// 获取档案并验证权限
profile, err := GetProfileWithPermissionCheck(uuid, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("档案不存在")
}
return nil, fmt.Errorf("查询档案失败: %w", err)
return nil, err
}
// 2. 验证权限
if profile.UserID != userID {
return nil, fmt.Errorf("无权操作此档案")
}
// 3. 检查角色名是否重复
// 检查角色名是否重复
if name != nil && *name != profile.Name {
existingName, err := repository.FindProfileByName(*name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询角色名失败: %w", err)
return nil, WrapError(err, "查询角色名失败")
}
if existingName != nil {
return nil, fmt.Errorf("角色名已被使用")
@@ -117,7 +104,7 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID,
profile.Name = *name
}
// 4. 更新皮肤和披风
// 更新皮肤和披风
if skinID != nil {
profile.SkinID = skinID
}
@@ -125,63 +112,37 @@ func UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID,
profile.CapeID = capeID
}
// 5. 保存更新
if err := repository.UpdateProfile(profile); err != nil {
return nil, fmt.Errorf("更新档案失败: %w", err)
return nil, WrapError(err, "更新档案失败")
}
// 6. 重新加载关联数据
return repository.FindProfileByUUID(uuid)
}
// DeleteProfile 删除档案
func DeleteProfile(db *gorm.DB, uuid string, userID int64) error {
// 1. 查询档案
profile, err := repository.FindProfileByUUID(uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("档案不存在")
}
return fmt.Errorf("查询档案失败: %w", err)
if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil {
return err
}
// 2. 验证权限
if profile.UserID != userID {
return fmt.Errorf("无权操作此档案")
}
// 3. 删除档案
if err := repository.DeleteProfile(uuid); err != nil {
return fmt.Errorf("删除档案失败: %w", err)
return WrapError(err, "删除档案失败")
}
return nil
}
// SetActiveProfile 设置活跃档案
func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error {
// 1. 查询档案
profile, err := repository.FindProfileByUUID(uuid)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("档案不存在")
}
return fmt.Errorf("查询档案失败: %w", err)
if _, err := GetProfileWithPermissionCheck(uuid, userID); err != nil {
return err
}
// 2. 验证权限
if profile.UserID != userID {
return fmt.Errorf("无权操作此档案")
}
// 3. 设置活跃状态
if err := repository.SetActiveProfile(uuid, userID); err != nil {
return fmt.Errorf("设置活跃状态失败: %w", err)
return WrapError(err, "设置活跃状态失败")
}
// 4. 更新最后使用时间
if err := repository.UpdateProfileLastUsedAt(uuid); err != nil {
return fmt.Errorf("更新使用时间失败: %w", err)
return WrapError(err, "更新使用时间失败")
}
return nil
@@ -191,25 +152,22 @@ func SetActiveProfile(db *gorm.DB, uuid string, userID int64) error {
func CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error {
count, err := repository.CountProfilesByUserID(userID)
if err != nil {
return fmt.Errorf("查询档案数量失败: %w", err)
return WrapError(err, "查询档案数量失败")
}
if int(count) >= maxProfiles {
return fmt.Errorf("已达到档案数量上限(%d个", maxProfiles)
}
return nil
}
// generateRSAPrivateKey 生成RSA-2048私钥PEM格式
func generateRSAPrivateKey() (string, error) {
// 生成2048位RSA密钥对
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", err
}
// 将私钥编码为PEM格式
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
@@ -229,7 +187,7 @@ func ValidateProfileByUserID(db *gorm.DB, userId int64, UUID string) (bool, erro
if errors.Is(err, pgx.ErrNoRows) {
return false, errors.New("配置文件不存在")
}
return false, fmt.Errorf("验证配置文件失败: %w", err)
return false, WrapError(err, "验证配置文件失败")
}
return profile.UserID == userId, nil
}
@@ -237,16 +195,15 @@ func ValidateProfileByUserID(db *gorm.DB, userId int64, UUID string) (bool, erro
func GetProfilesDataByNames(db *gorm.DB, names []string) ([]*model.Profile, error) {
profiles, err := repository.GetProfilesByNames(names)
if err != nil {
return nil, fmt.Errorf("查找失败: %w", err)
return nil, WrapError(err, "查找失败")
}
return profiles, nil
}
// GetProfileKeyPair 从 PostgreSQL 获取密钥对GORM 实现,无手动 SQL
func GetProfileKeyPair(db *gorm.DB, profileId string) (*model.KeyPair, error) {
keyPair, err := repository.GetProfileKeyPair(profileId)
if err != nil {
return nil, fmt.Errorf("查找失败: %w", err)
return nil, WrapError(err, "查找失败")
}
return keyPair, nil
}

View File

@@ -0,0 +1,142 @@
package service
import (
"context"
"fmt"
"time"
"carrotskin/pkg/redis"
)
const (
// 登录失败限制配置
MaxLoginAttempts = 5 // 最大登录失败次数
LoginLockDuration = 15 * time.Minute // 账号锁定时间
LoginAttemptWindow = 10 * time.Minute // 失败次数统计窗口
// 验证码错误限制配置
MaxVerifyAttempts = 5 // 最大验证码错误次数
VerifyLockDuration = 30 * time.Minute // 验证码锁定时间
// Redis Key 前缀
LoginAttemptKeyPrefix = "security:login_attempt:"
LoginLockedKeyPrefix = "security:login_locked:"
VerifyAttemptKeyPrefix = "security:verify_attempt:"
VerifyLockedKeyPrefix = "security:verify_locked:"
)
// CheckLoginLocked 检查账号是否被锁定
func CheckLoginLocked(ctx context.Context, redisClient *redis.Client, identifier string) (bool, time.Duration, error) {
key := LoginLockedKeyPrefix + identifier
ttl, err := redisClient.TTL(ctx, key)
if err != nil {
return false, 0, err
}
if ttl > 0 {
return true, ttl, nil
}
return false, 0, nil
}
// RecordLoginFailure 记录登录失败
func RecordLoginFailure(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) {
attemptKey := LoginAttemptKeyPrefix + identifier
// 增加失败次数
count, err := redisClient.Incr(ctx, attemptKey)
if err != nil {
return 0, fmt.Errorf("记录登录失败次数失败: %w", err)
}
// 设置过期时间(仅在第一次设置)
if count == 1 {
if err := redisClient.Expire(ctx, attemptKey, LoginAttemptWindow); err != nil {
return int(count), fmt.Errorf("设置过期时间失败: %w", err)
}
}
// 如果超过最大次数,锁定账号
if count >= MaxLoginAttempts {
lockedKey := LoginLockedKeyPrefix + identifier
if err := redisClient.Set(ctx, lockedKey, "1", LoginLockDuration); err != nil {
return int(count), fmt.Errorf("锁定账号失败: %w", err)
}
// 清除失败计数
_ = redisClient.Del(ctx, attemptKey)
}
return int(count), nil
}
// ClearLoginAttempts 清除登录失败记录(登录成功后调用)
func ClearLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) error {
attemptKey := LoginAttemptKeyPrefix + identifier
return redisClient.Del(ctx, attemptKey)
}
// GetRemainingLoginAttempts 获取剩余登录尝试次数
func GetRemainingLoginAttempts(ctx context.Context, redisClient *redis.Client, identifier string) (int, error) {
attemptKey := LoginAttemptKeyPrefix + identifier
countStr, err := redisClient.Get(ctx, attemptKey)
if err != nil {
// key 不存在,返回最大次数
return MaxLoginAttempts, nil
}
var count int
fmt.Sscanf(countStr, "%d", &count)
remaining := MaxLoginAttempts - count
if remaining < 0 {
remaining = 0
}
return remaining, nil
}
// CheckVerifyLocked 检查验证码是否被锁定
func CheckVerifyLocked(ctx context.Context, redisClient *redis.Client, email, codeType string) (bool, time.Duration, error) {
key := VerifyLockedKeyPrefix + codeType + ":" + email
ttl, err := redisClient.TTL(ctx, key)
if err != nil {
return false, 0, err
}
if ttl > 0 {
return true, ttl, nil
}
return false, 0, nil
}
// RecordVerifyFailure 记录验证码验证失败
func RecordVerifyFailure(ctx context.Context, redisClient *redis.Client, email, codeType string) (int, error) {
attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email
// 增加失败次数
count, err := redisClient.Incr(ctx, attemptKey)
if err != nil {
return 0, fmt.Errorf("记录验证码失败次数失败: %w", err)
}
// 设置过期时间
if count == 1 {
if err := redisClient.Expire(ctx, attemptKey, VerifyLockDuration); err != nil {
return int(count), err
}
}
// 如果超过最大次数,锁定验证
if count >= MaxVerifyAttempts {
lockedKey := VerifyLockedKeyPrefix + codeType + ":" + email
if err := redisClient.Set(ctx, lockedKey, "1", VerifyLockDuration); err != nil {
return int(count), err
}
_ = redisClient.Del(ctx, attemptKey)
}
return int(count), nil
}
// ClearVerifyAttempts 清除验证码失败记录(验证成功后调用)
func ClearVerifyAttempts(ctx context.Context, redisClient *redis.Client, email, codeType string) error {
attemptKey := VerifyAttemptKeyPrefix + codeType + ":" + email
return redisClient.Del(ctx, attemptKey)
}

View File

@@ -12,13 +12,9 @@ import (
// CreateTexture 创建材质
func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) {
// 验证用户存在
user, err := repository.FindUserByID(uploaderID)
if err != nil {
if _, err := EnsureUserExists(uploaderID); err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("用户不存在")
}
// 检查Hash是否已存在
existingTexture, err := repository.FindTextureByHash(hash)
@@ -30,14 +26,9 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType
}
// 转换材质类型
var textureTypeEnum model.TextureType
switch textureType {
case "SKIN":
textureTypeEnum = model.TextureTypeSkin
case "CAPE":
textureTypeEnum = model.TextureTypeCape
default:
return nil, errors.New("无效的材质类型")
textureTypeEnum, err := parseTextureType(textureType)
if err != nil {
return nil, err
}
// 创建材质
@@ -65,58 +56,27 @@ func CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType
// GetTextureByID 根据ID获取材质
func GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error) {
texture, err := repository.FindTextureByID(id)
if err != nil {
return nil, err
}
if texture == nil {
return nil, errors.New("材质不存在")
}
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return texture, nil
return EnsureTextureExists(id)
}
// GetUserTextures 获取用户上传的材质列表
func GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
page, pageSize = NormalizePagination(page, pageSize)
return repository.FindTexturesByUploaderID(uploaderID, page, pageSize)
}
// SearchTextures 搜索材质
func SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
page, pageSize = NormalizePagination(page, pageSize)
return repository.SearchTextures(keyword, textureType, publicOnly, page, pageSize)
}
// UpdateTexture 更新材质
func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) {
// 获取材质
texture, err := repository.FindTextureByID(textureID)
if err != nil {
// 获取材质并验证权限
if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil {
return nil, err
}
if texture == nil {
return nil, errors.New("材质不存在")
}
// 检查权限:只有上传者可以修改
if texture.UploaderID != uploaderID {
return nil, errors.New("无权修改此材质")
}
// 更新字段
updates := make(map[string]interface{})
@@ -136,46 +96,27 @@ func UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description s
}
}
// 返回更新后的材质
return repository.FindTextureByID(textureID)
}
// DeleteTexture 删除材质
func DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error {
// 获取材质
texture, err := repository.FindTextureByID(textureID)
if err != nil {
if _, err := GetTextureWithPermissionCheck(textureID, uploaderID); err != nil {
return err
}
if texture == nil {
return errors.New("材质不存在")
}
// 检查权限:只有上传者可以删除
if texture.UploaderID != uploaderID {
return errors.New("无权删除此材质")
}
return repository.DeleteTexture(textureID)
}
// RecordTextureDownload 记录下载
func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error {
// 检查材质是否存在
texture, err := repository.FindTextureByID(textureID)
if err != nil {
if _, err := EnsureTextureExists(textureID); err != nil {
return err
}
if texture == nil {
return errors.New("材质不存在")
}
// 增加下载次数
if err := repository.IncrementTextureDownloadCount(textureID); err != nil {
return err
}
// 创建下载日志
log := &model.TextureDownloadLog{
TextureID: textureID,
UserID: userID,
@@ -188,23 +129,17 @@ func RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddres
// ToggleTextureFavorite 切换收藏状态
func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
// 检查材质是否存在
texture, err := repository.FindTextureByID(textureID)
if err != nil {
if _, err := EnsureTextureExists(textureID); err != nil {
return false, err
}
if texture == nil {
return false, errors.New("材质不存在")
}
// 检查是否已收藏
isFavorited, err := repository.IsTextureFavorited(userID, textureID)
if err != nil {
return false, err
}
if isFavorited {
// 取消收藏
// 已收藏 -> 取消收藏
if err := repository.RemoveTextureFavorite(userID, textureID); err != nil {
return false, err
}
@@ -213,7 +148,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
}
return false, nil
} else {
// 添加收藏
// 未收藏 -> 添加收藏
if err := repository.AddTextureFavorite(userID, textureID); err != nil {
return false, err
}
@@ -226,13 +161,7 @@ func ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error) {
// GetUserTextureFavorites 获取用户收藏的材质列表
func GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
page, pageSize = NormalizePagination(page, pageSize)
return repository.GetUserTextureFavorites(userID, page, pageSize)
}
@@ -249,3 +178,15 @@ func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) err
return nil
}
// parseTextureType 解析材质类型
func parseTextureType(textureType string) (model.TextureType, error) {
switch textureType {
case "SKIN":
return model.TextureTypeSkin, nil
case "CAPE":
return model.TextureTypeCape, nil
default:
return "", errors.New("无效的材质类型")
}
}

View File

@@ -4,7 +4,10 @@ import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"context"
"errors"
"fmt"
"strings"
"time"
)
@@ -37,7 +40,12 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
// 确定头像URL优先使用用户提供的头像否则使用默认头像
avatarURL := avatar
if avatarURL == "" {
if avatarURL != "" {
// 验证用户提供的头像 URL 是否来自允许的域名
if err := ValidateAvatarURL(avatarURL); err != nil {
return nil, "", err
}
} else {
avatarURL = getDefaultAvatar()
}
@@ -49,8 +57,7 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
Avatar: avatarURL,
Role: "user",
Status: 1,
Points: 0, // 初始积分可以从配置读取
// Properties 字段使用 datatypes.JSON默认为 nil数据库会存储 NULL
Points: 0,
}
if err := repository.CreateUser(user); err != nil {
@@ -63,22 +70,34 @@ func RegisterUser(jwtService *auth.JWTService, username, password, email, avatar
return nil, "", errors.New("生成Token失败")
}
// TODO: 添加注册奖励积分
return user, token, nil
}
// LoginUser 用户登录(支持用户名或邮箱登录)
func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
return LoginUserWithRateLimit(nil, jwtService, usernameOrEmail, password, ipAddress, userAgent)
}
// LoginUserWithRateLimit 用户登录(带频率限制)
func LoginUserWithRateLimit(redisClient *redis.Client, jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error) {
ctx := context.Background()
// 检查账号是否被锁定(基于用户名/邮箱和IP
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
locked, ttl, err := CheckLoginLocked(ctx, redisClient, identifier)
if err == nil && locked {
return nil, "", fmt.Errorf("登录尝试次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
}
}
// 查找用户:判断是用户名还是邮箱
var user *model.User
var err error
if strings.Contains(usernameOrEmail, "@") {
// 包含@符号,认为是邮箱
user, err = repository.FindUserByEmail(usernameOrEmail)
} else {
// 否则认为是用户名
user, err = repository.FindUserByUsername(usernameOrEmail)
}
@@ -86,7 +105,16 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
return nil, "", err
}
if user == nil {
// 记录失败日志
// 记录失败尝试
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
remaining := MaxLoginAttempts - count
if remaining > 0 {
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
}
}
logFailedLogin(0, ipAddress, userAgent, "用户不存在")
return nil, "", errors.New("用户名/邮箱或密码错误")
}
@@ -99,10 +127,26 @@ func LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress
// 验证密码
if !auth.CheckPassword(user.Password, password) {
// 记录失败尝试
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
count, _ := RecordLoginFailure(ctx, redisClient, identifier)
remaining := MaxLoginAttempts - count
if remaining > 0 {
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
return nil, "", fmt.Errorf("用户名/邮箱或密码错误,还剩 %d 次尝试机会", remaining)
}
}
logFailedLogin(user.ID, ipAddress, userAgent, "密码错误")
return nil, "", errors.New("用户名/邮箱或密码错误")
}
// 登录成功,清除失败计数
if redisClient != nil {
identifier := usernameOrEmail + ":" + ipAddress
_ = ClearLoginAttempts(ctx, redisClient, identifier)
}
// 生成JWT Token
token, err := jwtService.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
@@ -141,24 +185,20 @@ func UpdateUserAvatar(userID int64, avatarURL string) error {
// ChangeUserPassword 修改密码
func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// 获取用户
user, err := repository.FindUserByID(userID)
if err != nil {
return errors.New("用户不存在")
}
// 验证旧密码
if !auth.CheckPassword(user.Password, oldPassword) {
return errors.New("原密码错误")
}
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
}
// 更新密码
return repository.UpdateUserFields(userID, map[string]interface{}{
"password": hashedPassword,
})
@@ -166,19 +206,16 @@ func ChangeUserPassword(userID int64, oldPassword, newPassword string) error {
// ResetUserPassword 重置密码(通过邮箱)
func ResetUserPassword(email, newPassword string) error {
// 查找用户
user, err := repository.FindUserByEmail(email)
if err != nil {
return errors.New("用户不存在")
}
// 加密新密码
hashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码加密失败")
}
// 更新密码
return repository.UpdateUserFields(user.ID, map[string]interface{}{
"password": hashedPassword,
})
@@ -186,7 +223,6 @@ func ResetUserPassword(email, newPassword string) error {
// ChangeUserEmail 更换邮箱
func ChangeUserEmail(userID int64, newEmail string) error {
// 检查新邮箱是否已被使用
existingUser, err := repository.FindUserByEmail(newEmail)
if err != nil {
return err
@@ -195,7 +231,6 @@ func ChangeUserEmail(userID int64, newEmail string) error {
return errors.New("邮箱已被其他用户使用")
}
// 更新邮箱
return repository.UpdateUserFields(userID, map[string]interface{}{
"email": newEmail,
})
@@ -228,18 +263,40 @@ func logFailedLogin(userID int64, ipAddress, userAgent, reason string) {
// getDefaultAvatar 获取默认头像URL
func getDefaultAvatar() string {
// 如果数据库中不存在默认头像配置,返回错误信息
const log = "数据库中不存在默认头像配置"
// 尝试从数据库读取配置
config, err := repository.GetSystemConfigByKey("default_avatar")
if err != nil || config == nil {
return log
if err != nil || config == nil || config.Value == "" {
return ""
}
return config.Value
}
// ValidateAvatarURL 验证头像URL是否合法
func ValidateAvatarURL(avatarURL string) error {
if avatarURL == "" {
return nil
}
// 允许的域名列表
allowedDomains := []string{
"rustfs.example.com",
"localhost",
"127.0.0.1",
}
for _, domain := range allowedDomains {
if strings.Contains(avatarURL, domain) {
return nil
}
}
if strings.HasPrefix(avatarURL, "/") {
return nil
}
return errors.New("头像URL不在允许的域名列表中")
}
// GetUserByEmail 根据邮箱获取用户
func GetUserByEmail(email string) (*model.User, error) {
user, err := repository.FindUserByEmail(email)
if err != nil {
@@ -247,3 +304,31 @@ func GetUserByEmail(email string) (*model.User, error) {
}
return user, nil
}
// GetMaxProfilesPerUser 获取每用户最大档案数量配置
func GetMaxProfilesPerUser() int {
config, err := repository.GetSystemConfigByKey("max_profiles_per_user")
if err != nil || config == nil {
return 5
}
var value int
fmt.Sscanf(config.Value, "%d", &value)
if value <= 0 {
return 5
}
return value
}
// GetMaxTexturesPerUser 获取每用户最大材质数量配置
func GetMaxTexturesPerUser() int {
config, err := repository.GetSystemConfigByKey("max_textures_per_user")
if err != nil || config == nil {
return 50
}
var value int
fmt.Sscanf(config.Value, "%d", &value)
if value <= 0 {
return 50
}
return value
}

View File

@@ -91,21 +91,36 @@ func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, cod
return nil
}
// 检查是否被锁定
locked, ttl, err := CheckVerifyLocked(ctx, redisClient, email, codeType)
if err == nil && locked {
return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1)
}
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码
storedCode, err := redisClient.Get(ctx, codeKey)
if err != nil {
// 记录失败尝试
RecordVerifyFailure(ctx, redisClient, email, codeType)
return fmt.Errorf("验证码已过期或不存在")
}
// 验证验证码
if storedCode != code {
// 记录失败尝试
count, _ := RecordVerifyFailure(ctx, redisClient, email, codeType)
remaining := MaxVerifyAttempts - count
if remaining > 0 {
return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining)
}
return fmt.Errorf("验证码错误")
}
// 验证成功,删除验证码
// 验证成功,删除验证码和失败计数
_ = redisClient.Del(ctx, codeKey)
_ = ClearVerifyAttempts(ctx, redisClient, email, codeType)
return nil
}

View File

@@ -3,6 +3,7 @@ package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
@@ -54,7 +55,8 @@ func VerifyPassword(db *gorm.DB, password string, Id int64) error {
if err != nil {
return errors.New("未生成密码")
}
if passwordStore != password {
// 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return errors.New("密码错误")
}
return nil
@@ -81,29 +83,36 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
// ResetYggdrasilPassword 重置并返回新的Yggdrasil密码
func ResetYggdrasilPassword(db *gorm.DB, userId int64) (string, error) {
// 生成新的16位随机密码
newPassword := model.GenerateRandomPassword(16)
// 生成新的16位随机密码(明文,返回给用户)
plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在
_, err := repository.GetYggdrasilPasswordById(userId)
_, err = repository.GetYggdrasilPasswordById(userId)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userId,
Password: newPassword,
Password: hashedPassword,
}
if err := db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return newPassword, nil
return plainPassword, nil
}
// 如果存在,更新密码
if err := repository.ResetYggdrasilPassword(userId, newPassword); err != nil {
// 如果存在,更新密码(存储加密后的密码)
if err := repository.ResetYggdrasilPassword(userId, hashedPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
return newPassword, nil
// 返回明文密码给用户
return plainPassword, nil
}
// JoinServer 记录玩家加入服务器的会话信息