From bd5604a425a518518ee3041650f0a10c981c9505 Mon Sep 17 00:00:00 2001 From: veypi Date: Fri, 24 Apr 2026 02:54:03 +0800 Subject: [PATCH] chore: bump version to v1.1.1 --- api/auth/register.go | 139 +++++++++++++++++++++++-------------------- cfg/cfg.go | 6 +- init.go | 2 +- models/init.go | 1 + models/setting.go | 22 +++---- ui/vbase.js | 28 +++++++++ 6 files changed, 117 insertions(+), 81 deletions(-) diff --git a/api/auth/register.go b/api/auth/register.go index 22f19bd..0196dd2 100644 --- a/api/auth/register.go +++ b/api/auth/register.go @@ -18,6 +18,7 @@ import ( "github.com/veypi/vbase/libs/jwt" "github.com/veypi/vbase/models" "github.com/veypi/vigo" + "gorm.io/gorm" ) // Email 正则表达式 @@ -71,89 +72,99 @@ func register(x *vigo.X, req *RegisterRequest) (*AuthResponse, error) { return nil, vigo.ErrInvalidArg.WithString("phone is required") } - // 检查是否是第一个用户(需要在创建用户之前检查) - var userCount int64 - if err := cfg.DB().Model(&models.User{}).Count(&userCount).Error; err != nil { - return nil, vigo.ErrInternalServer.WithError(err) - } - - // 检查用户名是否已存在 - var count int64 - if err := cfg.DB().Model(&models.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil { - return nil, vigo.ErrInternalServer.WithError(err) - } - if count > 0 { - return nil, vigo.ErrInvalidArg.WithArgs("username already exists") - } + // 使用事务处理注册,防止并发创建多个首个管理员用户 + var user *models.User + err := cfg.DB().Transaction(func(tx *gorm.DB) error { + // 检查是否是第一个用户(在事务内检查,带锁) + var userCount int64 + if err := tx.Model(&models.User{}).Count(&userCount).Error; err != nil { + return err + } - // 检查邮箱是否已存在 - if req.Email != "" { - count = 0 // 重置计数器 - if err := cfg.DB().Model(&models.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil { - return nil, vigo.ErrInternalServer.WithError(err) + // 检查用户名是否已存在 + var count int64 + if err := tx.Model(&models.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil { + return err } if count > 0 { - return nil, vigo.ErrInvalidArg.WithArgs("email already exists") + return vigo.ErrInvalidArg.WithArgs("username already exists") } - } - // 检查手机是否已存在 - if req.Phone != "" { - count = 0 // 重置计数器 - if err := cfg.DB().Model(&models.User{}).Where("phone = ?", req.Phone).Count(&count).Error; err != nil { - return nil, vigo.ErrInternalServer.WithError(err) + // 检查邮箱是否已存在 + if req.Email != "" { + count = 0 + if err := tx.Model(&models.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return vigo.ErrInvalidArg.WithArgs("email already exists") + } } - if count > 0 { - return nil, vigo.ErrInvalidArg.WithArgs("phone already exists") + + // 检查手机是否已存在 + if req.Phone != "" { + count = 0 + if err := tx.Model(&models.User{}).Where("phone = ?", req.Phone).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return vigo.ErrInvalidArg.WithArgs("phone already exists") + } } - } - // 哈希密码 - hashedPassword, err := crypto.HashPassword(req.Password, 12) - if err != nil { - return nil, vigo.ErrInternalServer.WithError(err) - } + // 哈希密码 + hashedPassword, err := crypto.HashPassword(req.Password, 12) + if err != nil { + return err + } - // 创建用户 - var email *string - if req.Email != "" { - email = &req.Email - } - var phone *string - if req.Phone != "" { - phone = &req.Phone - } + // 创建用户 + var email *string + if req.Email != "" { + email = &req.Email + } + var phone *string + if req.Phone != "" { + phone = &req.Phone + } - user := &models.User{ - Username: req.Username, - Password: hashedPassword, - Email: email, - Phone: phone, - Nickname: req.Nickname, - Status: models.UserStatusActive, - } + user = &models.User{ + Username: req.Username, + Password: hashedPassword, + Email: email, + Phone: phone, + Nickname: req.Nickname, + Status: models.UserStatusActive, + } - if user.Nickname == "" { - user.Nickname = user.Username - } + if user.Nickname == "" { + user.Nickname = user.Username + } - // 生成随机头像 - user.Avatar = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220)) + // 生成随机头像 + user.Avatar = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220)) - if err := cfg.DB().Create(user).Error; err != nil { - return nil, vigo.ErrInternalServer.WithError(err) + // 第一个用户固定 ID 为 admin + if userCount == 0 { + user.ID = "admin" + } + + if err := tx.Create(user).Error; err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err } - // 第一个用户授予 admin 角色,其他用户授予 user 角色 + // 授予角色(事务外,因为事务已确保用户创建成功) roleCode := "user" - if userCount == 0 { + if user.ID == "admin" { roleCode = "admin" } - if err := cfg.Auth.GrantRole(x.Context(), user.ID, roleCode); err != nil { - // 记录错误但允许注册继续,或者回滚 - // 这里简单处理,继续流程,用户可能需要管理员手动授权 - // 或者返回错误 return nil, vigo.ErrInternalServer.WithError(err) } diff --git a/cfg/cfg.go b/cfg/cfg.go index 3566d3b..eba5e2a 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -24,9 +24,6 @@ type Options struct { Redis config.Redis `json:"redis" usage:"Redis 配置,addr: memory 使用内存模式"` Key config.Key `json:"key" usage:"系统密钥,用于加密敏感数据(建议 32 位以上)"` - // === 文件存储 === - StoragePath string `json:"storage_path" usage:"文件存储路径"` - // === JWT 配置(安全敏感,保留在本地) === JWT JWTConfig `json:"jwt" usage:"JWT 配置"` @@ -61,8 +58,7 @@ var Global = &Options{ Redis: config.Redis{ Addr: "memory", }, - Key: "your-secret-key-change-in-production-min-32-characters", - StoragePath: "./data", + Key: "your-secret-key-change-in-production-min-32-characters", JWT: JWTConfig{ Secret: "", AccessExpiry: time.Hour * 24, diff --git a/init.go b/init.go index 5ae0b71..bdbefc9 100644 --- a/init.go +++ b/init.go @@ -18,7 +18,7 @@ import ( "github.com/veypi/vigo" ) -const Version = "v1.1.0" +const Version = "v1.1.1" var Router = vigo.NewRouter() var ( diff --git a/models/init.go b/models/init.go index cd3e41f..124d8b9 100644 --- a/models/init.go +++ b/models/init.go @@ -102,6 +102,7 @@ func initAdminUser() error { EmailVerified: email != nil, LastLoginAt: &now, } + user.ID = "admin" if err := db.Create(user).Error; err != nil { return fmt.Errorf("create admin user failed: %w", err) diff --git a/models/setting.go b/models/setting.go index 13ff7b7..65c7b9b 100644 --- a/models/setting.go +++ b/models/setting.go @@ -19,7 +19,7 @@ import ( type Setting struct { vigo.Model Key string `gorm:"uniqueIndex;size:100;not null" json:"key"` - Value string `gorm:"type:text" json:"value"` + Value string `gorm:"type:longtext" json:"value"` Type string `gorm:"size:20;default:'string'" json:"type"` // string/int/bool/json Category string `gorm:"index;size:50" json:"category"` Desc string `gorm:"size:200" json:"desc"` @@ -39,8 +39,8 @@ const ( // 配置键常量 const ( // 应用配置 - SettingAppName = "app.name" - SettingAppID = "app.id" + SettingAppName = "app.name" + SettingAppID = "app.id" // JWT 配置 SettingJWTSecret = "jwt.secret" @@ -51,17 +51,17 @@ const ( // 登录注册 SettingAuthRegRequireEmail = "auth.reg.require_email" SettingAuthRegRequirePhone = "auth.reg.require_phone" - SettingAuthLoginMethods = "auth.login.methods" // json: ["password", "email_code", "phone_code"] + SettingAuthLoginMethods = "auth.login.methods" // json: ["password", "email_code", "phone_code"] SettingAuthPasswordFields = "auth.login.password_fields" // json: ["username", "email", "phone"] // 安全/验证码 SettingSecurityBcryptCost = "security.bcrypt_cost" SettingSecurityMaxLoginAttempts = "security.max_login_attempts" SettingSecurityCaptchaEnabled = "security.captcha_enabled" - SettingCodeExpiry = "code.expiry" // 分钟 + SettingCodeExpiry = "code.expiry" // 分钟 SettingCodeLength = "code.length" SettingCodeMaxAttempt = "code.max_attempt" - SettingCodeSendInterval = "code.send_interval" // 秒 + SettingCodeSendInterval = "code.send_interval" // 秒 SettingCodeMaxDailyCount = "code.max_daily_count" // 邮件配置 @@ -75,13 +75,13 @@ const ( SettingEmailFromName = "email.from_name" // 短信配置 - SettingSMSEnabled = "sms.enabled" - SettingSMSProvider = "sms.provider" // aliyun/tencent - SettingSMSAccessKey = "sms.access_key" + SettingSMSEnabled = "sms.enabled" + SettingSMSProvider = "sms.provider" // aliyun/tencent + SettingSMSAccessKey = "sms.access_key" SettingSMSAccessSecret = "sms.access_secret" - SettingSMSSignName = "sms.sign_name" + SettingSMSSignName = "sms.sign_name" SettingSMSTemplateCode = "sms.template_code" - SettingSMSEndpoint = "sms.endpoint" + SettingSMSEndpoint = "sms.endpoint" ) // 默认配置值 diff --git a/ui/vbase.js b/ui/vbase.js index eff22ec..8fcd1b4 100644 --- a/ui/vbase.js +++ b/ui/vbase.js @@ -372,6 +372,34 @@ class VBase { } ); } + wrapFetch(urlprefix) { + const originalFetch = window.fetch; + const self = this; + return async function wrappedFetch(input, init = {}) { + let url; + if (typeof input === 'string') { + url = input; + } else if (input instanceof Request) { + url = input.url; + } else { + url = String(input); + } + + if (urlprefix && !url.startsWith('http://') && !url.startsWith('https://')) { + url = urlprefix + url; + } + + const headers = { ...init.headers }; + const authHeaders = self.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + if (!headers[key]) { + headers[key] = value; + } + } + + return originalFetch(url, { ...init, headers }); + }; + } _cachePublicUser(user) { if (!user?.id) return;