feat: improve MFA by using user's own Email and Phone (#2002)

* refactor: mfa

* fix: clean code

* fix: clean code

* fix: fix crash and improve robot
This commit is contained in:
Yaodong Yu 2023-06-21 18:56:37 +08:00 committed by GitHub
parent 6ebca6dbe7
commit c391af4552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 528 additions and 362 deletions

View File

@ -370,6 +370,7 @@ func (c *ApiController) GetAccount() {
user.Permissions = object.GetMaskedPermissions(user.Permissions) user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles) user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user)) organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
if err != nil { if err != nil {

View File

@ -71,7 +71,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if form.Password != "" && user.IsMfaEnabled() { if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId}) c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferMfa(true)} resp = &Response{Status: object.NextMfa, Data: user.GetPreferredMfaProps(true)}
return return
} }
@ -656,15 +656,20 @@ func (c *ApiController) Login() {
} }
if authForm.Passcode != "" { if authForm.Passcode != "" {
MfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferMfa(false)) mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
err = MfaUtil.Verify(authForm.Passcode) if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err = mfaUtil.Verify(authForm.Passcode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} }
if authForm.RecoveryCode != "" { if authForm.RecoveryCode != "" {
err = object.RecoverTfs(user, authForm.RecoveryCode) err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -177,6 +177,10 @@ func (c *ApiController) SetSessionData(s *SessionData) {
} }
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) { func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
if data == nil {
c.SetSession(object.MfaSessionUserId, nil)
return
}
c.SetSession(object.MfaSessionUserId, data.UserId) c.SetSession(object.MfaSessionUserId, data.UserId)
} }

View File

@ -34,7 +34,7 @@ import (
func (c *ApiController) MfaSetupInitiate() { func (c *ApiController) MfaSetupInitiate() {
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type") mfaType := c.Ctx.Request.Form.Get("mfaType")
userId := util.GetId(owner, name) userId := util.GetId(owner, name)
if len(userId) == 0 { if len(userId) == 0 {
@ -42,10 +42,11 @@ func (c *ApiController) MfaSetupInitiate() {
return return
} }
MfaUtil := object.GetMfaUtil(authType, nil) MfaUtil := object.GetMfaUtil(mfaType, nil)
if MfaUtil == nil { if MfaUtil == nil {
c.ResponseError("Invalid auth type") c.ResponseError("Invalid auth type")
} }
user, err := object.GetUser(userId) user, err := object.GetUser(userId)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
@ -79,16 +80,20 @@ func (c *ApiController) MfaSetupInitiate() {
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /mfa/setup/verify [post] // @router /mfa/setup/verify [post]
func (c *ApiController) MfaSetupVerify() { func (c *ApiController) MfaSetupVerify() {
authType := c.Ctx.Request.Form.Get("type") mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode") passcode := c.Ctx.Request.Form.Get("passcode")
if authType == "" || passcode == "" { if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode") c.ResponseError("missing auth type or passcode")
return return
} }
MfaUtil := object.GetMfaUtil(authType, nil) mfaUtil := object.GetMfaUtil(mfaType, nil)
if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err := MfaUtil.SetupVerify(c.Ctx, passcode) err := mfaUtil.SetupVerify(c.Ctx, passcode)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} else { } else {
@ -108,7 +113,7 @@ func (c *ApiController) MfaSetupVerify() {
func (c *ApiController) MfaSetupEnable() { func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type") mfaType := c.Ctx.Request.Form.Get("mfaType")
user, err := object.GetUser(util.GetId(owner, name)) user, err := object.GetUser(util.GetId(owner, name))
if err != nil { if err != nil {
@ -121,8 +126,13 @@ func (c *ApiController) MfaSetupEnable() {
return return
} }
twoFactor := object.GetMfaUtil(authType, nil) mfaUtil := object.GetMfaUtil(mfaType, nil)
err = twoFactor.Enable(c.Ctx, user) if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err = mfaUtil.Enable(c.Ctx, user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -137,11 +147,9 @@ func (c *ApiController) MfaSetupEnable() {
// @Description: Delete MFA // @Description: Delete MFA
// @param owner form string true "owner of user" // @param owner form string true "owner of user"
// @param name form string true "name of user" // @param name form string true "name of user"
// @param id form string true "id of user's MFA props"
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /delete-mfa/ [post] // @router /delete-mfa/ [post]
func (c *ApiController) DeleteMfa() { func (c *ApiController) DeleteMfa() {
id := c.Ctx.Request.Form.Get("id")
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name) userId := util.GetId(owner, name)
@ -151,28 +159,18 @@ func (c *ApiController) DeleteMfa() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if user == nil { if user == nil {
c.ResponseError("User doesn't exist") c.ResponseError("User doesn't exist")
return return
} }
mfaProps := user.MultiFactorAuths[:0] err = object.DisabledMultiFactorAuth(user)
i := 0
for _, mfaProp := range mfaProps {
if mfaProp.Id != id {
mfaProps[i] = mfaProp
i++
}
}
user.MultiFactorAuths = mfaProps
_, err = object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(user.MultiFactorAuths) c.ResponseOk(object.GetAllMfaProps(user, true))
} }
// SetPreferredMfa // SetPreferredMfa
@ -185,7 +183,7 @@ func (c *ApiController) DeleteMfa() {
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /set-preferred-mfa [post] // @router /set-preferred-mfa [post]
func (c *ApiController) SetPreferredMfa() { func (c *ApiController) SetPreferredMfa() {
id := c.Ctx.Request.Form.Get("id") mfaType := c.Ctx.Request.Form.Get("mfaType")
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name) userId := util.GetId(owner, name)
@ -195,29 +193,15 @@ func (c *ApiController) SetPreferredMfa() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if user == nil { if user == nil {
c.ResponseError("User doesn't exist") c.ResponseError("User doesn't exist")
return return
} }
mfaProps := user.MultiFactorAuths err = object.SetPreferredMultiFactorAuth(user, mfaType)
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Id == id {
mfaProps[i].IsPreferred = true
} else {
mfaProps[i].IsPreferred = false
}
}
_, err = object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(object.GetAllMfaProps(user, true))
for i, mfaProp := range mfaProps {
mfaProps[i] = object.GetMaskedProps(mfaProp)
}
c.ResponseOk(mfaProps)
} }

View File

@ -193,6 +193,7 @@ func (c *ApiController) GetUser() {
panic(err) panic(err)
} }
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
err = object.ExtendUserWithRolesAndPermissions(user) err = object.ExtendUserWithRolesAndPermissions(user)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -93,9 +93,10 @@ func (c *ApiController) SendVerificationCode() {
} }
} }
// mfaSessionData != nil, means method is MfaSetupVerification // mfaSessionData != nil, means method is MfaAuthVerification
if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil { if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil {
user, err = object.GetUser(mfaSessionData.UserId) user, err = object.GetUser(mfaSessionData.UserId)
c.setMfaSessionData(nil)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -129,7 +130,7 @@ func (c *ApiController) SendVerificationCode() {
} else if vform.Method == ResetVerification { } else if vform.Method == ResetVerification {
user = c.getCurrentUser() user = c.getCurrentUser()
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false) mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
@ -157,12 +158,14 @@ func (c *ApiController) SendVerificationCode() {
} }
vform.CountryCode = user.GetCountryCode(vform.CountryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} else if vform.Method == ResetVerification { } else if vform.Method == ResetVerification || vform.Method == MfaSetupVerification {
if vform.CountryCode == "" {
if user = c.getCurrentUser(); user != nil { if user = c.getCurrentUser(); user != nil {
vform.CountryCode = user.GetCountryCode(vform.CountryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
}
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false) mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }

View File

@ -27,9 +27,9 @@ type MfaSessionData struct {
} }
type MfaProps struct { type MfaProps struct {
Id string `json:"id"` Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"` IsPreferred bool `json:"isPreferred"`
AuthType string `json:"type" form:"type"` MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"` CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
@ -44,6 +44,7 @@ type MfaInterface interface {
} }
const ( const (
EmailType = "email"
SmsType = "sms" SmsType = "sms"
TotpType = "app" TotpType = "app"
) )
@ -54,10 +55,12 @@ const (
RequiredMfa = "RequiredMfa" RequiredMfa = "RequiredMfa"
) )
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface { func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface {
switch providerType { switch mfaType {
case SmsType: case SmsType:
return NewSmsTwoFactor(config) return NewSmsTwoFactor(config)
case EmailType:
return NewEmailTwoFactor(config)
case TotpType: case TotpType:
return nil return nil
} }
@ -65,17 +68,17 @@ func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
return nil return nil
} }
func RecoverTfs(user *User, recoveryCode string) error { func MfaRecover(user *User, recoveryCode string) error {
hit := false hit := false
twoFactor := user.GetPreferMfa(false) if len(user.RecoveryCodes) == 0 {
if len(twoFactor.RecoveryCodes) == 0 {
return fmt.Errorf("do not have recovery codes") return fmt.Errorf("do not have recovery codes")
} }
for _, code := range twoFactor.RecoveryCodes { for _, code := range user.RecoveryCodes {
if code == recoveryCode { if code == recoveryCode {
hit = true hit = true
user.RecoveryCodes = util.DeleteVal(user.RecoveryCodes, code)
break break
} }
} }
@ -83,30 +86,92 @@ func RecoverTfs(user *User, recoveryCode string) error {
return fmt.Errorf("recovery code not found") return fmt.Errorf("recovery code not found")
} }
affected, err := UpdateUser(user.GetId(), user, []string{"two_factor_auth"}, user.IsAdminUser()) _, err := UpdateUser(user.GetId(), user, []string{"recovery_codes"}, user.IsAdminUser())
if err != nil { if err != nil {
return err return err
} }
if !affected { return nil
return fmt.Errorf("") }
func GetAllMfaProps(user *User, masked bool) []*MfaProps {
mfaProps := []*MfaProps{}
if user.MfaPhoneEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(SmsType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: SmsType,
})
}
if user.MfaEmailEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(EmailType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: EmailType,
})
}
return mfaProps
}
func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps := &MfaProps{}
if mfaType == SmsType {
mfaProps = &MfaProps{
Enabled: user.MfaPhoneEnabled,
MfaType: mfaType,
CountryCode: user.CountryCode,
}
if masked {
mfaProps.Secret = util.GetMaskedPhone(user.Phone)
} else {
mfaProps.Secret = user.Phone
}
} else if mfaType == EmailType {
mfaProps = &MfaProps{
Enabled: user.MfaEmailEnabled,
MfaType: mfaType,
}
if masked {
mfaProps.Secret = util.GetMaskedEmail(user.Email)
} else {
mfaProps.Secret = user.Email
}
} else if mfaType == TotpType {
mfaProps = &MfaProps{
MfaType: mfaType,
}
}
if user.PreferredMfaType == mfaType {
mfaProps.IsPreferred = true
}
return mfaProps
}
func DisabledMultiFactorAuth(user *User) error {
user.PreferredMfaType = ""
user.RecoveryCodes = []string{}
user.MfaPhoneEnabled = false
user.MfaEmailEnabled = false
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled"}, user.IsAdminUser())
if err != nil {
return err
} }
return nil return nil
} }
func GetMaskedProps(props *MfaProps) *MfaProps { func SetPreferredMultiFactorAuth(user *User, mfaType string) error {
maskedProps := &MfaProps{ user.PreferredMfaType = mfaType
AuthType: props.AuthType,
Id: props.Id,
IsPreferred: props.IsPreferred,
}
if props.AuthType == SmsType { _, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type"}, user.IsAdminUser())
if !util.IsEmailValid(props.Secret) { if err != nil {
maskedProps.Secret = util.GetMaskedPhone(props.Secret) return err
} else {
maskedProps.Secret = util.GetMaskedEmail(props.Secret)
} }
} return nil
return maskedProps
} }

View File

@ -34,6 +34,21 @@ type SmsMfa struct {
Config *MfaProps Config *MfaProps
} }
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
recoveryCode := uuid.NewString()
err := ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
MfaType: mfa.Config.MfaType,
RecoveryCodes: []string{recoveryCode},
}
return &mfaProps, nil
}
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error { func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string) dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string) countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
@ -47,6 +62,45 @@ func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
return nil return nil
} }
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is empty")
}
columns := []string{"recovery_codes", "preferred_mfa_type"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType
}
if mfa.Config.MfaType == SmsType {
user.MfaPhoneEnabled = true
columns = append(columns, "mfa_phone_enabled")
if user.Phone == "" {
user.Phone = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
user.CountryCode = ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
columns = append(columns, "phone", "country_code")
}
} else if mfa.Config.MfaType == EmailType {
user.MfaEmailEnabled = true
columns = append(columns, "mfa_email_enabled")
if user.Email == "" {
user.Email = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
columns = append(columns, "email")
}
}
_, err := UpdateUser(user.GetId(), user, columns, false)
if err != nil {
return err
}
return nil
}
func (mfa *SmsMfa) Verify(passCode string) error { func (mfa *SmsMfa) Verify(passCode string) error {
if !util.IsEmailValid(mfa.Config.Secret) { if !util.IsEmailValid(mfa.Config.Secret) {
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode) mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode)
@ -57,65 +111,21 @@ func (mfa *SmsMfa) Verify(passCode string) error {
return nil return nil
} }
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
recoveryCode, err := uuid.NewRandom()
if err != nil {
return nil, err
}
err = ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode.String()})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
AuthType: SmsType,
RecoveryCodes: []string{recoveryCode.String()},
}
return &mfaProps, nil
}
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
if dest == "" || len(recoveryCodes) == 0 {
return fmt.Errorf("MFA dest or recovery codes is empty")
}
if !util.IsEmailValid(dest) {
mfa.Config.CountryCode = countryCode
}
mfa.Config.AuthType = SmsType
mfa.Config.Id = uuid.NewString()
mfa.Config.Secret = dest
mfa.Config.RecoveryCodes = recoveryCodes
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Secret == mfa.Config.Secret {
user.MultiFactorAuths = append(user.MultiFactorAuths[:i], user.MultiFactorAuths[i+1:]...)
}
}
user.MultiFactorAuths = append(user.MultiFactorAuths, mfa.Config)
affected, err := UpdateUser(user.GetId(), user, []string{"multi_factor_auths"}, user.IsAdminUser())
if err != nil {
return err
}
if !affected {
return fmt.Errorf("failed to enable two factor authentication")
}
return nil
}
func NewSmsTwoFactor(config *MfaProps) *SmsMfa { func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
if config == nil { if config == nil {
config = &MfaProps{ config = &MfaProps{
AuthType: SmsType, MfaType: SmsType,
}
}
return &SmsMfa{
Config: config,
}
}
func NewEmailTwoFactor(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
MfaType: EmailType,
} }
} }
return &SmsMfa{ return &SmsMfa{

View File

@ -159,7 +159,11 @@ type User struct {
Custom string `xorm:"custom varchar(100)" json:"custom"` Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"` WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
MultiFactorAuths []*MfaProps `json:"multiFactorAuths"` PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`
@ -425,6 +429,12 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.Password != "" { if user.Password != "" {
user.Password = "***" user.Password = "***"
} }
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
if user.ManagedAccounts != nil { if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts { for _, manageAccount := range user.ManagedAccounts {
@ -432,11 +442,6 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
} }
} }
if user.MultiFactorAuths != nil {
for i, props := range user.MultiFactorAuths {
user.MultiFactorAuths[i] = GetMaskedProps(props)
}
}
return user, nil return user, nil
} }
@ -823,35 +828,14 @@ func userChangeTrigger(oldName string, newName string) error {
} }
func (user *User) IsMfaEnabled() bool { func (user *User) IsMfaEnabled() bool {
return len(user.MultiFactorAuths) > 0 return user.PreferredMfaType != ""
} }
func (user *User) GetPreferMfa(masked bool) *MfaProps { func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
if len(user.MultiFactorAuths) == 0 { if user.PreferredMfaType == "" {
return nil return nil
} }
return user.GetMfaProps(user.PreferredMfaType, masked)
if masked {
if len(user.MultiFactorAuths) == 1 {
return GetMaskedProps(user.MultiFactorAuths[0])
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return GetMaskedProps(v)
}
}
return GetMaskedProps(user.MultiFactorAuths[0])
} else {
if len(user.MultiFactorAuths) == 1 {
return user.MultiFactorAuths[0]
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return v
}
}
return user.MultiFactorAuths[0]
}
} }
func AddUserkeys(user *User, isAdmin bool) (bool, error) { func AddUserkeys(user *User, isAdmin bool) (bool, error) {

View File

@ -288,9 +288,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }
oldUserTwoFactorAuthJson, _ := json.Marshal(oldUser.MultiFactorAuths) if oldUser.PreferredMfaType != newUser.PreferredMfaType {
newUserTwoFactorAuthJson, _ := json.Marshal(newUser.MultiFactorAuths)
if string(oldUserTwoFactorAuthJson) != string(newUserTwoFactorAuthJson) {
item := GetAccountItemByName("Multi-factor authentication", organization) item := GetAccountItemByName("Multi-factor authentication", organization)
itemsChanged = append(itemsChanged, item) itemsChanged = append(itemsChanged, item)
} }

View File

@ -34,7 +34,6 @@ import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend"; import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons"; import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import {SmsMfaType} from "./auth/MfaSetupPage";
import * as MfaBackend from "./backend/MfaBackend"; import * as MfaBackend from "./backend/MfaBackend";
const {Option} = Select; const {Option} = Select;
@ -189,16 +188,6 @@ class UserEditPage extends React.Component {
return this.props.account.countryCode; return this.props.account.countryCode;
} }
getMfaProps(type = "") {
if (!(this.state.multiFactorAuths?.length > 0)) {
return [];
}
if (type === "") {
return this.state.multiFactorAuths;
}
return this.state.multiFactorAuths.filter(mfaProps => mfaProps.type === type);
}
loadMore = (table, type) => { loadMore = (table, type) => {
return <div return <div
style={{ style={{
@ -216,13 +205,12 @@ class UserEditPage extends React.Component {
</div>; </div>;
}; };
deleteMfa = (id) => { deleteMfa = () => {
this.setState({ this.setState({
RemoveMfaLoading: true, RemoveMfaLoading: true,
}); });
DeleteMfa({ DeleteMfa({
id: id,
owner: this.state.user.owner, owner: this.state.user.owner,
name: this.state.user.name, name: this.state.user.name,
}).then((res) => { }).then((res) => {
@ -447,7 +435,7 @@ class UserEditPage extends React.Component {
<CountryCodeSelect <CountryCodeSelect
style={{width: "30%"}} style={{width: "30%"}}
// disabled={!Setting.isLocalAdminUser(this.props.account) ? true : disabled} // disabled={!Setting.isLocalAdminUser(this.props.account) ? true : disabled}
value={this.state.user.countryCode} initValue={this.state.user.countryCode}
onChange={(value) => { onChange={(value) => {
this.updateUserField("countryCode", value); this.updateUserField("countryCode", value);
}} }}
@ -860,29 +848,31 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} : {Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Card title={i18next.t("mfa:Multi-factor methods")}> <Card title={i18next.t("mfa:Multi-factor methods")}
<Card type="inner" title={i18next.t("mfa:SMS/Email message")}> extra={this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
/> : null
}>
<List <List
rowKey="mfaType"
itemLayout="horizontal" itemLayout="horizontal"
dataSource={this.getMfaProps(SmsMfaType)} dataSource={this.state.multiFactorAuths}
loadMore={this.loadMore(this.state.multiFactorAuths, SmsMfaType)}
renderItem={(item, index) => ( renderItem={(item, index) => (
<List.Item> <List.Item>
<div> <Space>
{item?.id === undefined ? {i18next.t("general:Type")}: {item.mfaType}
<Button type={"default"} onClick={() => { {item.secret}
Setting.goToLink("/mfa-authentication/setup"); </Space>
}}> {item.enabled ? (
{i18next.t("mfa:Setup")} <Space>
</Button> : {item.enabled ?
<Tag icon={<CheckCircleOutlined />} color="success"> <Tag icon={<CheckCircleOutlined />} color="success">
{i18next.t("general:Enabled")} {i18next.t("general:Enabled")}
</Tag> </Tag> : null
} }
{item.secret}
</div>
{item?.id === undefined ? null :
<div>
{item.isPreferred ? {item.isPreferred ?
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} > <Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
{i18next.t("mfa:preferred")} {i18next.t("mfa:preferred")}
@ -891,7 +881,7 @@ class UserEditPage extends React.Component {
const values = { const values = {
owner: this.state.user.owner, owner: this.state.user.owner,
name: this.state.user.name, name: this.state.user.name,
id: item.id, mfaType: item.mfaType,
}; };
MfaBackend.SetPreferredMfa(values).then((res) => { MfaBackend.SetPreferredMfa(values).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@ -904,18 +894,16 @@ class UserEditPage extends React.Component {
{i18next.t("mfa:Set preferred")} {i18next.t("mfa:Set preferred")}
</Button> </Button>
} }
<PopconfirmModal </Space>
title={i18next.t("general:Sure to delete") + "?"} ) : <Button type={"default"} onClick={() => {
onConfirm={() => this.deleteMfa(item.id)} Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`);
> }}>
</PopconfirmModal> {i18next.t("mfa:Setup")}
</div> </Button>}
}
</List.Item> </List.Item>
)} )}
/> />
</Card> </Card>
</Card>
</Col> </Col>
</Row> </Row>
) )

View File

@ -16,8 +16,8 @@ import React, {useState} from "react";
import i18next from "i18next"; import i18next from "i18next";
import {Button, Input} from "antd"; import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import {SmsMfaType} from "./MfaSetupPage"; import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm} from "./MfaVerifyForm"; import {MfaSmsVerifyForm, mfaAuth} from "./MfaVerifyForm";
export const NextMfa = "NextMfa"; export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa"; export const RequiredMfa = "RequiredMfa";
@ -26,20 +26,20 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
formValues.password = ""; formValues.password = "";
formValues.username = ""; formValues.username = "";
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [type, setType] = useState(mfaProps.type); const [mfaType, setMfaType] = useState(mfaProps.mfaType);
const [recoveryCode, setRecoveryCode] = useState(""); const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => { const verify = ({passcode}) => {
setLoading(true); setLoading(true);
const values = {...formValues, passcode, mfaType: type}; const values = {...formValues, passcode, mfaType};
AuthBackend.login(values, oAuthParams).then((res) => { AuthBackend.login(values, oAuthParams).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
onSuccess(res); onSuccess(res);
} else { } else {
onFail(res.msg); onFail(res.msg);
} }
}).catch((reason) => { }).catch((res) => {
onFail(reason.message); onFail(res.message);
}).finally(() => { }).finally(() => {
setLoading(false); setLoading(false);
}); });
@ -49,19 +49,18 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
setLoading(true); setLoading(true);
AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => { AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
onSuccess(); onSuccess(res);
} else { } else {
onFail(res.msg); onFail(res.msg);
} }
}).catch((reason) => { }).catch((res) => {
onFail(reason.message); onFail(res.message);
}).finally(() => { }).finally(() => {
setLoading(false); setLoading(false);
}); });
}; };
switch (type) { if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
case SmsMfaType:
return ( return (
<div style={{width: 300, height: 350}}> <div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@ -72,20 +71,21 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
</div> </div>
<MfaSmsVerifyForm <MfaSmsVerifyForm
mfaProps={mfaProps} mfaProps={mfaProps}
method={mfaAuth}
onFinish={verify} onFinish={verify}
application={application} application={application}
/> />
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")} {i18next.t("mfa:Have problems?")}
<a onClick={() => { <a onClick={() => {
setType("recovery"); setMfaType("recovery");
}}> }}>
{i18next.t("mfa:Use a recovery code")} {i18next.t("mfa:Use a recovery code")}
</a> </a>
</span> </span>
</div> </div>
); );
case "recovery": } else if (mfaType === "recovery") {
return ( return (
<div style={{width: 300, height: 350}}> <div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@ -108,14 +108,12 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")} {i18next.t("mfa:Have problems?")}
<a onClick={() => { <a onClick={() => {
setType(mfaProps.type); setMfaType(mfaProps.mfaType);
}}> }}>
{i18next.t("mfa:Use SMS verification code")} {i18next.t("mfa:Use SMS verification code")}
</a> </a>
</span> </span>
</div> </div>
); );
default:
return null;
} }
} }

View File

@ -12,18 +12,18 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd"; import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend"; import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons"; import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm} from "./MfaVerifyForm"; import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
import * as ApplicationBackend from "../backend/ApplicationBackend";
const {Step} = Steps; export const EmailMfaType = "email";
export const SmsMfaType = "sms"; export const SmsMfaType = "sms";
export const TotpMfaType = "app"; export const TotpMfaType = "app";
@ -76,12 +76,29 @@ function CheckPasswordForm({user, onSuccess, onFail}) {
); );
} }
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) { export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
mfaProps = mfaProps ?? {type: ""}; const [mfaProps, setMfaProps] = useState({mfaType: mfaType});
useEffect(() => {
if (mfaType === SmsMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.phone,
countryCode: user.countryCode,
});
}
if (mfaType === EmailMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.email,
});
}
}, [mfaType]);
const onFinish = ({passcode}) => { const onFinish = ({passcode}) => {
const data = {passcode, type: mfaProps.type, ...user}; const data = {passcode, mfaType: mfaType, ...user};
MfaBackend.MfaSetupVerify(data) MfaBackend.MfaSetupVerify(data)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@ -98,20 +115,24 @@ export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail})
}); });
}; };
if (mfaProps.type === SmsMfaType) { if (mfaType === null || mfaType === undefined || mfaProps.secret === undefined) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />; return <div></div>;
} else if (mfaProps.type === TotpMfaType) { }
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} method={mfaSetup} mfaProps={mfaProps} />;
} else if (mfaType === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} />;
} else { } else {
return <div></div>; return <div></div>;
} }
} }
function EnableMfaForm({user, mfaProps, onSuccess, onFail}) { function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const requestEnableTotp = () => { const requestEnableTotp = () => {
const data = { const data = {
type: mfaProps.type, mfaType,
...user, ...user,
}; };
setLoading(true); setLoading(true);
@ -131,7 +152,7 @@ function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
<div style={{width: "400px"}}> <div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p> <p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br /> <br />
<code style={{fontStyle: "solid"}}>{mfaProps.recoveryCodes[0]}</code> <code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => { <Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp(); requestEnableTotp();
}} block type="primary"> }} block type="primary">
@ -146,12 +167,13 @@ class MfaSetupPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
account: props.account, account: props.account,
applicationName: (props.applicationName ?? props.account?.signupApplication) ?? "", application: this.props.application ?? null,
applicationName: props.account.signupApplication ?? "",
isAuthenticated: props.isAuthenticated ?? false, isAuthenticated: props.isAuthenticated ?? false,
isPromptPage: props.isPromptPage, isPromptPage: props.isPromptPage,
redirectUri: props.redirectUri, redirectUri: props.redirectUri,
current: props.current ?? 0, current: props.current ?? 0,
type: props.type ?? SmsMfaType, mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
mfaProps: null, mfaProps: null,
}; };
} }
@ -163,7 +185,7 @@ class MfaSetupPage extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && this.state.mfaProps === null) { if (this.state.isAuthenticated === true && this.state.mfaProps === null) {
MfaBackend.MfaSetupInitiate({ MfaBackend.MfaSetupInitiate({
type: this.state.type, mfaType: this.state.mfaType,
...this.getUser(), ...this.getUser(),
}).then((res) => { }).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@ -178,6 +200,10 @@ class MfaSetupPage extends React.Component {
} }
getApplication() { getApplication() {
if (this.state.application !== null) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => { .then((application) => {
if (application !== null) { if (application !== null) {
@ -217,10 +243,12 @@ class MfaSetupPage extends React.Component {
return null; return null;
} }
return <MfaVerifyForm return (
mfaProps={this.state.mfaProps} <div>
<MfaVerifyForm
mfaType={this.state.mfaType}
application={this.state.application} application={this.state.application}
user={this.getUser()} user={this.props.account}
onSuccess={() => { onSuccess={() => {
this.setState({ this.setState({
current: this.state.current + 1, current: this.state.current + 1,
@ -229,13 +257,44 @@ class MfaSetupPage extends React.Component {
onFail={(res) => { onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify")); Setting.showMessage("error", i18next.t("general:Failed to verify"));
}} }}
/>; />
<Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${EmailMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${EmailMfaType}`);
}
this.setState({
mfaType: EmailMfaType,
});
}
}>{i18next.t("mfa:Use Email")}</Button>
}
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${SmsMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${SmsMfaType}`);
}
this.setState({
mfaType: SmsMfaType,
});
}
}>{i18next.t("mfa:Use SMS")}</Button>
}
</Col>
</div>
);
case 2: case 2:
if (!this.state.isAuthenticated) { if (!this.state.isAuthenticated) {
return null; return null;
} }
return <EnableMfaForm user={this.getUser()} mfaProps={{type: this.state.type, ...this.state.mfaProps}} return <EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => { onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully")); Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) { if (this.state.isPromptPage && this.state.redirectUri) {
@ -276,15 +335,14 @@ class MfaSetupPage extends React.Component {
</Row> </Row>
<Row> <Row>
<Col span={24}> <Col span={24}>
<Steps current={this.state.current} style={{ <Steps current={this.state.current}
width: "90%", items={[
maxWidth: "500px", {title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
margin: "auto", {title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
marginTop: "80px", {title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "80px",
}} > }} >
<Step title={i18next.t("mfa:Verify Password")} icon={<UserOutlined />} />
<Step title={i18next.t("mfa:Verify Code")} icon={<KeyOutlined />} />
<Step title={i18next.t("general:Enable")} icon={<CheckOutlined />} />
</Steps> </Steps>
</Col> </Col>
</Row> </Row>

View File

@ -20,23 +20,38 @@ import * as Setting from "../Setting";
import React from "react"; import React from "react";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType} from "./MfaSetupPage";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => { export const mfaAuth = "mfaAuth";
const [dest, setDest] = React.useState(mfaProps?.secret ?? ""); export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
const [dest, setDest] = React.useState(mfaProps.secret ?? "");
const [form] = Form.useForm(); const [form] = Form.useForm();
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return ( return (
<Form <Form
form={form} form={form}
style={{width: "300px"}} style={{width: "300px"}}
onFinish={onFinish} onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
> >
{mfaProps?.secret !== undefined ? {mfaProps.secret !== "" ?
<div style={{marginBottom: 20}}> <div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{Setting.IsEmail(dest) ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest} {isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {mfaProps.secret}
</div> : </div> :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}> <Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{Setting.IsEmail(dest) ? null : {isEmail() ? null :
<Form.Item <Form.Item
name="countryCode" name="countryCode"
noStyle noStyle
@ -48,6 +63,7 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
]} ]}
> >
<CountryCodeSelect <CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}} style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes} countryCodes={application.organizationObj.countryCodes}
/> />
@ -59,13 +75,15 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]} rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
> >
<Input <Input
style={{width: Setting.IsEmail(dest) ? "100% " : "70%"}} style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}} onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />} prefix={<UserOutlined />}
placeholder={i18next.t("general:Phone or email")} placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/> />
</Form.Item> </Form.Item>
</Input.Group> </Input.Group>
</React.Fragment>
)
} }
<Form.Item <Form.Item
name="passcode" name="passcode"
@ -73,8 +91,8 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
> >
<SendCodeInput <SendCodeInput
countryCode={form.getFieldValue("countryCode")} countryCode={form.getFieldValue("countryCode")}
method={mfaProps?.id === undefined ? "mfaSetup" : "mfaAuth"} method={method}
onButtonClickArgs={[dest, Setting.IsEmail(dest) ? "email" : "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "email" : "phone", Setting.getApplicationName(application)]}
application={application} application={application}
/> />
</Form.Item> </Form.Item>

View File

@ -28,14 +28,13 @@ import MfaSetupPage from "./MfaSetupPage";
class PromptPage extends React.Component { class PromptPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const params = new URLSearchParams(this.props.location.search);
this.state = { this.state = {
classes: props, classes: props,
type: props.type, type: props.type,
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName), applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
application: null, application: null,
user: null, user: null,
promptType: params.get("promptType"), promptType: new URLSearchParams(this.props.location.search).get("promptType"),
}; };
} }
@ -233,19 +232,22 @@ class PromptPage extends React.Component {
{this.renderContent(application)} {this.renderContent(application)}
<div style={{marginTop: "50px"}}> <div style={{marginTop: "50px"}}>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button> <Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
</div>; </div>
</>; </>;
} }
renderPromptMfa() { renderPromptMfa() {
return <MfaSetupPage return (
applicationName={this.getApplicationObj().name} <MfaSetupPage
application={this.getApplicationObj()}
account={this.props.account} account={this.props.account}
current={1} current={1}
isAuthenticated={true} isAuthenticated={true}
isPromptPage={true} isPromptPage={true}
redirectUri={this.getRedirectUrl()} redirectUri={this.getRedirectUrl()}
/>; {...this.props}
/>
);
} }
render() { render() {

View File

@ -18,7 +18,7 @@ export function MfaSetupInitiate(values) {
const formData = new FormData(); const formData = new FormData();
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
formData.append("type", values.type); formData.append("mfaType", values.mfaType);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
@ -30,7 +30,7 @@ export function MfaSetupVerify(values) {
const formData = new FormData(); const formData = new FormData();
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
formData.append("type", values.type); formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode); formData.append("passcode", values.passcode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST", method: "POST",
@ -41,7 +41,7 @@ export function MfaSetupVerify(values) {
export function MfaSetupEnable(values) { export function MfaSetupEnable(values) {
const formData = new FormData(); const formData = new FormData();
formData.append("type", values.type); formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
@ -53,7 +53,6 @@ export function MfaSetupEnable(values) {
export function DeleteMfa(values) { export function DeleteMfa(values) {
const formData = new FormData(); const formData = new FormData();
formData.append("id", values.id);
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/delete-mfa`, { return fetch(`${Setting.ServerUrl}/api/delete-mfa`, {
@ -65,7 +64,7 @@ export function DeleteMfa(values) {
export function SetPreferredMfa(values) { export function SetPreferredMfa(values) {
const formData = new FormData(); const formData = new FormData();
formData.append("id", values.id); formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/set-preferred-mfa`, { return fetch(`${Setting.ServerUrl}/api/set-preferred-mfa`, {

View File

@ -17,13 +17,17 @@ import * as Setting from "../../Setting";
import React from "react"; import React from "react";
export const CountryCodeSelect = (props) => { export const CountryCodeSelect = (props) => {
const {onChange, style, disabled} = props; const {onChange, style, disabled, initValue} = props;
const countryCodes = props.countryCodes ?? []; const countryCodes = props.countryCodes ?? [];
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState("");
React.useEffect(() => { React.useEffect(() => {
if (initValue !== undefined) {
setValue(initValue);
} else {
const initValue = countryCodes.length > 0 ? countryCodes[0] : ""; const initValue = countryCodes.length > 0 ? countryCodes[0] : "";
handleOnChange(initValue); handleOnChange(initValue);
}
}, []); }, []);
const handleOnChange = (value) => { const handleOnChange = (value) => {

View File

@ -202,6 +202,7 @@
"Delete": "Löschen", "Delete": "Löschen",
"Description": "Beschreibung", "Description": "Beschreibung",
"Description - Tooltip": "Detaillierte Beschreibungsinformationen zur Referenz, Casdoor selbst wird es nicht verwenden", "Description - Tooltip": "Detaillierte Beschreibungsinformationen zur Referenz, Casdoor selbst wird es nicht verwenden",
"Disable": "Disable",
"Display name": "Anzeigename", "Display name": "Anzeigename",
"Display name - Tooltip": "Ein benutzerfreundlicher, leicht lesbarer Name, der öffentlich in der Benutzeroberfläche angezeigt wird", "Display name - Tooltip": "Ein benutzerfreundlicher, leicht lesbarer Name, der öffentlich in der Benutzeroberfläche angezeigt wird",
"Down": "Nach unten", "Down": "Nach unten",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören", "Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
"Phone": "Telefon", "Phone": "Telefon",
"Phone - Tooltip": "Telefonnummer", "Phone - Tooltip": "Telefonnummer",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Pläne", "Plans": "Pläne",
@ -316,6 +316,7 @@
"Supported country codes": "Unterstützte Ländercodes", "Supported country codes": "Unterstützte Ländercodes",
"Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden", "Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden",
"Sure to delete": "Sicher zu löschen", "Sure to delete": "Sicher zu löschen",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Synchronisieren", "Sync": "Synchronisieren",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Delete", "Delete": "Delete",
"Description": "Description", "Description": "Description",
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it", "Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
"Disable": "Disable",
"Display name": "Display name", "Display name": "Display name",
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI", "Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",
"Down": "Down", "Down": "Down",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Permissions owned by this user", "Permissions - Tooltip": "Permissions owned by this user",
"Phone": "Phone", "Phone": "Phone",
"Phone - Tooltip": "Phone number", "Phone - Tooltip": "Phone number",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans", "Plans": "Plans",
@ -316,6 +316,7 @@
"Supported country codes": "Supported country codes", "Supported country codes": "Supported country codes",
"Supported country codes - Tooltip": "Country codes supported by the organization. These codes can be selected as a prefix when sending SMS verification codes", "Supported country codes - Tooltip": "Country codes supported by the organization. These codes can be selected as a prefix when sending SMS verification codes",
"Sure to delete": "Sure to delete", "Sure to delete": "Sure to delete",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sync", "Sync": "Sync",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Eliminar", "Delete": "Eliminar",
"Description": "Descripción", "Description": "Descripción",
"Description - Tooltip": "Información detallada de descripción para referencia, Casdoor en sí no la utilizará", "Description - Tooltip": "Información detallada de descripción para referencia, Casdoor en sí no la utilizará",
"Disable": "Disable",
"Display name": "Nombre de pantalla", "Display name": "Nombre de pantalla",
"Display name - Tooltip": "Un nombre fácil de usar y leer que se muestra públicamente en la interfaz de usuario", "Display name - Tooltip": "Un nombre fácil de usar y leer que se muestra públicamente en la interfaz de usuario",
"Down": "Abajo", "Down": "Abajo",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Permisos propiedad de este usuario", "Permissions - Tooltip": "Permisos propiedad de este usuario",
"Phone": "Teléfono", "Phone": "Teléfono",
"Phone - Tooltip": "Número de teléfono", "Phone - Tooltip": "Número de teléfono",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Planes", "Plans": "Planes",
@ -316,6 +316,7 @@
"Supported country codes": "Códigos de país admitidos", "Supported country codes": "Códigos de país admitidos",
"Supported country codes - Tooltip": "Códigos de país compatibles con la organización. Estos códigos se pueden seleccionar como prefijo al enviar códigos de verificación SMS", "Supported country codes - Tooltip": "Códigos de país compatibles con la organización. Estos códigos se pueden seleccionar como prefijo al enviar códigos de verificación SMS",
"Sure to delete": "Seguro que eliminar", "Sure to delete": "Seguro que eliminar",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sincronización", "Sync": "Sincronización",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Supprimer", "Delete": "Supprimer",
"Description": "Description", "Description": "Description",
"Description - Tooltip": "Informations détaillées pour référence, Casdoor ne l'utilisera pas en soi", "Description - Tooltip": "Informations détaillées pour référence, Casdoor ne l'utilisera pas en soi",
"Disable": "Disable",
"Display name": "Nom d'affichage", "Display name": "Nom d'affichage",
"Display name - Tooltip": "Un nom convivial et facilement lisible affiché publiquement dans l'interface utilisateur", "Display name - Tooltip": "Un nom convivial et facilement lisible affiché publiquement dans l'interface utilisateur",
"Down": "En bas", "Down": "En bas",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Autorisations détenues par cet utilisateur", "Permissions - Tooltip": "Autorisations détenues par cet utilisateur",
"Phone": "Téléphone", "Phone": "Téléphone",
"Phone - Tooltip": "Numéro de téléphone", "Phone - Tooltip": "Numéro de téléphone",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans", "Plans": "Plans",
@ -316,6 +316,7 @@
"Supported country codes": "Codes de pays pris en charge", "Supported country codes": "Codes de pays pris en charge",
"Supported country codes - Tooltip": "Codes de pays pris en charge par l'organisation. Ces codes peuvent être sélectionnés comme préfixe lors de l'envoi de codes de vérification SMS", "Supported country codes - Tooltip": "Codes de pays pris en charge par l'organisation. Ces codes peuvent être sélectionnés comme préfixe lors de l'envoi de codes de vérification SMS",
"Sure to delete": "Sûr de supprimer", "Sure to delete": "Sûr de supprimer",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Synchronisation", "Sync": "Synchronisation",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Hapus", "Delete": "Hapus",
"Description": "Deskripsi", "Description": "Deskripsi",
"Description - Tooltip": "Informasi deskripsi terperinci untuk referensi, Casdoor itu sendiri tidak akan menggunakannya", "Description - Tooltip": "Informasi deskripsi terperinci untuk referensi, Casdoor itu sendiri tidak akan menggunakannya",
"Disable": "Disable",
"Display name": "Nama tampilan", "Display name": "Nama tampilan",
"Display name - Tooltip": "Sebuah nama yang mudah digunakan dan mudah dibaca yang ditampilkan secara publik di UI", "Display name - Tooltip": "Sebuah nama yang mudah digunakan dan mudah dibaca yang ditampilkan secara publik di UI",
"Down": "Turun", "Down": "Turun",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Izin dimiliki oleh pengguna ini", "Permissions - Tooltip": "Izin dimiliki oleh pengguna ini",
"Phone": "Telepon", "Phone": "Telepon",
"Phone - Tooltip": "Nomor telepon", "Phone - Tooltip": "Nomor telepon",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Rencana", "Plans": "Rencana",
@ -316,6 +316,7 @@
"Supported country codes": "Kode negara yang didukung", "Supported country codes": "Kode negara yang didukung",
"Supported country codes - Tooltip": "Kode negara yang didukung oleh organisasi. Kode-kode ini dapat dipilih sebagai awalan saat mengirim kode verifikasi SMS", "Supported country codes - Tooltip": "Kode negara yang didukung oleh organisasi. Kode-kode ini dapat dipilih sebagai awalan saat mengirim kode verifikasi SMS",
"Sure to delete": "Pasti untuk menghapus", "Sure to delete": "Pasti untuk menghapus",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sinkronisasi", "Sync": "Sinkronisasi",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "削除", "Delete": "削除",
"Description": "説明", "Description": "説明",
"Description - Tooltip": "参照用の詳細な説明情報です。Casdoor自体はそれを使用しません", "Description - Tooltip": "参照用の詳細な説明情報です。Casdoor自体はそれを使用しません",
"Disable": "Disable",
"Display name": "表示名", "Display name": "表示名",
"Display name - Tooltip": "UIで公開されている使いやすく読みやすい名前", "Display name - Tooltip": "UIで公開されている使いやすく読みやすい名前",
"Down": "ダウン", "Down": "ダウン",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "このユーザーが所有する権限", "Permissions - Tooltip": "このユーザーが所有する権限",
"Phone": "電話", "Phone": "電話",
"Phone - Tooltip": "電話番号", "Phone - Tooltip": "電話番号",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "プラン", "Plans": "プラン",
@ -316,6 +316,7 @@
"Supported country codes": "サポートされている国コード", "Supported country codes": "サポートされている国コード",
"Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます", "Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます",
"Sure to delete": "削除することが確実です", "Sure to delete": "削除することが確実です",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "同期", "Sync": "同期",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "삭제하기", "Delete": "삭제하기",
"Description": "설명", "Description": "설명",
"Description - Tooltip": "참고용으로 자세한 설명 정보가 제공됩니다. Casdoor 자체는 사용하지 않습니다", "Description - Tooltip": "참고용으로 자세한 설명 정보가 제공됩니다. Casdoor 자체는 사용하지 않습니다",
"Disable": "Disable",
"Display name": "디스플레이 이름", "Display name": "디스플레이 이름",
"Display name - Tooltip": "UI에서 공개적으로 표시되는 사용자 친화적이고 쉽게 읽을 수 있는 이름", "Display name - Tooltip": "UI에서 공개적으로 표시되는 사용자 친화적이고 쉽게 읽을 수 있는 이름",
"Down": "아래로", "Down": "아래로",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "이 사용자가 소유한 권한", "Permissions - Tooltip": "이 사용자가 소유한 권한",
"Phone": "전화기", "Phone": "전화기",
"Phone - Tooltip": "전화 번호", "Phone - Tooltip": "전화 번호",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "플랜", "Plans": "플랜",
@ -316,6 +316,7 @@
"Supported country codes": "지원되는 국가 코드들", "Supported country codes": "지원되는 국가 코드들",
"Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다", "Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다",
"Sure to delete": "삭제하시겠습니까?", "Sure to delete": "삭제하시겠습니까?",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "싱크", "Sync": "싱크",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Excluir", "Delete": "Excluir",
"Description": "Descrição", "Description": "Descrição",
"Description - Tooltip": "Informações de descrição detalhadas para referência, o Casdoor em si não irá utilizá-las", "Description - Tooltip": "Informações de descrição detalhadas para referência, o Casdoor em si não irá utilizá-las",
"Disable": "Disable",
"Display name": "Nome de exibição", "Display name": "Nome de exibição",
"Display name - Tooltip": "Um nome amigável e facilmente legível exibido publicamente na interface do usuário", "Display name - Tooltip": "Um nome amigável e facilmente legível exibido publicamente na interface do usuário",
"Down": "Descer", "Down": "Descer",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Permissões pertencentes a este usuário", "Permissions - Tooltip": "Permissões pertencentes a este usuário",
"Phone": "Telefone", "Phone": "Telefone",
"Phone - Tooltip": "Número de telefone", "Phone - Tooltip": "Número de telefone",
"Phone or email": "Telefone ou email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Kế hoạch", "Plans": "Kế hoạch",
@ -316,6 +316,7 @@
"Supported country codes": "Códigos de país suportados", "Supported country codes": "Códigos de país suportados",
"Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS", "Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS",
"Sure to delete": "Tem certeza que deseja excluir", "Sure to delete": "Tem certeza que deseja excluir",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sincronizar", "Sync": "Sincronizar",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta", "Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso", "Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
"Passcode": "Código de acesso", "Passcode": "Código de acesso",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores", "Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
"Recovery code": "Código de recuperação", "Recovery code": "Código de recuperação",
"SMS/Email message": "Mensagem SMS/E-mail",
"Set preferred": "Definir preferido", "Set preferred": "Definir preferido",
"Setup": "Configuração", "Setup": "Configuração",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS", "Use SMS verification code": "Usar código de verificação SMS",
"Use a recovery code": "Usar um código de recuperação", "Use a recovery code": "Usar um código de recuperação",
"Verification failed": "Verificação falhou", "Verification failed": "Verificação falhou",

View File

@ -202,6 +202,7 @@
"Delete": "Удалить", "Delete": "Удалить",
"Description": "Описание", "Description": "Описание",
"Description - Tooltip": "Подробная описательная информация для справки, Casdoor сам не будет использовать ее", "Description - Tooltip": "Подробная описательная информация для справки, Casdoor сам не будет использовать ее",
"Disable": "Disable",
"Display name": "Отображаемое имя", "Display name": "Отображаемое имя",
"Display name - Tooltip": "Понятное для пользователя имя, легко читаемое и отображаемое публично в пользовательском интерфейсе (UI)", "Display name - Tooltip": "Понятное для пользователя имя, легко читаемое и отображаемое публично в пользовательском интерфейсе (UI)",
"Down": "вниз", "Down": "вниз",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю", "Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
"Phone": "Телефон", "Phone": "Телефон",
"Phone - Tooltip": "Номер телефона", "Phone - Tooltip": "Номер телефона",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Планы", "Plans": "Планы",
@ -316,6 +316,7 @@
"Supported country codes": "Поддерживаемые коды стран", "Supported country codes": "Поддерживаемые коды стран",
"Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения", "Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения",
"Sure to delete": "Обязательное удаление", "Sure to delete": "Обязательное удаление",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Синхронизация", "Sync": "Синхронизация",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "Xóa", "Delete": "Xóa",
"Description": "Mô tả", "Description": "Mô tả",
"Description - Tooltip": "Thông tin chi tiết mô tả cho tham khảo, Casdoor chính nó sẽ không sử dụng nó", "Description - Tooltip": "Thông tin chi tiết mô tả cho tham khảo, Casdoor chính nó sẽ không sử dụng nó",
"Disable": "Disable",
"Display name": "Tên hiển thị", "Display name": "Tên hiển thị",
"Display name - Tooltip": "Một tên dễ sử dụng, dễ đọc được hiển thị công khai trên giao diện người dùng", "Display name - Tooltip": "Một tên dễ sử dụng, dễ đọc được hiển thị công khai trên giao diện người dùng",
"Down": "Xuống", "Down": "Xuống",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "Quyền sở hữu của người dùng này", "Permissions - Tooltip": "Quyền sở hữu của người dùng này",
"Phone": "Điện thoại", "Phone": "Điện thoại",
"Phone - Tooltip": "Số điện thoại", "Phone - Tooltip": "Số điện thoại",
"Phone or email": "Phone or email",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Kế hoạch", "Plans": "Kế hoạch",
@ -316,6 +316,7 @@
"Supported country codes": "Các mã quốc gia được hỗ trợ", "Supported country codes": "Các mã quốc gia được hỗ trợ",
"Supported country codes - Tooltip": "Mã quốc gia được hỗ trợ bởi tổ chức. Những mã này có thể được chọn làm tiền tố khi gửi mã xác nhận SMS", "Supported country codes - Tooltip": "Mã quốc gia được hỗ trợ bởi tổ chức. Những mã này có thể được chọn làm tiền tố khi gửi mã xác nhận SMS",
"Sure to delete": "Chắc chắn muốn xóa", "Sure to delete": "Chắc chắn muốn xóa",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove", "Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Đồng bộ hoá", "Sync": "Đồng bộ hoá",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip", "Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code", "Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed", "Verification failed": "Verification failed",

View File

@ -202,6 +202,7 @@
"Delete": "删除", "Delete": "删除",
"Description": "描述信息", "Description": "描述信息",
"Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用", "Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用",
"Disable": "关闭",
"Display name": "显示名称", "Display name": "显示名称",
"Display name - Tooltip": "在界面里公开显示的、易读的名称", "Display name - Tooltip": "在界面里公开显示的、易读的名称",
"Down": "下移", "Down": "下移",
@ -272,7 +273,6 @@
"Permissions - Tooltip": "该用户所拥有的权限", "Permissions - Tooltip": "该用户所拥有的权限",
"Phone": "手机号", "Phone": "手机号",
"Phone - Tooltip": "手机号", "Phone - Tooltip": "手机号",
"Phone or email": "手机或邮箱",
"Plan": "计划", "Plan": "计划",
"Plan - Tooltip": "订阅里的计划", "Plan - Tooltip": "订阅里的计划",
"Plans": "计划", "Plans": "计划",
@ -316,6 +316,7 @@
"Supported country codes": "支持的国家代码", "Supported country codes": "支持的国家代码",
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀", "Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
"Sure to delete": "确定删除", "Sure to delete": "确定删除",
"Sure to disable": "确认关闭",
"Sure to remove": "确定移除", "Sure to remove": "确定移除",
"Swagger": "API文档", "Swagger": "API文档",
"Sync": "同步", "Sync": "同步",
@ -437,12 +438,15 @@
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip", "Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板", "Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
"Passcode": "认证码", "Passcode": "认证码",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "请先绑定邮箱,之后会自动使用该邮箱作为多因素认证的方式",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "请先绑定手机号,之后会自动使用该手机号作为多因素认证的方式",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证",
"Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户", "Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户",
"Recovery code": "恢复码", "Recovery code": "恢复码",
"SMS/Email message": "短信或邮件认证",
"Set preferred": "设为首选", "Set preferred": "设为首选",
"Setup": "设置", "Setup": "设置",
"Use Email": "使用电子邮件",
"Use SMS": "使用短信",
"Use SMS verification code": "使用手机或电子邮件发送验证码认证", "Use SMS verification code": "使用手机或电子邮件发送验证码认证",
"Use a recovery code": "使用恢复代码", "Use a recovery code": "使用恢复代码",
"Verification failed": "验证失败", "Verification failed": "验证失败",