Compare commits

..

No commits in common. 'v1.1.1' and 'master' have entirely different histories.

@ -18,7 +18,6 @@ import (
"github.com/veypi/vbase/libs/jwt" "github.com/veypi/vbase/libs/jwt"
"github.com/veypi/vbase/models" "github.com/veypi/vbase/models"
"github.com/veypi/vigo" "github.com/veypi/vigo"
"gorm.io/gorm"
) )
// Email 正则表达式 // Email 正则表达式
@ -72,99 +71,89 @@ func register(x *vigo.X, req *RegisterRequest) (*AuthResponse, error) {
return nil, vigo.ErrInvalidArg.WithString("phone is required") return nil, vigo.ErrInvalidArg.WithString("phone is required")
} }
// 使用事务处理注册,防止并发创建多个首个管理员用户 // 检查是否是第一个用户(需要在创建用户之前检查)
var user *models.User var userCount int64
err := cfg.DB().Transaction(func(tx *gorm.DB) error { if err := cfg.DB().Model(&models.User{}).Count(&userCount).Error; err != nil {
// 检查是否是第一个用户(在事务内检查,带锁) return nil, vigo.ErrInternalServer.WithError(err)
var userCount int64 }
if err := tx.Model(&models.User{}).Count(&userCount).Error; err != nil {
return 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 vigo.ErrInvalidArg.WithArgs("username already exists")
}
// 检查邮箱是否已存在 // 检查用户名是否已存在
if req.Email != "" { var count int64
count = 0 if err := cfg.DB().Model(&models.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil {
if err := tx.Model(&models.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil { return nil, vigo.ErrInternalServer.WithError(err)
return err }
} if count > 0 {
if count > 0 { return nil, vigo.ErrInvalidArg.WithArgs("username already exists")
return vigo.ErrInvalidArg.WithArgs("email already exists") }
}
}
// 检查手机是否已存在 // 检查邮箱是否已存在
if req.Phone != "" { if req.Email != "" {
count = 0 count = 0 // 重置计数器
if err := tx.Model(&models.User{}).Where("phone = ?", req.Phone).Count(&count).Error; err != nil { if err := cfg.DB().Model(&models.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil {
return err return nil, vigo.ErrInternalServer.WithError(err)
}
if count > 0 {
return vigo.ErrInvalidArg.WithArgs("phone already exists")
}
} }
if count > 0 {
// 哈希密码 return nil, vigo.ErrInvalidArg.WithArgs("email already exists")
hashedPassword, err := crypto.HashPassword(req.Password, 12)
if err != nil {
return err
} }
}
// 创建用户 // 检查手机是否已存在
var email *string if req.Phone != "" {
if req.Email != "" { count = 0 // 重置计数器
email = &req.Email if err := cfg.DB().Model(&models.User{}).Where("phone = ?", req.Phone).Count(&count).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
} }
var phone *string if count > 0 {
if req.Phone != "" { return nil, vigo.ErrInvalidArg.WithArgs("phone already exists")
phone = &req.Phone
} }
}
user = &models.User{ // 哈希密码
Username: req.Username, hashedPassword, err := crypto.HashPassword(req.Password, 12)
Password: hashedPassword, if err != nil {
Email: email, return nil, vigo.ErrInternalServer.WithError(err)
Phone: phone, }
Nickname: req.Nickname,
Status: models.UserStatusActive,
}
if user.Nickname == "" { // 创建用户
user.Nickname = user.Username var email *string
} if req.Email != "" {
email = &req.Email
}
var phone *string
if req.Phone != "" {
phone = &req.Phone
}
// 生成随机头像 user := &models.User{
user.Avatar = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220)) Username: req.Username,
Password: hashedPassword,
Email: email,
Phone: phone,
Nickname: req.Nickname,
Status: models.UserStatusActive,
}
// 第一个用户固定 ID 为 admin if user.Nickname == "" {
if userCount == 0 { user.Nickname = user.Username
user.ID = "admin" }
}
if err := tx.Create(user).Error; err != nil { // 生成随机头像
return err user.Avatar = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220))
}
return nil if err := cfg.DB().Create(user).Error; err != nil {
}) return nil, vigo.ErrInternalServer.WithError(err)
if err != nil {
return nil, err
} }
// 授予角色(事务外,因为事务已确保用户创建成功) // 第一个用户授予 admin 角色,其他用户授予 user 角色
roleCode := "user" roleCode := "user"
if user.ID == "admin" { if userCount == 0 {
roleCode = "admin" roleCode = "admin"
} }
if err := cfg.Auth.GrantRole(x.Context(), user.ID, roleCode); err != nil { if err := cfg.Auth.GrantRole(x.Context(), user.ID, roleCode); err != nil {
// 记录错误但允许注册继续,或者回滚
// 这里简单处理,继续流程,用户可能需要管理员手动授权
// 或者返回错误
return nil, vigo.ErrInternalServer.WithError(err) return nil, vigo.ErrInternalServer.WithError(err)
} }

@ -24,6 +24,9 @@ type Options struct {
Redis config.Redis `json:"redis" usage:"Redis 配置addr: memory 使用内存模式"` Redis config.Redis `json:"redis" usage:"Redis 配置addr: memory 使用内存模式"`
Key config.Key `json:"key" usage:"系统密钥,用于加密敏感数据(建议 32 位以上)"` Key config.Key `json:"key" usage:"系统密钥,用于加密敏感数据(建议 32 位以上)"`
// === 文件存储 ===
StoragePath string `json:"storage_path" usage:"文件存储路径"`
// === JWT 配置(安全敏感,保留在本地) === // === JWT 配置(安全敏感,保留在本地) ===
JWT JWTConfig `json:"jwt" usage:"JWT 配置"` JWT JWTConfig `json:"jwt" usage:"JWT 配置"`
@ -58,7 +61,8 @@ var Global = &Options{
Redis: config.Redis{ Redis: config.Redis{
Addr: "memory", Addr: "memory",
}, },
Key: "your-secret-key-change-in-production-min-32-characters", Key: "your-secret-key-change-in-production-min-32-characters",
StoragePath: "./data",
JWT: JWTConfig{ JWT: JWTConfig{
Secret: "", Secret: "",
AccessExpiry: time.Hour * 24, AccessExpiry: time.Hour * 24,

@ -18,7 +18,7 @@ import (
"github.com/veypi/vigo" "github.com/veypi/vigo"
) )
const Version = "v1.1.1" const Version = "v0.6.4"
var Router = vigo.NewRouter() var Router = vigo.NewRouter()
var ( var (

@ -102,7 +102,6 @@ func initAdminUser() error {
EmailVerified: email != nil, EmailVerified: email != nil,
LastLoginAt: &now, LastLoginAt: &now,
} }
user.ID = "admin"
if err := db.Create(user).Error; err != nil { if err := db.Create(user).Error; err != nil {
return fmt.Errorf("create admin user failed: %w", err) return fmt.Errorf("create admin user failed: %w", err)

@ -19,7 +19,7 @@ import (
type Setting struct { type Setting struct {
vigo.Model vigo.Model
Key string `gorm:"uniqueIndex;size:100;not null" json:"key"` Key string `gorm:"uniqueIndex;size:100;not null" json:"key"`
Value string `gorm:"type:longtext" json:"value"` Value string `gorm:"type:text" json:"value"`
Type string `gorm:"size:20;default:'string'" json:"type"` // string/int/bool/json Type string `gorm:"size:20;default:'string'" json:"type"` // string/int/bool/json
Category string `gorm:"index;size:50" json:"category"` Category string `gorm:"index;size:50" json:"category"`
Desc string `gorm:"size:200" json:"desc"` Desc string `gorm:"size:200" json:"desc"`
@ -39,8 +39,8 @@ const (
// 配置键常量 // 配置键常量
const ( const (
// 应用配置 // 应用配置
SettingAppName = "app.name" SettingAppName = "app.name"
SettingAppID = "app.id" SettingAppID = "app.id"
// JWT 配置 // JWT 配置
SettingJWTSecret = "jwt.secret" SettingJWTSecret = "jwt.secret"
@ -51,17 +51,17 @@ const (
// 登录注册 // 登录注册
SettingAuthRegRequireEmail = "auth.reg.require_email" SettingAuthRegRequireEmail = "auth.reg.require_email"
SettingAuthRegRequirePhone = "auth.reg.require_phone" 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"] SettingAuthPasswordFields = "auth.login.password_fields" // json: ["username", "email", "phone"]
// 安全/验证码 // 安全/验证码
SettingSecurityBcryptCost = "security.bcrypt_cost" SettingSecurityBcryptCost = "security.bcrypt_cost"
SettingSecurityMaxLoginAttempts = "security.max_login_attempts" SettingSecurityMaxLoginAttempts = "security.max_login_attempts"
SettingSecurityCaptchaEnabled = "security.captcha_enabled" SettingSecurityCaptchaEnabled = "security.captcha_enabled"
SettingCodeExpiry = "code.expiry" // 分钟 SettingCodeExpiry = "code.expiry" // 分钟
SettingCodeLength = "code.length" SettingCodeLength = "code.length"
SettingCodeMaxAttempt = "code.max_attempt" SettingCodeMaxAttempt = "code.max_attempt"
SettingCodeSendInterval = "code.send_interval" // 秒 SettingCodeSendInterval = "code.send_interval" // 秒
SettingCodeMaxDailyCount = "code.max_daily_count" SettingCodeMaxDailyCount = "code.max_daily_count"
// 邮件配置 // 邮件配置
@ -75,13 +75,13 @@ const (
SettingEmailFromName = "email.from_name" SettingEmailFromName = "email.from_name"
// 短信配置 // 短信配置
SettingSMSEnabled = "sms.enabled" SettingSMSEnabled = "sms.enabled"
SettingSMSProvider = "sms.provider" // aliyun/tencent SettingSMSProvider = "sms.provider" // aliyun/tencent
SettingSMSAccessKey = "sms.access_key" SettingSMSAccessKey = "sms.access_key"
SettingSMSAccessSecret = "sms.access_secret" SettingSMSAccessSecret = "sms.access_secret"
SettingSMSSignName = "sms.sign_name" SettingSMSSignName = "sms.sign_name"
SettingSMSTemplateCode = "sms.template_code" SettingSMSTemplateCode = "sms.template_code"
SettingSMSEndpoint = "sms.endpoint" SettingSMSEndpoint = "sms.endpoint"
) )
// 默认配置值 // 默认配置值

@ -372,34 +372,6 @@ 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) { _cachePublicUser(user) {
if (!user?.id) return; if (!user?.id) return;

Loading…
Cancel
Save