feat: support multiple country codes for sending SMS (#1557)

* feat: support multiple country code

* feat: improve UI

* feat: migrate accountItem

* fix: Aliyun compatible

* fix: phone validate

* fix: typo
This commit is contained in:
Yaodong Yu 2023-02-16 22:53:28 +08:00 committed by GitHub
parent 4605938f8e
commit eb72c9f273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 560 additions and 258 deletions

View File

@ -58,7 +58,7 @@ type RequestForm struct {
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
PhonePrefix string `json:"phonePrefix"`
CountryCode string `json:"countryCode"`
AutoSignin bool `json:"autoSignin"`
@ -121,7 +121,7 @@ func (c *ApiController) Signup() {
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization))
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.FirstName, form.LastName, form.Email, form.Phone, form.Affiliation, c.GetAcceptLanguage())
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.FirstName, form.LastName, form.Email, form.Phone, form.CountryCode, form.Affiliation, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
@ -137,7 +137,7 @@ func (c *ApiController) Signup() {
var checkPhone string
if application.IsSignupItemVisible("Phone") && form.Phone != "" {
checkPhone = fmt.Sprintf("+%s%s", form.PhonePrefix, form.Phone)
checkPhone, _ = util.GetE164Number(form.Phone, form.CountryCode)
checkResult := object.CheckVerificationCode(checkPhone, form.PhoneCode, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(c.T("account:Phone: %s"), checkResult)
@ -179,6 +179,7 @@ func (c *ApiController) Signup() {
Avatar: organization.DefaultAvatar,
Email: form.Email,
Phone: form.Phone,
CountryCode: form.CountryCode,
Address: []string{},
Affiliation: form.Affiliation,
IdCard: form.IdCard,

View File

@ -263,34 +263,32 @@ func (c *ApiController) Login() {
checkDest = form.Username
} else {
verificationCodeType = "phone"
if len(form.PhonePrefix) == 0 {
responseText := fmt.Sprintf(c.T("auth:%s No phone prefix"), verificationCodeType)
c.ResponseError(responseText)
return
}
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
checkDest = fmt.Sprintf("+%s%s", form.PhonePrefix, form.Username)
}
user = object.GetUserByFields(form.Organization, form.Username)
if user == nil {
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
return
}
if verificationCodeType == "phone" {
form.CountryCode = user.GetCountryCode(form.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
return
}
}
checkResult = object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
responseText := fmt.Sprintf("%s - %s", verificationCodeType, checkResult)
c.ResponseError(responseText)
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return
}
// disable the verification code
if strings.Contains(form.Username, "@") {
object.DisableVerificationCode(form.Username)
} else {
object.DisableVerificationCode(fmt.Sprintf("+%s%s", form.PhonePrefix, form.Username))
}
object.DisableVerificationCode(checkDest)
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
if application == nil {

View File

@ -130,13 +130,13 @@ func (c *ApiController) SendSms() {
return
}
org := object.GetOrganization(smsForm.OrgId)
var invalidReceivers []string
for idx, receiver := range smsForm.Receivers {
if !util.IsPhoneCnValid(receiver) {
// The receiver phone format: E164 like +8613854673829 +441932567890
if !util.IsPhoneValid(receiver, "") {
invalidReceivers = append(invalidReceivers, receiver)
} else {
smsForm.Receivers[idx] = fmt.Sprintf("+%s%s", org.PhonePrefix, receiver)
smsForm.Receivers[idx] = receiver
}
}

View File

@ -62,6 +62,10 @@ func checkPermissionForUpdateUser(userId string, newUser object.User, c *ApiCont
item := object.GetAccountItemByName("Phone", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.CountryCode != newUser.CountryCode {
item := object.GetAccountItemByName("Country code", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.Region != newUser.Region {
item := object.GetAccountItemByName("Country/Region", organization)
itemsChanged = append(itemsChanged, item)

View File

@ -24,6 +24,13 @@ import (
"github.com/casdoor/casdoor/util"
)
const (
SignupVerification = "signup"
ResetVerification = "reset"
LoginVerification = "login"
ForgetVerification = "forget"
)
func (c *ApiController) getCurrentUser() *object.User {
var user *object.User
userId := c.GetSessionUsername()
@ -42,18 +49,15 @@ func (c *ApiController) getCurrentUser() *object.User {
func (c *ApiController) SendVerificationCode() {
destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("countryCode")
checkType := c.Ctx.Request.Form.Get("checkType")
checkId := c.Ctx.Request.Form.Get("checkId")
checkKey := c.Ctx.Request.Form.Get("checkKey")
checkUser := c.Ctx.Request.Form.Get("checkUser")
applicationId := c.Ctx.Request.Form.Get("applicationId")
method := c.Ctx.Request.Form.Get("method")
checkUser := c.Ctx.Request.Form.Get("checkUser")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if destType == "" {
c.ResponseError(c.T("general:Missing parameter") + ": type.")
return
}
if dest == "" {
c.ResponseError(c.T("general:Missing parameter") + ": dest.")
return
@ -62,98 +66,101 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(c.T("general:Missing parameter") + ": applicationId.")
return
}
if !strings.Contains(applicationId, "/") {
c.ResponseError(c.T("verification:Wrong parameter") + ": applicationId.")
return
}
if checkType == "" {
c.ResponseError(c.T("general:Missing parameter") + ": checkType.")
return
}
captchaProvider := captcha.GetCaptchaProvider(checkType)
if captchaProvider != nil {
if checkKey == "" {
c.ResponseError(c.T("general:Missing parameter") + ": checkKey.")
return
}
isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId)
if err != nil {
c.ResponseError(err.Error())
return
}
if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
if checkKey == "" {
c.ResponseError(c.T("general:Missing parameter") + ": checkKey.")
return
}
if !strings.Contains(applicationId, "/") {
c.ResponseError(c.T("verification:Wrong parameter") + ": applicationId.")
return
}
if captchaProvider := captcha.GetCaptchaProvider(checkType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + checkType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
user := c.getCurrentUser()
application := object.GetApplication(applicationId)
organization := object.GetOrganization(fmt.Sprintf("%s/%s", application.Owner, application.Organization))
organization := object.GetOrganization(util.GetId(application.Owner, application.Organization))
if organization == nil {
c.ResponseError(c.T("verification:Organization does not exist"))
return
}
if checkUser == "true" && user == nil && object.GetUserByFields(organization.Name, dest) == nil {
c.ResponseError(c.T("general:Please login first"))
return
var user *object.User
// checkUser != "", means method is ForgetVerification
if checkUser != "" {
owner := application.Organization
user = object.GetUser(util.GetId(owner, checkUser))
}
sendResp := errors.New("invalid dest type")
if user == nil && checkUser != "" && checkUser != "true" {
name := application.Organization
user = object.GetUser(fmt.Sprintf("%s/%s", name, checkUser))
}
switch destType {
case "email":
if user != nil && util.GetMaskedEmail(user.Email) == dest {
dest = user.Email
}
if !util.IsEmailValid(dest) {
c.ResponseError(c.T("verification:Email is invalid"))
return
}
userByEmail := object.GetUserByEmail(organization.Name, dest)
if userByEmail == nil && method != "signup" && method != "reset" {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedEmail(user.Email) == dest {
dest = user.Email
}
user = object.GetUserByEmail(organization.Name, dest)
if user == nil {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
}
} else if method == ResetVerification {
user = c.getCurrentUser()
}
provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone":
if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone
}
if !util.IsPhoneCnValid(dest) {
c.ResponseError(c.T("verification:Phone number is invalid"))
return
if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone
}
if user = object.GetUserByPhone(organization.Name, dest); user == nil {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
}
countryCode = user.GetCountryCode(countryCode)
} else if method == ResetVerification {
if user = c.getCurrentUser(); user != nil {
countryCode = user.GetCountryCode(countryCode)
}
}
userByPhone := object.GetUserByPhone(organization.Name, dest)
if userByPhone == nil && method != "signup" && method != "reset" {
c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return
}
dest = fmt.Sprintf("+%s%s", organization.PhonePrefix, dest)
provider := application.GetSmsProvider()
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest)
if phone, ok := util.GetE164Number(dest, countryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), countryCode))
return
} else {
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone)
}
}
if sendResp != nil {
c.Data["json"] = Response{Status: "error", Msg: sendResp.Error()}
c.ResponseError(sendResp.Error())
} else {
c.Data["json"] = Response{Status: "ok"}
c.ResponseOk()
}
c.ServeJSON()
}
// ResetEmailOrPhone ...
@ -169,7 +176,8 @@ func (c *ApiController) ResetEmailOrPhone() {
destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest")
code := c.Ctx.Request.Form.Get("code")
if len(dest) == 0 || len(code) == 0 || len(destType) == 0 {
if util.IsStrsEmpty(destType, dest, code) {
c.ResponseError(c.T("general:Missing parameter"))
return
}
@ -192,12 +200,10 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseError(errMsg)
return
}
phonePrefix := "86"
if organization != nil && organization.PhonePrefix != "" {
phonePrefix = organization.PhonePrefix
if checkDest, ok = util.GetE164Number(dest, user.GetCountryCode("")); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode))
return
}
checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest)
} else if destType == "email" {
if object.HasUserByField(user.Owner, "email", user.Email) {
c.ResponseError(c.T("check:Email already exists"))
@ -215,8 +221,8 @@ func (c *ApiController) ResetEmailOrPhone() {
return
}
}
if ret := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage()); len(ret) != 0 {
c.ResponseError(ret)
if msg := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage()); len(msg) != 0 {
c.ResponseError(msg)
return
}
@ -233,8 +239,7 @@ func (c *ApiController) ResetEmailOrPhone() {
}
object.DisableVerificationCode(checkDest)
c.Data["json"] = Response{Status: "ok"}
c.ServeJSON()
c.ResponseOk()
}
// VerifyCaptcha ...

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.75.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0

2
go.sum
View File

@ -354,6 +354,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/nyaruka/phonenumbers v1.1.5 h1:vYy2DI+z5hdaemqVzXYJ4CVyK92IG484CirEY+40GTo=
github.com/nyaruka/phonenumbers v1.1.5/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"%s No phone prefix": "%s No phone prefix",
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist"
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@ -130,7 +131,7 @@
"Email is invalid": "Email is invalid",
"Invalid captcha provider.": "Invalid captcha provider.",
"Organization does not exist": "Organization does not exist",
"Phone number is invalid": "Phone number is invalid",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",

View File

@ -9,7 +9,6 @@
"The application does not allow to sign up new account": "该应用不允许注册新用户"
},
"auth": {
"%s No phone prefix": "%s 无此手机号前缀",
"Challenge method should be S256": "Challenge 方法应该为 S256",
"Failed to create user, user information is invalid: %s": "创建用户失败,用户信息无效: %s",
"Failed to login in: %s": "登录失败: %s",
@ -57,13 +56,15 @@
"Username is too long (maximum is 39 characters).": "用户名过长最大允许长度为39个字符",
"Username must have at least 2 characters": "用户名至少要有2个字符",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"password or code is incorrect, you have %d remaining chances": "密码错误,您还有 %d 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s"
},
"general": {
"Missing parameter": "缺少参数",
"Please login first": "请先登录",
"The user: %s doesn't exist": "用户: %s 不存在"
"The user: %s doesn't exist": "用户: %s 不存在",
"don't support captchaProvider: ": "不支持验证码提供商: "
},
"ldap": {
"Ldap server exist": "LDAP服务器已存在"
@ -130,7 +131,7 @@
"Email is invalid": "非法的邮箱",
"Invalid captcha provider.": "非法的验证码提供商",
"Organization does not exist": "组织不存在",
"Phone number is invalid": "非法的手机号码",
"Phone number is invalid in your region %s": "您所在地区的电话号码无效 %s",
"Turing test failed.": "验证码还未发送",
"Unable to get the email modify rule.": "无法获取邮箱修改规则",
"Unable to get the phone modify rule.": "无法获取手机号修改规则",

View File

@ -7,7 +7,7 @@
"websiteUrl": "",
"favicon": "",
"passwordType": "",
"phonePrefix": "",
"countryCodes": [""],
"defaultAvatar": "",
"tags": [""]
}
@ -107,6 +107,7 @@
"avatar": "",
"email": "",
"phone": "",
"countryCode": "",
"address": [],
"affiliation": "",
"tag": "",

View File

@ -42,7 +42,7 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, affiliation string, lang string) string {
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, countryCode string, affiliation string, lang string) string {
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
@ -107,7 +107,9 @@ func CheckUserSignup(application *Application, organization *Organization, usern
if HasUserByField(organization.Name, "phone", phone) {
return i18n.Translate(lang, "check:Phone already exists")
} else if organization.PhonePrefix == "86" && !util.IsPhoneCnValid(phone) {
} else if !util.IsPhoneAllowInRegin(countryCode, organization.CountryCodes) {
return i18n.Translate(lang, "check:Your region is not allow to signup by phone")
} else if !util.IsPhoneValid(phone, countryCode) {
return i18n.Translate(lang, "check:Phone number is invalid")
}
}

View File

@ -53,7 +53,7 @@ func initBuiltInOrganization() bool {
WebsiteUrl: "https://example.com",
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain",
PhonePrefix: "86",
CountryCodes: []string{"CN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{},
Languages: []string{"en", "zh", "es", "fr", "de", "ja", "ko", "ru"},
@ -68,6 +68,7 @@ func initBuiltInOrganization() bool {
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Email", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Phone", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "CountryCode", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
@ -109,6 +110,7 @@ func initBuiltInUser() {
Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Email: "admin@example.com",
Phone: "12345678910",
CountryCode: "CN",
Address: []string{},
Affiliation: "Example Inc.",
Tag: "staff",

View File

@ -189,6 +189,7 @@ func initDefinedOrganization(organization *Organization) {
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Email", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Phone", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "CountryCode", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},

View File

@ -26,6 +26,7 @@ func DoMigration() {
&Migrator_1_101_0_PR_1083{},
&Migrator_1_235_0_PR_1530{},
&Migrator_1_240_0_PR_1539{},
&Migrator_1_245_0_PR_1557{},
// more migrators add here in chronological order...
}

View File

@ -0,0 +1,92 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/nyaruka/phonenumbers"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_245_0_PR_1557 struct{}
func (*Migrator_1_245_0_PR_1557) IsMigrationNeeded() bool {
exist, _ := adapter.Engine.IsTableExist("organization")
if exist {
return true
}
return false
}
func (*Migrator_1_245_0_PR_1557) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230215organization--transfer phonePrefix to defaultCountryCode, countryCodes",
Migrate: func(engine *xorm.Engine) error {
organizations := []*Organization{}
err := engine.Table("organization").Find(&organizations, &Organization{})
if err != nil {
panic(err)
}
for _, organization := range organizations {
organization.AccountItems = []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Email", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Phone", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "CountryCode", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}
sql := fmt.Sprintf("select phone_prefix from organization where owner='%s' and name='%s'", organization.Owner, organization.Name)
results, _ := engine.Query(sql)
phonePrefix := util.ParseInt(string(results[0]["phone_prefix"]))
organization.CountryCodes = []string{phonenumbers.GetRegionCodeForCountryCode(phonePrefix)}
UpdateOrganization(util.GetId(organization.Owner, organization.Name), organization)
}
return err
},
}
return &migration
}

View File

@ -49,7 +49,7 @@ type Organization struct {
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`

View File

@ -14,7 +14,11 @@
package object
import "github.com/casdoor/go-sms-sender"
import (
"strings"
"github.com/casdoor/go-sms-sender"
)
func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
client, err := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
@ -25,6 +29,12 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
return err
}
if provider.Type == go_sms_sender.Aliyun {
for i, number := range phoneNumbers {
phoneNumbers[i] = strings.TrimPrefix(number, "+")
}
}
params := map[string]string{}
if provider.Type == go_sms_sender.TencentCloud {
params["0"] = content

View File

@ -46,7 +46,8 @@ type User struct {
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `xorm:"varchar(100) index" json:"phone"`
Phone string `xorm:"varchar(20) index" json:"phone"`
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
Location string `xorm:"varchar(100)" json:"location"`
Address []string `json:"address"`
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
@ -454,7 +455,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
}
}
if isGlobalAdmin {
columns = append(columns, "name", "email", "phone")
columns = append(columns, "name", "email", "phone", "country_code")
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)

View File

@ -170,3 +170,18 @@ func ClearUserOAuthProperties(user *User, providerType string) bool {
return affected != 0
}
func (user *User) GetCountryCode(countryCode string) string {
if countryCode != "" {
return countryCode
}
if user != nil && user.CountryCode != "" {
return user.CountryCode
}
if org := GetOrganizationByUser(user); org != nil && len(org.CountryCodes) > 0 {
return org.CountryCodes[0]
}
return ""
}

View File

@ -14,6 +14,8 @@
package util
import "sort"
func DeleteVal(values []string, val string) []string {
newValues := []string{}
for _, v := range values {
@ -23,3 +25,8 @@ func DeleteVal(values []string, val string) []string {
}
return newValues
}
func ContainsString(values []string, val string) bool {
sort.Strings(values)
return sort.SearchStrings(values, val) != len(values)
}

View File

@ -227,7 +227,7 @@ func IsChinese(str string) bool {
}
func GetMaskedPhone(phone string) string {
return getMaskedPhone(phone)
return rePhone.ReplaceAllString(phone, "$1****$2")
}
func GetMaskedEmail(email string) string {

View File

@ -17,16 +17,13 @@ package util
import (
"net/mail"
"regexp"
"github.com/nyaruka/phonenumbers"
)
var (
rePhoneCn *regexp.Regexp
rePhone *regexp.Regexp
)
var rePhone *regexp.Regexp
func init() {
// https://learnku.com/articles/31543
rePhoneCn, _ = regexp.Compile(`^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$`)
rePhone, _ = regexp.Compile("(\\d{3})\\d*(\\d{4})")
}
@ -35,10 +32,19 @@ func IsEmailValid(email string) bool {
return err == nil
}
func IsPhoneCnValid(phone string) bool {
return rePhoneCn.MatchString(phone)
func IsPhoneValid(phone string, countryCode string) bool {
phoneNumber, err := phonenumbers.Parse(phone, countryCode)
if err != nil {
return false
}
return phonenumbers.IsValidNumber(phoneNumber)
}
func getMaskedPhone(phone string) string {
return rePhone.ReplaceAllString(phone, "$1****$2")
func IsPhoneAllowInRegin(countryCode string, allowRegions []string) bool {
return !ContainsString(allowRegions, countryCode)
}
func GetE164Number(phone string, countryCode string) (string, bool) {
phoneNumber, _ := phonenumbers.Parse(phone, countryCode)
return phonenumbers.Format(phoneNumber, phonenumbers.E164), phonenumbers.IsValidNumber(phoneNumber)
}

View File

@ -15,7 +15,6 @@ describe("Login test", () => {
"password": "123",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("ok");
@ -40,7 +39,6 @@ describe("Login test", () => {
"password": "1234",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("error");

View File

@ -34,7 +34,6 @@ Cypress.Commands.add('login', ()=>{
"password": "123",
"autoSignin": true,
"type": "login",
"phonePrefix": "86",
},
}).then((Response) => {
expect(Response).property("body").property("status").to.equal("ok");

View File

@ -21,6 +21,7 @@
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qs": "^6.10.2",
"react": "^18.2.0",

View File

@ -78,6 +78,7 @@ class AccountTable extends React.Component {
{name: "Password", displayName: i18next.t("general:Password")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Country code", displayName: i18next.t("user:Country code")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "Location", displayName: i18next.t("user:Location")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},

View File

@ -184,12 +184,20 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone prefix"), i18next.t("general:Phone prefix - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Supported country code"), i18next.t("general:Supported country code - Tooltip"))} :
</Col>
<Col span={22} >
<Input addonBefore={"+"} value={this.state.organization.phonePrefix} onChange={e => {
this.updateOrganizationField("phonePrefix", e.target.value);
}} />
<Select virtual={false} mode={"multiple"} style={{width: "100%"}} value={this.state.organization.countryCodes ?? []}
options={Setting.getCountriesData().map(country => {
return Setting.getOption(
<>
{Setting.countryFlag(country)}
{`${country.name} +${country.phone}`}
</>,
country.code);
})} onChange={value => {
this.updateOrganizationField("countryCodes", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@ -257,22 +265,13 @@ class OrganizationEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}}
value={this.state.organization.languages}
options={Setting.Countries.map((item) => {
return Setting.getOption(item.label, item.key);
})}
value={this.state.organization.languages ?? []}
onChange={(value => {
this.updateOrganizationField("languages", value);
})} >
{
[
{value: "en", label: "English"},
{value: "zh", label: "简体中文"},
{value: "es", label: "Español"},
{value: "fr", label: "Français"},
{value: "de", label: "Deutsch"},
{value: "ja", label: "日本語"},
{value: "ko", label: "한국어"},
{value: "ru", label: "Русский"},
].map((item, index) => <Option key={index} value={item.value}>{item.label}</Option>)
}
</Select>
</Col>
</Row>

View File

@ -33,7 +33,7 @@ class OrganizationListPage extends BaseListPage {
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
passwordType: "plain",
PasswordSalt: "",
phonePrefix: "86",
countryCodes: ["CN"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
tags: [],

View File

@ -18,16 +18,6 @@ import {Dropdown} from "antd";
import "./App.less";
import {GlobalOutlined} from "@ant-design/icons";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
function flagIcon(country, alt) {
return (
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
@ -39,15 +29,15 @@ class SelectLanguageBox extends React.Component {
super(props);
this.state = {
classes: props,
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
languages: props.languages ?? Setting.Countries.map(item => item.key),
};
Countries.forEach((country) => {
Setting.Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
});
}
items = Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
getOrganizationLanguages(languages) {
const select = [];

View File

@ -49,9 +49,9 @@ class SelectRegionBox extends React.Component {
}
>
{
Setting.getCountryNames().map((item) => (
Setting.getCountriesData().map((item) => (
<Option key={item.code} value={item.code} label={`${item.name} (${item.code})`} >
<img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} />
{Setting.countryFlag(item)}
{`${item.name} (${item.code})`}
</Option>
))

View File

@ -23,6 +23,7 @@ import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js";
import * as path from "path-browserify";
export const ServerUrl = "";
@ -30,6 +31,16 @@ export const ServerUrl = "";
// export const StaticBaseUrl = "https://cdn.jsdelivr.net/gh/casbin/static";
export const StaticBaseUrl = "https://cdn.casbin.org";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
export function getThemeData(organization, application) {
if (application?.themeData?.isEnabled) {
return application.themeData;
@ -188,20 +199,33 @@ export const OtherProviderInfo = {
},
};
export function getCountriesData() {
export function initCountries() {
const countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + getLanguage() + ".json"));
return countries;
}
export function getCountryNames() {
const data = getCountriesData().getNames(getLanguage(), {select: "official"});
return Object.entries(data).map(items => {
return {code: items[0], name: items[1]};
export function getCountriesData(countryCodes = phoneNumber.getCountries()) {
return countryCodes?.map((countryCode) => {
if (phoneNumber.isSupportedCountry(countryCode)) {
const name = initCountries().getName(countryCode, getLanguage());
return {
code: countryCode,
name: name || "",
phone: phoneNumber.getCountryCallingCode(countryCode),
};
}
});
}
export function countryFlag(country) {
return <img src={`${StaticBaseUrl}/flag-icons/${country.code}.svg`} alt={country.name} height={20} style={{marginRight: 10}} />;
}
export function getPhoneCodeFromCountryCode(countryCode) {
return phoneNumber.isSupportedCountry(countryCode) ? phoneNumber.getCountryCallingCode(countryCode) : "";
}
export function initServerUrl() {
// const hostname = window.location.hostname;
// if (hostname === "localhost") {
@ -299,16 +323,14 @@ export function isValidEmail(email) {
return emailRegex.test(email);
}
export function isValidPhone(phone) {
return phone !== "";
export function isValidPhone(phone, countryCode = "") {
if (countryCode !== "") {
return phoneNumber.isValidPhoneNumber(phone, countryCode);
}
// if (phone === "") {
// return false;
// }
//
// // https://learnku.com/articles/31543, `^s*$` filter empty email individually.
// const phoneRegex = /^\s*$|^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
// return phoneRegex.test(phone);
const phoneRegex = /[0-9]{4,15}$/;
return phoneRegex.test(phone);
}
export function isValidInvoiceTitle(invoiceTitle) {

View File

@ -29,6 +29,7 @@ import SelectRegionBox from "./SelectRegionBox";
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
import ManagedAccountTable from "./ManagedAccountTable";
import PropertyTable from "./propertyTable";
import PhoneNumberInput from "./common/PhoneNumberInput";
const {Option} = Select;
@ -286,11 +287,13 @@ class UserEditPage extends React.Component {
<Col style={{paddingRight: "20px"}} span={11} >
{Setting.isLocalAdminUser(this.props.account) ?
(<Input value={this.state.user.email}
style={{width: "280Px"}}
disabled={disabled}
onChange={e => {
this.updateUserField("email", e.target.value);
}} />) :
(<Select virtual={false} value={this.state.user.email}
style={{width: "280Px"}}
options={[Setting.getItem(this.state.user.email, this.state.user.email)]}
disabled={disabled}
onChange={e => {
@ -298,7 +301,7 @@ class UserEditPage extends React.Component {
}} />)
}
</Col>
<Col span={11} >
<Col span={Setting.isMobile() ? 22 : 11} >
{/* backend auto get the current user, so admin can not edit. Just self can reset*/}
{this.isSelf() ? <ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} /> : null}
</Col>
@ -307,24 +310,37 @@ class UserEditPage extends React.Component {
} else if (accountItem.name === "Phone") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col>
<Col style={{paddingRight: "20px"}} span={11} >
{Setting.isLocalAdminUser(this.props.account) ?
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
disabled={disabled}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} /> :
(<Select virtual={false} value={`+${this.state.application?.organizationObj.phonePrefix} ${this.state.user.phone}`}
options={[Setting.getItem(`+${this.state.application?.organizationObj.phonePrefix} ${this.state.user.phone}`, this.state.user.phone)]}
<Input.Group compact style={{width: "280Px"}}>
<PhoneNumberInput
style={{width: "30%"}}
value={this.state.user.countryCode}
onChange={(value) => {
this.updateUserField("countryCode", value);
}}
countryCodes={this.state.application?.organizationObj.countryCodes}
/>
<Input value={this.state.user.phone}
style={{width: "70%"}}
disabled={disabled}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} />
</Input.Group>
:
(<Select virtual={false} value={this.state.user.phone === "" ? null : `+${Setting.getPhoneCodeFromCountryCode(this.state.user.countryCode)} ${this.state.user.phone}`}
options={this.state.user.phone === "" ? null : [Setting.getItem(`+${Setting.getPhoneCodeFromCountryCode(this.state.user.countryCode)} ${this.state.user.phone}`, this.state.user.phone)]}
disabled={disabled}
style={{width: "280px"}}
onChange={e => {
this.updateUserField("phone", e.target.value);
}} />)}
</Col>
<Col span={11} >
<Col span={Setting.isMobile() ? 24 : 11} >
{this.isSelf() ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
</Col>
</Row>

View File

@ -49,6 +49,7 @@ class UserListPage extends BaseListPage {
avatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
email: `${randomName}@example.com`,
phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
address: [],
affiliation: "Example Inc.",
tag: "staff",
@ -261,7 +262,7 @@ class UserListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("region"),
render: (text, record, index) => {
return Setting.getCountriesData().getName(record.region, Setting.getLanguage(), {select: "official"});
return Setting.initCountries().getName(record.region, Setting.getLanguage(), {select: "official"});
},
},
{

View File

@ -140,7 +140,6 @@ class ForgetPage extends React.Component {
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.getApplicationObj()?.organizationObj.phonePrefix,
type: "login",
}, oAuthParams).then(res => {
if (res.status === "ok") {

View File

@ -189,7 +189,6 @@ class LoginPage extends React.Component {
} else {
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
if (oAuthParams !== null) {
values["samlRequest"] = oAuthParams.samlRequest;
@ -204,6 +203,7 @@ class LoginPage extends React.Component {
values["organization"] = this.getApplicationObj().organization;
}
}
postCodeLoginAction(res) {
const application = this.getApplicationObj();
const ths = this;
@ -364,7 +364,8 @@ class LoginPage extends React.Component {
title={i18next.t("application:Sign Up Error")}
subTitle={i18next.t("application:The application does not allow to sign up new account")}
extra={[
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
<Button type="primary" key="signin"
onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
{
i18next.t("login:Sign In")
}
@ -384,7 +385,9 @@ class LoginPage extends React.Component {
application: application.name,
autoSignin: true,
}}
onFinish={(values) => {this.onFinish(values);}}
onFinish={(values) => {
this.onFinish(values);
}}
style={{width: "300px"}}
size="large"
ref={this.form}
@ -424,7 +427,7 @@ class LoginPage extends React.Component {
{
validator: (_, value) => {
if (this.state.loginMethod === "verificationCode") {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
if (!Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
@ -444,7 +447,7 @@ class LoginPage extends React.Component {
]}
>
<Input
id = "input"
id="input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={(this.state.loginMethod === "verificationCode") ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
@ -774,13 +777,18 @@ class LoginPage extends React.Component {
const items = [
{label: i18next.t("login:Password"), key: "password"},
];
application.enableCodeSignin ? items.push({label: i18next.t("login:Verification Code"), key: "verificationCode"}) : null;
application.enableCodeSignin ? items.push({
label: i18next.t("login:Verification Code"),
key: "verificationCode",
}) : null;
application.enableWebAuthn ? items.push({label: i18next.t("login:WebAuthn"), key: "webAuthn"}) : null;
if (application.enableCodeSignin || application.enableWebAuthn) {
return (
<div>
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {
this.setState({loginMethod: key});
}} centered>
</Tabs>
</div>
);
@ -823,7 +831,7 @@ class LoginPage extends React.Component {
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
</div>
<div className="login-form">
<div >
<div>
<div>
{
Setting.renderHelmet(application)

View File

@ -26,6 +26,7 @@ import SelectRegionBox from "../SelectRegionBox";
import CustomGithubCorner from "../CustomGithubCorner";
import SelectLanguageBox from "../SelectLanguageBox";
import {withRouter} from "react-router-dom";
import PhoneNumberInput from "../common/PhoneNumberInput";
const formItemLayout = {
labelCol: {
@ -68,6 +69,7 @@ class SignupPage extends React.Component {
application: null,
email: "",
phone: "",
countryCode: "",
emailCode: "",
phoneCode: "",
validEmail: false,
@ -157,7 +159,6 @@ class SignupPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
values.phonePrefix = application.organizationObj.phonePrefix;
AuthBackend.signup(values)
.then((res) => {
if (res.status === "ok") {
@ -378,35 +379,66 @@ class SignupPage extends React.Component {
} else if (signupItem.name === "Phone") {
return (
<React.Fragment>
<Form.Item
name="phone"
key="phone"
label={i18next.t("general:Phone")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
<Form.Item label={i18next.t("general:Phone")} required>
<Input.Group compact>
<Form.Item
name="countryCode"
key="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone, this.state.countryCode)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
style={{
width: "100%",
}}
addonBefore={`+${this.getApplicationObj()?.organizationObj.phonePrefix}`}
onChange={e => this.setState({phone: e.target.value})}
/>
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<PhoneNumberInput
showSearsh={true}
style={{width: "35%"}}
value={this.state.countryCode}
onChange={(value) => {this.setState({countryCode: value});}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
key="phone"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
{
validator: (_, value) => {
if (this.state.phone !== "" && !Setting.isValidPhone(this.state.phone, this.state.countryCode)) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
style={{width: "65%"}}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item
name="phoneCode"

View File

@ -0,0 +1,58 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Select} from "antd";
import * as Setting from "../Setting";
import React from "react";
const {Option} = Select;
export default function PhoneNumberInput(props) {
const {onChange, style, showSearch} = props;
const value = props.value ?? "CN";
const countryCodes = props.countryCodes ?? [];
const handleOnChange = (e) => {
onChange?.(e);
};
return (
<Select
virtual={false}
style={style}
value={value}
dropdownMatchSelectWidth={false}
optionLabelProp={"label"}
showSearch={showSearch}
onChange={handleOnChange}
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
>
{
Setting.getCountriesData(countryCodes).map((country) => (
<Option key={country.code} value={country.code} label={`+${country.phone}`} >
<div style={{display: "flex", justifyContent: "space-between"}}>
<div>
{Setting.countryFlag(country)}
{`${country.name}`}
</div>
{`+${country.phone}`}
</div>
</Option>
))
}
</Select>
);
}

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Telefon",
"Phone - Tooltip": "Phone",
"Phone prefix": "Telefonpräfix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Vorschau",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Oder melden Sie sich mit einem anderen Konto an",
"Password": "Passwort",
"Password - Tooltip": "Passwort - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Bitte gib deinen Code ein!",
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Anmelden",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",
"Terms of Use": "Nutzungsbedingungen",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code gesendet",
"Country code": "Country code",
"Country/Region": "Land/Region",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Benutzer bearbeiten",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Phone",
"Phone - Tooltip": "Phone - Tooltip",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
"Preview": "Preview",
"Preview - Tooltip": "Preview - Tooltip",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Or sign in with another account",
"Password": "Password",
"Password - Tooltip": "Password - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Please input your code!",
"Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent",
"Country code": "Country code",
"Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region - Tooltip",
"Edit User": "Edit User",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permisos - Tooltip",
"Phone": "Teléfono",
"Phone - Tooltip": "Teléfono - Tooltip",
"Phone prefix": "Prefijo teléfonico",
"Phone prefix - Tooltip": "Prefijo teléfonico - Tooltip",
"Preview": "Previsualizar",
"Preview - Tooltip": "Previsualizar - Tooltip",
"Products": "Productos",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sincronizador",
"Syncers": "Sincronizadores",
@ -311,10 +311,10 @@
"Or sign in with another account": "O inicia sesión con otra cuenta",
"Password": "Contraseña",
"Password - Tooltip": "Contraseña - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "¡Por favor ingrese su código!",
"Please input your password!": "¡Por favor ingrese su contraseña!",
"Please input your password, at least 6 characters!": "Su contraseña debe contener al menos 6 caracteres.",
"Please input your username, Email or phone!": "¡Ingrese su nombre de usuario, correo electrónico o teléfono!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Iniciar de sesión",
"Sign in with WebAuthn": "Iniciar de sesión con WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Por favor, ingrese su apellido!",
"Please input your phone number!": "Por favor, ingrese su número teléfonico!",
"Please input your real name!": "Por favor, ingrese un nombre real!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Por favor, seleccione su pais/region!",
"Terms of Use": "Términos de Uso",
"The input is not invoice Tax ID!": "El valor ingresado no es un número de identificación fiscal de factura!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Fallo la verificación del Captcha",
"Captcha Verify Success": "Captcha verificado con éxito",
"Code Sent": "Código enviado",
"Country code": "Country code",
"Country/Region": "Pais/Región",
"Country/Region - Tooltip": "Pais/Región - Tooltip",
"Edit User": "Editar usuario",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Téléphone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Préfixe du téléphone",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Aperçu",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Synchronisateurs",
@ -311,10 +311,10 @@
"Or sign in with another account": "Ou connectez-vous avec un autre compte",
"Password": "Mot de passe",
"Password - Tooltip": "Mot de passe - Info-bulle",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Veuillez saisir votre code !",
"Please input your password!": "Veuillez saisir votre mot de passe !",
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Se connecter",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Veuillez entrer votre numéro de téléphone!",
"Please input your real name!": "Veuillez entrer votre nom personnel !",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",
"Terms of Use": "Conditions d'utilisation",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code envoyé",
"Country code": "Country code",
"Country/Region": "Pays/Région",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Editer l'utilisateur",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "電話番号",
"Phone - Tooltip": "Phone",
"Phone prefix": "電話プレフィクス",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "プレビュー",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "または別のアカウントでサインイン",
"Password": "パスワード",
"Password - Tooltip": "パスワード → ツールチップ",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "コードを入力してください!",
"Please input your password!": "パスワードを入力してください!",
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください",
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "サインイン",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "電話番号を入力してください!",
"Please input your real name!": "個人名を入力してください!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "あなたの国/地域を選択してください!",
"Terms of Use": "利用規約",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "コードを送信しました",
"Country code": "Country code",
"Country/Region": "国/地域",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "ユーザーを編集",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Phone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Preview",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -311,10 +311,10 @@
"Or sign in with another account": "Or sign in with another account",
"Password": "Password",
"Password - Tooltip": "Password - Tooltip",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Please input your code!",
"Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Code Sent",
"Country code": "Country code",
"Country/Region": "Country/Region",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Edit User",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "Permissions - Tooltip",
"Phone": "Телефон",
"Phone - Tooltip": "Phone",
"Phone prefix": "Префикс телефона",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Предпросмотр",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Продукты",
@ -252,6 +250,8 @@
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully saved": "Successfully saved",
"Supported country code": "Supported country code",
"Supported country code - Tooltip": "Supported country code - Tooltip",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Синхронизаторы",
@ -311,10 +311,10 @@
"Or sign in with another account": "Или войти с помощью другой учетной записи",
"Password": "Пароль",
"Password - Tooltip": "Пароль - Подсказка",
"Please input your Email or Phone!": "Please input your Email or Phone!",
"Please input your code!": "Пожалуйста, введите ваш код!",
"Please input your password!": "Пожалуйста, введите ваш пароль!",
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Войти",
"Sign in with WebAuthn": "Sign in with WebAuthn",
@ -640,6 +640,7 @@
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Пожалуйста, введите ваш номер телефона!",
"Please input your real name!": "Пожалуйста, введите ваше личное имя!",
"Please select your country code!": "Please select your country code!",
"Please select your country/region!": "Пожалуйста, выберите вашу страну/регион!",
"Terms of Use": "Условия использования",
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "Captcha Verify Failed",
"Captcha Verify Success": "Captcha Verify Success",
"Code Sent": "Код отправлен",
"Country code": "Country code",
"Country/Region": "Страна/регион",
"Country/Region - Tooltip": "Country/Region",
"Edit User": "Изменить пользователя",

View File

@ -219,8 +219,6 @@
"Permissions - Tooltip": "权限",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone prefix": "手机号前缀",
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Preview": "预览",
"Preview - Tooltip": "预览",
"Products": "商品",
@ -252,6 +250,8 @@
"Successfully added": "添加成功",
"Successfully deleted": "删除成功",
"Successfully saved": "保存成功",
"Supported country code": "支持的国家代码",
"Supported country code - Tooltip": "支持发送短信的国家 - Tooltip",
"Swagger": "API文档",
"Sync": "同步",
"Syncers": "同步器",
@ -311,10 +311,10 @@
"Or sign in with another account": "或者,登录其他账号",
"Password": "密码",
"Password - Tooltip": "密码",
"Please input your Email or Phone!": "请输入您的Email或手机号!",
"Please input your code!": "请输入您的验证码!",
"Please input your password!": "请输入您的密码!",
"Please input your password, at least 6 characters!": "请输入您的密码不少于6位",
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号",
"Redirecting, please wait.": "正在跳转, 请稍等.",
"Sign In": "登录",
"Sign in with WebAuthn": "WebAuthn登录",
@ -640,6 +640,7 @@
"Please input your last name!": "请输入您的姓氏!",
"Please input your phone number!": "请输入您的手机号码!",
"Please input your real name!": "请输入您的姓名!",
"Please select your country code!": "请选择国家代码!",
"Please select your country/region!": "请选择您的国家/地区",
"Terms of Use": "《用户协议》",
"The input is not invoice Tax ID!": "您输入的纳税人识别号有误!",
@ -726,6 +727,7 @@
"Captcha Verify Failed": "验证码校验失败",
"Captcha Verify Success": "验证码校验成功",
"Code Sent": "验证码已发送",
"Country code": "国家代码",
"Country/Region": "国家/地区",
"Country/Region - Tooltip": "国家/地区",
"Edit User": "编辑用户",

View File

@ -7829,6 +7829,11 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
libphonenumber-js@^1.10.19:
version "1.10.20"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.20.tgz#03c310adf83381eeceb4bd6830442fd14e64964d"
integrity sha512-kQovlKNdLcVzerbTPmJ+Fx4R+7/pYXmPDIllHjg7IxL4X6MsMG7jaT5opfYrBok0uqkByVif//JUR8e11l/V7w==
lilconfig@2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"