mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 14:50:11 +00:00
feat: add passkey-based two-factor authentication
This commit is contained in:
@@ -340,6 +340,28 @@ export async function createAccountPasskeyCredential(
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTwoFactorPasskeyCredential(options: unknown): Promise<Record<string, unknown>> {
|
||||
if (!window.PublicKeyCredential || !navigator.credentials) {
|
||||
throw new Error(t('txt_passkey_browser_not_supported'));
|
||||
}
|
||||
const credential = await navigator.credentials.create({ publicKey: cloneCreationOptions(options) });
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error(t('txt_no_passkey_created'));
|
||||
}
|
||||
return attestationRequest(credential);
|
||||
}
|
||||
|
||||
export async function assertTwoFactorPasskey(options: unknown): Promise<string> {
|
||||
if (!window.PublicKeyCredential || !navigator.credentials) {
|
||||
throw new Error(t('txt_passkey_browser_not_supported'));
|
||||
}
|
||||
const credential = await navigator.credentials.get({ publicKey: cloneRequestOptions(options) });
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error(t('txt_invalid_passkey_assertion_response'));
|
||||
}
|
||||
return JSON.stringify(assertionRequest(credential));
|
||||
}
|
||||
|
||||
function parseRsaEncryptedUserKey(value: string): Uint8Array {
|
||||
const text = String(value || '').trim();
|
||||
const [type, payload] = text.split('.');
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
SessionState,
|
||||
TokenError,
|
||||
TokenSuccess,
|
||||
TwoFactorPasskeySettings,
|
||||
YubiKeyOtpSettings,
|
||||
} from '../types';
|
||||
import type { AccountPasskeyAssertion, AccountPasskeyPrfKeySet } from '../account-passkeys';
|
||||
@@ -756,6 +757,99 @@ export async function disableYubiKeyOtp(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTwoFactorPasskeySettings(raw: any): TwoFactorPasskeySettings {
|
||||
const keys = Array.isArray(raw?.keys) ? raw.keys : Array.isArray(raw?.Keys) ? raw.Keys : [];
|
||||
return {
|
||||
enabled: !!(raw?.enabled ?? raw?.Enabled),
|
||||
keys: keys
|
||||
.map((item: any) => ({
|
||||
id: Number(item?.id ?? item?.Id),
|
||||
name: String(item?.name || item?.Name || ''),
|
||||
migrated: !!(item?.migrated ?? item?.Migrated),
|
||||
}))
|
||||
.filter((item: { id: number }) => Number.isInteger(item.id) && item.id > 0),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTwoFactorPasskeySettings(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string
|
||||
): Promise<TwoFactorPasskeySettings> {
|
||||
const resp = await authedFetch('/api/two-factor/get-webauthn', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await parseJson<TokenError>(resp);
|
||||
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_master_password_verify_failed')));
|
||||
}
|
||||
return normalizeTwoFactorPasskeySettings(await parseJson<unknown>(resp));
|
||||
}
|
||||
|
||||
export async function getTwoFactorPasskeyChallenge(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string
|
||||
): Promise<unknown> {
|
||||
const resp = await authedFetch('/api/two-factor/get-webauthn-challenge', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await parseJson<TokenError>(resp);
|
||||
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_passkey_setup_failed')));
|
||||
}
|
||||
return parseJson<unknown>(resp);
|
||||
}
|
||||
|
||||
export async function saveTwoFactorPasskey(
|
||||
authedFetch: AuthedFetch,
|
||||
payload: { id?: number; name: string; masterPasswordHash: string; deviceResponse: unknown }
|
||||
): Promise<TwoFactorPasskeySettings> {
|
||||
const resp = await authedFetch('/api/two-factor/webauthn', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await parseJson<TokenError>(resp);
|
||||
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_passkey_setup_failed')));
|
||||
}
|
||||
return normalizeTwoFactorPasskeySettings(await parseJson<unknown>(resp));
|
||||
}
|
||||
|
||||
export async function deleteTwoFactorPasskey(
|
||||
authedFetch: AuthedFetch,
|
||||
payload: { id: number; masterPasswordHash: string }
|
||||
): Promise<TwoFactorPasskeySettings> {
|
||||
const resp = await authedFetch('/api/two-factor/webauthn', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await parseJson<TokenError>(resp);
|
||||
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_delete_item_failed')));
|
||||
}
|
||||
return normalizeTwoFactorPasskeySettings(await parseJson<unknown>(resp));
|
||||
}
|
||||
|
||||
export async function disableTwoFactorPasskeys(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string
|
||||
): Promise<void> {
|
||||
const resp = await authedFetch('/api/two-factor/disable', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 7, masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await parseJson<TokenError>(resp);
|
||||
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_disable_passkey_two_step_failed')));
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyMasterPassword(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string
|
||||
@@ -913,7 +1007,7 @@ export async function getVaultRevisionDate(authedFetch: AuthedFetch): Promise<nu
|
||||
return stamp;
|
||||
}
|
||||
|
||||
export async function getTwoFactorProviderStatus(authedFetch: AuthedFetch): Promise<{ totpEnabled: boolean; yubikeyEnabled: boolean }> {
|
||||
export async function getTwoFactorProviderStatus(authedFetch: AuthedFetch): Promise<{ totpEnabled: boolean; yubikeyEnabled: boolean; passkeyEnabled: boolean }> {
|
||||
const resp = await authedFetch('/api/two-factor');
|
||||
if (!resp.ok) throw new Error('Failed to load two-factor status');
|
||||
const body = (await parseJson<{ data?: unknown[]; Data?: unknown[] }>(resp)) || {};
|
||||
@@ -926,6 +1020,7 @@ export async function getTwoFactorProviderStatus(authedFetch: AuthedFetch): Prom
|
||||
return {
|
||||
totpEnabled: enabledTypes.has(0),
|
||||
yubikeyEnabled: enabledTypes.has(3),
|
||||
passkeyEnabled: enabledTypes.has(7),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+107
-20
@@ -35,6 +35,9 @@ export interface PendingTotp {
|
||||
masterKey: Uint8Array;
|
||||
kdfIterations: number;
|
||||
providerType: number;
|
||||
providerData?: unknown;
|
||||
availableProviders: number[];
|
||||
providerDataByType: Record<number, unknown>;
|
||||
}
|
||||
|
||||
export interface PendingPasskeyPassword {
|
||||
@@ -73,27 +76,94 @@ export interface CompletedLogin {
|
||||
|
||||
const TWO_FACTOR_PROVIDER_AUTHENTICATOR = 0;
|
||||
const TWO_FACTOR_PROVIDER_YUBIKEY = 3;
|
||||
const TWO_FACTOR_PROVIDER_WEBAUTHN = 7;
|
||||
const SUPPORTED_TWO_FACTOR_PROVIDERS = [
|
||||
TWO_FACTOR_PROVIDER_WEBAUTHN,
|
||||
TWO_FACTOR_PROVIDER_YUBIKEY,
|
||||
TWO_FACTOR_PROVIDER_AUTHENTICATOR,
|
||||
] as const;
|
||||
|
||||
function readTokenUserVerificationToken(token: TokenSuccess): string | null {
|
||||
return String(token.UserVerificationToken || token.userVerificationToken || '').trim() || null;
|
||||
}
|
||||
|
||||
function resolvePendingTwoFactorProvider(providers: unknown): number {
|
||||
type TwoFactorTokenError = {
|
||||
TwoFactorProviders?: unknown;
|
||||
TwoFactorProviders2?: unknown;
|
||||
CustomResponse?: {
|
||||
TwoFactorProviders?: unknown;
|
||||
TwoFactorProviders2?: unknown;
|
||||
};
|
||||
error_description?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function readTwoFactorProviders(error: TwoFactorTokenError): unknown {
|
||||
return error.TwoFactorProviders ?? error.CustomResponse?.TwoFactorProviders ?? error.TwoFactorProviders2 ?? error.CustomResponse?.TwoFactorProviders2;
|
||||
}
|
||||
|
||||
function readTwoFactorProviderData(error: TwoFactorTokenError, providerType: number): unknown {
|
||||
const providers2 = error.TwoFactorProviders2 ?? error.CustomResponse?.TwoFactorProviders2;
|
||||
if (!providers2 || typeof providers2 !== 'object') return undefined;
|
||||
const record = providers2 as Record<string, unknown>;
|
||||
return record[String(providerType)] ?? (providerType === TWO_FACTOR_PROVIDER_WEBAUTHN ? record.WebAuthn : undefined);
|
||||
}
|
||||
|
||||
function twoFactorProviderTypeFromValue(value: unknown): number | null {
|
||||
const raw = value && typeof value === 'object'
|
||||
? (value as Record<string, unknown>).Type ?? (value as Record<string, unknown>).type
|
||||
: value;
|
||||
const text = String(raw ?? '').trim();
|
||||
if (!text) return null;
|
||||
const normalized = text.toLowerCase();
|
||||
const numeric = Number(text);
|
||||
const provider = Number.isFinite(numeric)
|
||||
? numeric
|
||||
: normalized === 'webauthn'
|
||||
? TWO_FACTOR_PROVIDER_WEBAUTHN
|
||||
: normalized === 'yubikey' || normalized === 'yubikeyotp'
|
||||
? TWO_FACTOR_PROVIDER_YUBIKEY
|
||||
: normalized === 'authenticator' || normalized === 'totp'
|
||||
? TWO_FACTOR_PROVIDER_AUTHENTICATOR
|
||||
: Number.NaN;
|
||||
return SUPPORTED_TWO_FACTOR_PROVIDERS.includes(provider as any) ? provider : null;
|
||||
}
|
||||
|
||||
function sortTwoFactorProviders(providerTypes: number[]): number[] {
|
||||
const unique = new Set(providerTypes);
|
||||
return SUPPORTED_TWO_FACTOR_PROVIDERS.filter((provider) => unique.has(provider));
|
||||
}
|
||||
|
||||
function readTwoFactorProviderTypes(providers: unknown): number[] {
|
||||
const providerTypes: number[] = [];
|
||||
if (Array.isArray(providers)) {
|
||||
if (providers.some((provider: any) => Number(
|
||||
provider && typeof provider === 'object' ? provider.Type ?? provider.type : provider
|
||||
) === TWO_FACTOR_PROVIDER_YUBIKEY)) {
|
||||
return TWO_FACTOR_PROVIDER_YUBIKEY;
|
||||
for (const provider of providers) {
|
||||
const providerType = twoFactorProviderTypeFromValue(provider);
|
||||
if (providerType != null) providerTypes.push(providerType);
|
||||
}
|
||||
return TWO_FACTOR_PROVIDER_AUTHENTICATOR;
|
||||
}
|
||||
if (providers && typeof providers === 'object') {
|
||||
const record = providers as Record<string, unknown>;
|
||||
if (record[String(TWO_FACTOR_PROVIDER_YUBIKEY)] || record.YubiKey || record.Yubikey) {
|
||||
return TWO_FACTOR_PROVIDER_YUBIKEY;
|
||||
} else if (providers && typeof providers === 'object') {
|
||||
for (const [key, value] of Object.entries(providers as Record<string, unknown>)) {
|
||||
if (!value) continue;
|
||||
const providerType = twoFactorProviderTypeFromValue(key);
|
||||
if (providerType != null) providerTypes.push(providerType);
|
||||
}
|
||||
}
|
||||
return TWO_FACTOR_PROVIDER_AUTHENTICATOR;
|
||||
return sortTwoFactorProviders(providerTypes);
|
||||
}
|
||||
|
||||
function readTwoFactorProviderDataMap(error: TwoFactorTokenError): Record<number, unknown> {
|
||||
const providers2 = error.TwoFactorProviders2 ?? error.CustomResponse?.TwoFactorProviders2;
|
||||
if (!providers2 || typeof providers2 !== 'object') return {};
|
||||
const out: Record<number, unknown> = {};
|
||||
for (const [key, value] of Object.entries(providers2 as Record<string, unknown>)) {
|
||||
const providerType = twoFactorProviderTypeFromValue(key);
|
||||
if (providerType != null) out[providerType] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolvePendingTwoFactorProvider(providers: unknown): number {
|
||||
return readTwoFactorProviderTypes(providers)[0] ?? TWO_FACTOR_PROVIDER_AUTHENTICATOR;
|
||||
}
|
||||
|
||||
export type PasswordLoginResult =
|
||||
@@ -438,8 +508,12 @@ export async function performPasswordLogin(
|
||||
};
|
||||
}
|
||||
|
||||
const tokenError = token as { TwoFactorProviders?: unknown; error_description?: string; error?: string };
|
||||
if (tokenError.TwoFactorProviders) {
|
||||
const tokenError = token as TwoFactorTokenError;
|
||||
const providers = readTwoFactorProviders(tokenError);
|
||||
if (providers) {
|
||||
const providerType = resolvePendingTwoFactorProvider(providers);
|
||||
const availableProviders = readTwoFactorProviderTypes(providers);
|
||||
const providerDataByType = readTwoFactorProviderDataMap(tokenError);
|
||||
return {
|
||||
kind: 'totp',
|
||||
pendingTotp: {
|
||||
@@ -447,7 +521,10 @@ export async function performPasswordLogin(
|
||||
passwordHash: derived.hash,
|
||||
masterKey: derived.masterKey,
|
||||
kdfIterations: derived.kdfIterations,
|
||||
providerType: resolvePendingTwoFactorProvider(tokenError.TwoFactorProviders),
|
||||
providerType,
|
||||
providerData: providerDataByType[providerType] ?? readTwoFactorProviderData(tokenError, providerType),
|
||||
availableProviders: availableProviders.length ? availableProviders : [providerType],
|
||||
providerDataByType,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -528,7 +605,10 @@ export async function performTotpLogin(
|
||||
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey, pendingTotp.kdfIterations, pendingTotp.passwordHash);
|
||||
}
|
||||
const tokenError = token as { error_description?: string; error?: string };
|
||||
throw new Error(translateServerError(tokenError.error_description || tokenError.error, t('txt_totp_verify_failed')));
|
||||
const fallback = pendingTotp.providerType === TWO_FACTOR_PROVIDER_WEBAUTHN
|
||||
? t('txt_passkey_verification_failed')
|
||||
: t('txt_totp_verify_failed');
|
||||
throw new Error(translateServerError(tokenError.error_description || tokenError.error, fallback));
|
||||
}
|
||||
|
||||
export async function performRecoverTwoFactorLogin(
|
||||
@@ -608,7 +688,7 @@ export async function performUnlock(
|
||||
return unlockOffline();
|
||||
}
|
||||
|
||||
let token: TokenSuccess | { TwoFactorProviders?: unknown; error_description?: string; error?: string };
|
||||
let token: TokenSuccess | TwoFactorTokenError;
|
||||
try {
|
||||
token = await loginWithPassword(normalizedEmail, derived.hash, {
|
||||
useRememberToken: true,
|
||||
@@ -630,8 +710,12 @@ export async function performUnlock(
|
||||
};
|
||||
}
|
||||
|
||||
const tokenError = token as { TwoFactorProviders?: unknown; error_description?: string; error?: string };
|
||||
if (tokenError.TwoFactorProviders) {
|
||||
const tokenError = token as TwoFactorTokenError;
|
||||
const providers = readTwoFactorProviders(tokenError);
|
||||
if (providers) {
|
||||
const providerType = resolvePendingTwoFactorProvider(providers);
|
||||
const availableProviders = readTwoFactorProviderTypes(providers);
|
||||
const providerDataByType = readTwoFactorProviderDataMap(tokenError);
|
||||
return {
|
||||
kind: 'totp',
|
||||
pendingTotp: {
|
||||
@@ -639,7 +723,10 @@ export async function performUnlock(
|
||||
passwordHash: derived.hash,
|
||||
masterKey: derived.masterKey,
|
||||
kdfIterations: derived.kdfIterations,
|
||||
providerType: resolvePendingTwoFactorProvider(tokenError.TwoFactorProviders),
|
||||
providerType,
|
||||
providerData: providerDataByType[providerType] ?? readTwoFactorProviderData(tokenError, providerType),
|
||||
availableProviders: availableProviders.length ? availableProviders : [providerType],
|
||||
providerDataByType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -907,6 +907,7 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
||||
adminLoading: false,
|
||||
adminError: '',
|
||||
totpEnabled: true,
|
||||
passkey2faEnabled: false,
|
||||
authorizedDevices: state.authorizedDevices,
|
||||
authorizedDevicesLoading: false,
|
||||
authorizedDevicesError: '',
|
||||
@@ -1060,6 +1061,16 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
||||
onSavePasswordHint: readonly,
|
||||
onEnableTotp: readonly,
|
||||
onOpenDisableTotp: readonlyVoid,
|
||||
onGetTwoFactorPasskeySettings: async () => ({ enabled: false, keys: [] }),
|
||||
onCreateTwoFactorPasskey: async () => {
|
||||
await readonly();
|
||||
return { enabled: false, keys: [] };
|
||||
},
|
||||
onDeleteTwoFactorPasskey: async () => {
|
||||
await readonly();
|
||||
return { enabled: false, keys: [] };
|
||||
},
|
||||
onDisableTwoFactorPasskeys: readonly,
|
||||
onGetRecoveryCode: readonlyString,
|
||||
onGetApiKey: readonlyString,
|
||||
onRotateApiKey: readonlyString,
|
||||
|
||||
@@ -806,6 +806,23 @@ const en: Record<string, string> = {
|
||||
"txt_password_hint_too_long": "Password hint must be 120 characters or fewer",
|
||||
"txt_passkey": "Passkey",
|
||||
"txt_passkeys": "Passkeys",
|
||||
"txt_register": "Register",
|
||||
"txt_key_list": "Key list",
|
||||
"txt_select_another_verification_method": "Select another verification method",
|
||||
"txt_select_two_step_login_method": "Select two-step login method",
|
||||
"txt_two_step_passkeys": "Passkey two-step login",
|
||||
"txt_two_step_passkeys_help": "Manage passkeys used only for two-step login.",
|
||||
"txt_two_step_passkey_name_placeholder": "Security key",
|
||||
"txt_add_two_step_passkey": "Add passkey",
|
||||
"txt_two_step_passkey_added": "Passkey two-step login updated",
|
||||
"txt_two_step_passkey_removed": "Passkey removed",
|
||||
"txt_two_step_passkeys_disabled": "Passkey two-step login disabled",
|
||||
"txt_disable_passkey_two_step_failed": "Failed to disable passkey two-step login",
|
||||
"txt_use_passkey_to_complete_two_step_verification": "Use your passkey to complete two-step verification.",
|
||||
"txt_touch_your_passkey_when_prompted": "Continue and approve the browser passkey prompt.",
|
||||
"txt_no_two_step_passkeys": "No two-step passkeys",
|
||||
"txt_remove_last_passkey_hint": "Disable passkey two-step login to remove the last key.",
|
||||
"txt_passkey_setup_failed": "Passkey setup failed",
|
||||
"txt_passkey_created_at_value": "Created on {value}",
|
||||
"txt_account_passkey": "Account passkey",
|
||||
"txt_account_passkeys": "Account passkeys",
|
||||
|
||||
@@ -806,6 +806,23 @@ const es: Record<string, string> = {
|
||||
"txt_password_hint_too_long": "La pista de contraseña debe tener 120 caracteres o menos",
|
||||
"txt_passkey": "Clave de acceso",
|
||||
"txt_passkeys": "Claves de acceso",
|
||||
"txt_register": "Registrar",
|
||||
"txt_key_list": "Lista de claves",
|
||||
"txt_select_another_verification_method": "Seleccionar otro método de verificación",
|
||||
"txt_select_two_step_login_method": "Seleccionar método de inicio de sesión en dos pasos",
|
||||
"txt_two_step_passkeys": "Inicio de sesión en dos pasos con clave de acceso",
|
||||
"txt_two_step_passkeys_help": "Administra claves de acceso usadas solo para el inicio de sesión en dos pasos.",
|
||||
"txt_two_step_passkey_name_placeholder": "Llave de seguridad",
|
||||
"txt_add_two_step_passkey": "Agregar clave de acceso",
|
||||
"txt_two_step_passkey_added": "Inicio de sesión en dos pasos con clave de acceso actualizado",
|
||||
"txt_two_step_passkey_removed": "Clave de acceso eliminada",
|
||||
"txt_two_step_passkeys_disabled": "Inicio de sesión en dos pasos con clave de acceso desactivado",
|
||||
"txt_disable_passkey_two_step_failed": "No se pudo desactivar el inicio de sesión en dos pasos con clave de acceso",
|
||||
"txt_use_passkey_to_complete_two_step_verification": "Usa tu clave de acceso para completar la verificación en dos pasos.",
|
||||
"txt_touch_your_passkey_when_prompted": "Continúa y aprueba la solicitud de clave de acceso del navegador.",
|
||||
"txt_no_two_step_passkeys": "No hay claves de acceso en dos pasos",
|
||||
"txt_remove_last_passkey_hint": "Desactiva el inicio de sesión en dos pasos con clave de acceso para eliminar la última clave.",
|
||||
"txt_passkey_setup_failed": "Error al configurar la clave de acceso",
|
||||
"txt_passkey_created_at_value": "Creado el {value}",
|
||||
"txt_account_passkey": "Clave de acceso de cuenta",
|
||||
"txt_account_passkeys": "Claves de acceso de cuenta",
|
||||
|
||||
@@ -806,6 +806,23 @@ const ru: Record<string, string> = {
|
||||
"txt_password_hint_too_long": "Подсказка к паролю должна содержать не более 120 символов.",
|
||||
"txt_passkey": "Ключ доступа",
|
||||
"txt_passkeys": "Ключи доступа",
|
||||
"txt_register": "Зарегистрировать",
|
||||
"txt_key_list": "Список ключей",
|
||||
"txt_select_another_verification_method": "Выбрать другой способ проверки",
|
||||
"txt_select_two_step_login_method": "Выберите способ двухэтапного входа",
|
||||
"txt_two_step_passkeys": "Двухэтапный вход с ключом доступа",
|
||||
"txt_two_step_passkeys_help": "Управление ключами доступа, которые используются только для двухэтапного входа.",
|
||||
"txt_two_step_passkey_name_placeholder": "Ключ безопасности",
|
||||
"txt_add_two_step_passkey": "Добавить ключ доступа",
|
||||
"txt_two_step_passkey_added": "Двухэтапный вход с ключом доступа обновлен",
|
||||
"txt_two_step_passkey_removed": "Ключ доступа удален",
|
||||
"txt_two_step_passkeys_disabled": "Двухэтапный вход с ключом доступа отключен",
|
||||
"txt_disable_passkey_two_step_failed": "Не удалось отключить двухэтапный вход с ключом доступа",
|
||||
"txt_use_passkey_to_complete_two_step_verification": "Используйте ключ доступа, чтобы завершить двухэтапную проверку.",
|
||||
"txt_touch_your_passkey_when_prompted": "Продолжите и подтвердите запрос ключа доступа в браузере.",
|
||||
"txt_no_two_step_passkeys": "Нет ключей доступа для двухэтапного входа",
|
||||
"txt_remove_last_passkey_hint": "Отключите двухэтапный вход с ключом доступа, чтобы удалить последний ключ.",
|
||||
"txt_passkey_setup_failed": "Не удалось настроить ключ доступа",
|
||||
"txt_passkey_created_at_value": "Создано {value}",
|
||||
"txt_account_passkey": "Ключ доступа аккаунта",
|
||||
"txt_account_passkeys": "Ключи доступа аккаунта",
|
||||
|
||||
@@ -806,6 +806,23 @@ const zhCN: Record<string, string> = {
|
||||
"txt_password_hint_too_long": "密码提示最多只能输入 120 个字符",
|
||||
"txt_passkey": "通行密钥",
|
||||
"txt_passkeys": "通行密钥",
|
||||
"txt_register": "注册",
|
||||
"txt_key_list": "密钥列表",
|
||||
"txt_select_another_verification_method": "选择其他验证方式",
|
||||
"txt_select_two_step_login_method": "选择验证方式",
|
||||
"txt_two_step_passkeys": "通行密钥二步登录",
|
||||
"txt_two_step_passkeys_help": "管理仅用于二步登录的通行密钥。",
|
||||
"txt_two_step_passkey_name_placeholder": "安全密钥",
|
||||
"txt_add_two_step_passkey": "添加通行密钥",
|
||||
"txt_two_step_passkey_added": "通行密钥二步登录已更新",
|
||||
"txt_two_step_passkey_removed": "通行密钥已移除",
|
||||
"txt_two_step_passkeys_disabled": "通行密钥二步登录已禁用",
|
||||
"txt_disable_passkey_two_step_failed": "禁用通行密钥二步登录失败",
|
||||
"txt_use_passkey_to_complete_two_step_verification": "使用通行密钥完成二步验证。",
|
||||
"txt_touch_your_passkey_when_prompted": "继续并在浏览器提示中批准通行密钥验证。",
|
||||
"txt_no_two_step_passkeys": "暂无二步登录通行密钥",
|
||||
"txt_remove_last_passkey_hint": "请禁用通行密钥二步登录来移除最后一把密钥。",
|
||||
"txt_passkey_setup_failed": "通行密钥设置失败",
|
||||
"txt_passkey_created_at_value": "创建于 {value}",
|
||||
"txt_account_passkey": "账号通行密钥",
|
||||
"txt_account_passkeys": "账号通行密钥",
|
||||
|
||||
@@ -806,6 +806,23 @@ const zhTW: Record<string, string> = {
|
||||
"txt_password_hint_too_long": "密碼提示最多隻能輸入 120 個字符",
|
||||
"txt_passkey": "通行密鑰",
|
||||
"txt_passkeys": "通行密鑰",
|
||||
"txt_register": "註冊",
|
||||
"txt_key_list": "密鑰列表",
|
||||
"txt_select_another_verification_method": "選擇其他驗證方式",
|
||||
"txt_select_two_step_login_method": "選擇驗證方式",
|
||||
"txt_two_step_passkeys": "通行密鑰兩步登入",
|
||||
"txt_two_step_passkeys_help": "管理僅用於兩步登入的通行密鑰。",
|
||||
"txt_two_step_passkey_name_placeholder": "安全密鑰",
|
||||
"txt_add_two_step_passkey": "新增通行密鑰",
|
||||
"txt_two_step_passkey_added": "通行密鑰兩步登入已更新",
|
||||
"txt_two_step_passkey_removed": "通行密鑰已移除",
|
||||
"txt_two_step_passkeys_disabled": "通行密鑰兩步登入已停用",
|
||||
"txt_disable_passkey_two_step_failed": "停用通行密鑰兩步登入失敗",
|
||||
"txt_use_passkey_to_complete_two_step_verification": "使用通行密鑰完成兩步驗證。",
|
||||
"txt_touch_your_passkey_when_prompted": "繼續並在瀏覽器提示中批准通行密鑰驗證。",
|
||||
"txt_no_two_step_passkeys": "暫無兩步登入通行密鑰",
|
||||
"txt_remove_last_passkey_hint": "請停用通行密鑰兩步登入來移除最後一把密鑰。",
|
||||
"txt_passkey_setup_failed": "通行密鑰設置失敗",
|
||||
"txt_passkey_created_at_value": "創建於 {value}",
|
||||
"txt_account_passkey": "賬號通行密鑰",
|
||||
"txt_account_passkeys": "賬號通行密鑰",
|
||||
|
||||
@@ -350,6 +350,17 @@ export interface AccountPasskeyCredential {
|
||||
revisionDate?: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorPasskeyCredential {
|
||||
id: number;
|
||||
name: string;
|
||||
migrated?: boolean;
|
||||
}
|
||||
|
||||
export interface TwoFactorPasskeySettings {
|
||||
enabled: boolean;
|
||||
keys: TwoFactorPasskeyCredential[];
|
||||
}
|
||||
|
||||
export interface AuthRequest {
|
||||
id: string;
|
||||
publicKey: string;
|
||||
|
||||
Reference in New Issue
Block a user