feat: add multi-factor authentication (MFA) feature (#1800)

* feat: add two-factor authentication interface and api

* merge

* feat: add Two-factor authentication accountItem and two-factor api in frontend

* feat: add basic 2fa setup UI

* rebase

* feat: finish the two-factor authentication

* rebase

* feat: support recover code

* chore: fix eslint error

* feat: support multiple sms account

* fix: client application login

* fix: lint

* Update authz.go

* Update mfa.go

* fix: support phone

* fix: i18n

* fix: i18n

* fix: support preferred mfa methods

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
Yaodong Yu 2023-05-05 21:23:59 +08:00 committed by GitHub
parent 5b27f939b8
commit eb39e9e044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 4215 additions and 2776 deletions

View File

@ -24,7 +24,6 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf"
@ -70,6 +69,12 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return
}
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferMfa(true)}
return
}
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
@ -133,11 +138,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// if user did not check auto signin
if resp.Status == "ok" && !form.AutoSignin {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
c.setExpireForSession()
}
if resp.Status == "ok" && user.Owner == object.CasdoorOrganization && application.Name == object.CasdoorApplication {
@ -298,7 +299,6 @@ func (c *ApiController) Login() {
password := authForm.Password
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
}
if msg != "" {
@ -522,6 +522,38 @@ func (c *ApiController) Login() {
resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked}
}
}
} else if c.getMfaSessionData() != nil {
mfaSession := c.getMfaSessionData()
user := object.GetUser(mfaSession.UserId)
if authForm.Passcode != "" {
MfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferMfa(false))
err = MfaUtil.Verify(authForm.Passcode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
if authForm.RecoveryCode != "" {
err = object.RecoverTfs(user, authForm.RecoveryCode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
application := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else {
if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
@ -539,7 +571,7 @@ func (c *ApiController) Login() {
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else {
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), authForm = %s"), util.StructToJson(authForm)))
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(authForm)))
return
}
}

View File

@ -168,6 +168,30 @@ func (c *ApiController) SetSessionData(s *SessionData) {
c.SetSession("SessionData", util.StructToJson(s))
}
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
c.SetSession(object.MfaSessionUserId, data.UserId)
}
func (c *ApiController) getMfaSessionData() *object.MfaSessionData {
userId := c.GetSession(object.MfaSessionUserId)
if userId == nil {
return nil
}
data := &object.MfaSessionData{
UserId: userId.(string),
}
return data
}
func (c *ApiController) setExpireForSession() {
timestamp := time.Now().Unix()
timestamp += 3600 * 24
c.SetSessionData(&SessionData{
ExpireTime: timestamp,
})
}
func wrapActionResponse(affected bool) *Response {
if affected {
return &Response{Status: "ok", Msg: "", Data: "Affected"}

190
controllers/mfa.go Normal file
View File

@ -0,0 +1,190 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"net/http"
"github.com/beego/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// MfaSetupInitiate
// @Title MfaSetupInitiate
// @Tag MFA API
// @Description setup MFA
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param type form string true "MFA auth type"
// @Success 200 {object} The Response object
// @router /mfa/setup/initiate [post]
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")
userId := util.GetId(owner, name)
if len(userId) == 0 {
c.ResponseError(http.StatusText(http.StatusBadRequest))
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
if MfaUtil == nil {
c.ResponseError("Invalid auth type")
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError("User doesn't exist")
return
}
issuer := beego.AppConfig.String("appname")
accountName := user.GetId()
mfaProps, err := MfaUtil.Initiate(c.Ctx, issuer, accountName)
if err != nil {
c.ResponseError(err.Error())
return
}
resp := mfaProps
c.ResponseOk(resp)
}
// MfaSetupVerify
// @Title MfaSetupVerify
// @Tag MFA API
// @Description setup verify totp
// @param secret form string true "MFA secret"
// @param passcode form string true "MFA passcode"
// @Success 200 {object} Response object
// @router /mfa/setup/verify [post]
func (c *ApiController) MfaSetupVerify() {
authType := c.Ctx.Request.Form.Get("type")
passcode := c.Ctx.Request.Form.Get("passcode")
if authType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode")
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
err := MfaUtil.SetupVerify(c.Ctx, passcode)
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseOk(http.StatusText(http.StatusOK))
}
}
// MfaSetupEnable
// @Title MfaSetupEnable
// @Tag MFA API
// @Description enable totp
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param type form string true "MFA auth type"
// @Success 200 {object} Response object
// @router /mfa/setup/enable [post]
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")
user := object.GetUser(util.GetId(owner, name))
if user == nil {
c.ResponseError("User doesn't exist")
return
}
twoFactor := object.GetMfaUtil(authType, nil)
err := twoFactor.Enable(c.Ctx, user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(http.StatusText(http.StatusOK))
}
// DeleteMfa
// @Title DeleteMfa
// @Tag MFA API
// @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)
user := object.GetUser(userId)
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
object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
c.ResponseOk(user.MultiFactorAuths)
}
// SetPreferredMfa
// @Title SetPreferredMfa
// @Tag MFA API
// @Description: Set specific Mfa Preferred
// @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 /set-preferred-mfa [post]
func (c *ApiController) SetPreferredMfa() {
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)
user := object.GetUser(userId)
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
}
}
object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
c.ResponseOk(user.MultiFactorAuths)
}

View File

@ -27,10 +27,12 @@ import (
)
const (
SignupVerification = "signup"
ResetVerification = "reset"
LoginVerification = "login"
ForgetVerification = "forget"
SignupVerification = "signup"
ResetVerification = "reset"
LoginVerification = "login"
ForgetVerification = "forget"
MfaSetupVerification = "mfaSetup"
MfaAuthVerification = "mfaAuth"
)
// SendVerificationCode ...
@ -78,6 +80,11 @@ func (c *ApiController) SendVerificationCode() {
user = object.GetUser(util.GetId(owner, vform.CheckUser))
}
// mfaSessionData != nil, means method is MfaSetupVerification
if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil {
user = object.GetUser(mfaSessionData.UserId)
}
sendResp := errors.New("invalid dest type")
switch vform.Type {
@ -99,6 +106,11 @@ func (c *ApiController) SendVerificationCode() {
}
} else if vform.Method == ResetVerification {
user = c.getCurrentUser()
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false)
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
}
provider := application.GetEmailProvider()
@ -119,6 +131,13 @@ func (c *ApiController) SendVerificationCode() {
if user = c.getCurrentUser(); user != nil {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
vform.CountryCode = mfaProps.CountryCode
}
provider := application.GetSmsProvider()
@ -130,6 +149,11 @@ func (c *ApiController) SendVerificationCode() {
}
}
if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaSmsCountryCodeSession, vform.CountryCode)
c.SetSession(object.MfaSmsDestSession, vform.Dest)
}
if sendResp != nil {
c.ResponseError(sendResp.Error())
} else {

View File

@ -50,4 +50,8 @@ type AuthForm struct {
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
}

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Service %s und %s stimmen nicht überein"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Nicht in der Lage, die E-Mail-Änderungsregel zu erhalten.",
"Unable to get the phone modify rule.": "Nicht in der Lage, die Telefon-Änderungsregel zu erhalten.",
"Unknown type": "Unbekannter Typ",
"Wrong parameter": "Falscher Parameter",
"Wrong verification code!": "Falscher Bestätigungscode!",
"You should verify your code in %d min!": "Du solltest deinen Code in %d Minuten verifizieren!",
"the user does not exist, please sign up first": "Der Benutzer existiert nicht, bitte zuerst anmelden"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong parameter": "Wrong parameter",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Los servicios %s y %s no coinciden"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "No se puede obtener la regla de modificación de correo electrónico.",
"Unable to get the phone modify rule.": "No se pudo obtener la regla de modificación del teléfono.",
"Unknown type": "Tipo desconocido",
"Wrong parameter": "Parámetro incorrecto",
"Wrong verification code!": "¡Código de verificación incorrecto!",
"You should verify your code in %d min!": "¡Deberías verificar tu código en %d minutos!",
"the user does not exist, please sign up first": "El usuario no existe, por favor regístrese primero"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Les services %s et %s ne correspondent pas"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Incapable d'obtenir la règle de modification de courriel.",
"Unable to get the phone modify rule.": "Impossible d'obtenir la règle de modification de téléphone.",
"Unknown type": "Type inconnu",
"Wrong parameter": "Mauvais paramètre",
"Wrong verification code!": "Mauvais code de vérification !",
"You should verify your code in %d min!": "Vous devriez vérifier votre code en %d min !",
"the user does not exist, please sign up first": "L'utilisateur n'existe pas, veuillez vous inscrire d'abord"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Layanan %s dan %s tidak cocok"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.",
"Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.",
"Unknown type": "Tipe tidak diketahui",
"Wrong parameter": "Parameter yang salah",
"Wrong verification code!": "Kode verifikasi salah!",
"You should verify your code in %d min!": "Anda harus memverifikasi kode Anda dalam %d menit!",
"the user does not exist, please sign up first": "Pengguna tidak ada, silakan daftar terlebih dahulu"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "サービス%sと%sは一致しません"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "所属は空白にできません",
"DisplayName cannot be blank": "表示名は空白にできません",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "電子メール変更規則を取得できません。",
"Unable to get the phone modify rule.": "電話の変更ルールを取得できません。",
"Unknown type": "不明なタイプ",
"Wrong parameter": "誤ったパラメータ",
"Wrong verification code!": "誤った検証コードです!",
"You should verify your code in %d min!": "あなたは%d分であなたのコードを確認する必要があります",
"the user does not exist, please sign up first": "ユーザーは存在しません。まず登録してください"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "서비스 %s와 %s는 일치하지 않습니다"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "이메일 수정 규칙을 가져올 수 없습니다.",
"Unable to get the phone modify rule.": "전화 수정 규칙을 가져올 수 없습니다.",
"Unknown type": "알 수 없는 유형",
"Wrong parameter": "잘못된 매개 변수입니다",
"Wrong verification code!": "잘못된 인증 코드입니다!",
"You should verify your code in %d min!": "당신은 %d분 안에 코드를 검증해야 합니다!",
"the user does not exist, please sign up first": "사용자가 존재하지 않습니다. 먼저 회원 가입 해주세요"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Сервисы %s и %s не совпадают"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
"DisplayName cannot be blank": "Имя отображения не может быть пустым",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
"Unable to get the phone modify rule.": "Невозможно получить правило изменения телефона.",
"Unknown type": "Неизвестный тип",
"Wrong parameter": "Неправильный параметр",
"Wrong verification code!": "Неправильный код подтверждения!",
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "Dịch sang tiếng Việt: Dịch vụ %s và %s không khớp"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
"DisplayName cannot be blank": "Tên hiển thị không thể để trống",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "Không thể lấy quy tắc sửa đổi email.",
"Unable to get the phone modify rule.": "Không thể thay đổi quy tắc trên điện thoại.",
"Unknown type": "Loại không xác định",
"Wrong parameter": "Tham số không đúng",
"Wrong verification code!": "Mã xác thực sai!",
"You should verify your code in %d min!": "Bạn nên kiểm tra mã của mình trong %d phút!",
"the user does not exist, please sign up first": "Người dùng không tồn tại, vui lòng đăng ký trước"

View File

@ -23,6 +23,14 @@
"cas": {
"Service %s and %s do not match": "服务%s与%s不匹配"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "工作单位不可为空",
"DisplayName cannot be blank": "显示名称不可为空",
@ -130,7 +138,6 @@
"Unable to get the email modify rule.": "无法获取邮箱修改规则",
"Unable to get the phone modify rule.": "无法获取手机号修改规则",
"Unknown type": "未知类型",
"Wrong parameter": "参数错误",
"Wrong verification code!": "验证码错误!",
"You should verify your code in %d min!": "请在 %d 分钟内输入正确验证码",
"the user does not exist, please sign up first": "用户不存在,请先注册"

View File

@ -67,6 +67,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}

107
object/mfa.go Normal file
View File

@ -0,0 +1,107 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/beego/beego/context"
)
type MfaSessionData struct {
UserId string
}
type MfaProps struct {
Id string `json:"id"`
IsPreferred bool `json:"isPreferred"`
AuthType string `json:"type" form:"type"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
}
type MfaInterface interface {
SetupVerify(ctx *context.Context, passCode string) error
Verify(passCode string) error
Initiate(ctx *context.Context, name1 string, name2 string) (*MfaProps, error)
Enable(ctx *context.Context, user *User) error
}
const (
SmsType = "sms"
TotpType = "app"
)
const (
MfaSessionUserId = "MfaSessionUserId"
NextMfa = "NextMfa"
)
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
switch providerType {
case SmsType:
return NewSmsTwoFactor(config)
case TotpType:
return nil
}
return nil
}
func RecoverTfs(user *User, recoveryCode string) error {
hit := false
twoFactor := user.GetPreferMfa(false)
if len(twoFactor.RecoveryCodes) == 0 {
return fmt.Errorf("do not have recovery codes")
}
for _, code := range twoFactor.RecoveryCodes {
if code == recoveryCode {
hit = true
break
}
}
if !hit {
return fmt.Errorf("recovery code not found")
}
affected := UpdateUser(user.GetId(), user, []string{"two_factor_auth"}, user.IsAdminUser())
if !affected {
return fmt.Errorf("")
}
return nil
}
func GetMaskedProps(props *MfaProps) *MfaProps {
maskedProps := &MfaProps{
AuthType: props.AuthType,
Id: props.Id,
IsPreferred: props.IsPreferred,
}
if props.AuthType == SmsType {
if !util.IsEmailValid(props.Secret) {
maskedProps.Secret = util.GetMaskedPhone(props.Secret)
} else {
maskedProps.Secret = util.GetMaskedEmail(props.Secret)
}
}
return maskedProps
}

120
object/mfa_sms.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/beego/beego/context"
"github.com/google/uuid"
)
const (
MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest"
MfaSmsRecoveryCodesSession = "mfa_recovery_codes"
)
type SmsMfa struct {
Config *MfaProps
}
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
if countryCode != "" {
dest, _ = util.GetE164Number(dest, countryCode)
}
if result := CheckVerificationCode(dest, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg)
}
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)
}
if result := CheckVerificationCode(mfa.Config.Secret, passCode, "en"); result.Code != VerificationSuccess {
return errors.New(result.Msg)
}
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 := UpdateUser(user.GetId(), user, []string{"multi_factor_auths"}, user.IsAdminUser())
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,
}
}
return &SmsMfa{
Config: config,
}
}

View File

@ -157,6 +157,7 @@ type User struct {
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
MultiFactorAuths []*MfaProps `json:"multiFactorAuths"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
@ -401,6 +402,12 @@ func GetMaskedUser(user *User) *User {
manageAccount.Password = "***"
}
}
if user.MultiFactorAuths != nil {
for i, props := range user.MultiFactorAuths {
user.MultiFactorAuths[i] = GetMaskedProps(props)
}
}
return user
}
@ -733,3 +740,35 @@ func (user *User) refreshAvatar() bool {
return false
}
func (user *User) IsMfaEnabled() bool {
return len(user.MultiFactorAuths) > 0
}
func (user *User) GetPreferMfa(masked bool) *MfaProps {
if len(user.MultiFactorAuths) == 0 {
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]
}
}

View File

@ -265,6 +265,13 @@ 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) {
item := GetAccountItemByName("Multi-factor authentication", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsAdmin != newUser.IsAdmin {
item := GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item)

View File

@ -77,7 +77,7 @@ func getObject(ctx *context.Context) (string, string) {
body := ctx.Input.RequestBody
if len(body) == 0 {
return "", ""
return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name")
}
var obj Object

View File

@ -238,6 +238,12 @@ func initAPI() {
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate")
beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify")
beego.Router("/api/mfa/setup/enable", &controllers.ApiController{}, "POST:MfaSetupEnable")
beego.Router("/api/delete-mfa", &controllers.ApiController{}, "POST:DeleteMfa")
beego.Router("/api/set-preferred-mfa", &controllers.ApiController{}, "POST:SetPreferredMfa")
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")

View File

@ -59,6 +59,9 @@ func GetCountryCode(prefix string, phone string) (string, error) {
if err != nil {
return "", err
}
if err != nil {
return "", err
}
countryCode := phonenumbers.GetRegionCodeForNumber(phoneNumber)
if countryCode == "" {

View File

@ -82,6 +82,12 @@
}
],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"react/no-unknown-property": [
"error",
{
"ignore": ["css"]
}
],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
@ -98,7 +104,6 @@
"no-console": "error",
"eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off",
"react/display-name": "off",
"react/react-in-jsx-scope": "off",

View File

@ -24,6 +24,7 @@
"i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"qs": "^6.10.2",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",

View File

@ -76,6 +76,7 @@ import AdapterEditPage from "./AdapterEditPage";
import {withTranslation} from "react-i18next";
import ThemeSelect from "./common/select/ThemeSelect";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
const {Header, Footer, Content} = Layout;
@ -560,6 +561,7 @@ class App extends Component {
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/mfa-authentication/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

View File

@ -362,6 +362,8 @@ class OrganizationEditPage extends React.Component {
submitOrganizationEdit(willExist) {
const organization = Setting.deepCopy(this.state.organization);
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
.then((res) => {
if (res.status === "ok") {

View File

@ -63,6 +63,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},

View File

@ -1057,13 +1057,17 @@ export function getMaskedEmail(email) {
return `${username}@${domainTokens.join(".")}`;
}
export function IsEmail(s) {
return s.includes("@");
}
export function getArrayItem(array, key, value) {
const res = array.filter(item => item[key] === value)[0];
return res;
}
export function getDeduplicatedArray(array, filterArray, key) {
const res = array.filter(item => filterArray.filter(filterItem => filterItem[key] === item[key]).length === 0);
const res = array.filter(item => !filterArray.some(tableItem => tableItem[key] === item[key]));
return res;
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Result, Row, Select, Spin, Switch} from "antd";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Spin, Switch, Tag} from "antd";
import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
@ -30,6 +30,11 @@ import WebAuthnCredentialTable from "./table/WebauthnCredentialTable";
import ManagedAccountTable from "./table/ManagedAccountTable";
import PropertyTable from "./table/propertyTable";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined} from "@ant-design/icons";
import {SmsMfaType} from "./auth/MfaSetupPage";
import * as MfaBackend from "./backend/MfaBackend";
const {Option} = Select;
@ -64,6 +69,7 @@ class UserEditPage extends React.Component {
if (data.status === null || data.status !== "error") {
this.setState({
user: data,
multiFactorAuths: data?.multiFactorAuths ?? [],
});
}
this.setState({
@ -142,6 +148,58 @@ 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={{
textAlign: "center",
marginTop: 12,
height: 32,
lineHeight: "32px",
}}
>
<Button onClick={() => {
this.setState({
multiFactorAuths: Setting.addRow(table, {"type": type}),
});
}}>{i18next.t("general:Add")}</Button>
</div>;
};
deleteMfa = (id) => {
this.setState({
RemoveMfaLoading: true,
});
DeleteMfa({
id: id,
owner: this.state.user.owner,
name: this.state.user.name,
}).then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
multiFactorAuths: res.data,
});
} else {
Setting.showMessage("error", i18next.t("general:Failed to delete"));
}
}).finally(() => {
this.setState({
RemoveMfaLoading: false,
});
});
};
renderAccountItem(accountItem) {
if (!accountItem.visible) {
return null;
@ -695,6 +753,74 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Multi-factor authentication") {
return (
!this.isSelfOrAdmin() ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{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> :
<Tag icon={<CheckCircleOutlined />} color="success">
{i18next.t("general:Enabled")}
</Tag>
}
{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>
</Card>
</Col>
</Row>
)
);
} else if (accountItem.name === "WebAuthn credentials") {
return (
<Row style={{marginTop: "20px"}} >

View File

@ -48,7 +48,7 @@ export function getEmailAndPhone(organization, username) {
export function oAuthParamsToQuery(oAuthParams) {
// login
if (oAuthParams === null) {
if (oAuthParams === null || oAuthParams === undefined) {
return "";
}

View File

@ -33,6 +33,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal} from "../common/modal/CaptchaModal";
import {CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
import {MfaAuthVerifyForm, NextMfa} from "./MfaAuthVerifyForm";
class LoginPage extends React.Component {
constructor(props) {
@ -323,7 +324,7 @@ class LoginPage extends React.Component {
this.populateOauthValues(values);
AuthBackend.login(values, oAuthParams)
.then((res) => {
if (res.status === "ok") {
const callback = (res) => {
const responseType = values["type"];
if (responseType === "login") {
@ -350,6 +351,25 @@ class LoginPage extends React.Component {
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
}
}
};
if (res.status === "ok") {
callback();
} else if (res.status === NextMfa) {
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data}
formValues={values}
oAuthParams={oAuthParams}
application={this.getApplicationObj()}
onFail={() => {
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
}}
onSuccess={(res) => callback(res)}
/>);
},
});
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -827,12 +847,9 @@ class LoginPage extends React.Component {
Setting.renderLogo(application)
}
<LanguageSelect languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
{this.state.getVerifyTotp !== undefined ? null : this.renderSignedInBox()}
{this.state.getVerifyTotp !== undefined ? null : this.renderForm(application)}
{this.state.getVerifyTotp !== undefined ? this.state.getVerifyTotp() : null}
</div>
</div>
</div>

View File

@ -0,0 +1,120 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 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";
export const NextMfa = "NextMfa";
export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, application, onSuccess, onFail}) {
formValues.password = "";
formValues.username = "";
const [loading, setLoading] = useState(false);
const [type, setType] = useState(mfaProps.type);
const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => {
setLoading(true);
const values = {...formValues, passcode, mfaType: type};
AuthBackend.login(values, oAuthParams).then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).finally(() => {
setLoading(false);
});
};
const recover = () => {
setLoading(true);
AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => {
if (res.status === "ok") {
onSuccess();
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).finally(() => {
setLoading(false);
});
};
switch (type) {
case SmsMfaType:
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor authentication")}
</div>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor authentication description")}
</div>
<MfaSmsVerifyForm
mfaProps={mfaProps}
onFinish={verify}
application={application}
/>
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType("recovery");
}}>
{i18next.t("mfa:Use a recovery code")}
</a>
</span>
</div>
);
case "recovery":
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
{i18next.t("mfa:Multi-factor recover")}
</div>
<div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor recover description")}
</div>
<Input placeholder={i18next.t("mfa:Recovery code")}
style={{marginBottom: 24}}
type={"passcode"}
size={"large"}
onChange={event => setRecoveryCode(event.target.value)}
/>
<Button style={{width: "100%", marginBottom: 20}} size={"large"} loading={loading}
type={"primary"} onClick={() => {
recover();
}}>{i18next.t("forget:Verify")}
</Button>
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType(mfaProps.type);
}}>
{i18next.t("mfa:Use SMS verification code")}
</a>
</span>
</div>
);
default:
return null;
}
}

View File

@ -0,0 +1,275 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
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";
const {Step} = Steps;
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({password}) => {
const data = {...user, password};
UserBackend.checkUserPassword(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.finally(() => {
form.setFieldsValue({password: ""});
});
};
return (
<Form
form={form}
style={{width: "300px", marginTop: "20px"}}
onFinish={onFinish}
>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
}
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, type: mfaProps.type, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
})
.finally(() => {
form.setFieldsValue({passcode: ""});
});
};
if (mfaProps.type === SmsMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />;
} else if (mfaProps.type === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
type: mfaProps.type,
...user,
};
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
}
).finally(() => {
setLoading(false);
});
};
return (
<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>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
class MfaSetupPage extends React.Component {
constructor(props) {
super(props);
this.state = {
account: props.account,
current: 0,
type: props.type ?? SmsMfaType,
mfaProps: null,
};
}
componentDidMount() {
this.getApplication();
}
getApplication() {
ApplicationBackend.getApplication("admin", this.state.account.signupApplication)
.then((application) => {
if (application !== null) {
this.setState({
application: application,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to get application"));
}
});
}
getUser() {
return {
name: this.state.account.name,
owner: this.state.account.owner,
};
}
renderStep() {
switch (this.state.current) {
case 0:
return <CheckPasswordForm
user={this.getUser()}
onSuccess={() => {
MfaBackend.MfaSetupInitiate({
type: this.state.type,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
current: this.state.current + 1,
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}}
/>;
case 1:
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"));
}}
/>;
case 2:
return <EnableMfaForm user={this.getUser()} mfaProps={{type: this.state.type, ...this.state.mfaProps}}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
Setting.goToLinkSoft(this, "/account");
}}
onFail={(res) => {
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
}} />;
default:
return null;
}
}
render() {
if (!this.props.account) {
return (
<Result
status="403"
title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
return (
<Row>
<Col span={24} style={{justifyContent: "center"}}>
<Row>
<Col span={24}>
<div style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("mfa:Protect your account with Multi-factor authentication")}</div>
<div style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</div>
</Col>
</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>
</Col>
</Row>
</Col>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "10px", textAlign: "center"}}>{this.renderStep()}</div>
</Col>
</Row>
);
}
}
export default MfaSetupPage;

View File

@ -0,0 +1,163 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Form, Input, Row} from "antd";
import i18next from "i18next";
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {SendCodeInput} from "../common/SendCodeInput";
import * as Setting from "../Setting";
import React from "react";
import QRCode from "qrcode.react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
const [dest, setDest] = React.useState(mfaProps?.secret ?? "");
const [form] = Form.useForm();
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{mfaProps?.secret !== undefined ?
<div style={{marginBottom: 20}}>
{Setting.IsEmail(dest) ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{Setting.IsEmail(dest) ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</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>
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={mfaProps?.id === undefined ? "mfaSetup" : "mfaAuth"}
onButtonClickArgs={[dest, Setting.IsEmail(dest) ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};
export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
<Row type="flex" justify="center" align="middle">
<Col>
<QRCode value={mfaProps.url} size={200} />
</Col>
</Row>
<Row type="flex" justify="center" align="middle">
<Col>
{Setting.getLabel(
i18next.t("mfa:Multi-factor secret"),
i18next.t("mfa:Multi-factor secret - Tooltip")
)}
:
</Col>
<Col>
<Input value={mfaProps.secret} />
</Col>
<Col>
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Col>
</Row>
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
};

View File

@ -0,0 +1,76 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function MfaSetupInitiate(values) {
const formData = new FormData();
formData.append("owner", values.owner);
formData.append("name", values.name);
formData.append("type", values.type);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, {
method: "POST",
credentials: "include",
body: formData,
}).then(res => res.json());
}
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("passcode", values.passcode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST",
credentials: "include",
body: formData,
}).then(res => res.json());
}
export function MfaSetupEnable(values) {
const formData = new FormData();
formData.append("type", values.type);
formData.append("owner", values.owner);
formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
method: "POST",
credentials: "include",
body: formData,
}).then(res => res.json());
}
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`, {
method: "POST",
credentials: "include",
body: formData,
}).then(res => res.json());
}
export function SetPreferredMfa(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/set-preferred-mfa`, {
method: "POST",
credentials: "include",
body: formData,
}).then((res) => res.json());
}

View File

@ -203,3 +203,11 @@ export function verifyCode(values) {
},
}).then(res => res.json());
}
export function checkUserPassword(values) {
return fetch(`${Setting.ServerUrl}/api/check-user-password`, {
method: "POST",
credentials: "include",
body: JSON.stringify(values),
}).then(res => res.json());
}

View File

@ -21,10 +21,8 @@ import {CaptchaModal} from "./modal/CaptchaModal";
const {Search} = Input;
export const SendCodeInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs, application, method, countryCode} = props;
export const SendCodeInput = ({value, disabled, textBefore, onChange, onButtonClickArgs, application, method, countryCode}) => {
const [visible, setVisible] = React.useState(false);
const [buttonLeftTime, setButtonLeftTime] = React.useState(0);
const [buttonLoading, setButtonLoading] = React.useState(false);
@ -62,6 +60,7 @@ export const SendCodeInput = (props) => {
<Search
addonBefore={textBefore}
disabled={disabled}
value={value}
prefix={<SafetyOutlined />}
placeholder={i18next.t("code:Enter your code")}
onChange={e => onChange(e.target.value)}

View File

@ -29,8 +29,6 @@ export const THEMES = {
comic: `${Setting.StaticBaseUrl}/img/theme_comic.svg`,
};
Object.values(THEMES).map(value => new Image().src = value);
const themeTypes = {
default: "Default", // i18next.t("theme:Default")
dark: "Dark", // i18next.t("theme:Dark")

View File

@ -193,11 +193,16 @@
"Edit": "Bearbeiten",
"Email": "E-Mail",
"Email - Tooltip": "Gültige E-Mail-Adresse",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Fehler beim hinzufügen",
"Failed to connect to server": "Die Verbindung zum Server konnte nicht hergestellt werden",
"Failed to delete": "Konnte nicht gelöscht werden",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Konnte nicht gespeichert werden",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"First name": "Vorname",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
"Phone": "Telefon",
"Phone - Tooltip": "Telefonnummer",
"Phone or email": "Phone or email",
"Preview": "Vorschau",
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
"Products": "Produkte",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Modell bearbeiten",
"Model text": "Modelltext",
@ -797,6 +833,7 @@
"Location - Tooltip": "Stadt des Wohnsitzes",
"Managed accounts": "Verwaltete Konten",
"Modify password...": "Passwort ändern...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Neue E-Mail",
"New Password": "Neues Passwort",
"New User": "Neuer Benutzer",

View File

@ -193,11 +193,16 @@
"Edit": "Edit",
"Email": "Email",
"Email - Tooltip": "Valid email address",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Failed to add",
"Failed to connect to server": "Failed to connect to server",
"Failed to delete": "Failed to delete",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Failed to save",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"First name": "First name",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "Permissions owned by this user",
"Phone": "Phone",
"Phone - Tooltip": "Phone number",
"Phone or email": "Phone or email",
"Preview": "Preview",
"Preview - Tooltip": "Preview the configured effects",
"Products": "Products",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Setup Multi-factor authentication",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "If you are unable to access your device, enter your recovery code to verify your identity",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Edit Model",
"Model text": "Model text",
@ -797,6 +833,7 @@
"Location - Tooltip": "City of residence",
"Managed accounts": "Managed accounts",
"Modify password...": "Modify password...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "New Email",
"New Password": "New Password",
"New User": "New User",

View File

@ -193,11 +193,16 @@
"Edit": "Editar",
"Email": "Correo electrónico",
"Email - Tooltip": "Dirección de correo electrónico válida",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "No se pudo agregar",
"Failed to connect to server": "No se pudo conectar al servidor",
"Failed to delete": "No se pudo eliminar",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "No se pudo guardar",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL del icono Favicon utilizado en todas las páginas de Casdoor de la organización",
"First name": "Nombre de pila",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "Permisos propiedad de este usuario",
"Phone": "Teléfono",
"Phone - Tooltip": "Número de teléfono",
"Phone or email": "Phone or email",
"Preview": "Avance",
"Preview - Tooltip": "Vista previa de los efectos configurados",
"Products": "Productos",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Editar modelo",
"Model text": "Texto modelo",
@ -797,6 +833,7 @@
"Location - Tooltip": "Ciudad de residencia",
"Managed accounts": "Cuentas gestionadas",
"Modify password...": "Modificar contraseña...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Nuevo correo electrónico",
"New Password": "Nueva contraseña",
"New User": "Nuevo Usuario",

View File

@ -193,11 +193,16 @@
"Edit": "Modifier",
"Email": "Email",
"Email - Tooltip": "Adresse e-mail valide",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Échec d'ajout",
"Failed to connect to server": "Échec de la connexion au serveur",
"Failed to delete": "Échec de la suppression",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Échec de sauvegarde",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "L'URL de l'icône Favicon utilisée dans toutes les pages Casdoor de l'organisation",
"First name": "Prénom",
@ -244,6 +249,7 @@
"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",
"Preview": "Aperçu",
"Preview - Tooltip": "Prévisualisez les effets configurés",
"Products": "Produits",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Modifier le modèle",
"Model text": "Texte modèle",
@ -797,6 +833,7 @@
"Location - Tooltip": "Ville de résidence",
"Managed accounts": "Comptes gérés",
"Modify password...": "Modifier le mot de passe...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Nouveau courrier électronique",
"New Password": "Nouveau mot de passe",
"New User": "Nouvel utilisateur",

View File

@ -193,11 +193,16 @@
"Edit": "Mengedit",
"Email": "Email",
"Email - Tooltip": "Alamat email yang valid",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Gagal menambahkan",
"Failed to connect to server": "Gagal terhubung ke server",
"Failed to delete": "Gagal menghapus",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Gagal menyimpan",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL ikon Favicon yang digunakan di semua halaman Casdoor organisasi",
"First name": "Nama depan",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "Izin dimiliki oleh pengguna ini",
"Phone": "Telepon",
"Phone - Tooltip": "Nomor telepon",
"Phone or email": "Phone or email",
"Preview": "Tinjauan",
"Preview - Tooltip": "Mengawali pratinjau efek yang sudah dikonfigurasi",
"Products": "Produk",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Mengedit Model",
"Model text": "Teks Model",
@ -797,6 +833,7 @@
"Location - Tooltip": "Kota tempat tinggal",
"Managed accounts": "Akun yang dikelola",
"Modify password...": "Mengubah kata sandi...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Email baru",
"New Password": "Kata Sandi Baru",
"New User": "Pengguna Baru",

View File

@ -193,11 +193,16 @@
"Edit": "編集",
"Email": "電子メール",
"Email - Tooltip": "有効な電子メールアドレス",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "追加できませんでした",
"Failed to connect to server": "サーバーに接続できませんでした",
"Failed to delete": "削除に失敗しました",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "保存に失敗しました",
"Failed to verify": "Failed to verify",
"Favicon": "ファビコン",
"Favicon - Tooltip": "組織のすべてのCasdoorページに使用されるFaviconアイコンのURL",
"First name": "名前",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "このユーザーが所有する権限",
"Phone": "電話",
"Phone - Tooltip": "電話番号",
"Phone or email": "Phone or email",
"Preview": "プレビュー",
"Preview - Tooltip": "構成されたエフェクトをプレビューする",
"Products": "製品",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "編集モデル",
"Model text": "モデルテキスト",
@ -797,6 +833,7 @@
"Location - Tooltip": "居住都市",
"Managed accounts": "管理アカウント",
"Modify password...": "パスワードを変更する...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "新しいメール",
"New Password": "新しいパスワード",
"New User": "新しいユーザー",

View File

@ -193,11 +193,16 @@
"Edit": "편집",
"Email": "이메일",
"Email - Tooltip": "유효한 이메일 주소",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "추가하지 못했습니다",
"Failed to connect to server": "서버에 연결하지 못했습니다",
"Failed to delete": "삭제에 실패했습니다",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "저장에 실패했습니다",
"Failed to verify": "Failed to verify",
"Favicon": "파비콘",
"Favicon - Tooltip": "조직의 모든 Casdoor 페이지에서 사용되는 Favicon 아이콘 URL",
"First name": "이름",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "이 사용자가 소유한 권한",
"Phone": "전화기",
"Phone - Tooltip": "전화 번호",
"Phone or email": "Phone or email",
"Preview": "미리보기",
"Preview - Tooltip": "구성된 효과를 미리보기합니다",
"Products": "제품들",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "편집 형태 모델",
"Model text": "모델 텍스트",
@ -797,6 +833,7 @@
"Location - Tooltip": "거주 도시",
"Managed accounts": "관리 계정",
"Modify password...": "비밀번호 수정하기...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "새 이메일",
"New Password": "새로운 비밀번호",
"New User": "새로운 사용자",

View File

@ -193,11 +193,16 @@
"Edit": "Редактировать",
"Email": "Электронная почта",
"Email - Tooltip": "Действительный адрес электронной почты",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Не удалось добавить",
"Failed to connect to server": "Не удалось подключиться к серверу",
"Failed to delete": "Не удалось удалить",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Не удалось сохранить",
"Failed to verify": "Failed to verify",
"Favicon": "Фавикон",
"Favicon - Tooltip": "URL иконки Favicon, используемый на всех страницах организации Casdoor",
"First name": "Имя",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
"Phone": "Телефон",
"Phone - Tooltip": "Номер телефона",
"Phone or email": "Phone or email",
"Preview": "Предварительный просмотр",
"Preview - Tooltip": "Предварительный просмотр настроенных эффектов",
"Products": "Продукты",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Редактировать модель",
"Model text": "Модельный текст",
@ -797,6 +833,7 @@
"Location - Tooltip": "Город проживания",
"Managed accounts": "Управляемые счета",
"Modify password...": "Изменить пароль...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Новое электронное письмо",
"New Password": "Новый пароль",
"New User": "Новый пользователь",

View File

@ -193,11 +193,16 @@
"Edit": "Chỉnh sửa",
"Email": "Email: Thư điện tử",
"Email - Tooltip": "Địa chỉ email hợp lệ",
"Enable": "Enable",
"Enabled": "Enabled",
"Enabled successfully": "Enabled successfully",
"Failed to add": "Không thể thêm được",
"Failed to connect to server": "Không thể kết nối đến máy chủ",
"Failed to delete": "Không thể xoá",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to save": "Không thể lưu được",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL biểu tượng Favicon được sử dụng trong tất cả các trang của tổ chức Casdoor",
"First name": "Tên đầu tiên",
@ -244,6 +249,7 @@
"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",
"Preview": "Xem trước",
"Preview - Tooltip": "Xem trước các hiệu ứng đã cấu hình",
"Products": "Sản phẩm",
@ -367,6 +373,36 @@
"Text": "Text",
"Text - Tooltip": "Text - Tooltip"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Each time you sign in to your Account, you'll need your password and a authentication code",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
"Multi-factor authentication": "Multi-factor authentication",
"Multi-factor authentication - Tooltip ": "Multi-factor authentication - Tooltip ",
"Multi-factor authentication description": "Multi-factor authentication description",
"Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"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 SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
"Verify Code": "Verify Code",
"Verify Password": "Verify Password",
"Your email is": "Your email is",
"Your phone is": "Your phone is",
"preferred": "preferred"
},
"model": {
"Edit Model": "Chỉnh sửa mô hình",
"Model text": "Văn bản mẫu",
@ -797,6 +833,7 @@
"Location - Tooltip": "Thành phố cư trú",
"Managed accounts": "Quản lý tài khoản",
"Modify password...": "Sửa đổi mật khẩu...",
"Multi-factor authentication": "Multi-factor authentication",
"New Email": "Email mới",
"New Password": "Mật khẩu mới",
"New User": "Người dùng mới",

View File

@ -193,11 +193,16 @@
"Edit": "编辑",
"Email": "电子邮箱",
"Email - Tooltip": "合法的电子邮件地址",
"Enable": "启用",
"Enabled": "已开启",
"Enabled successfully": "启用成功",
"Failed to add": "添加失败",
"Failed to connect to server": "连接服务器失败",
"Failed to delete": "删除失败",
"Failed to enable": "启用失败",
"Failed to get answer": "获取回答失败",
"Failed to save": "保存失败",
"Failed to verify": "验证失败",
"Favicon": "Favicon",
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标URL",
"First name": "名字",
@ -244,6 +249,7 @@
"Permissions - Tooltip": "该用户所拥有的权限",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone or email": "手机或邮箱",
"Preview": "预览",
"Preview - Tooltip": "可预览所配置的效果",
"Products": "商品",
@ -367,6 +373,36 @@
"Text": "内容",
"Text - Tooltip": "消息的内容"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "每次登录帐户时,都需要密码和认证码",
"Failed to get application": "获取应用失败",
"Failed to initiate MFA": "初始化 MFA 失败",
"Have problems?": "遇到问题?",
"Multi-factor authentication": "多因素认证",
"Multi-factor authentication - Tooltip ": "多因素认证 - Tooltip ",
"Multi-factor authentication description": "您已经启用多因素认证,请输入认证码",
"Multi-factor methods": "多因素认证方式",
"Multi-factor recover": "重置多因素认证",
"Multi-factor recover description": "如果您无法访问您的设备,输入您的多因素认证恢复代码来确认您的身份",
"Multi-factor secret": "多因素密钥",
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
"Passcode": "认证码",
"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 SMS verification code": "使用手机或电子邮件发送验证码认证",
"Use a recovery code": "使用恢复代码",
"Verification failed": "验证失败",
"Verify Code": "验证码",
"Verify Password": "验证密码",
"Your email is": "你的电子邮件",
"Your phone is": "你的手机号",
"preferred": "首选"
},
"model": {
"Edit Model": "编辑模型",
"Model text": "模型文本",
@ -797,6 +833,7 @@
"Location - Tooltip": "居住地址所在的城市",
"Managed accounts": "托管账户",
"Modify password...": "编辑密码...",
"Multi-factor authentication": "多因素认证",
"New Email": "新邮箱",
"New Password": "新密码",
"New User": "添加用户",

View File

@ -61,6 +61,51 @@ class AccountTable extends React.Component {
this.updateTable(table);
}
getItems = () => {
return [
{name: "Organization", label: i18next.t("general:Organization")},
{name: "ID", label: i18next.t("general:ID")},
{name: "Name", label: i18next.t("general:Name")},
{name: "Display name", label: i18next.t("general:Display name")},
{name: "Avatar", label: i18next.t("general:Avatar")},
{name: "User type", label: i18next.t("general:User type")},
{name: "Password", label: i18next.t("general:Password")},
{name: "Email", label: i18next.t("general:Email")},
{name: "Phone", label: i18next.t("general:Phone")},
{name: "Country code", label: i18next.t("user:Country code")},
{name: "Country/Region", label: i18next.t("user:Country/Region")},
{name: "Location", label: i18next.t("user:Location")},
{name: "Address", label: i18next.t("user:Address")},
{name: "Affiliation", label: i18next.t("user:Affiliation")},
{name: "Title", label: i18next.t("user:Title")},
{name: "ID card type", label: i18next.t("user:ID card type")},
{name: "ID card", label: i18next.t("user:ID card")},
{name: "Homepage", label: i18next.t("user:Homepage")},
{name: "Bio", label: i18next.t("user:Bio")},
{name: "Tag", label: i18next.t("user:Tag")},
{name: "Language", label: i18next.t("user:Language")},
{name: "Gender", label: i18next.t("user:Gender")},
{name: "Birthday", label: i18next.t("user:Birthday")},
{name: "Education", label: i18next.t("user:Education")},
{name: "Score", label: i18next.t("user:Score")},
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")},
{name: "Roles", label: i18next.t("general:Roles")},
{name: "Permissions", label: i18next.t("general:Permissions")},
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
{name: "Properties", label: i18next.t("user:Properties")},
{name: "Is online", label: i18next.t("user:Is online")},
{name: "Is admin", label: i18next.t("user:Is admin")},
{name: "Is global admin", label: i18next.t("user:Is global admin")},
{name: "Is forbidden", label: i18next.t("user:Is forbidden")},
{name: "Is deleted", label: i18next.t("user:Is deleted")},
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
];
};
renderTable(table) {
const columns = [
{
@ -68,65 +113,14 @@ class AccountTable extends React.Component {
dataIndex: "name",
key: "name",
render: (text, record, index) => {
const items = [
{name: "Organization", displayName: i18next.t("general:Organization")},
{name: "ID", displayName: i18next.t("general:ID")},
{name: "Name", displayName: i18next.t("general:Name")},
{name: "Display name", displayName: i18next.t("general:Display name")},
{name: "Avatar", displayName: i18next.t("general:Avatar")},
{name: "User type", displayName: i18next.t("general:User type")},
{name: "Password", displayName: i18next.t("general:Password")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Country code", displayName: i18next.t("user:Country code")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "Location", displayName: i18next.t("user:Location")},
{name: "Address", displayName: i18next.t("user:Address")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{name: "Title", displayName: i18next.t("user:Title")},
{name: "ID card type", displayName: i18next.t("user:ID card type")},
{name: "ID card", displayName: i18next.t("user:ID card")},
{name: "Homepage", displayName: i18next.t("user:Homepage")},
{name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Language", displayName: i18next.t("user:Language")},
{name: "Gender", displayName: i18next.t("user:Gender")},
{name: "Birthday", displayName: i18next.t("user:Birthday")},
{name: "Education", displayName: i18next.t("user:Education")},
{name: "Score", displayName: i18next.t("user:Score")},
{name: "Karma", displayName: i18next.t("user:Karma")},
{name: "Ranking", displayName: i18next.t("user:Ranking")},
{name: "Signup application", displayName: i18next.t("general:Signup application")},
{name: "Roles", displayName: i18next.t("general:Roles")},
{name: "Permissions", displayName: i18next.t("general:Permissions")},
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
{name: "Properties", displayName: i18next.t("user:Properties")},
{name: "Is online", displayName: i18next.t("user:Is online")},
{name: "Is admin", displayName: i18next.t("user:Is admin")},
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", displayName: i18next.t("user:Managed accounts")},
];
const getItemDisplayName = (text) => {
const item = items.filter(item => item.name === text);
if (item.length === 0) {
return "";
}
return item[0].displayName;
};
const items = this.getItems();
return (
<Select virtual={false} style={{width: "100%"}}
value={getItemDisplayName(text)}
options={Setting.getDeduplicatedArray(items, table, "name").map(item => Setting.getOption(item.label, item.name))}
value={text}
onChange={value => {
this.updateField(table, index, "name", value);
}} >
{
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
}
</Select>
);
},

File diff suppressed because it is too large Load Diff