mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
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:
parent
6ebca6dbe7
commit
c391af4552
@ -370,6 +370,7 @@ func (c *ApiController) GetAccount() {
|
||||
|
||||
user.Permissions = object.GetMaskedPermissions(user.Permissions)
|
||||
user.Roles = object.GetMaskedRoles(user.Roles)
|
||||
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
|
||||
|
||||
organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
|
||||
if err != nil {
|
||||
|
@ -71,7 +71,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
|
||||
if form.Password != "" && user.IsMfaEnabled() {
|
||||
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
|
||||
}
|
||||
|
||||
@ -656,15 +656,20 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
if authForm.Passcode != "" {
|
||||
MfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferMfa(false))
|
||||
err = MfaUtil.Verify(authForm.Passcode)
|
||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
|
||||
if mfaUtil == nil {
|
||||
c.ResponseError("Invalid multi-factor authentication type")
|
||||
return
|
||||
}
|
||||
|
||||
err = mfaUtil.Verify(authForm.Passcode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if authForm.RecoveryCode != "" {
|
||||
err = object.RecoverTfs(user, authForm.RecoveryCode)
|
||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@ -177,6 +177,10 @@ func (c *ApiController) SetSessionData(s *SessionData) {
|
||||
}
|
||||
|
||||
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
|
||||
if data == nil {
|
||||
c.SetSession(object.MfaSessionUserId, nil)
|
||||
return
|
||||
}
|
||||
c.SetSession(object.MfaSessionUserId, data.UserId)
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ import (
|
||||
func (c *ApiController) MfaSetupInitiate() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
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)
|
||||
|
||||
if len(userId) == 0 {
|
||||
@ -42,10 +42,11 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
return
|
||||
}
|
||||
|
||||
MfaUtil := object.GetMfaUtil(authType, nil)
|
||||
MfaUtil := object.GetMfaUtil(mfaType, nil)
|
||||
if MfaUtil == nil {
|
||||
c.ResponseError("Invalid auth type")
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@ -79,16 +80,20 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
// @Success 200 {object} Response object
|
||||
// @router /mfa/setup/verify [post]
|
||||
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")
|
||||
|
||||
if authType == "" || passcode == "" {
|
||||
if mfaType == "" || passcode == "" {
|
||||
c.ResponseError("missing auth type or passcode")
|
||||
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 {
|
||||
c.ResponseError(err.Error())
|
||||
} else {
|
||||
@ -108,7 +113,7 @@ func (c *ApiController) MfaSetupVerify() {
|
||||
func (c *ApiController) MfaSetupEnable() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
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))
|
||||
if err != nil {
|
||||
@ -121,8 +126,13 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
return
|
||||
}
|
||||
|
||||
twoFactor := object.GetMfaUtil(authType, nil)
|
||||
err = twoFactor.Enable(c.Ctx, user)
|
||||
mfaUtil := object.GetMfaUtil(mfaType, nil)
|
||||
if mfaUtil == nil {
|
||||
c.ResponseError("Invalid multi-factor authentication type")
|
||||
return
|
||||
}
|
||||
|
||||
err = mfaUtil.Enable(c.Ctx, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -137,11 +147,9 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
// @Description: Delete MFA
|
||||
// @param owner form string true "owner 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
|
||||
// @router /delete-mfa/ [post]
|
||||
func (c *ApiController) DeleteMfa() {
|
||||
id := c.Ctx.Request.Form.Get("id")
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
userId := util.GetId(owner, name)
|
||||
@ -151,28 +159,18 @@ func (c *ApiController) DeleteMfa() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError("User doesn't exist")
|
||||
return
|
||||
}
|
||||
|
||||
mfaProps := user.MultiFactorAuths[:0]
|
||||
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())
|
||||
err = object.DisabledMultiFactorAuth(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(user.MultiFactorAuths)
|
||||
c.ResponseOk(object.GetAllMfaProps(user, true))
|
||||
}
|
||||
|
||||
// SetPreferredMfa
|
||||
@ -185,7 +183,7 @@ func (c *ApiController) DeleteMfa() {
|
||||
// @Success 200 {object} Response object
|
||||
// @router /set-preferred-mfa [post]
|
||||
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")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
userId := util.GetId(owner, name)
|
||||
@ -195,29 +193,15 @@ func (c *ApiController) SetPreferredMfa() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError("User doesn't exist")
|
||||
return
|
||||
}
|
||||
|
||||
mfaProps := user.MultiFactorAuths
|
||||
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())
|
||||
err = object.SetPreferredMultiFactorAuth(user, mfaType)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for i, mfaProp := range mfaProps {
|
||||
mfaProps[i] = object.GetMaskedProps(mfaProp)
|
||||
}
|
||||
c.ResponseOk(mfaProps)
|
||||
c.ResponseOk(object.GetAllMfaProps(user, true))
|
||||
}
|
||||
|
@ -193,6 +193,7 @@ func (c *ApiController) GetUser() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
|
||||
err = object.ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -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 {
|
||||
user, err = object.GetUser(mfaSessionData.UserId)
|
||||
c.setMfaSessionData(nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -129,7 +130,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
} else if vform.Method == ResetVerification {
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferMfa(false)
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
@ -157,12 +158,14 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
|
||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||
} else if vform.Method == ResetVerification {
|
||||
if user = c.getCurrentUser(); user != nil {
|
||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||
} else if vform.Method == ResetVerification || vform.Method == MfaSetupVerification {
|
||||
if vform.CountryCode == "" {
|
||||
if user = c.getCurrentUser(); user != nil {
|
||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||
}
|
||||
}
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferMfa(false)
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
|
117
object/mfa.go
117
object/mfa.go
@ -27,9 +27,9 @@ type MfaSessionData struct {
|
||||
}
|
||||
|
||||
type MfaProps struct {
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsPreferred bool `json:"isPreferred"`
|
||||
AuthType string `json:"type" form:"type"`
|
||||
MfaType string `json:"mfaType" form:"mfaType"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
CountryCode string `json:"countryCode,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
@ -44,8 +44,9 @@ type MfaInterface interface {
|
||||
}
|
||||
|
||||
const (
|
||||
SmsType = "sms"
|
||||
TotpType = "app"
|
||||
EmailType = "email"
|
||||
SmsType = "sms"
|
||||
TotpType = "app"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -54,10 +55,12 @@ const (
|
||||
RequiredMfa = "RequiredMfa"
|
||||
)
|
||||
|
||||
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
|
||||
switch providerType {
|
||||
func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface {
|
||||
switch mfaType {
|
||||
case SmsType:
|
||||
return NewSmsTwoFactor(config)
|
||||
case EmailType:
|
||||
return NewEmailTwoFactor(config)
|
||||
case TotpType:
|
||||
return nil
|
||||
}
|
||||
@ -65,17 +68,17 @@ func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RecoverTfs(user *User, recoveryCode string) error {
|
||||
func MfaRecover(user *User, recoveryCode string) error {
|
||||
hit := false
|
||||
|
||||
twoFactor := user.GetPreferMfa(false)
|
||||
if len(twoFactor.RecoveryCodes) == 0 {
|
||||
if len(user.RecoveryCodes) == 0 {
|
||||
return fmt.Errorf("do not have recovery codes")
|
||||
}
|
||||
|
||||
for _, code := range twoFactor.RecoveryCodes {
|
||||
for _, code := range user.RecoveryCodes {
|
||||
if code == recoveryCode {
|
||||
hit = true
|
||||
user.RecoveryCodes = util.DeleteVal(user.RecoveryCodes, code)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -83,30 +86,92 @@ func RecoverTfs(user *User, recoveryCode string) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if !affected {
|
||||
return fmt.Errorf("")
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetMaskedProps(props *MfaProps) *MfaProps {
|
||||
maskedProps := &MfaProps{
|
||||
AuthType: props.AuthType,
|
||||
Id: props.Id,
|
||||
IsPreferred: props.IsPreferred,
|
||||
}
|
||||
func SetPreferredMultiFactorAuth(user *User, mfaType string) error {
|
||||
user.PreferredMfaType = mfaType
|
||||
|
||||
if props.AuthType == SmsType {
|
||||
if !util.IsEmailValid(props.Secret) {
|
||||
maskedProps.Secret = util.GetMaskedPhone(props.Secret)
|
||||
} else {
|
||||
maskedProps.Secret = util.GetMaskedEmail(props.Secret)
|
||||
}
|
||||
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type"}, user.IsAdminUser())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return maskedProps
|
||||
return nil
|
||||
}
|
||||
|
@ -34,6 +34,21 @@ type SmsMfa struct {
|
||||
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 {
|
||||
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
|
||||
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
|
||||
@ -47,6 +62,45 @@ func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
|
||||
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 {
|
||||
if !util.IsEmailValid(mfa.Config.Secret) {
|
||||
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode)
|
||||
@ -57,65 +111,21 @@ func (mfa *SmsMfa) Verify(passCode string) error {
|
||||
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 {
|
||||
if config == nil {
|
||||
config = &MfaProps{
|
||||
AuthType: SmsType,
|
||||
MfaType: SmsType,
|
||||
}
|
||||
}
|
||||
return &SmsMfa{
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewEmailTwoFactor(config *MfaProps) *SmsMfa {
|
||||
if config == nil {
|
||||
config = &MfaProps{
|
||||
MfaType: EmailType,
|
||||
}
|
||||
}
|
||||
return &SmsMfa{
|
||||
|
@ -159,7 +159,11 @@ type User struct {
|
||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||
|
||||
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"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
@ -425,6 +429,12 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
|
||||
if user.Password != "" {
|
||||
user.Password = "***"
|
||||
}
|
||||
if user.AccessSecret != "" {
|
||||
user.AccessSecret = "***"
|
||||
}
|
||||
if user.RecoveryCodes != nil {
|
||||
user.RecoveryCodes = nil
|
||||
}
|
||||
|
||||
if user.ManagedAccounts != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -823,35 +828,14 @@ func userChangeTrigger(oldName string, newName string) error {
|
||||
}
|
||||
|
||||
func (user *User) IsMfaEnabled() bool {
|
||||
return len(user.MultiFactorAuths) > 0
|
||||
return user.PreferredMfaType != ""
|
||||
}
|
||||
|
||||
func (user *User) GetPreferMfa(masked bool) *MfaProps {
|
||||
if len(user.MultiFactorAuths) == 0 {
|
||||
func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
|
||||
if user.PreferredMfaType == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
return user.GetMfaProps(user.PreferredMfaType, masked)
|
||||
}
|
||||
|
||||
func AddUserkeys(user *User, isAdmin bool) (bool, error) {
|
||||
|
@ -288,9 +288,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
oldUserTwoFactorAuthJson, _ := json.Marshal(oldUser.MultiFactorAuths)
|
||||
newUserTwoFactorAuthJson, _ := json.Marshal(newUser.MultiFactorAuths)
|
||||
if string(oldUserTwoFactorAuthJson) != string(newUserTwoFactorAuthJson) {
|
||||
if oldUser.PreferredMfaType != newUser.PreferredMfaType {
|
||||
item := GetAccountItemByName("Multi-factor authentication", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {DeleteMfa} from "./backend/MfaBackend";
|
||||
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import {SmsMfaType} from "./auth/MfaSetupPage";
|
||||
import * as MfaBackend from "./backend/MfaBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
@ -189,16 +188,6 @@ class UserEditPage extends React.Component {
|
||||
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) => {
|
||||
return <div
|
||||
style={{
|
||||
@ -216,13 +205,12 @@ class UserEditPage extends React.Component {
|
||||
</div>;
|
||||
};
|
||||
|
||||
deleteMfa = (id) => {
|
||||
deleteMfa = () => {
|
||||
this.setState({
|
||||
RemoveMfaLoading: true,
|
||||
});
|
||||
|
||||
DeleteMfa({
|
||||
id: id,
|
||||
owner: this.state.user.owner,
|
||||
name: this.state.user.name,
|
||||
}).then((res) => {
|
||||
@ -447,7 +435,7 @@ class UserEditPage extends React.Component {
|
||||
<CountryCodeSelect
|
||||
style={{width: "30%"}}
|
||||
// disabled={!Setting.isLocalAdminUser(this.props.account) ? true : disabled}
|
||||
value={this.state.user.countryCode}
|
||||
initValue={this.state.user.countryCode}
|
||||
onChange={(value) => {
|
||||
this.updateUserField("countryCode", value);
|
||||
}}
|
||||
@ -860,61 +848,61 @@ class UserEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Card title={i18next.t("mfa:Multi-factor methods")}>
|
||||
<Card type="inner" title={i18next.t("mfa:SMS/Email message")}>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.getMfaProps(SmsMfaType)}
|
||||
loadMore={this.loadMore(this.state.multiFactorAuths, SmsMfaType)}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div>
|
||||
{item?.id === undefined ?
|
||||
<Button type={"default"} onClick={() => {
|
||||
Setting.goToLink("/mfa-authentication/setup");
|
||||
}}>
|
||||
{i18next.t("mfa:Setup")}
|
||||
</Button> :
|
||||
<Card title={i18next.t("mfa:Multi-factor methods")}
|
||||
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
|
||||
rowKey="mfaType"
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.state.multiFactorAuths}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<Space>
|
||||
{i18next.t("general:Type")}: {item.mfaType}
|
||||
{item.secret}
|
||||
</Space>
|
||||
{item.enabled ? (
|
||||
<Space>
|
||||
{item.enabled ?
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
{i18next.t("general:Enabled")}
|
||||
</Tag>
|
||||
</Tag> : null
|
||||
}
|
||||
{item.secret}
|
||||
</div>
|
||||
{item?.id === undefined ? null :
|
||||
<div>
|
||||
{item.isPreferred ?
|
||||
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
|
||||
{i18next.t("mfa:preferred")}
|
||||
</Tag> :
|
||||
<Button type="primary" style={{marginRight: 20}} onClick={() => {
|
||||
const values = {
|
||||
owner: this.state.user.owner,
|
||||
name: this.state.user.name,
|
||||
id: item.id,
|
||||
};
|
||||
MfaBackend.SetPreferredMfa(values).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
multiFactorAuths: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
{i18next.t("mfa:Set preferred")}
|
||||
</Button>
|
||||
}
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + "?"}
|
||||
onConfirm={() => this.deleteMfa(item.id)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
{item.isPreferred ?
|
||||
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
|
||||
{i18next.t("mfa:preferred")}
|
||||
</Tag> :
|
||||
<Button type="primary" style={{marginRight: 20}} onClick={() => {
|
||||
const values = {
|
||||
owner: this.state.user.owner,
|
||||
name: this.state.user.name,
|
||||
mfaType: item.mfaType,
|
||||
};
|
||||
MfaBackend.SetPreferredMfa(values).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
multiFactorAuths: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
{i18next.t("mfa:Set preferred")}
|
||||
</Button>
|
||||
}
|
||||
</Space>
|
||||
) : <Button type={"default"} onClick={() => {
|
||||
Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`);
|
||||
}}>
|
||||
{i18next.t("mfa:Setup")}
|
||||
</Button>}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -16,8 +16,8 @@ import React, {useState} from "react";
|
||||
import i18next from "i18next";
|
||||
import {Button, Input} from "antd";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import {SmsMfaType} from "./MfaSetupPage";
|
||||
import {MfaSmsVerifyForm} from "./MfaVerifyForm";
|
||||
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
|
||||
import {MfaSmsVerifyForm, mfaAuth} from "./MfaVerifyForm";
|
||||
|
||||
export const NextMfa = "NextMfa";
|
||||
export const RequiredMfa = "RequiredMfa";
|
||||
@ -26,20 +26,20 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
|
||||
formValues.password = "";
|
||||
formValues.username = "";
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [type, setType] = useState(mfaProps.type);
|
||||
const [mfaType, setMfaType] = useState(mfaProps.mfaType);
|
||||
const [recoveryCode, setRecoveryCode] = useState("");
|
||||
|
||||
const verify = ({passcode}) => {
|
||||
setLoading(true);
|
||||
const values = {...formValues, passcode, mfaType: type};
|
||||
const values = {...formValues, passcode, mfaType};
|
||||
AuthBackend.login(values, oAuthParams).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
onSuccess(res);
|
||||
} else {
|
||||
onFail(res.msg);
|
||||
}
|
||||
}).catch((reason) => {
|
||||
onFail(reason.message);
|
||||
}).catch((res) => {
|
||||
onFail(res.message);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
@ -49,19 +49,18 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
|
||||
setLoading(true);
|
||||
AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => {
|
||||
if (res.status === "ok") {
|
||||
onSuccess();
|
||||
onSuccess(res);
|
||||
} else {
|
||||
onFail(res.msg);
|
||||
}
|
||||
}).catch((reason) => {
|
||||
onFail(reason.message);
|
||||
}).catch((res) => {
|
||||
onFail(res.message);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case SmsMfaType:
|
||||
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
|
||||
return (
|
||||
<div style={{width: 300, height: 350}}>
|
||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||
@ -72,20 +71,21 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
|
||||
</div>
|
||||
<MfaSmsVerifyForm
|
||||
mfaProps={mfaProps}
|
||||
method={mfaAuth}
|
||||
onFinish={verify}
|
||||
application={application}
|
||||
/>
|
||||
<span style={{float: "right"}}>
|
||||
{i18next.t("mfa:Have problems?")}
|
||||
<a onClick={() => {
|
||||
setType("recovery");
|
||||
setMfaType("recovery");
|
||||
}}>
|
||||
{i18next.t("mfa:Use a recovery code")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case "recovery":
|
||||
} else if (mfaType === "recovery") {
|
||||
return (
|
||||
<div style={{width: 300, height: 350}}>
|
||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||
@ -108,14 +108,12 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
|
||||
<span style={{float: "right"}}>
|
||||
{i18next.t("mfa:Have problems?")}
|
||||
<a onClick={() => {
|
||||
setType(mfaProps.type);
|
||||
setMfaType(mfaProps.mfaType);
|
||||
}}>
|
||||
{i18next.t("mfa:Use SMS verification code")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -12,18 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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 * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import * as MfaBackend from "../backend/MfaBackend";
|
||||
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||
|
||||
import * as UserBackend from "../backend/UserBackend";
|
||||
import {MfaSmsVerifyForm, MfaTotpVerifyForm} from "./MfaVerifyForm";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
|
||||
|
||||
const {Step} = Steps;
|
||||
export const EmailMfaType = "email";
|
||||
export const SmsMfaType = "sms";
|
||||
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();
|
||||
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 data = {passcode, type: mfaProps.type, ...user};
|
||||
const data = {passcode, mfaType: mfaType, ...user};
|
||||
MfaBackend.MfaSetupVerify(data)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@ -98,20 +115,24 @@ export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail})
|
||||
});
|
||||
};
|
||||
|
||||
if (mfaProps.type === SmsMfaType) {
|
||||
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />;
|
||||
} else if (mfaProps.type === TotpMfaType) {
|
||||
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
|
||||
if (mfaType === null || mfaType === undefined || mfaProps.secret === undefined) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
|
||||
return <MfaSmsVerifyForm onFinish={onFinish} application={application} method={mfaSetup} mfaProps={mfaProps} />;
|
||||
} else if (mfaType === TotpMfaType) {
|
||||
return <MfaTotpVerifyForm onFinish={onFinish} />;
|
||||
} else {
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
|
||||
function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
|
||||
function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const requestEnableTotp = () => {
|
||||
const data = {
|
||||
type: mfaProps.type,
|
||||
mfaType,
|
||||
...user,
|
||||
};
|
||||
setLoading(true);
|
||||
@ -131,7 +152,7 @@ function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
|
||||
<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>
|
||||
<br />
|
||||
<code style={{fontStyle: "solid"}}>{mfaProps.recoveryCodes[0]}</code>
|
||||
<code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
|
||||
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
|
||||
requestEnableTotp();
|
||||
}} block type="primary">
|
||||
@ -146,12 +167,13 @@ class MfaSetupPage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
account: props.account,
|
||||
applicationName: (props.applicationName ?? props.account?.signupApplication) ?? "",
|
||||
application: this.props.application ?? null,
|
||||
applicationName: props.account.signupApplication ?? "",
|
||||
isAuthenticated: props.isAuthenticated ?? false,
|
||||
isPromptPage: props.isPromptPage,
|
||||
redirectUri: props.redirectUri,
|
||||
current: props.current ?? 0,
|
||||
type: props.type ?? SmsMfaType,
|
||||
mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
|
||||
mfaProps: null,
|
||||
};
|
||||
}
|
||||
@ -163,7 +185,7 @@ class MfaSetupPage extends React.Component {
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (this.state.isAuthenticated === true && this.state.mfaProps === null) {
|
||||
MfaBackend.MfaSetupInitiate({
|
||||
type: this.state.type,
|
||||
mfaType: this.state.mfaType,
|
||||
...this.getUser(),
|
||||
}).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@ -178,6 +200,10 @@ class MfaSetupPage extends React.Component {
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
if (this.state.application !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((application) => {
|
||||
if (application !== null) {
|
||||
@ -217,25 +243,58 @@ class MfaSetupPage extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MfaVerifyForm
|
||||
mfaProps={this.state.mfaProps}
|
||||
application={this.state.application}
|
||||
user={this.getUser()}
|
||||
onSuccess={() => {
|
||||
this.setState({
|
||||
current: this.state.current + 1,
|
||||
});
|
||||
}}
|
||||
onFail={(res) => {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to verify"));
|
||||
}}
|
||||
/>;
|
||||
return (
|
||||
<div>
|
||||
<MfaVerifyForm
|
||||
mfaType={this.state.mfaType}
|
||||
application={this.state.application}
|
||||
user={this.props.account}
|
||||
onSuccess={() => {
|
||||
this.setState({
|
||||
current: this.state.current + 1,
|
||||
});
|
||||
}}
|
||||
onFail={(res) => {
|
||||
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:
|
||||
if (!this.state.isAuthenticated) {
|
||||
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={() => {
|
||||
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
||||
if (this.state.isPromptPage && this.state.redirectUri) {
|
||||
@ -276,15 +335,14 @@ class MfaSetupPage extends React.Component {
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Steps current={this.state.current} 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 current={this.state.current}
|
||||
items={[
|
||||
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
|
||||
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
|
||||
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
|
||||
]}
|
||||
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "80px",
|
||||
}} >
|
||||
</Steps>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -20,52 +20,70 @@ import * as Setting from "../Setting";
|
||||
import React from "react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
|
||||
import {EmailMfaType} from "./MfaSetupPage";
|
||||
|
||||
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
|
||||
const [dest, setDest] = React.useState(mfaProps?.secret ?? "");
|
||||
export const mfaAuth = "mfaAuth";
|
||||
export const mfaSetup = "mfaSetup";
|
||||
|
||||
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
|
||||
const [dest, setDest] = React.useState(mfaProps.secret ?? "");
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const isEmail = () => {
|
||||
return mfaProps.mfaType === EmailMfaType;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
style={{width: "300px"}}
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
countryCode: mfaProps.countryCode,
|
||||
}}
|
||||
>
|
||||
{mfaProps?.secret !== undefined ?
|
||||
<div style={{marginBottom: 20}}>
|
||||
{Setting.IsEmail(dest) ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
|
||||
{mfaProps.secret !== "" ?
|
||||
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
|
||||
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {mfaProps.secret}
|
||||
</div> :
|
||||
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
|
||||
{Setting.IsEmail(dest) ? null :
|
||||
(<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"}}>
|
||||
{isEmail() ? null :
|
||||
<Form.Item
|
||||
name="countryCode"
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: i18next.t("signup:Please select your country code!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CountryCodeSelect
|
||||
initValue={mfaProps.countryCode}
|
||||
style={{width: "30%"}}
|
||||
countryCodes={application.organizationObj.countryCodes}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
<Form.Item
|
||||
name="countryCode"
|
||||
name="dest"
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: i18next.t("signup:Please select your country code!"),
|
||||
},
|
||||
]}
|
||||
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
|
||||
>
|
||||
<CountryCodeSelect
|
||||
style={{width: "30%"}}
|
||||
countryCodes={application.organizationObj.countryCodes}
|
||||
<Input
|
||||
style={{width: isEmail() ? "100% " : "70%"}}
|
||||
onChange={(e) => {setDest(e.target.value);}}
|
||||
prefix={<UserOutlined />}
|
||||
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
<Form.Item
|
||||
name="dest"
|
||||
noStyle
|
||||
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
|
||||
>
|
||||
<Input
|
||||
style={{width: Setting.IsEmail(dest) ? "100% " : "70%"}}
|
||||
onChange={(e) => {setDest(e.target.value);}}
|
||||
prefix={<UserOutlined />}
|
||||
placeholder={i18next.t("general:Phone or email")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Input.Group>
|
||||
</Input.Group>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Form.Item
|
||||
name="passcode"
|
||||
@ -73,8 +91,8 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
|
||||
>
|
||||
<SendCodeInput
|
||||
countryCode={form.getFieldValue("countryCode")}
|
||||
method={mfaProps?.id === undefined ? "mfaSetup" : "mfaAuth"}
|
||||
onButtonClickArgs={[dest, Setting.IsEmail(dest) ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
method={method}
|
||||
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
@ -28,14 +28,13 @@ import MfaSetupPage from "./MfaSetupPage";
|
||||
class PromptPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
type: props.type,
|
||||
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
|
||||
application: 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)}
|
||||
<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>
|
||||
</div>;
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
renderPromptMfa() {
|
||||
return <MfaSetupPage
|
||||
applicationName={this.getApplicationObj().name}
|
||||
account={this.props.account}
|
||||
current={1}
|
||||
isAuthenticated={true}
|
||||
isPromptPage={true}
|
||||
redirectUri={this.getRedirectUrl()}
|
||||
/>;
|
||||
return (
|
||||
<MfaSetupPage
|
||||
application={this.getApplicationObj()}
|
||||
account={this.props.account}
|
||||
current={1}
|
||||
isAuthenticated={true}
|
||||
isPromptPage={true}
|
||||
redirectUri={this.getRedirectUrl()}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -18,7 +18,7 @@ export function MfaSetupInitiate(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
formData.append("type", values.type);
|
||||
formData.append("mfaType", values.mfaType);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@ -30,7 +30,7 @@ export function MfaSetupVerify(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
formData.append("type", values.type);
|
||||
formData.append("mfaType", values.mfaType);
|
||||
formData.append("passcode", values.passcode);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
|
||||
method: "POST",
|
||||
@ -41,7 +41,7 @@ export function MfaSetupVerify(values) {
|
||||
|
||||
export function MfaSetupEnable(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("type", values.type);
|
||||
formData.append("mfaType", values.mfaType);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
|
||||
@ -53,7 +53,6 @@ export function MfaSetupEnable(values) {
|
||||
|
||||
export function DeleteMfa(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", values.id);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-mfa`, {
|
||||
@ -65,7 +64,7 @@ export function DeleteMfa(values) {
|
||||
|
||||
export function SetPreferredMfa(values) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", values.id);
|
||||
formData.append("mfaType", values.mfaType);
|
||||
formData.append("owner", values.owner);
|
||||
formData.append("name", values.name);
|
||||
return fetch(`${Setting.ServerUrl}/api/set-preferred-mfa`, {
|
||||
|
@ -17,13 +17,17 @@ import * as Setting from "../../Setting";
|
||||
import React from "react";
|
||||
|
||||
export const CountryCodeSelect = (props) => {
|
||||
const {onChange, style, disabled} = props;
|
||||
const {onChange, style, disabled, initValue} = props;
|
||||
const countryCodes = props.countryCodes ?? [];
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const initValue = countryCodes.length > 0 ? countryCodes[0] : "";
|
||||
handleOnChange(initValue);
|
||||
if (initValue !== undefined) {
|
||||
setValue(initValue);
|
||||
} else {
|
||||
const initValue = countryCodes.length > 0 ? countryCodes[0] : "";
|
||||
handleOnChange(initValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Löschen",
|
||||
"Description": "Beschreibung",
|
||||
"Description - Tooltip": "Detaillierte Beschreibungsinformationen zur Referenz, Casdoor selbst wird es nicht verwenden",
|
||||
"Disable": "Disable",
|
||||
"Display name": "Anzeigename",
|
||||
"Display name - Tooltip": "Ein benutzerfreundlicher, leicht lesbarer Name, der öffentlich in der Benutzeroberfläche angezeigt wird",
|
||||
"Down": "Nach unten",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
|
||||
"Phone": "Telefon",
|
||||
"Phone - Tooltip": "Telefonnummer",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Pläne",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Sicher zu löschen",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Synchronisieren",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Delete",
|
||||
"Description": "Description",
|
||||
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
|
||||
"Disable": "Disable",
|
||||
"Display name": "Display name",
|
||||
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",
|
||||
"Down": "Down",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Permissions owned by this user",
|
||||
"Phone": "Phone",
|
||||
"Phone - Tooltip": "Phone number",
|
||||
"Phone or email": "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": "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 disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sync",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Eliminar",
|
||||
"Description": "Descripción",
|
||||
"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 - Tooltip": "Un nombre fácil de usar y leer que se muestra públicamente en la interfaz de usuario",
|
||||
"Down": "Abajo",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Permisos propiedad de este usuario",
|
||||
"Phone": "Teléfono",
|
||||
"Phone - Tooltip": "Número de teléfono",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Planes",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Seguro que eliminar",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sincronización",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Supprimer",
|
||||
"Description": "Description",
|
||||
"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 - Tooltip": "Un nom convivial et facilement lisible affiché publiquement dans l'interface utilisateur",
|
||||
"Down": "En bas",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Autorisations détenues par cet utilisateur",
|
||||
"Phone": "Téléphone",
|
||||
"Phone - Tooltip": "Numéro de téléphone",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Plans",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Sûr de supprimer",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Synchronisation",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Hapus",
|
||||
"Description": "Deskripsi",
|
||||
"Description - Tooltip": "Informasi deskripsi terperinci untuk referensi, Casdoor itu sendiri tidak akan menggunakannya",
|
||||
"Disable": "Disable",
|
||||
"Display name": "Nama tampilan",
|
||||
"Display name - Tooltip": "Sebuah nama yang mudah digunakan dan mudah dibaca yang ditampilkan secara publik di UI",
|
||||
"Down": "Turun",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Izin dimiliki oleh pengguna ini",
|
||||
"Phone": "Telepon",
|
||||
"Phone - Tooltip": "Nomor telepon",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Rencana",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Pasti untuk menghapus",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sinkronisasi",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "削除",
|
||||
"Description": "説明",
|
||||
"Description - Tooltip": "参照用の詳細な説明情報です。Casdoor自体はそれを使用しません",
|
||||
"Disable": "Disable",
|
||||
"Display name": "表示名",
|
||||
"Display name - Tooltip": "UIで公開されている使いやすく読みやすい名前",
|
||||
"Down": "ダウン",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "このユーザーが所有する権限",
|
||||
"Phone": "電話",
|
||||
"Phone - Tooltip": "電話番号",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "プラン",
|
||||
@ -316,6 +316,7 @@
|
||||
"Supported country codes": "サポートされている国コード",
|
||||
"Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます",
|
||||
"Sure to delete": "削除することが確実です",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "同期",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "삭제하기",
|
||||
"Description": "설명",
|
||||
"Description - Tooltip": "참고용으로 자세한 설명 정보가 제공됩니다. Casdoor 자체는 사용하지 않습니다",
|
||||
"Disable": "Disable",
|
||||
"Display name": "디스플레이 이름",
|
||||
"Display name - Tooltip": "UI에서 공개적으로 표시되는 사용자 친화적이고 쉽게 읽을 수 있는 이름",
|
||||
"Down": "아래로",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "이 사용자가 소유한 권한",
|
||||
"Phone": "전화기",
|
||||
"Phone - Tooltip": "전화 번호",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "플랜",
|
||||
@ -316,6 +316,7 @@
|
||||
"Supported country codes": "지원되는 국가 코드들",
|
||||
"Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다",
|
||||
"Sure to delete": "삭제하시겠습니까?",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "싱크",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Excluir",
|
||||
"Description": "Descrição",
|
||||
"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 - Tooltip": "Um nome amigável e facilmente legível exibido publicamente na interface do usuário",
|
||||
"Down": "Descer",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Permissões pertencentes a este usuário",
|
||||
"Phone": "Telefone",
|
||||
"Phone - Tooltip": "Número de telefone",
|
||||
"Phone or email": "Telefone ou email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Kế hoạch",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Tem certeza que deseja excluir",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sincronizar",
|
||||
@ -437,12 +438,15 @@
|
||||
"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",
|
||||
"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",
|
||||
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
|
||||
"Recovery code": "Código de recuperação",
|
||||
"SMS/Email message": "Mensagem SMS/E-mail",
|
||||
"Set preferred": "Definir preferido",
|
||||
"Setup": "Configuração",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Usar código de verificação SMS",
|
||||
"Use a recovery code": "Usar um código de recuperação",
|
||||
"Verification failed": "Verificação falhou",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Удалить",
|
||||
"Description": "Описание",
|
||||
"Description - Tooltip": "Подробная описательная информация для справки, Casdoor сам не будет использовать ее",
|
||||
"Disable": "Disable",
|
||||
"Display name": "Отображаемое имя",
|
||||
"Display name - Tooltip": "Понятное для пользователя имя, легко читаемое и отображаемое публично в пользовательском интерфейсе (UI)",
|
||||
"Down": "вниз",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
|
||||
"Phone": "Телефон",
|
||||
"Phone - Tooltip": "Номер телефона",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Планы",
|
||||
@ -316,6 +316,7 @@
|
||||
"Supported country codes": "Поддерживаемые коды стран",
|
||||
"Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения",
|
||||
"Sure to delete": "Обязательное удаление",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Синхронизация",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "Xóa",
|
||||
"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ó",
|
||||
"Disable": "Disable",
|
||||
"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",
|
||||
"Down": "Xuống",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "Quyền sở hữu của người dùng này",
|
||||
"Phone": "Điện thoại",
|
||||
"Phone - Tooltip": "Số điện thoại",
|
||||
"Phone or email": "Phone or email",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Kế hoạch",
|
||||
@ -316,6 +316,7 @@
|
||||
"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",
|
||||
"Sure to delete": "Chắc chắn muốn xóa",
|
||||
"Sure to disable": "Sure to disable",
|
||||
"Sure to remove": "Sure to remove",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Đồng bộ hoá",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "Multi-factor secret - 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 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",
|
||||
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
|
||||
"Recovery code": "Recovery code",
|
||||
"SMS/Email message": "SMS/Email message",
|
||||
"Set preferred": "Set preferred",
|
||||
"Setup": "Setup",
|
||||
"Use Email": "Use Email",
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
|
@ -202,6 +202,7 @@
|
||||
"Delete": "删除",
|
||||
"Description": "描述信息",
|
||||
"Description - Tooltip": "供人参考的详细描述信息,Casdoor平台本身不会使用",
|
||||
"Disable": "关闭",
|
||||
"Display name": "显示名称",
|
||||
"Display name - Tooltip": "在界面里公开显示的、易读的名称",
|
||||
"Down": "下移",
|
||||
@ -272,7 +273,6 @@
|
||||
"Permissions - Tooltip": "该用户所拥有的权限",
|
||||
"Phone": "手机号",
|
||||
"Phone - Tooltip": "手机号",
|
||||
"Phone or email": "手机或邮箱",
|
||||
"Plan": "计划",
|
||||
"Plan - Tooltip": "订阅里的计划",
|
||||
"Plans": "计划",
|
||||
@ -316,6 +316,7 @@
|
||||
"Supported country codes": "支持的国家代码",
|
||||
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
|
||||
"Sure to delete": "确定删除",
|
||||
"Sure to disable": "确认关闭",
|
||||
"Sure to remove": "确定移除",
|
||||
"Swagger": "API文档",
|
||||
"Sync": "同步",
|
||||
@ -437,12 +438,15 @@
|
||||
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
|
||||
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
|
||||
"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": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证",
|
||||
"Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户",
|
||||
"Recovery code": "恢复码",
|
||||
"SMS/Email message": "短信或邮件认证",
|
||||
"Set preferred": "设为首选",
|
||||
"Setup": "设置",
|
||||
"Use Email": "使用电子邮件",
|
||||
"Use SMS": "使用短信",
|
||||
"Use SMS verification code": "使用手机或电子邮件发送验证码认证",
|
||||
"Use a recovery code": "使用恢复代码",
|
||||
"Verification failed": "验证失败",
|
||||
|
Loading…
x
Reference in New Issue
Block a user