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

3
.gitignore vendored
View File

@@ -23,7 +23,8 @@ dist/
build/ build/
# Compiled binaries # Compiled binaries
server /server
server.exe
# IDE files # IDE files
.vscode/ .vscode/

123
cmd/server/main.go Normal file
View File

@@ -0,0 +1,123 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
_ "carrotskin/docs" // Swagger文档
"carrotskin/internal/handler"
"carrotskin/internal/middleware"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/email"
"carrotskin/pkg/logger"
"carrotskin/pkg/redis"
"carrotskin/pkg/storage"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func main() {
// 初始化配置
if err := config.Init(); err != nil {
log.Fatalf("配置加载失败: %v", err)
}
cfg := config.MustGetConfig()
// 初始化日志
if err := logger.Init(cfg.Log); err != nil {
log.Fatalf("日志初始化失败: %v", err)
}
loggerInstance := logger.MustGetLogger()
defer loggerInstance.Sync()
// 初始化数据库
if err := database.Init(cfg.Database, loggerInstance); err != nil {
loggerInstance.Fatal("数据库初始化失败", zap.Error(err))
}
defer database.Close()
// 执行数据库迁移
if err := database.AutoMigrate(loggerInstance); err != nil {
loggerInstance.Fatal("数据库迁移失败", zap.Error(err))
}
// 初始化JWT服务
if err := auth.Init(cfg.JWT); err != nil {
loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err))
}
// 初始化Redis
if err := redis.Init(cfg.Redis, loggerInstance); err != nil {
loggerInstance.Fatal("Redis连接失败", zap.Error(err))
}
defer redis.MustGetClient().Close()
// 初始化对象存储 (RustFS - S3兼容)
// 如果对象存储未配置或连接失败,记录警告但不退出(某些功能可能不可用)
if err := storage.Init(cfg.RustFS); err != nil {
loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err))
} else {
loggerInstance.Info("对象存储连接成功")
}
// 初始化邮件服务
if err := email.Init(cfg.Email, loggerInstance); err != nil {
loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err))
}
// 设置Gin模式
if cfg.Server.Mode == "production" {
gin.SetMode(gin.ReleaseMode)
}
// 创建路由
router := gin.New()
// 添加中间件
router.Use(middleware.Logger(loggerInstance))
router.Use(middleware.Recovery(loggerInstance))
router.Use(middleware.CORS())
// 注册路由
handler.RegisterRoutes(router)
// 创建HTTP服务器
srv := &http.Server{
Addr: cfg.Server.Port,
Handler: router,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
// 启动服务器
go func() {
loggerInstance.Info("服务器启动", zap.String("port", cfg.Server.Port))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
loggerInstance.Fatal("服务器启动失败", zap.Error(err))
}
}()
// 等待中断信号优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
loggerInstance.Info("正在关闭服务器...")
// 设置关闭超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
loggerInstance.Fatal("服务器强制关闭", zap.Error(err))
}
loggerInstance.Info("服务器已关闭")
}

17
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/lib/pq v1.10.9
github.com/minio/minio-go/v7 v7.0.66 github.com/minio/minio-go/v7 v7.0.66
github.com/redis/go-redis/v9 v9.0.5 github.com/redis/go-redis/v9 v9.0.5
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
@@ -17,13 +18,19 @@ require (
github.com/wenlng/go-captcha-assets v1.0.7 github.com/wenlng/go-captcha-assets v1.0.7
github.com/wenlng/go-captcha/v2 v2.0.4 github.com/wenlng/go-captcha/v2 v2.0.4
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
gorm.io/driver/postgres v1.5.4 gorm.io/datatypes v1.2.7
gorm.io/gorm v1.25.5 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/image v0.16.0 // indirect golang.org/x/image v0.16.0 // indirect
golang.org/x/sync v0.16.0 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
) )
require ( require (
@@ -47,10 +54,10 @@ require (
github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.5.0 github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.4.3 github.com/jackc/pgx/v5 v5.6.0
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect

46
go.sum
View File

@@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@@ -54,12 +56,19 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -67,14 +76,16 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -102,12 +113,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
@@ -254,8 +271,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -37,6 +37,9 @@ func RegisterRoutes(router *gin.Engine) {
// 更换邮箱 // 更换邮箱
userGroup.POST("/change-email", ChangeEmail) 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/service"
"carrotskin/internal/types" "carrotskin/internal/types"
"carrotskin/pkg/config" "carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/logger" "carrotskin/pkg/logger"
"carrotskin/pkg/redis" "carrotskin/pkg/redis"
"carrotskin/pkg/storage" "carrotskin/pkg/storage"
@@ -413,3 +414,49 @@ func ChangeEmail(c *gin.Context) {
UpdatedAt: user.UpdatedAt, 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,6 +2,8 @@ package model
import ( import (
"time" "time"
"gorm.io/datatypes"
) )
// User 用户模型 // User 用户模型
@@ -14,7 +16,7 @@ type User struct {
Points int `gorm:"column:points;type:integer;not null;default:0" json:"points"` 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"` 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:删除 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类型 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"` 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"` 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"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`

View File

@@ -14,3 +14,9 @@ func GetYggdrasilPasswordById(Id int64) (string, error) {
} }
return yggdrasil.Password, nil 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, redisDataJSON,
expireTime, expireTime,
); err != nil { ); err != nil {
return "", "", "", 0, fmt.Errorf("存储验证码到Redis失败: %w", err) return "", "", "", 0, fmt.Errorf("存储验证码到redis失败: %w", err)
} }
return mBase64, tBase64, captchaID, y - 10, nil return mBase64, tBase64, captchaID, y - 10, nil
} }
// VerifyCaptchaData 验证用户验证码 // VerifyCaptchaData 验证用户验证码
func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, id string) (bool, error) { 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 redisKey := redisKeyPrefix + id
// 从Redis获取验证信息使用注入的客户端 // 从Redis获取验证信息使用注入的客户端
@@ -144,11 +150,11 @@ func VerifyCaptchaData(ctx context.Context, redisClient *redis.Client, dx int, i
if redisClient.Nil(err) { // 使用封装客户端的Nil错误 if redisClient.Nil(err) { // 使用封装客户端的Nil错误
return false, errors.New("验证码已过期或无效") return false, errors.New("验证码已过期或无效")
} }
return false, fmt.Errorf("Redis查询失败: %w", err) return false, fmt.Errorf("redis查询失败: %w", err)
} }
var redisData RedisData var redisData RedisData
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil { 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 tx := redisData.Tx
ty := redisData.Ty ty := redisData.Ty

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"math/big" "math/big"
"time" "time"
"carrotskin/pkg/config"
"carrotskin/pkg/email" "carrotskin/pkg/email"
"carrotskin/pkg/redis" "carrotskin/pkg/redis"
) )
@@ -39,6 +40,12 @@ func GenerateVerificationCode() (string, error) {
// SendVerificationCode 发送验证码 // SendVerificationCode 发送验证码
func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailService *email.Service, email, codeType string) error { 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) rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email)
exists, err := redisClient.Exists(ctx, rateLimitKey) exists, err := redisClient.Exists(ctx, rateLimitKey)
@@ -78,6 +85,12 @@ func SendVerificationCode(ctx context.Context, redisClient *redis.Client, emailS
// VerifyCode 验证验证码 // VerifyCode 验证验证码
func VerifyCode(ctx context.Context, redisClient *redis.Client, email, code, codeType string) error { 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) codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
// 从Redis获取验证码 // 从Redis获取验证码

View File

@@ -8,11 +8,12 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"go.uber.org/zap"
"net" "net"
"strings" "strings"
"time" "time"
"go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -78,6 +79,33 @@ func GetPasswordByUserId(db *gorm.DB, userId int64) (string, error) {
return passwordStore, nil 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 记录玩家加入服务器的会话信息 // JoinServer 记录玩家加入服务器的会话信息
func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error { func JoinServer(db *gorm.DB, logger *zap.Logger, redisClient *redis.Client, serverId, accessToken, selectedProfile, ip string) error {
// 输入验证 // 输入验证

View File

@@ -12,6 +12,7 @@ import (
// Config 应用配置结构体 // Config 应用配置结构体
type Config struct { type Config struct {
Environment string `mapstructure:"environment"`
Server ServerConfig `mapstructure:"server"` Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"` Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"` Redis RedisConfig `mapstructure:"redis"`
@@ -301,4 +302,14 @@ func overrideFromEnv(config *Config) {
if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" { if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" {
config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1" config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1"
} }
// 处理环境配置
if env := os.Getenv("ENVIRONMENT"); env != "" {
config.Environment = env
}
}
// IsTestEnvironment 判断是否为测试环境
func (c *Config) IsTestEnvironment() bool {
return c.Environment == "test"
} }

View File

@@ -59,7 +59,8 @@ func AutoMigrate(logger *zap.Logger) error {
logger.Info("开始执行数据库迁移...") logger.Info("开始执行数据库迁移...")
// 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表 // 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表
err = db.AutoMigrate( // 使用分批迁移,避免某些表的问题影响其他表
tables := []interface{}{
// 用户相关表(先创建,因为其他表可能引用它) // 用户相关表(先创建,因为其他表可能引用它)
&model.User{}, &model.User{},
&model.UserPointLog{}, &model.UserPointLog{},
@@ -87,11 +88,30 @@ func AutoMigrate(logger *zap.Logger) error {
// Casbin权限规则表 // Casbin权限规则表
&model.CasbinRule{}, &model.CasbinRule{},
) }
if err != nil { // 逐个迁移表,以便更好地定位问题
logger.Error("数据库迁移失败", zap.Error(err)) for _, table := range tables {
return fmt.Errorf("数据库迁移失败: %w", err) tableName := fmt.Sprintf("%T", table)
logger.Info("正在迁移表", zap.String("table", tableName))
if err := db.AutoMigrate(table); err != nil {
logger.Error("数据库迁移失败", zap.Error(err), zap.String("table", tableName))
// 如果是 User 表且错误是 insufficient arguments可能是 Properties 字段问题
if tableName == "*model.User" {
logger.Warn("User 表迁移失败,可能是 Properties 字段问题,尝试修复...")
// 尝试手动添加 properties 字段(如果不存在)
if err := db.Exec("ALTER TABLE \"user\" ADD COLUMN IF NOT EXISTS properties jsonb").Error; err != nil {
logger.Error("添加 properties 字段失败", zap.Error(err))
}
// 再次尝试迁移
if err := db.AutoMigrate(table); err != nil {
return fmt.Errorf("数据库迁移失败 (表: %T): %w", table, err)
}
} else {
return fmt.Errorf("数据库迁移失败 (表: %T): %w", table, err)
}
}
logger.Info("表迁移成功", zap.String("table", tableName))
} }
logger.Info("数据库迁移完成") logger.Info("数据库迁移完成")

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CarrotSkin 测试账户生成器
此脚本创建新用户、角色,并输出所有测试信息
"""
import json
import random
import sys
from datetime import datetime
from pathlib import Path
try:
import requests
except ImportError:
print("错误: 需要安装 requests 库")
print("请运行: pip install requests")
sys.exit(1)
# 颜色输出支持Windows 和 Unix 都支持)
try:
from colorama import init, Fore, Style
init(autoreset=True)
HAS_COLORAMA = True
# colorama 没有 GRAY使用 WHITE 代替
Fore.GRAY = Fore.WHITE
except ImportError:
# 如果没有 colorama使用空字符串
class Fore:
CYAN = YELLOW = GREEN = RED = GRAY = WHITE = RESET = ""
class Style:
RESET_ALL = ""
HAS_COLORAMA = False
# API 基础 URL
BASE_URL = "http://localhost:8080/api/v1"
def print_colored(text, color=Fore.RESET):
"""打印彩色文本"""
print(f"{color}{text}{Style.RESET_ALL}")
def print_header(text):
"""打印标题"""
print_colored(f"\n{'=' * 40}", Fore.CYAN)
print_colored(f" {text}", Fore.CYAN)
print_colored(f"{'=' * 40}\n", Fore.CYAN)
def print_step(text):
"""打印步骤标题"""
print_colored(f"\n=== {text} ===", Fore.YELLOW)
def print_success(text):
"""打印成功消息"""
print_colored(f"{text}", Fore.GREEN)
def print_error(text):
"""打印错误消息"""
print_colored(f"{text}", Fore.RED)
def print_info(text):
"""打印信息消息"""
print_colored(f" {text}", Fore.GRAY)
def register_user():
"""步骤1: 注册新用户"""
print_step("步骤1: 注册新用户")
random_num = random.randint(10000, 99999)
username = f"testuser{random_num}"
email = f"test{random_num}@example.com"
login_password = "password123"
verification_code = "123456"
print_info(f"用户名: {username}")
print_info(f"邮箱: {email}")
print_info(f"密码: {login_password}")
register_data = {
"username": username,
"email": email,
"password": login_password,
"verification_code": verification_code
}
try:
response = requests.post(
f"{BASE_URL}/auth/register",
json=register_data,
headers={"Content-Type": "application/json"},
timeout=10
)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
jwt_token = result["data"]["token"]
user_id = result["data"]["user_info"]["id"]
user_role = result["data"]["user_info"]["role"]
print_success("注册成功!")
print_info(f"用户ID: {user_id}")
print_info(f"角色: {user_role}")
return {
"username": username,
"email": email,
"login_password": login_password,
"jwt_token": jwt_token,
"user_id": user_id,
"user_role": user_role
}
else:
print_error(f"注册失败: {result.get('message', '未知错误')}")
sys.exit(1)
except requests.exceptions.RequestException as e:
print_error(f"注册失败: {str(e)}")
sys.exit(1)
def create_profile(jwt_token):
"""步骤2: 创建Minecraft角色"""
print_step("步骤2: 创建Minecraft角色")
profile_name = f"TestPlayer{random.randint(100, 999)}"
print_info(f"角色名: {profile_name}")
profile_data = {
"name": profile_name
}
headers = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
try:
response = requests.post(
f"{BASE_URL}/profile/",
json=profile_data,
headers=headers,
timeout=10
)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
profile_uuid = result["data"]["uuid"]
print_success("角色创建成功!")
print_info(f"角色UUID: {profile_uuid}")
print_info(f"角色名: {result['data']['name']}")
return {
"profile_name": profile_name,
"profile_uuid": profile_uuid
}
else:
print_error(f"角色创建失败: {result.get('message', '未知错误')}")
sys.exit(1)
except requests.exceptions.RequestException as e:
print_error(f"角色创建失败: {str(e)}")
sys.exit(1)
def reset_yggdrasil_password(jwt_token):
"""步骤3: 重置并获取Yggdrasil密码"""
print_step("步骤3: 重置并获取Yggdrasil密码")
headers = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
try:
response = requests.post(
f"{BASE_URL}/user/yggdrasil-password/reset",
headers=headers,
timeout=10
)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
yggdrasil_password = result["data"]["password"]
print_success("Yggdrasil密码重置成功!")
print_info(f"Yggdrasil密码: {yggdrasil_password}")
return yggdrasil_password
else:
print_error(f"Yggdrasil密码重置失败: {result.get('message', '未知错误')}")
return ""
except requests.exceptions.RequestException as e:
print_error(f"Yggdrasil密码重置失败: {str(e)}")
return ""
def generate_output(user_info, profile_info, yggdrasil_password):
"""生成输出信息"""
output = f"""========================================
CarrotSkin 测试账户信息
========================================
=== 账户信息 ===
用户名: {user_info['username']}
邮箱: {user_info['email']}
登录密码: {user_info['login_password']}
用户ID: {user_info['user_id']}
=== JWT Token (API认证) ===
Token: {user_info['jwt_token']}
=== 角色信息 ===
角色名: {profile_info['profile_name']}
角色UUID: {profile_info['profile_uuid']}
=== Yggdrasil信息 ===
Yggdrasil密码: {yggdrasil_password}
=== 测试命令 ===
# 1. API登录
curl -X POST http://localhost:8080/api/v1/auth/login \\
-H 'Content-Type: application/json' \\
-d '{{"username":"{user_info['username']}","password":"{user_info['login_password']}"}}'
# 2. 创建角色
curl -X POST http://localhost:8080/api/v1/profile/ \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer {user_info['jwt_token']}' \\
-d '{{"name":"NewProfile"}}'
# 3. 重置Yggdrasil密码
curl -X POST http://localhost:8080/api/v1/user/yggdrasil-password/reset \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer {user_info['jwt_token']}'
# 4. Yggdrasil认证
curl -X POST http://localhost:8080/api/v1/yggdrasil/authserver/authenticate \\
-H 'Content-Type: application/json' \\
-d '{{
"username": "{user_info['username']}",
"password": "{yggdrasil_password}",
"requestUser": true
}}'
# 5. 获取角色信息
curl -X GET http://localhost:8080/api/v1/profile/{profile_info['profile_uuid']}
========================================
"""
return output
def main():
"""主函数"""
print_header("CarrotSkin 测试账户生成器")
# 步骤1: 注册用户
user_info = register_user()
# 步骤2: 创建角色
profile_info = create_profile(user_info["jwt_token"])
# 步骤3: 重置Yggdrasil密码
yggdrasil_password = reset_yggdrasil_password(user_info["jwt_token"])
# 步骤4: 输出所有信息
print_header("测试账户信息汇总")
output = generate_output(user_info, profile_info, yggdrasil_password)
print(output)
# 保存到文件
output_file = f"test_account_{user_info['username']}.txt"
try:
with open(output_file, "w", encoding="utf-8") as f:
f.write(output)
print_success(f"信息已保存到文件: {output_file}")
except Exception as e:
print_error(f"保存文件失败: {str(e)}")
print_header("测试完成!")
if __name__ == "__main__":
main()

View File

@@ -5,9 +5,9 @@
# 设置环境变量 # 设置环境变量
export DATABASE_HOST=192.168.10.205 export DATABASE_HOST=192.168.10.205
export DATABASE_PORT=5432 export DATABASE_PORT=5432
export DATABASE_USERNAME=skin export DATABASE_USERNAME=skin3
export DATABASE_PASSWORD=lanyimin123 export DATABASE_PASSWORD=lanyimin123
export DATABASE_NAME=skin export DATABASE_NAME=skin3
export DATABASE_SSL_MODE=disable export DATABASE_SSL_MODE=disable
export DATABASE_TIMEZONE=Asia/Shanghai export DATABASE_TIMEZONE=Asia/Shanghai