// // Copyright (C) 2024 veypi // 2025-03-04 16:08:06 // Distributed under terms of the MIT license. // package auth import ( "fmt" "math/rand" "regexp" "strings" "time" "github.com/veypi/vbase/cfg" "github.com/veypi/vbase/libs/crypto" "github.com/veypi/vbase/libs/jwt" "github.com/veypi/vbase/models" "github.com/veypi/vigo" "github.com/veypi/vigo/logv" "gorm.io/gorm" ) // Email 正则表达式 var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) // RegisterRequest 注册请求 type RegisterRequest struct { Username string `json:"username" src:"json" desc:"用户名"` Password string `json:"password" src:"json" desc:"密码"` Email string `json:"email,omitempty" src:"json" desc:"邮箱"` Phone string `json:"phone,omitempty" src:"json" desc:"手机号"` Nickname string `json:"nickname,omitempty" src:"json" desc:"昵称"` } // register 用户注册 func register(x *vigo.X, req *RegisterRequest) (*AuthResponse, error) { // 验证用户名 if strings.TrimSpace(req.Username) == "" { return nil, vigo.ErrInvalidArg.WithString("username is required") } if len(req.Username) < 3 || len(req.Username) > 50 { return nil, vigo.ErrInvalidArg.WithString("username must be between 3 and 50 characters") } // 用户名只能包含字母、数字和下划线 if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(req.Username) { return nil, vigo.ErrInvalidArg.WithString("username can only contain letters, numbers and underscores") } // 验证密码 if strings.TrimSpace(req.Password) == "" { return nil, vigo.ErrInvalidArg.WithString("password is required") } if len(req.Password) < 8 { return nil, vigo.ErrInvalidArg.WithString("password must be at least 8 characters") } // 验证邮箱格式 if req.Email != "" && !emailRegex.MatchString(req.Email) { return nil, vigo.ErrInvalidArg.WithString("invalid email format") } // 检查注册配置 requireEmail, _ := models.GetSettingBool(models.SettingAuthRegRequireEmail) requirePhone, _ := models.GetSettingBool(models.SettingAuthRegRequirePhone) // 校验必填字段 if requireEmail && req.Email == "" { return nil, vigo.ErrInvalidArg.WithString("email is required") } if requirePhone && req.Phone == "" { return nil, vigo.ErrInvalidArg.WithString("phone is required") } // 使用事务处理注册,防止并发创建多个首个管理员用户 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 } // 检查用户名是否已存在 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 != "" { 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 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 err } // 创建用户 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, } 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)) // 第一个用户固定 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 } if err := cfg.OnUserCreate(user.ID); err != nil { logv.Warn().Msgf("user create hook failed: %s", err) } // 授予角色(事务外,因为事务已确保用户创建成功) roleCode := "user" if user.ID == "admin" { roleCode = "admin" } if err := cfg.Auth.GrantRole(x.Context(), user.ID, roleCode); err != nil { return nil, vigo.ErrInternalServer.WithError(err) } // 生成token emailStr := "" if user.Email != nil { emailStr = *user.Email } tokenPair, err := jwt.GenerateTokenPair( user.ID, user.Username, user.Nickname, user.Avatar, emailStr, ) if err != nil { return nil, vigo.ErrInternalServer.WithError(err) } return &AuthResponse{ AccessToken: tokenPair.AccessToken, RefreshToken: tokenPair.RefreshToken, TokenType: tokenPair.TokenType, ExpiresIn: tokenPair.ExpiresIn, User: &UserInfo{ ID: user.ID, Username: user.Username, Nickname: user.Nickname, Email: user.Email, Avatar: user.Avatar, }, }, nil }