feat: add SMS verification registration with UniSMS provider
- Add phone field to user model with index and helper methods - Implement SMS provider interface with UniSMS (合一短信) implementation - Add SMS verification code sending endpoint with rate limiting (1/60s) - Support SMS registration in Register() (mutually exclusive with email) - Add SMS configuration to admin settings (provider, keys, signature, template) - Display phone number in admin user list contact column - Add i18n translations for all SMS-related messages (zh-CN, en, zh-TW) - Add Claude Code skills: sync-upstream, migrate-server - Update CLAUDE.md with git conventions and deployment guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/i18n"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/oauth"
|
||||
@@ -51,6 +52,7 @@ func GetStatus(c *gin.Context) {
|
||||
"version": common.Version,
|
||||
"start_time": common.StartTime,
|
||||
"email_verification": common.EmailVerificationEnabled,
|
||||
"sms_verification": common.SMSVerificationEnabled,
|
||||
"github_oauth": common.GitHubOAuthEnabled,
|
||||
"github_client_id": common.GitHubClientId,
|
||||
"discord_oauth": system_setting.GetDiscordSettings().Enabled,
|
||||
@@ -299,6 +301,38 @@ func SendEmailVerification(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func SendSmsVerification(c *gin.Context) {
|
||||
phone := c.Query("phone")
|
||||
if phone == "" {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserPhoneEmpty)
|
||||
return
|
||||
}
|
||||
if !common.IsValidPhone(phone) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserPhoneFormatInvalid)
|
||||
return
|
||||
}
|
||||
if !common.SMSVerificationEnabled {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserSmsNotConfigured)
|
||||
return
|
||||
}
|
||||
if model.IsPhoneAlreadyTaken(phone) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserPhoneAlreadyTaken)
|
||||
return
|
||||
}
|
||||
code := common.GenerateVerificationCode(6)
|
||||
common.RegisterVerificationCodeWithKey(phone, code, common.SmsVerificationPurpose)
|
||||
err := common.SendSMS(phone, code)
|
||||
if err != nil {
|
||||
common.SysLog("failed to send SMS: " + err.Error())
|
||||
common.ApiErrorI18n(c, i18n.MsgUserSendSmsFailed)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
|
||||
func SendPasswordResetEmail(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||
|
||||
@@ -239,7 +239,7 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
|
||||
user.Username = provider.GetProviderPrefix() + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
|
||||
if oauthUser.Username != "" {
|
||||
if exists, err := model.CheckUserExistOrDeleted(oauthUser.Username, ""); err == nil && !exists {
|
||||
if exists, err := model.CheckUserExistOrDeleted(oauthUser.Username, "", ""); err == nil && !exists {
|
||||
// 防止索引退化
|
||||
if len(oauthUser.Username) <= model.UserNameMaxLength {
|
||||
user.Username = oauthUser.Username
|
||||
|
||||
@@ -157,7 +157,21 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
exist, err := model.CheckUserExistOrDeleted(user.Username, user.Email)
|
||||
if common.SMSVerificationEnabled {
|
||||
if user.Phone == "" || user.SmsVerificationCode == "" {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserSmsVerificationRequired)
|
||||
return
|
||||
}
|
||||
if !common.IsValidPhone(user.Phone) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserPhoneFormatInvalid)
|
||||
return
|
||||
}
|
||||
if !common.VerifyCodeWithKey(user.Phone, user.SmsVerificationCode, common.SmsVerificationPurpose) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
|
||||
return
|
||||
}
|
||||
}
|
||||
exist, err := model.CheckUserExistOrDeleted(user.Username, user.Email, user.Phone)
|
||||
if err != nil {
|
||||
common.ApiErrorI18n(c, i18n.MsgDatabaseError)
|
||||
common.SysLog(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
|
||||
@@ -179,6 +193,9 @@ func Register(c *gin.Context) {
|
||||
if common.EmailVerificationEnabled {
|
||||
cleanUser.Email = user.Email
|
||||
}
|
||||
if common.SMSVerificationEnabled {
|
||||
cleanUser.Phone = user.Phone
|
||||
}
|
||||
if err := cleanUser.Insert(inviterId); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
@@ -390,6 +407,7 @@ func GetSelf(c *gin.Context) {
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
"email": user.Email,
|
||||
"phone": user.Phone,
|
||||
"github_id": user.GitHubId,
|
||||
"discord_id": user.DiscordId,
|
||||
"oidc_id": user.OidcId,
|
||||
|
||||
Reference in New Issue
Block a user