feat: fix bugs in MFA (#2033)

* fix: prompt mfa binding

* fix: clean session when leave promptpage

* fix: css

* fix: force enable mfa

* fix: add prompt rule

* fix: refactor directory structure

* fix: prompt notification

* fix: fix some bug and clean code

* fix: rebase

* fix: improve notification

* fix: i18n

* fix: router

* fix: prompt

* fix: remove localStorage
This commit is contained in:
Yaodong Yu 2023-07-07 12:30:07 +08:00 committed by GitHub
parent 6edfc08b28
commit 347d3d2b53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 941 additions and 607 deletions

View File

@ -78,12 +78,6 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
}
}
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferredMfaProps(true)}
return
}
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
@ -353,17 +347,26 @@ func (c *ApiController) Login() {
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
organization, err := object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
}
if user != nil && organization.HasRequiredMfa() && !user.IsMfaEnabled() {
resp.Msg = object.RequiredMfa
if object.IsNeedPromptMfa(organization, user) {
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return
}
if user.IsMfaEnabled() {
c.setMfaUserSession(user.GetId())
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
@ -649,13 +652,16 @@ 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, err := object.GetUser(mfaSession.UserId)
} else if c.getMfaUserSession() != "" {
user, err := object.GetUser(c.getMfaUserSession())
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError("expired user session")
return
}
if authForm.Passcode != "" {
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
@ -669,13 +675,15 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
}
if authForm.RecoveryCode != "" {
} else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil {
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError("missing passcode or recovery code")
return
}
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
@ -690,6 +698,7 @@ func (c *ApiController) Login() {
}
resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("")
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization

View File

@ -178,24 +178,16 @@ func (c *ApiController) SetSessionData(s *SessionData) {
c.SetSession("SessionData", util.StructToJson(s))
}
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
if data == nil {
c.SetSession(object.MfaSessionUserId, nil)
return
}
c.SetSession(object.MfaSessionUserId, data.UserId)
func (c *ApiController) setMfaUserSession(userId string) {
c.SetSession(object.MfaSessionUserId, userId)
}
func (c *ApiController) getMfaSessionData() *object.MfaSessionData {
userId := c.GetSession(object.MfaSessionUserId)
func (c *ApiController) getMfaUserSession() string {
userId := c.Ctx.Input.CruSession.Get(object.MfaSessionUserId)
if userId == nil {
return nil
return ""
}
data := &object.MfaSessionData{
UserId: userId.(string),
}
return data
return userId.(string)
}
func (c *ApiController) setExpireForSession() {

View File

@ -93,10 +93,9 @@ func (c *ApiController) SendVerificationCode() {
}
}
// mfaSessionData != nil, means method is MfaAuthVerification
if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil {
user, err = object.GetUser(mfaSessionData.UserId)
c.setMfaSessionData(nil)
// mfaUserSession != "", means method is MfaAuthVerification
if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
user, err = object.GetUser(mfaUserSession)
if err != nil {
c.ResponseError(err.Error())
return
@ -134,6 +133,8 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
} else if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaDestSession, vform.Dest)
}
provider, err := application.GetEmailProvider()
@ -164,6 +165,11 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
}
if vform.Method == MfaSetupVerification {
c.SetSession(object.MfaCountryCodeSession, vform.CountryCode)
c.SetSession(object.MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
@ -187,11 +193,6 @@ 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

@ -24,10 +24,6 @@ import (
const MfaRecoveryCodesSession = "mfa_recovery_codes"
type MfaSessionData struct {
UserId string
}
type MfaProps struct {
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`

View File

@ -24,8 +24,8 @@ import (
)
const (
MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
)
type SmsMfa struct {
@ -48,9 +48,19 @@ func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, err
}
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
destSession := ctx.Input.CruSession.Get(MfaDestSession)
if destSession == nil {
return errors.New("dest session is missing")
}
dest := destSession.(string)
if !util.IsEmailValid(dest) {
countryCodeSession := ctx.Input.CruSession.Get(MfaCountryCodeSession)
if countryCodeSession == nil {
return errors.New("country code is missing")
}
countryCode := countryCodeSession.(string)
dest, _ = util.GetE164Number(dest, countryCode)
}
@ -78,8 +88,8 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
columns = append(columns, "mfa_phone_enabled")
if user.Phone == "" {
user.Phone = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
user.CountryCode = ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
user.Phone = ctx.Input.CruSession.Get(MfaDestSession).(string)
user.CountryCode = ctx.Input.CruSession.Get(MfaCountryCodeSession).(string)
columns = append(columns, "phone", "country_code")
}
} else if mfa.Config.MfaType == EmailType {
@ -87,7 +97,7 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
columns = append(columns, "mfa_email_enabled")
if user.Email == "" {
user.Email = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
user.Email = ctx.Input.CruSession.Get(MfaDestSession).(string)
columns = append(columns, "email")
}
}
@ -96,6 +106,11 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
if err != nil {
return err
}
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaDestSession)
ctx.Input.CruSession.Delete(MfaCountryCodeSession)
return nil
}

View File

@ -72,8 +72,11 @@ func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, er
}
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
result := totp.Validate(passcode, secret)
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession)
if secret == nil {
return errors.New("totp secret is missing")
}
result := totp.Validate(passcode, secret.(string))
if result {
return nil
@ -104,6 +107,10 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
if err != nil {
return err
}
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
ctx.Input.CruSession.Delete(MfaTotpSecretSession)
return nil
}

View File

@ -476,11 +476,22 @@ func organizationChangeTrigger(oldName string, newName string) error {
return session.Commit()
}
func (org *Organization) HasRequiredMfa() bool {
func IsNeedPromptMfa(org *Organization, user *User) bool {
if org == nil || user == nil {
return false
}
for _, item := range org.MfaItems {
if item.Rule == "Required" {
if item.Name == EmailType && !user.MfaEmailEnabled {
return true
}
if item.Name == SmsType && !user.MfaPhoneEnabled {
return true
}
if item.Name == TotpType && user.TotpSecret == "" {
return true
}
}
}
return false
}

View File

@ -160,8 +160,8 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret,omitempty"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
@ -832,11 +832,14 @@ func userChangeTrigger(oldName string, newName string) error {
}
func (user *User) IsMfaEnabled() bool {
if user == nil {
return false
}
return user.PreferredMfaType != ""
}
func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
if user.PreferredMfaType == "" {
if user == nil || user.PreferredMfaType == "" {
return nil
}
return user.GetMfaProps(user.PreferredMfaType, masked)

View File

@ -15,9 +15,11 @@
import React, {Component} from "react";
import "./App.less";
import {Helmet} from "react-helmet";
import EnableMfaNotification from "./common/notifaction/EnableMfaNotification";
import GroupTreePage from "./GroupTreePage";
import GroupEditPage from "./GroupEdit";
import GroupListPage from "./GroupList";
import {MfaRuleRequired} from "./Setting";
import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
@ -64,6 +66,13 @@ import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import PaymentResultPage from "./PaymentResultPage";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import SystemInfo from "./SystemInfo";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./common/CustomGithubCorner";
@ -73,19 +82,12 @@ import * as Auth from "./auth/Auth";
import EntryPage from "./EntryPage";
import * as AuthBackend from "./auth/AuthBackend";
import AuthCallback from "./auth/AuthCallback";
import LanguageSelect from "./common/select/LanguageSelect";
import i18next from "i18next";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from "./auth/SamlCallback";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import SystemInfo from "./SystemInfo";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import i18next from "i18next";
import {withTranslation} from "react-i18next";
import LanguageSelect from "./common/select/LanguageSelect";
import ThemeSelect from "./common/select/ThemeSelect";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import OrganizationSelect from "./common/select/OrganizationSelect";
const {Header, Footer, Content} = Layout;
@ -102,6 +104,7 @@ class App extends Component {
themeAlgorithm: ["default"],
themeData: Conf.ThemeDefault,
logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)),
requiredEnableMfa: false,
};
Setting.initServerUrl();
@ -116,12 +119,28 @@ class App extends Component {
this.getAccount();
}
componentDidUpdate() {
// eslint-disable-next-line no-restricted-globals
componentDidUpdate(prevProps, prevState, snapshot) {
const uri = location.pathname;
if (this.state.uri !== uri) {
this.updateMenuKey();
}
if (this.state.account !== prevState.account) {
const requiredEnableMfa = Setting.isRequiredEnableMfa(this.state.account, this.state.account?.organization);
this.setState({
requiredEnableMfa: requiredEnableMfa,
});
}
if (this.state.requiredEnableMfa !== prevState.requiredEnableMfa || this.state.account !== prevState.account) {
if (this.state.requiredEnableMfa === true) {
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [MfaRuleRequired])
.find((item) => item.rule === MfaRuleRequired)?.name;
if (mfaType !== undefined) {
this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"});
}
}
}
}
updateMenuKey() {
@ -341,6 +360,7 @@ class App extends Component {
renderRightDropdown() {
const items = [];
if (this.state.requiredEnableMfa === false) {
items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>,
"/account"
));
@ -349,6 +369,7 @@ class App extends Component {
"/chat"
));
}
}
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout"));
@ -547,14 +568,6 @@ class App extends Component {
return res;
}
renderHomeIfLoggedIn(component) {
if (this.state.account !== null && this.state.account !== undefined) {
return <Redirect to="/" />;
} else {
return component;
}
}
renderLoginIfNotLoggedIn(component) {
if (this.state.account === null) {
sessionStorage.setItem("from", window.location.pathname);
@ -566,12 +579,6 @@ class App extends Component {
}
}
isStartPages() {
return window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/signup") ||
window.location.pathname === "/";
}
renderRouter() {
return (
<Switch>
@ -629,7 +636,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="/mfa/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} onfinish={result => this.setState({requiredEnableMfa: result})} {...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.")}
@ -659,20 +666,25 @@ class App extends Component {
const onClick = ({key}) => {
if (key === "/swagger") {
window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank");
} else {
if (this.state.requiredEnableMfa) {
Setting.showMessage("info", "Please enable MFA first!");
} else {
this.props.history.push(key);
}
}
};
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 260px)" : "260px";
return (
<Layout id="parent-area">
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}}>
<EnableMfaNotification account={this.state.account} />
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}} >
{Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link>
)}
{Setting.isMobile() ?
{this.state.requiredEnableMfa || (Setting.isMobile() ?
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
<Menu
@ -695,7 +707,7 @@ class App extends Component {
selectedKeys={[this.state.selectedMenuKey]}
style={{position: "absolute", left: "145px", right: menuStyleRight}}
/>
}
)}
{
this.renderAccountMenu()
}
@ -759,9 +771,11 @@ class App extends Component {
<EntryPage
account={this.state.account}
theme={this.state.themeData}
onUpdateAccount={(account) => {
this.onUpdateAccount(account);
onLoginSuccess={(redirectUrl) => {
localStorage.setItem("mfaRedirectUrl", redirectUrl);
this.getAccount();
}}
onUpdateAccount={(account) => this.onUpdateAccount(account)}
updataThemeData={this.setTheme}
/> :
<Switch>

View File

@ -482,6 +482,26 @@ export function isPromptAnswered(user, application) {
return true;
}
export const MfaRuleRequired = "Required";
export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) {
return false;
}
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
}
export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) {
return [];
}
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
}
export function parseObject(s) {
try {
return eval("(" + s + ")");

View File

@ -14,6 +14,7 @@
import React from "react";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd";
import {withRouter} from "react-router-dom";
import * as GroupBackend from "./backend/GroupBackend";
import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
@ -922,7 +923,7 @@ class UserEditPage extends React.Component {
}
</Space>
) : <Button type={"default"} onClick={() => {
Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`);
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
}}>
{i18next.t("mfa:Setup")}
</Button>}
@ -1118,4 +1119,4 @@ class UserEditPage extends React.Component {
}
}
export default UserEditPage;
export default withRouter(UserEditPage);

View File

@ -15,6 +15,7 @@
import React from "react";
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import {withRouter} from "react-router-dom";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import OrganizationSelect from "../common/select/OrganizationSelect";
import * as Conf from "../Conf";
@ -34,7 +35,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, RequiredMfa} from "./MfaAuthVerifyForm";
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
class LoginPage extends React.Component {
constructor(props) {
@ -254,8 +255,13 @@ class LoginPage extends React.Component {
const code = resp.data;
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const noRedirect = oAuthParams.noRedirect;
const redirectUrl = `${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`;
if (resp.data === RequiredMfa) {
this.props.onLoginSuccess(window.location.href);
return;
}
if (Setting.hasPromptPage(application) || resp.msg === RequiredMfa) {
if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount()
.then((res) => {
if (res.status === "ok") {
@ -263,13 +269,8 @@ class LoginPage extends React.Component {
account.organization = res.data2;
this.onUpdateAccount(account);
if (resp.msg === RequiredMfa) {
Setting.goToLink(`/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}&promptType=mfa`);
return;
}
if (Setting.isPromptAnswered(account, application)) {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
Setting.goToLink(redirectUrl);
} else {
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
}
@ -280,7 +281,7 @@ class LoginPage extends React.Component {
} else {
if (noRedirect === "true") {
window.close();
const newWindow = window.open(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
const newWindow = window.open(redirectUrl);
if (newWindow) {
setInterval(() => {
if (!newWindow.closed) {
@ -289,7 +290,7 @@ class LoginPage extends React.Component {
}, 1000);
}
} else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
Setting.goToLink(redirectUrl);
this.sendPopupData({type: "loginSuccess", data: {code: code, state: oAuthParams.state}}, oAuthParams.redirectUri);
}
}
@ -355,20 +356,8 @@ class LoginPage extends React.Component {
const responseType = values["type"];
if (responseType === "login") {
if (res.msg === RequiredMfa) {
AuthBackend.getAccount().then((res) => {
if (res.status === "ok") {
const account = res.data;
account.organization = res.data2;
this.onUpdateAccount(account);
}
});
Setting.goToLink(`/prompt/${this.getApplicationObj().name}?promptType=mfa`);
} else {
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
const link = Setting.getFromLink();
Setting.goToLink(link);
}
this.props.onLoginSuccess();
} else if (responseType === "code") {
this.postCodeLoginAction(res);
} else if (responseType === "token" || responseType === "id_token") {
@ -391,13 +380,12 @@ class LoginPage extends React.Component {
};
if (res.status === "ok") {
callback(res);
} else if (res.status === NextMfa) {
if (res.data === NextMfa) {
this.setState({
getVerifyTotp: () => {
return (
<MfaAuthVerifyForm
mfaProps={res.data}
mfaProps={res.data2}
formValues={values}
oAuthParams={oAuthParams}
application={this.getApplicationObj()}
@ -408,6 +396,9 @@ class LoginPage extends React.Component {
/>);
},
});
} else {
callback(res);
}
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@ -998,4 +989,4 @@ class LoginPage extends React.Component {
}
}
export default LoginPage;
export default withRouter(LoginPage);

View File

@ -12,181 +12,55 @@
// 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 React from "react";
import {Button, Col, Result, Row, Steps} from "antd";
import {withRouter} from "react-router-dom";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserBackend from "../backend/UserBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
import {CheckOutlined, KeyOutlined, UserOutlined} from "@ant-design/icons";
import CheckPasswordForm from "./mfa/CheckPasswordForm";
import MfaEnableForm from "./mfa/MfaEnableForm";
import {MfaVerifyForm} from "./mfa/MfaVerifyForm";
export const EmailMfaType = "email";
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
export const RecoveryMfaType = "recovery";
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, mfaType: mfaProps.mfaType, ...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 === undefined || mfaProps === null) {
return <div></div>;
}
if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaSmsVerifyForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaProps.mfaType === TotpMfaType) {
return <MfaTotpVerifyForm mfaProps={mfaProps} onFinish={onFinish} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
mfaType,
...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"}}>{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);
const params = new URLSearchParams(props.location.search);
const {location} = this.props;
this.state = {
account: props.account,
application: this.props.application ?? null,
application: null,
applicationName: props.account.signupApplication ?? "",
isAuthenticated: props.isAuthenticated ?? false,
isPromptPage: props.isPromptPage,
redirectUri: props.redirectUri,
current: props.current ?? 0,
mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
current: location.state?.from !== undefined ? 1 : 0,
mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType,
isPromptPage: props.isPromptPage || location.state?.from !== undefined,
};
}
componentDidMount() {
this.getApplication();
if (this.state.current === 1) {
this.initMfaProps();
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && (this.state.mfaProps === null || this.state.mfaType !== prevState.mfaType)) {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
if (this.state.mfaType !== prevState.mfaType || this.state.current !== prevState.current) {
if (this.state.current === 1) {
this.initMfaProps();
}
});
}
}
getApplication() {
if (this.state.application !== null) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((res) => {
if (res !== null) {
@ -203,11 +77,75 @@ class MfaSetupPage extends React.Component {
});
}
initMfaProps() {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
}
getUser() {
return {
name: this.state.account.name,
owner: this.state.account.owner,
return this.props.account;
}
renderMfaTypeSwitch() {
const renderSmsLink = () => {
if (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${SmsMfaType}`);
}
}>{i18next.t("mfa:Use SMS")}</Button>
);
};
const renderEmailLink = () => {
if (this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${EmailMfaType}`);
}
}>{i18next.t("mfa:Use Email")}</Button>
);
};
const renderTotpLink = () => {
if (this.state.mfaType === TotpMfaType || this.props.account.totpSecret !== "") {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${TotpMfaType}`);
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
);
};
return !this.state.isPromptPage ? (
<React.Fragment>
{renderSmsLink()}
{renderEmailLink()}
{renderTotpLink()}
</React.Fragment>
) : null;
}
renderStep() {
@ -219,19 +157,14 @@ class MfaSetupPage extends React.Component {
onSuccess={() => {
this.setState({
current: this.state.current + 1,
isAuthenticated: true,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA") + ": " + res.msg);
}}
/>
);
case 1:
if (!this.state.isAuthenticated) {
return null;
}
return (
<div>
<MfaVerifyForm
@ -244,52 +177,25 @@ class MfaSetupPage extends React.Component {
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify"));
Setting.showMessage("error", i18next.t("general:Failed to verify") + ": " + res.msg);
}}
/>
<Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
}
}>{i18next.t("mfa:Use Email")}</Button>
}
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
}
}>{i18next.t("mfa:Use SMS")}</Button>
}
{
(this.state.mfaType === TotpMfaType) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
}
{this.renderMfaTypeSwitch()}
</Col>
</div>
);
case 2:
if (!this.state.isAuthenticated) {
return null;
}
return (
<EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) {
Setting.goToLink(this.state.redirectUri);
this.props.onfinish(true);
if (localStorage.getItem("mfaRedirectUrl") !== null) {
Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
localStorage.removeItem("mfaRedirectUrl");
} else {
Setting.goToLink("/account");
this.props.history.push("/account");
}
}}
onFail={(res) => {
@ -308,7 +214,7 @@ class MfaSetupPage extends React.Component {
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>}
extra={<a href="/web/public"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
@ -343,4 +249,4 @@ class MfaSetupPage extends React.Component {
}
}
export default MfaSetupPage;
export default withRouter(MfaSetupPage);

View File

@ -1,193 +0,0 @@
// 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, QRCode, Space} 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, {useEffect} from "react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method, user}) => {
const [dest, setDest] = React.useState(mfaProps.secret ?? "");
const [form] = Form.useForm();
useEffect(() => {
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
}
}, [mfaProps.mfaType]);
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
>
{dest !== "" ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={method}
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "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();
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
}
return (
<React.Fragment>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<QRCode
errorLevel="H"
value={mfaProps.url}
icon={"https://cdn.casdoor.com/static/favicon.png"}
/>
</Col>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Scan the QR code with your Authenticator App")}</p>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Or copy the secret to your Authenticator App")}</p>
<Col span={24}>
<Space>
<Input value={mfaProps.secret} />
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Space>
</Col>
</React.Fragment>
);
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{renderSecret()}
<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

@ -16,14 +16,13 @@ import React from "react";
import {Button, Card, Col, Result, Row} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as UserBackend from "../backend/UserBackend";
import * as AuthBackend from "./AuthBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import AffiliationSelect from "../common/select/AffiliationSelect";
import OAuthWidget from "../common/OAuthWidget";
import RegionSelect from "../common/select/RegionSelect";
import {withRouter} from "react-router-dom";
import MfaSetupPage from "./MfaSetupPage";
import * as AuthBackend from "./AuthBackend";
class PromptPage extends React.Component {
constructor(props) {
@ -34,7 +33,9 @@ class PromptPage extends React.Component {
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
application: null,
user: null,
promptType: new URLSearchParams(this.props.location.search).get("promptType"),
steps: null,
current: 0,
finished: false,
};
}
@ -45,6 +46,12 @@ class PromptPage extends React.Component {
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.user !== null && this.getApplicationObj() !== null && this.state.steps === null) {
this.initSteps(this.state.user, this.getApplicationObj());
}
}
getUser() {
const organizationName = this.props.account.owner;
const userName = this.props.account.name;
@ -198,19 +205,22 @@ class PromptPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
this.onUpdateAccount(null);
let redirectUrl = this.getRedirectUrl();
if (redirectUrl === "") {
redirectUrl = res.data2;
} else {
Setting.showMessage("error", res.msg);
}
});
}
finishAndJump() {
this.setState({
finished: true,
}, () => {
const redirectUrl = this.getRedirectUrl();
if (redirectUrl !== "" && redirectUrl !== null) {
Setting.goToLink(redirectUrl);
} else {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
}
} else {
Setting.showMessage("error", `Failed to log out: ${res.msg}`);
}
});
}
@ -221,8 +231,7 @@ class PromptPage extends React.Component {
if (res.status === "ok") {
if (isFinal) {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.logout();
this.finishAndJump();
}
} else {
if (isFinal) {
@ -238,25 +247,45 @@ class PromptPage extends React.Component {
}
renderPromptProvider(application) {
return <>
return (
<div style={{display: "flex", alignItems: "center", flexDirection: "column"}}>
{this.renderContent(application)}
<div style={{marginTop: "50px"}}>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
</div>
</>;
<Button style={{marginTop: "50px", width: "200px"}}
disabled={!Setting.isPromptAnswered(this.state.user, application)}
type="primary" size="large" onClick={() => {
this.submitUserEdit(true);
}}>
{i18next.t("code:Submit and complete")}
</Button>
</div>);
}
initSteps(user, application) {
const steps = [];
if (!Setting.isPromptAnswered(user, application) && this.state.promptType === "provider") {
steps.push({
content: this.renderPromptProvider(application),
name: "provider",
title: i18next.t("application:Binding providers"),
});
}
this.setState({
steps: steps,
});
}
renderSteps() {
if (this.state.steps === null || this.state.steps?.length === 0) {
return null;
}
renderPromptMfa() {
return (
<MfaSetupPage
application={this.getApplicationObj()}
account={this.props.account}
current={1}
isAuthenticated={true}
isPromptPage={true}
redirectUri={this.getRedirectUrl()}
{...this.props}
/>
<Card style={{marginTop: "20px", marginBottom: "20px"}}
title={this.state.steps[this.state.current].title}
>
<div >{this.state.steps[this.state.current].content}</div>
</Card>
);
}
@ -266,7 +295,7 @@ class PromptPage extends React.Component {
return null;
}
if (!Setting.hasPromptPage(application) && this.state.promptType !== "mfa") {
if (this.state.steps?.length === 0) {
return (
<Result
style={{display: "flex", flex: "1 1 0%", justifyContent: "center", flexDirection: "column"}}
@ -287,17 +316,7 @@ class PromptPage extends React.Component {
return (
<div style={{display: "flex", flex: "1", justifyContent: "center"}}>
<Card>
<div style={{marginTop: "30px", marginBottom: "30px", textAlign: "center"}}>
{
Setting.renderHelmet(application)
}
{
Setting.renderLogo(application)
}
{this.state.promptType !== "mfa" ? this.renderPromptProvider(application) : this.renderPromptMfa(application)}
</div>
</Card>
{this.renderSteps()}
</div>
);
}

View File

@ -0,0 +1,56 @@
import {LockOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
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 default CheckPasswordForm;

View File

@ -15,9 +15,11 @@
import React, {useState} from "react";
import i18next from "i18next";
import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend";
import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaAuth} from "./MfaVerifyForm";
import * as AuthBackend from "../AuthBackend";
import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "../MfaSetupPage";
import {mfaAuth} from "./MfaVerifyForm";
import MfaVerifySmsForm from "./MfaVerifySmsForm";
import MfaVerifyTotpForm from "./MfaVerifyTotpForm";
export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa";
@ -70,13 +72,13 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
{i18next.t("mfa:Multi-factor authentication description")}
</div>
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
<MfaSmsVerifyForm
<MfaVerifySmsForm
mfaProps={mfaProps}
method={mfaAuth}
onFinish={verify}
application={application}
/>) : (
<MfaTotpVerifyForm
<MfaVerifyTotpForm
mfaProps={mfaProps}
onFinish={verify}
/>

View File

@ -0,0 +1,40 @@
import {Button} from "antd";
import i18next from "i18next";
import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableMfa = () => {
const data = {
mfaType,
...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"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableMfa();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
export default MfaEnableForm;

View File

@ -0,0 +1,58 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Form} from "antd";
import i18next from "i18next";
import * as MfaBackend from "../../backend/MfaBackend";
import * as Setting from "../../Setting";
import React from "react";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "../MfaSetupPage";
import MfaVerifySmsForm from "./MfaVerifySmsForm";
import MfaVerifyTotpForm from "./MfaVerifyTotpForm";
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...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 === undefined || mfaProps === null || application === undefined || application === null || user === undefined || user === null) {
return <div></div>;
}
if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaVerifySmsForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaProps.mfaType === TotpMfaType) {
return <MfaVerifyTotpForm mfaProps={mfaProps} onFinish={onFinish} />;
} else {
return <div></div>;
}
}

View File

@ -0,0 +1,125 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
import {SendCodeInput} from "../../common/SendCodeInput";
import * as Setting from "../../Setting";
import {EmailMfaType, SmsMfaType} from "../MfaSetupPage";
import {mfaAuth} from "./MfaVerifyForm";
export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}) => {
const [dest, setDest] = React.useState("");
const [form] = Form.useForm();
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
return;
}
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
return;
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
}
}, [mfaProps.mfaType]);
const isShowText = () => {
if (method === mfaAuth) {
return true;
}
if (mfaProps.mfaType === SmsMfaType && user.phone !== "") {
return true;
}
if (mfaProps.mfaType === EmailMfaType && user.email !== "") {
return true;
}
return false;
};
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
>
{isShowText() ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={method}
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "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 default MfaVerifySmsForm;

View File

@ -0,0 +1,79 @@
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import copy from "copy-to-clipboard";
import i18next from "i18next";
import React from "react";
import * as Setting from "../../Setting";
export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
}
return (
<React.Fragment>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<QRCode
errorLevel="H"
value={mfaProps.url}
icon={"https://cdn.casdoor.com/static/favicon.png"}
/>
</Col>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Scan the QR code with your Authenticator App")}</p>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Or copy the secret to your Authenticator App")}</p>
<Col span={24}>
<Space>
<Input value={mfaProps.secret} />
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Space>
</Col>
</React.Fragment>
);
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
>
{renderSecret()}
<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>
);
};
export default MfaVerifyTotpForm;

View File

@ -0,0 +1,87 @@
import {Button, Space, Tag, notification} from "antd";
import i18next from "i18next";
import {useEffect} from "react";
import {useHistory, useLocation} from "react-router-dom";
import * as Setting from "../../Setting";
import {MfaRulePrompted, MfaRuleRequired} from "../../Setting";
const EnableMfaNotification = ({account}) => {
const [api, contextHolder] = notification.useNotification();
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (account === null) {
return;
}
const mfaItems = Setting.getMfaItemsByRules(account, account?.organization, [MfaRuleRequired, MfaRulePrompted]);
if (location.state?.from === "/login" && mfaItems.length !== 0) {
if (mfaItems.some((item) => item.rule === MfaRuleRequired)) {
openRequiredEnableNotification(mfaItems.find((item) => item.rule === MfaRuleRequired).name);
} else {
openPromptEnableNotification(mfaItems.filter((item) => item.rule === MfaRulePrompted)?.map((item) => item.name));
}
}
}, [account, location.state?.from]);
const openPromptEnableNotification = (mfaTypes) => {
const key = `open${Date.now()}`;
const btn = (
<Space>
<Button type="link" size="small" onClick={() => api.destroy(key)}>
{i18next.t("general:Later")}
</Button>
<Button type="primary" size="small" onClick={() => {
history.push(`/mfa/setup?mfaType=${mfaTypes[0]}`, {from: "/"});
api.destroy(key);
}}
>
{i18next.t("general:Go to enable")}
</Button>
</Space>
);
api.open({
message: i18next.t("mfa:Enable multi-factor authentication"),
description:
<Space direction={"vertical"}>
{i18next.t("mfa:To ensure the security of your account, it is recommended that you enable multi-factor authentication")}
<Space>{mfaTypes.map((item) => <Tag color="orange" key={item}>{item}</Tag>)}</Space>
</Space>,
btn,
key,
});
};
const openRequiredEnableNotification = (mfaType) => {
const key = `open${Date.now()}`;
const btn = (
<Space>
<Button type="primary" size="small" onClick={() => {
api.destroy(key);
}}
>
{i18next.t("general:Confirm")}
</Button>
</Space>
);
api.open({
message: i18next.t("mfa:Enable multi-factor authentication"),
description:
<Space direction={"vertical"}>
{i18next.t("mfa:To ensure the security of your account, it is required to enable multi-factor authentication")}
<Space><Tag color="orange">{mfaType}</Tag></Space>
</Space>,
btn,
key,
});
};
return (
<>
{contextHolder}
</>
);
};
export default EnableMfaNotification;

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Wenn eine angemeldete Session in Casdoor vorhanden ist, wird diese automatisch für die Anmeldung auf Anwendungsebene verwendet",
"Background URL": "Background-URL",
"Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite angezeigt wird",
"Binding providers": "Binding providers",
"Center": "Zentrum",
"Copy SAML metadata URL": "SAML-Metadaten-URL kopieren",
"Copy prompt page URL": "URL der Prompt-Seite kopieren",
@ -227,6 +228,7 @@
"Forget URL": "Passwort vergessen URL",
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Sprachen",
"Languages - Tooltip": "Verfügbare Sprachen",
"Last name": "Nachname",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Symbole, die die Anwendung der Außenwelt präsentiert",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Regel ändern",
"New Organization": "Neue Organisation",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Softe Löschung",
"Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login",
"Background URL": "Background URL",
"Background URL - Tooltip": "URL of the background image used in the login page",
"Binding providers": "Binding providers",
"Center": "Center",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
@ -227,6 +228,7 @@
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL",
"Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Languages",
"Languages - Tooltip": "Available languages",
"Last name": "Last name",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Icons that the application presents to the outside world",
"MFA items": "MFA items",
@ -426,15 +429,16 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"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 authentication description": "Multi-factor authentication description",
"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 recover description": "Multi-factor recover description",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Modify rule",
"New Organization": "New Organization",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "When enabled, deleting users will not completely remove them from the database. Instead, they will be marked as deleted",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Cuando existe una sesión iniciada en Casdoor, se utiliza automáticamente para el inicio de sesión del lado de la aplicación",
"Background URL": "URL de fondo",
"Background URL - Tooltip": "URL de la imagen de fondo utilizada en la página de inicio de sesión",
"Binding providers": "Binding providers",
"Center": "Centro",
"Copy SAML metadata URL": "Copia la URL de metadatos SAML",
"Copy prompt page URL": "Copiar URL de la página del prompt",
@ -227,6 +228,7 @@
"Forget URL": "Olvide la URL",
"Forget URL - Tooltip": "URL personalizada para la página \"Olvidé mi contraseña\". Si no se establece, se utilizará la página \"Olvidé mi contraseña\" predeterminada de Casdoor. Cuando se establezca, el enlace \"Olvidé mi contraseña\" en la página de inicio de sesión redireccionará a esta URL",
"Found some texts still not translated? Please help us translate at": "¿Encontraste algunos textos que aún no están traducidos? Por favor, ayúdanos a traducirlos en",
"Go to enable": "Go to enable",
"Go to writable demo site?": "¿Ir al sitio demo editable?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Idiomas",
"Languages - Tooltip": "Idiomas disponibles",
"Last name": "Apellido",
"Later": "Later",
"Logo": "Logotipo",
"Logo - Tooltip": "Iconos que la aplicación presenta al mundo exterior",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Modificar regla",
"New Organization": "Nueva organización",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Eliminación suave",
"Soft deletion - Tooltip": "Cuando se habilita, la eliminación de usuarios no los eliminará por completo de la base de datos. En su lugar, se marcarán como eliminados",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Lorsqu'une session connectée existe dans Casdoor, elle est automatiquement utilisée pour la connexion côté application",
"Background URL": "URL de fond",
"Background URL - Tooltip": "\"L'URL de l'image de fond utilisée sur la page de connexion\"",
"Binding providers": "Binding providers",
"Center": "Centre",
"Copy SAML metadata URL": "Copiez l'URL de métadonnées SAML",
"Copy prompt page URL": "Copier l'URL de la page de l'invite",
@ -227,6 +228,7 @@
"Forget URL": "Oubliez l'URL",
"Forget URL - Tooltip": "URL personnalisée pour la page \"Mot de passe oublié\". Si elle n'est pas définie, la page par défaut \"Mot de passe oublié\" de Casdoor sera utilisée. Lorsqu'elle est définie, le lien \"Mot de passe oublié\" sur la page de connexion sera redirigé vers cette URL",
"Found some texts still not translated? Please help us translate at": "Trouvé des textes encore non traduits ? Veuillez nous aider à les traduire",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Allez sur le site de démonstration modifiable ?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Langues",
"Languages - Tooltip": "Langues disponibles",
"Last name": "Nom de famille",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Icônes que l'application présente au monde extérieur",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Modifier la règle",
"New Organization": "Nouvelle organisation",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Suppression douce",
"Soft deletion - Tooltip": "Lorsqu'elle est activée, la suppression d'utilisateurs ne les retirera pas complètement de la base de données. Au lieu de cela, ils seront marqués comme supprimés",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Ketika sesi masuk yang terdaftar ada di Casdoor, secara otomatis digunakan untuk masuk ke sisi aplikasi",
"Background URL": "URL latar belakang",
"Background URL - Tooltip": "URL dari gambar latar belakang yang digunakan di halaman login",
"Binding providers": "Binding providers",
"Center": "pusat",
"Copy SAML metadata URL": "Salin URL metadata SAML",
"Copy prompt page URL": "Salin URL halaman prompt",
@ -227,6 +228,7 @@
"Forget URL": "Lupakan URL",
"Forget URL - Tooltip": "URL kustom untuk halaman \"Lupa kata sandi\". Jika tidak diatur, halaman \"Lupa kata sandi\" default Casdoor akan digunakan. Ketika diatur, tautan \"Lupa kata sandi\" pada halaman masuk akan diarahkan ke URL ini",
"Found some texts still not translated? Please help us translate at": "Menemukan beberapa teks yang masih belum diterjemahkan? Tolong bantu kami menerjemahkan di",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Bahasa-bahasa",
"Languages - Tooltip": "Bahasa yang tersedia",
"Last name": "Nama belakang",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Ikon-ikon yang disajikan aplikasi ke dunia luar",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Mengubah aturan",
"New Organization": "Organisasi baru",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Penghapusan lunak",
"Soft deletion - Tooltip": "Ketika diaktifkan, menghapus pengguna tidak akan sepenuhnya menghapus mereka dari database. Sebaliknya, mereka akan ditandai sebagai dihapus",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Casdoorにログインセッションが存在する場合、アプリケーション側のログインに自動的に使用されます",
"Background URL": "背景URL",
"Background URL - Tooltip": "ログインページで使用される背景画像のURL",
"Binding providers": "Binding providers",
"Center": "センター",
"Copy SAML metadata URL": "SAMLメタデータのURLをコピーしてください",
"Copy prompt page URL": "プロンプトページのURLをコピーしてください",
@ -227,6 +228,7 @@
"Forget URL": "URLを忘れてください",
"Forget URL - Tooltip": "「パスワードをお忘れの場合」ページのカスタムURL。未設定の場合、デフォルトのCasdoor「パスワードをお忘れの場合」ページが使用されます。設定された場合、ログインページの「パスワードをお忘れの場合」リンクはこのURLにリダイレクトされます",
"Found some texts still not translated? Please help us translate at": "まだ翻訳されていない文章が見つかりましたか?是非とも翻訳のお手伝いをお願いします",
"Go to enable": "Go to enable",
"Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "言語",
"Languages - Tooltip": "利用可能な言語",
"Last name": "苗字",
"Later": "Later",
"Logo": "ロゴ",
"Logo - Tooltip": "アプリケーションが外部世界に示すアイコン",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "ルールを変更する",
"New Organization": "新しい組織",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "ソフト削除",
"Soft deletion - Tooltip": "有効になっている場合、ユーザーを削除しても完全にデータベースから削除されません。代わりに、削除されたとマークされます",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "카스도어에 로그인된 세션이 존재할 때, 애플리케이션 쪽 로그인에 자동으로 사용됩니다",
"Background URL": "배경 URL",
"Background URL - Tooltip": "로그인 페이지에서 사용된 배경 이미지의 URL",
"Binding providers": "Binding providers",
"Center": "중앙",
"Copy SAML metadata URL": "SAML 메타데이터 URL 복사",
"Copy prompt page URL": "프롬프트 페이지 URL을 복사하세요",
@ -227,6 +228,7 @@
"Forget URL": "URL을 잊어버려라",
"Forget URL - Tooltip": "\"비밀번호를 잊어버렸을 경우\" 페이지에 대한 사용자 정의 URL. 설정되지 않은 경우 기본 Casdoor \"비밀번호를 잊어버렸을 경우\" 페이지가 사용됩니다. 설정된 경우 로그인 페이지의 \"비밀번호를 잊으셨나요?\" 링크는 이 URL로 리디렉션됩니다",
"Found some texts still not translated? Please help us translate at": "아직 번역되지 않은 텍스트가 있나요? 번역에 도움을 주실 수 있나요?",
"Go to enable": "Go to enable",
"Go to writable demo site?": "쓰기 가능한 데모 사이트로 이동하시겠습니까?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "언어",
"Languages - Tooltip": "사용 가능한 언어",
"Last name": "성",
"Later": "Later",
"Logo": "로고",
"Logo - Tooltip": "애플리케이션이 외부 세계에 제시하는 아이콘들",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "규칙 수정",
"New Organization": "새로운 조직",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "소프트 삭제",
"Soft deletion - Tooltip": "사용 가능한 경우, 사용자 삭제 시 데이터베이스에서 완전히 삭제되지 않습니다. 대신 삭제됨으로 표시됩니다",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Quando uma sessão logada existe no Casdoor, ela é automaticamente usada para o login no lado da aplicação",
"Background URL": "URL de Fundo",
"Background URL - Tooltip": "URL da imagem de fundo usada na página de login",
"Binding providers": "Binding providers",
"Center": "Centro",
"Copy SAML metadata URL": "Copiar URL de metadados SAML",
"Copy prompt page URL": "Copiar URL da página de prompt",
@ -227,6 +228,7 @@
"Forget URL": "URL de Esqueci a Senha",
"Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL",
"Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Acessar o site de demonstração gravável?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Idiomas",
"Languages - Tooltip": "Idiomas disponíveis",
"Last name": "Sobrenome",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Ícones que o aplicativo apresenta para o mundo externo",
"MFA items": "MFA items",
@ -425,38 +428,41 @@
"Text - Tooltip": "Texto - Dica de ferramenta"
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "Cada vez que você entrar na sua Conta, você precisará da sua senha e de um código de autenticação",
"Failed to get application": "Falha ao obter o aplicativo",
"Failed to initiate MFA": "Falha ao iniciar MFA",
"Have problems?": "Está com problemas?",
"Multi-factor authentication": "Autenticação de vários fatores",
"Multi-factor authentication - Tooltip ": "Autenticação de vários fatores - Dica de ferramenta",
"Multi-factor authentication description": "Configurar autenticação de vários fatores",
"Multi-factor methods": "Métodos de vários fatores",
"Multi-factor recover": "Recuperação de vários fatores",
"Multi-factor recover description": "Se você não conseguir acessar seu dispositivo, insira seu código de recuperação para verificar sua identidade",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"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 to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Código de acesso",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
"Recovery code": "Código de recuperação",
"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",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Definir preferido",
"Setup": "Configuração",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS",
"Use a recovery code": "Usar um código de recuperação",
"Verification failed": "Verificação falhou",
"Verify Code": "Verificar Código",
"Verify Password": "Verificar Senha",
"Your email is": "Seu e-mail é",
"Your phone is": "Seu telefone é",
"preferred": "Preferido"
"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",
@ -477,6 +483,7 @@
"Modify rule": "Modificar regra",
"New Organization": "Nova Organização",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Exclusão suave",
"Soft deletion - Tooltip": "Quando ativada, a exclusão de usuários não os removerá completamente do banco de dados. Em vez disso, eles serão marcados como excluídos",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Когда существует активная сессия входа в Casdoor, она автоматически используется для входа на стороне приложения",
"Background URL": "Фоновый URL",
"Background URL - Tooltip": "URL фонового изображения, используемого на странице входа",
"Binding providers": "Binding providers",
"Center": "Центр",
"Copy SAML metadata URL": "Скопируйте URL метаданных SAML",
"Copy prompt page URL": "Скопируйте URL страницы предложения",
@ -227,6 +228,7 @@
"Forget URL": "Забудьте URL",
"Forget URL - Tooltip": "Настроенный URL для страницы \"Забыли пароль\". Если не установлено, будет использоваться стандартная страница \"Забыли пароль\" Casdoor. При установке, ссылка \"Забыли пароль\" на странице входа будет перенаправляться на этот URL",
"Found some texts still not translated? Please help us translate at": "Нашли некоторые тексты, которые еще не переведены? Пожалуйста, помогите нам перевести на",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Перейти на демонстрационный сайт для записи данных?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Языки",
"Languages - Tooltip": "Доступные языки",
"Last name": "Фамилия",
"Later": "Later",
"Logo": "Логотип",
"Logo - Tooltip": "Иконки, которые приложение представляет во внешний мир",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Изменить правило",
"New Organization": "Новая организация",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Мягкое удаление",
"Soft deletion - Tooltip": "Когда включено, удаление пользователей не полностью удаляет их из базы данных. Вместо этого они будут помечены как удаленные",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "Khi một phiên đăng nhập đã được tạo trong Casdoor, nó sẽ tự động được sử dụng để đăng nhập tại ứng dụng",
"Background URL": "URL nền",
"Background URL - Tooltip": "Đường dẫn URL của hình ảnh nền được sử dụng trong trang đăng nhập",
"Binding providers": "Binding providers",
"Center": "Trung tâm",
"Copy SAML metadata URL": "Sao chép URL siêu dữ liệu SAML",
"Copy prompt page URL": "Sao chép URL của trang nhắc nhở",
@ -227,6 +228,7 @@
"Forget URL": "Quên đường dẫn URL",
"Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này",
"Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại",
"Go to enable": "Go to enable",
"Go to writable demo site?": "Bạn có muốn đi đến trang demo có thể viết được không?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "Ngôn ngữ",
"Languages - Tooltip": "Các ngôn ngữ hiện có",
"Last name": "Họ",
"Later": "Later",
"Logo": "Logo",
"Logo - Tooltip": "Biểu tượng mà ứng dụng hiển thị ra ngoài thế giới",
"MFA items": "MFA items",
@ -426,6 +429,7 @@
},
"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",
"Enable multi-factor authentication": "Enable multi-factor authentication",
"Failed to get application": "Failed to get application",
"Failed to initiate MFA": "Failed to initiate MFA",
"Have problems?": "Have problems?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred",
"Setup": "Setup",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "To ensure the security of your account, it is recommended that you enable multi-factor authentication",
"To ensure the security of your account, it is required to enable multi-factor authentication": "To ensure the security of your account, it is required to enable multi-factor authentication",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
@ -477,6 +483,7 @@
"Modify rule": "Sửa đổi quy tắc",
"New Organization": "Tổ chức mới",
"Optional": "Optional",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Xóa mềm",
"Soft deletion - Tooltip": "Khi được bật, việc xóa người dùng sẽ không hoàn toàn loại bỏ họ khỏi cơ sở dữ liệu. Thay vào đó, họ sẽ được đánh dấu là đã bị xóa",

View File

@ -20,6 +20,7 @@
"Auto signin - Tooltip": "当Casdoor存在已登录会话时自动采用该会话进行应用端的登录",
"Background URL": "背景图URL",
"Background URL - Tooltip": "登录页背景图的链接",
"Binding providers": "绑定提供商",
"Center": "居中",
"Copy SAML metadata URL": "复制SAML元数据URL",
"Copy prompt page URL": "复制提醒页面URL",
@ -227,6 +228,7 @@
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "自定义忘记密码页面的URL不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
"Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:",
"Go to enable": "前往启用",
"Go to writable demo site?": "跳转至可写演示站点?",
"Groups": "群组",
"Groups - Tooltip": "Groups - Tooltip",
@ -241,6 +243,7 @@
"Languages": "语言",
"Languages - Tooltip": "可选语言",
"Last name": "姓氏",
"Later": "稍后",
"Logo": "Logo",
"Logo - Tooltip": "应用程序向外展示的图标",
"MFA items": "MFA 项",
@ -426,6 +429,7 @@
},
"mfa": {
"Each time you sign in to your Account, you'll need your password and a authentication code": "每次登录帐户时,都需要密码和认证码",
"Enable multi-factor authentication": "启用多因素认证",
"Failed to get application": "获取应用失败",
"Failed to initiate MFA": "初始化 MFA 失败",
"Have problems?": "遇到问题?",
@ -446,6 +450,8 @@
"Scan the QR code with your Authenticator App": "用你的身份验证应用扫描二维码",
"Set preferred": "设为首选",
"Setup": "设置",
"To ensure the security of your account, it is recommended that you enable multi-factor authentication": "为了确保您的帐户安全, 建议您启用多因素认证",
"To ensure the security of your account, it is required to enable multi-factor authentication": "为了确保您的帐户安全,您需要启用多因素身份验证",
"Use Authenticator App": "使用身份验证应用",
"Use Email": "使用电子邮件",
"Use SMS": "使用短信",
@ -477,6 +483,7 @@
"Modify rule": "修改规则",
"New Organization": "添加组织",
"Optional": "可选",
"Prompt": "提示",
"Required": "必须",
"Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除一个用户时不会在数据库彻底清除,只会标记为已删除状态",

View File

@ -15,14 +15,23 @@
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Table, Tooltip} from "antd";
import {EmailMfaType, SmsMfaType, TotpMfaType} from "../auth/MfaSetupPage";
import {MfaRuleOptional, MfaRulePrompted, MfaRuleRequired} from "../Setting";
import * as Setting from "../Setting";
import i18next from "i18next";
const {Option} = Select;
const MfaItems = [
{name: "Phone"},
{name: "Email"},
{name: "Phone", value: SmsMfaType},
{name: "Email", value: EmailMfaType},
{name: "App", value: TotpMfaType},
];
const RuleItems = [
{value: MfaRuleOptional, label: i18next.t("organization:Optional")},
{value: MfaRulePrompted, label: i18next.t("organization:Prompt")},
{value: MfaRuleRequired, label: i18next.t("organization:Required")},
];
class MfaTable extends React.Component {
@ -80,7 +89,7 @@ class MfaTable extends React.Component {
this.updateField(table, index, "name", value);
}} >
{
Setting.getDeduplicatedArray(MfaItems, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
Setting.getDeduplicatedArray(MfaItems, table, "name").map((item, index) => <Option key={index} value={item.value}>{item.name}</Option>)
}
</Select>
);
@ -96,12 +105,21 @@ class MfaTable extends React.Component {
<Select virtual={false} style={{width: "100%"}}
value={text}
defaultValue="Optional"
options={[
{value: "Optional", label: i18next.t("organization:Optional")},
{value: "Required", label: i18next.t("organization:Required")}].map((item) =>
options={RuleItems.map((item) =>
Setting.getOption(item.label, item.value))
}
onChange={value => {
let requiredCount = 0;
table.forEach((item) => {
if (item.rule === MfaRuleRequired) {
requiredCount++;
}
});
if (value === MfaRuleRequired && requiredCount >= 1) {
Setting.showMessage("error", "Only 1 MFA methods can be required");
return;
}
this.updateField(table, index, "rule", value);
}} >
</Select>
@ -135,7 +153,7 @@ class MfaTable extends React.Component {
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
<Button disabled={table.length >= MfaItems.length} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>