Compare commits

..

2 Commits

Author SHA1 Message Date
veypi bd5604a425 chore: bump version to v1.1.1 7 days ago
veypi adc335b536 chore: bump version to v1.1.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 week ago

@ -18,6 +18,7 @@ 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 正则表达式
@ -71,89 +72,99 @@ 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 userCount int64 var user *models.User
if err := cfg.DB().Model(&models.User{}).Count(&userCount).Error; err != nil { err := cfg.DB().Transaction(func(tx *gorm.DB) error {
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 := 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")
}
// 检查邮箱是否已存在 // 检查用户名是否已存在
if req.Email != "" { var count int64
count = 0 // 重置计数器 if err := tx.Model(&models.User{}).Where("username = ?", req.Username).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 { if count > 0 {
return nil, vigo.ErrInvalidArg.WithArgs("email already exists") return vigo.ErrInvalidArg.WithArgs("username already exists")
} }
}
// 检查手机是否已存在 // 检查邮箱是否已存在
if req.Phone != "" { if req.Email != "" {
count = 0 // 重置计数器 count = 0
if err := cfg.DB().Model(&models.User{}).Where("phone = ?", req.Phone).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 {
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) hashedPassword, err := crypto.HashPassword(req.Password, 12)
if err != nil { if err != nil {
return nil, vigo.ErrInternalServer.WithError(err) return err
} }
// 创建用户 // 创建用户
var email *string var email *string
if req.Email != "" { if req.Email != "" {
email = &req.Email email = &req.Email
} }
var phone *string var phone *string
if req.Phone != "" { if req.Phone != "" {
phone = &req.Phone phone = &req.Phone
} }
user := &models.User{ user = &models.User{
Username: req.Username, Username: req.Username,
Password: hashedPassword, Password: hashedPassword,
Email: email, Email: email,
Phone: phone, Phone: phone,
Nickname: req.Nickname, Nickname: req.Nickname,
Status: models.UserStatusActive, Status: models.UserStatusActive,
} }
if user.Nickname == "" { if user.Nickname == "" {
user.Nickname = user.Username 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 { // 第一个用户固定 ID 为 admin
return nil, vigo.ErrInternalServer.WithError(err) 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" roleCode := "user"
if userCount == 0 { if user.ID == "admin" {
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,9 +24,6 @@ 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 配置"`
@ -61,8 +58,7 @@ 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 = "v0.6.4" const Version = "v1.1.1"
var Router = vigo.NewRouter() var Router = vigo.NewRouter()
var ( var (

@ -102,6 +102,7 @@ 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:text" json:"value"` Value string `gorm:"type:longtext" 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,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) { _cachePublicUser(user) {
if (!user?.id) return; if (!user?.id) return;

Loading…
Cancel
Save