mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): support managing user mfa on user details page (#4498)
This commit is contained in:
parent
e69f941e38
commit
9c8b9e4853
25 changed files with 325 additions and 50 deletions
26
packages/console/src/components/MfaFactorName/index.tsx
Normal file
26
packages/console/src/components/MfaFactorName/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { type AdminConsoleKey } from '@logto/phrases';
|
||||
import { MfaFactor } from '@logto/schemas';
|
||||
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
||||
const factorNameLabel: Record<MfaFactor, AdminConsoleKey> = {
|
||||
[MfaFactor.TOTP]: 'mfa.totp',
|
||||
[MfaFactor.WebAuthn]: 'mfa.webauthn',
|
||||
[MfaFactor.BackupCode]: 'mfa.backup_code',
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
type: MfaFactor;
|
||||
agent?: string;
|
||||
};
|
||||
|
||||
function MfaFactorName({ type, agent }: Props) {
|
||||
return (
|
||||
<>
|
||||
<DynamicT forKey={factorNameLabel[type]} />
|
||||
{agent && ` - ${agent}`}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MfaFactorName;
|
|
@ -0,0 +1,11 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.factorTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.factorIcon {
|
||||
color: var(--color-text-secondary);
|
||||
margin-right: _.unit(3);
|
||||
}
|
28
packages/console/src/components/MfaFactorTitle/index.tsx
Normal file
28
packages/console/src/components/MfaFactorTitle/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { MfaFactor } from '@logto/schemas';
|
||||
|
||||
import FactorBackupCode from '@/assets/icons/factor-backup-code.svg';
|
||||
import FactorTotp from '@/assets/icons/factor-totp.svg';
|
||||
import FactorWebAuthn from '@/assets/icons/factor-webauthn.svg';
|
||||
|
||||
import MfaFactorName, { type Props as MfaFactorNameProps } from '../MfaFactorName';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const factorIcon: Record<MfaFactor, SvgComponent> = {
|
||||
[MfaFactor.TOTP]: FactorTotp,
|
||||
[MfaFactor.WebAuthn]: FactorWebAuthn,
|
||||
[MfaFactor.BackupCode]: FactorBackupCode,
|
||||
};
|
||||
|
||||
function MfaFactorTitle({ type, agent }: MfaFactorNameProps) {
|
||||
const Icon = factorIcon[type];
|
||||
|
||||
return (
|
||||
<div className={styles.factorTitle}>
|
||||
<Icon className={styles.factorIcon} />
|
||||
<MfaFactorName type={type} agent={agent} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MfaFactorTitle;
|
|
@ -4,17 +4,9 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(1);
|
||||
}
|
||||
|
||||
.factorDescription {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.factorTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
|
||||
.factorIcon {
|
||||
color: var(--color-text-secondary);
|
||||
margin-right: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import { type AdminConsoleKey } from '@logto/phrases';
|
||||
import { type ReactElement, cloneElement } from 'react';
|
||||
import { MfaFactor } from '@logto/schemas';
|
||||
|
||||
import MfaFactorTitle from '@/components/MfaFactorTitle';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: AdminConsoleKey;
|
||||
description: AdminConsoleKey;
|
||||
icon: ReactElement;
|
||||
type: MfaFactor;
|
||||
};
|
||||
|
||||
function FactorLabel({ title, description, icon }: Props) {
|
||||
const factorDescriptionLabel: Record<MfaFactor, AdminConsoleKey> = {
|
||||
[MfaFactor.TOTP]: 'mfa.otp_description',
|
||||
[MfaFactor.WebAuthn]: 'mfa.webauthn_description',
|
||||
[MfaFactor.BackupCode]: 'mfa.backup_code_description',
|
||||
};
|
||||
|
||||
function FactorLabel({ type }: Props) {
|
||||
return (
|
||||
<div className={styles.factorLabel}>
|
||||
<div className={styles.factorTitle}>
|
||||
{cloneElement(icon, { className: styles.factorIcon })}
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<div>
|
||||
<DynamicT forKey={description} />
|
||||
<MfaFactorTitle type={type} />
|
||||
<div className={styles.factorDescription}>
|
||||
<DynamicT forKey={factorDescriptionLabel[type]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { MfaPolicy, type SignInExperience } from '@logto/schemas';
|
||||
import { MfaFactor, MfaPolicy, type SignInExperience } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FactorBackupCode from '@/assets/icons/factor-backup-code.svg';
|
||||
import FactorOtp from '@/assets/icons/factor-totp.svg';
|
||||
import FactorWebAuthn from '@/assets/icons/factor-webauthn.svg';
|
||||
import DetailsForm from '@/components/DetailsForm';
|
||||
import FormCard from '@/components/FormCard';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
|
@ -83,24 +80,9 @@ function MfaForm({ data, onMfaUpdated }: Props) {
|
|||
<DynamicT forKey="mfa.multi_factors_description" />
|
||||
</div>
|
||||
<div className={styles.factorField}>
|
||||
<Switch label={<FactorLabel type={MfaFactor.TOTP} />} {...register('totpEnabled')} />
|
||||
<Switch
|
||||
label={
|
||||
<FactorLabel
|
||||
title="mfa.totp"
|
||||
description="mfa.otp_description"
|
||||
icon={<FactorOtp />}
|
||||
/>
|
||||
}
|
||||
{...register('totpEnabled')}
|
||||
/>
|
||||
<Switch
|
||||
label={
|
||||
<FactorLabel
|
||||
title="mfa.webauthn"
|
||||
description="mfa.webauthn_description"
|
||||
icon={<FactorWebAuthn />}
|
||||
/>
|
||||
}
|
||||
label={<FactorLabel type={MfaFactor.WebAuthn} />}
|
||||
{...register('webAuthnEnabled')}
|
||||
/>
|
||||
<div className={styles.backupCodeField}>
|
||||
|
@ -108,13 +90,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
|
|||
<DynamicT forKey="mfa.backup_code_setup_hint" />
|
||||
</div>
|
||||
<Switch
|
||||
label={
|
||||
<FactorLabel
|
||||
title="mfa.backup_code"
|
||||
description="mfa.backup_code_description"
|
||||
icon={<FactorBackupCode />}
|
||||
/>
|
||||
}
|
||||
label={<FactorLabel type={MfaFactor.BackupCode} />}
|
||||
hasError={!isBackupCodeAllowed}
|
||||
{...register('backupCodeEnabled')}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.fieldDescription {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
margin: _.unit(1) 0 _.unit(2);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { type UserMfaVerificationResponse } from '@logto/schemas';
|
||||
import { useCallback } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import MfaFactorName from '@/components/MfaFactorName';
|
||||
import MfaFactorTitle from '@/components/MfaFactorTitle';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Table from '@/ds-components/Table';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import { type UserMfaVerification } from '@/types/mfa';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
function UserMfaVerifications({ userId }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.user_details.mfa' });
|
||||
const {
|
||||
data: mfaVerifications,
|
||||
error,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR<UserMfaVerificationResponse, RequestError>(`api/users/${userId}/mfa-verifications`);
|
||||
|
||||
const api = useApi();
|
||||
const { show: showConfirm } = useConfirmModal();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (mfaVerification: UserMfaVerification) => {
|
||||
const [result] = await showConfirm({
|
||||
ModalContent: () => (
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="deletion_confirmation"
|
||||
components={{
|
||||
name: <MfaFactorName {...mfaVerification} />,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
confirmButtonText: 'general.remove',
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.delete(`api/users/${userId}/mfa-verifications/${mfaVerification.id}`);
|
||||
void mutate(mfaVerifications?.filter((item) => item.id !== mfaVerification.id));
|
||||
},
|
||||
[api, mfaVerifications, mutate, showConfirm, t, userId]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && !error && (
|
||||
<div className={styles.fieldDescription}>
|
||||
{t(mfaVerifications?.length ? 'field_description' : 'field_description_empty')}
|
||||
</div>
|
||||
)}
|
||||
{(Boolean(mfaVerifications?.length) || error) && (
|
||||
<Table
|
||||
hasBorder
|
||||
rowGroups={[{ key: 'mfaVerifications', data: mfaVerifications }]}
|
||||
rowIndexKey="id"
|
||||
isLoading={isLoading}
|
||||
errorMessage={error?.body?.message ?? error?.message}
|
||||
columns={[
|
||||
{
|
||||
title: t('name_column'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 13,
|
||||
render: (mfaVerification) => <MfaFactorTitle {...mfaVerification} />,
|
||||
},
|
||||
{
|
||||
title: null,
|
||||
dataIndex: 'action',
|
||||
colSpan: 3,
|
||||
render: (mfaVerification) => (
|
||||
<Button
|
||||
title="general.remove"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
void handleDelete(mfaVerification);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
onRetry={() => {
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserMfaVerifications;
|
|
@ -10,6 +10,7 @@ import { useOutletContext } from 'react-router-dom';
|
|||
import DetailsForm from '@/components/DetailsForm';
|
||||
import FormCard from '@/components/FormCard';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
|
@ -24,6 +25,7 @@ import { uriValidator } from '@/utils/validator';
|
|||
import type { UserDetailsForm, UserDetailsOutletContext } from '../types';
|
||||
import { userDetailsParser } from '../utils';
|
||||
|
||||
import UserMfaVerifications from './UserMfaVerifications';
|
||||
import UserSocialIdentities from './components/UserSocialIdentities';
|
||||
|
||||
function UserSettings() {
|
||||
|
@ -160,6 +162,11 @@ function UserSettings() {
|
|||
}}
|
||||
/>
|
||||
</FormField>
|
||||
{isDevFeaturesEnabled && (
|
||||
<FormField title="user_details.mfa.field_name">
|
||||
<UserMfaVerifications userId={user.id} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormField
|
||||
isRequired
|
||||
title="user_details.field_custom_data"
|
||||
|
|
3
packages/console/src/types/mfa.ts
Normal file
3
packages/console/src/types/mfa.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { type UserMfaVerificationResponse } from '@logto/schemas';
|
||||
|
||||
export type UserMfaVerification = UserMfaVerificationResponse[number];
|
|
@ -44,6 +44,15 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Du entfernst die bestehende <name/> Identität. Bist du sicher, dass du das tun möchtest?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Zwei-Faktor-Authentifizierung',
|
||||
field_description: 'Dieser Benutzer hat 2-Stufen-Authentifizierungsfaktoren aktiviert.',
|
||||
name_column: 'Zwei-Faktor',
|
||||
field_description_empty:
|
||||
'Dieser Benutzer hat keine zweistufigen Authentifizierungsfaktoren aktiviert.',
|
||||
deletion_confirmation:
|
||||
'Sie entfernen den bestehenden <name/> für den zweistufigen Authentifikator. Sind Sie sicher, dass Sie das tun möchten?',
|
||||
},
|
||||
suspended: 'Gesperrt',
|
||||
suspend_user: 'Benutzer sperren',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -42,6 +42,14 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'You are removing the existing <name/> identity. Are you sure you want to do that?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Multi-factor authentication',
|
||||
field_description: 'This user has enabled 2-step authentication factors.',
|
||||
name_column: 'Multi-Factor',
|
||||
field_description_empty: 'This user has not enabled 2-step authentication factors.',
|
||||
deletion_confirmation:
|
||||
'You are removing the existing <name/> for the 2-step authenticator. Are you sure you want to do that?',
|
||||
},
|
||||
suspended: 'Suspended',
|
||||
suspend_user: 'Suspend user',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -44,6 +44,14 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Está eliminando la identidad de <name/> existente. ¿Está seguro de que desea hacer esto?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Autenticación de dos factores',
|
||||
field_description: 'Este usuario ha habilitado factores de autenticación de 2 pasos.',
|
||||
name_column: 'Autenticación de dos factores',
|
||||
field_description_empty: 'Este usuario no ha habilitado factores de autenticación de 2 pasos.',
|
||||
deletion_confirmation:
|
||||
'Está eliminando el <name/> existente del autenticador de 2 pasos. ¿Está seguro de que desea hacerlo?',
|
||||
},
|
||||
suspended: 'Suspendido',
|
||||
suspend_user: 'Suspender usuario',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -44,6 +44,15 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
"Vous supprimez l'identité existante <nom/>. Etes-vous sûr de vouloir faire ça ?",
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Authentification à deux facteurs',
|
||||
field_description: "Cet utilisateur a activé des facteurs d'authentification à 2 étapes.",
|
||||
name_column: 'Authentification à deux facteurs',
|
||||
field_description_empty:
|
||||
"Cet utilisateur n'a pas activé les facteurs d'authentification à deux étapes.",
|
||||
deletion_confirmation:
|
||||
"Vous supprimez l'<name/> existant pour l'authentification à deux étapes. Êtes-vous sûr de vouloir faire cela ?",
|
||||
},
|
||||
suspended: 'Suspendu',
|
||||
suspend_user: "Suspendre l'utilisateur",
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -44,6 +44,15 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
"Stai rimuovendo l'identità esistente <name/>. Sei sicuro di voler procedere?",
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Autenticazione a due fattori',
|
||||
field_description: 'Questo utente ha abilitato fattori di autenticazione a 2 passaggi.',
|
||||
name_column: 'Autenticazione a due fattori',
|
||||
field_description_empty:
|
||||
'Questo utente non ha abilitato fattori di autenticazione a due fattori.',
|
||||
deletion_confirmation:
|
||||
"Stai rimuovendo il <name/> esistente per l'autenticatore a due fattori. Sei sicuro di volerlo fare?",
|
||||
},
|
||||
suspended: 'Sospeso',
|
||||
suspend_user: 'Sospendi utente',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -42,6 +42,13 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'既存の<name/>アイデンティティを削除しています。本当にそれをやり遂げますか?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: '多要素認証',
|
||||
field_description: 'このユーザーは2段階認証要素を有効にしました。',
|
||||
name_column: '多要素認証',
|
||||
field_description_empty: 'このユーザーは2段階認証の要因を有効にしていません。',
|
||||
deletion_confirmation: '2段階認証の既存の<name/>を削除しています。本当にそれを行いたいですか?',
|
||||
},
|
||||
suspended: '停止中',
|
||||
suspend_user: 'ユーザーを一時停止',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -41,6 +41,14 @@ const user_details = {
|
|||
not_connected: '이 사용자는 아직 소셜에 연동되지 않았아요.',
|
||||
deletion_confirmation: '<name/> 신원을 삭제하려고 해요. 정말로 진행할까요?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: '다단계 인증',
|
||||
field_description: '이 사용자는 2단계 인증 요소를 활성화했습니다.',
|
||||
name_column: '다단계 인증',
|
||||
field_description_empty: '이 사용자는 2단계 인증 요소를 활성화하지 않았습니다.',
|
||||
deletion_confirmation:
|
||||
'2단계 인증기에 대한 기존 <name/>을 제거하려고 합니다. 정말로 그렇게 하시겠습니까?',
|
||||
},
|
||||
suspended: '정지됨',
|
||||
suspend_user: '사용자 정지',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -41,6 +41,14 @@ const user_details = {
|
|||
not_connected: 'Użytkownik nie jest połączony z żadnym połączeniem społecznościowym',
|
||||
deletion_confirmation: 'Usuwasz istniejącą tożsamość <name/>. Czy na pewno chcesz to zrobić?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Wieloetapowa autoryzacja',
|
||||
field_description: 'Ten użytkownik włączył czynniki autoryzacji dwuetapowej.',
|
||||
name_column: 'Wieloetapowa autoryzacja',
|
||||
field_description_empty: 'Ten użytkownik nie włączył czynników uwierzytelniania dwuetapowego.',
|
||||
deletion_confirmation:
|
||||
'Usuwasz istniejący <name/> dla autentykatora dwuetapowego. Czy na pewno chcesz to zrobić?',
|
||||
},
|
||||
suspended: 'Zawieszony',
|
||||
suspend_user: 'Zawieś użytkownika',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -42,6 +42,14 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Você está removendo a identidade <name/> existente. Você tem certeza que deseja fazer isso?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Autenticação de dois fatores',
|
||||
field_description: 'Este usuário habilitou fatores de autenticação de 2 etapas.',
|
||||
name_column: 'Autenticação de dois fatores',
|
||||
field_description_empty: 'Este usuário não habilitou fatores de autenticação em duas etapas.',
|
||||
deletion_confirmation:
|
||||
'Você está removendo o <name/> existente para o autenticador em duas etapas. Tem certeza de que deseja fazer isso?',
|
||||
},
|
||||
suspended: 'Suspenso',
|
||||
suspend_user: 'Suspender usuário',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -44,6 +44,14 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Está removendo a identidade <name/> existente. Tem a certeza que deseja fazer isso?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Autenticação de dois fatores',
|
||||
field_description: 'Este utilizador ativou fatores de autenticação de 2 etapas.',
|
||||
name_column: 'Autenticação de dois fatores',
|
||||
field_description_empty: 'Este utilizador não ativou fatores de autenticação de 2 passos.',
|
||||
deletion_confirmation:
|
||||
'Está a remover o <name/> existente para o autenticador de 2 passos. Tem a certeza de que deseja fazê-lo?',
|
||||
},
|
||||
suspended: 'suspenso',
|
||||
suspend_user: 'Suspender utilizador',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -42,6 +42,14 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Вы удаляете существующий идентификатор <name/>. Вы уверены, что хотите это сделать?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Двухфакторная аутентификация',
|
||||
field_description: 'Этот пользователь включил двухэтапные факторы аутентификации.',
|
||||
name_column: 'Двухфакторная аутентификация',
|
||||
field_description_empty: 'Этот пользователь не включил двухфакторную аутентификацию.',
|
||||
deletion_confirmation:
|
||||
'Вы удаляете существующий <name/> для двухфакторного аутентификатора. Вы уверены, что хотите это сделать?',
|
||||
},
|
||||
suspended: 'Приостановлен',
|
||||
suspend_user: 'Приостановить пользователя',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -42,6 +42,15 @@ const user_details = {
|
|||
deletion_confirmation:
|
||||
'Mevcut <name/> kimliğini kaldırıyorsunuz. Bunu yapmak istediğinizden emin misiniz?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: 'Çok faktörlü kimlik doğrulama',
|
||||
field_description: 'Bu kullanıcı 2 adımlı kimlik doğrulama faktörlerini etkinleştirdi.',
|
||||
name_column: 'Çok Faktörlü Kimlik Doğrulama',
|
||||
field_description_empty:
|
||||
'Bu kullanıcı 2 aşamalı kimlik doğrulama faktörlerini etkinleştirmedi.',
|
||||
deletion_confirmation:
|
||||
'2 aşamalı kimlik doğrulama için mevcut olan <name/> kaldırıyorsunuz. Bunun yapmak istediğinizden emin misiniz?',
|
||||
},
|
||||
suspended: 'Askıya alınmış',
|
||||
suspend_user: 'Kullanıcıyı Askıya Al',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -40,6 +40,13 @@ const user_details = {
|
|||
not_connected: '该用户还没有绑定社交帐号',
|
||||
deletion_confirmation: '你在正要删除现有的 <name /> 身份,是否确认?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: '多因素认证',
|
||||
field_description: '该用户已启用2步认证因素。',
|
||||
name_column: '多因素认证',
|
||||
field_description_empty: '此用户尚未启用两步身份验证因素。',
|
||||
deletion_confirmation: '您正在移除现有的2步身份验证器的<name/>。您确定要这样做吗?',
|
||||
},
|
||||
suspended: '已禁用',
|
||||
suspend_user: '禁用用户',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -40,6 +40,13 @@ const user_details = {
|
|||
not_connected: '該用戶還沒有綁定社交帳號',
|
||||
deletion_confirmation: '你在正要刪除現有的 <name /> 身份,是否確認?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: '多重因素驗證',
|
||||
field_description: '這個使用者已啟用2步驗證因素。',
|
||||
name_column: '多重因素驗證',
|
||||
field_description_empty: '此用戶尚未啟用兩步驟身份驗證因素。',
|
||||
deletion_confirmation: '您正在移除現有的2步驗證器的<name/>。您確定要這樣做嗎?',
|
||||
},
|
||||
suspended: '已禁用',
|
||||
suspend_user: '禁用用户',
|
||||
suspend_user_reminder:
|
||||
|
|
|
@ -40,6 +40,13 @@ const user_details = {
|
|||
not_connected: '該用戶還沒有綁定社交帳號',
|
||||
deletion_confirmation: '你在正要刪除現有的 <name /> 身份,是否確認?',
|
||||
},
|
||||
mfa: {
|
||||
field_name: '多因素驗證',
|
||||
field_description: '這個用戶已啟用2步驗證因素。',
|
||||
name_column: '多因素驗證',
|
||||
field_description_empty: '此使用者尚未啟用兩步驗證因素。',
|
||||
deletion_confirmation: '您正在移除現有的2步驗證器的<name/>。您確定要這樣做嗎?',
|
||||
},
|
||||
suspended: '已禁用',
|
||||
suspend_user: '禁用用戶',
|
||||
suspend_user_reminder:
|
||||
|
|
Loading…
Reference in a new issue