feat: Add YubiKey OTP support and management features

- Implemented YubiKey OTP settings management in useAccountSecurityActions hook.
- Added API functions for retrieving, saving, and bootstrapping YubiKey OTP credentials.
- Enhanced authentication flow to support multiple two-factor providers, including YubiKey.
- Updated localization files to include new YubiKey-related strings in English, Spanish, Russian, and Chinese.
- Introduced new styles for YubiKey management UI components.
- Created utility functions for YubiKey OTP validation and credential handling.
This commit is contained in:
shuaiplus
2026-07-04 02:49:46 +08:00
parent c7eb6c663d
commit f63b745d05
27 changed files with 1325 additions and 61 deletions
+121 -6
View File
@@ -7,6 +7,7 @@ import type {
SessionState,
TokenError,
TokenSuccess,
YubiKeyOtpSettings,
} from '../types';
import type { AccountPasskeyAssertion, AccountPasskeyPrfKeySet } from '../account-passkeys';
import { recordNodeWardenReachable, recordNodeWardenUnreachable } from '../network-status';
@@ -240,6 +241,7 @@ export async function loginWithPassword(
passwordHash: string,
options?: {
totpCode?: string;
twoFactorProvider?: number;
rememberDevice?: boolean;
useRememberToken?: boolean;
signal?: AbortSignal;
@@ -259,7 +261,7 @@ export async function loginWithPassword(
body.set('twoFactorProvider', '5');
body.set('twoFactorToken', rememberedToken);
} else if (options?.totpCode) {
body.set('twoFactorProvider', '0');
body.set('twoFactorProvider', String(options.twoFactorProvider ?? 0));
body.set('twoFactorToken', options.totpCode);
if (options.rememberDevice) {
body.set('twoFactorRemember', '1');
@@ -650,6 +652,110 @@ export async function setTotp(
}
}
function normalizeYubiKeySettings(raw: any): YubiKeyOtpSettings {
return {
enabled: !!(raw?.enabled ?? raw?.Enabled),
keys: [
String(raw?.key1 ?? raw?.Key1 ?? ''),
String(raw?.key2 ?? raw?.Key2 ?? ''),
String(raw?.key3 ?? raw?.Key3 ?? ''),
String(raw?.key4 ?? raw?.Key4 ?? ''),
String(raw?.key5 ?? raw?.Key5 ?? ''),
],
nfc: !!(raw?.nfc ?? raw?.Nfc),
yubicoConfigured: !!(raw?.yubicoConfigured ?? raw?.YubicoConfigured),
yubicoClientId: String(raw?.yubicoClientId ?? raw?.YubicoClientId ?? ''),
yubicoSecretKey: String(raw?.yubicoSecretKey ?? raw?.YubicoSecretKey ?? ''),
};
}
export async function getYubiKeyOtpSettings(
authedFetch: AuthedFetch,
masterPasswordHash: string
): Promise<YubiKeyOtpSettings> {
const resp = await authedFetch('/api/two-factor/get-yubikey', {
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 normalizeYubiKeySettings(await parseJson<unknown>(resp));
}
export async function saveYubiKeyOtpSettings(
authedFetch: AuthedFetch,
payload: { keys: string[]; nfc: boolean; masterPasswordHash: string }
): Promise<YubiKeyOtpSettings> {
const resp = await authedFetch('/api/two-factor/yubikey', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key1: payload.keys[0] || '',
key2: payload.keys[1] || '',
key3: payload.keys[2] || '',
key4: payload.keys[3] || '',
key5: payload.keys[4] || '',
nfc: payload.nfc,
masterPasswordHash: payload.masterPasswordHash,
}),
});
if (!resp.ok) {
const body = await parseJson<TokenError>(resp);
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_yubikey_update_failed')));
}
return normalizeYubiKeySettings(await parseJson<unknown>(resp));
}
export async function saveYubiKeyOtpApiCredentials(
authedFetch: AuthedFetch,
payload: { masterPasswordHash: string; yubicoClientId: string; yubicoSecretKey: string }
): Promise<YubiKeyOtpSettings> {
const resp = await authedFetch('/api/two-factor/yubikey/config', {
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_yubikey_config_update_failed')));
}
return normalizeYubiKeySettings(await parseJson<unknown>(resp));
}
export async function bootstrapYubiKeyOtpApiCredentials(
authedFetch: AuthedFetch,
payload: { masterPasswordHash: string; otp: string }
): Promise<YubiKeyOtpSettings> {
const resp = await authedFetch('/api/two-factor/yubikey/bootstrap', {
method: 'POST',
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_yubikey_auto_config_failed')));
}
return normalizeYubiKeySettings(await parseJson<unknown>(resp));
}
export async function disableYubiKeyOtp(
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: 3, masterPasswordHash }),
});
if (!resp.ok) {
const body = await parseJson<TokenError>(resp);
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_disable_yubikey_failed')));
}
}
export async function verifyMasterPassword(
authedFetch: AuthedFetch,
masterPasswordHash: string
@@ -807,11 +913,20 @@ export async function getVaultRevisionDate(authedFetch: AuthedFetch): Promise<nu
return stamp;
}
export async function getTotpStatus(authedFetch: AuthedFetch): Promise<{ enabled: boolean }> {
const resp = await authedFetch('/api/accounts/totp');
if (!resp.ok) throw new Error('Failed to load TOTP status');
const body = (await parseJson<{ enabled?: boolean }>(resp)) || {};
return { enabled: !!body.enabled };
export async function getTwoFactorProviderStatus(authedFetch: AuthedFetch): Promise<{ totpEnabled: boolean; yubikeyEnabled: 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)) || {};
const providers = Array.isArray(body.data) ? body.data : Array.isArray(body.Data) ? body.Data : [];
const enabledTypes = new Set(
providers
.map((provider: any) => Number(provider?.type ?? provider?.Type))
.filter((type) => Number.isFinite(type))
);
return {
totpEnabled: enabledTypes.has(0),
yubikeyEnabled: enabledTypes.has(3),
};
}
export async function getTotpRecoveryCode(
+25
View File
@@ -34,6 +34,7 @@ export interface PendingTotp {
passwordHash: string;
masterKey: Uint8Array;
kdfIterations: number;
providerType: number;
}
export interface PendingPasskeyPassword {
@@ -70,10 +71,31 @@ export interface CompletedLogin {
freshUserVerificationToken?: string | null;
}
const TWO_FACTOR_PROVIDER_AUTHENTICATOR = 0;
const TWO_FACTOR_PROVIDER_YUBIKEY = 3;
function readTokenUserVerificationToken(token: TokenSuccess): string | null {
return String(token.UserVerificationToken || token.userVerificationToken || '').trim() || null;
}
function resolvePendingTwoFactorProvider(providers: unknown): 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;
}
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;
}
}
return TWO_FACTOR_PROVIDER_AUTHENTICATOR;
}
export type PasswordLoginResult =
| { kind: 'success'; login: CompletedLogin }
| { kind: 'totp'; pendingTotp: PendingTotp }
@@ -425,6 +447,7 @@ export async function performPasswordLogin(
passwordHash: derived.hash,
masterKey: derived.masterKey,
kdfIterations: derived.kdfIterations,
providerType: resolvePendingTwoFactorProvider(tokenError.TwoFactorProviders),
},
};
}
@@ -498,6 +521,7 @@ export async function performTotpLogin(
): Promise<CompletedLogin> {
const token = await loginWithPassword(pendingTotp.email, pendingTotp.passwordHash, {
totpCode: totpCode.trim(),
twoFactorProvider: pendingTotp.providerType,
rememberDevice,
});
if ('access_token' in token && token.access_token) {
@@ -615,6 +639,7 @@ export async function performUnlock(
passwordHash: derived.hash,
masterKey: derived.masterKey,
kdfIterations: derived.kdfIterations,
providerType: resolvePendingTwoFactorProvider(tokenError.TwoFactorProviders),
},
};
}
+31
View File
@@ -27,6 +27,35 @@ const en: Record<string, string> = {
"txt_passkey_provider_help": "Use a FIDO2-compatible security key or biometric authenticator.",
"txt_yubico_otp_security_key": "Yubico OTP security key",
"txt_yubico_otp_security_key_help": "Use a YubiKey 4, 5, or NEO device.",
"txt_yubikey_setup_intro": "Insert your YubiKey into a USB port. Select the first empty YubiKey field below, touch the YubiKey button, then save the form.",
"txt_yubikey_plug_in": "Insert your YubiKey into a USB port.",
"txt_yubikey_select_empty_field": "Select the first empty YubiKey input field below.",
"txt_yubikey_touch_button": "Touch the YubiKey button.",
"txt_yubikey_save_form": "Save the form.",
"txt_yubikey_x": "YubiKey {index}",
"txt_nfc_support": "NFC support",
"txt_yubikey_supports_nfc": "One of my keys supports NFC.",
"txt_yubikey_supports_nfc_desc": "If one of your YubiKeys supports NFC, mobile apps can prompt you when NFC is available.",
"txt_disable_all_keys": "Disable all keys",
"txt_yubikeys_updated": "YubiKeys updated",
"txt_yubikey_update_failed": "Failed to update YubiKeys",
"txt_disable_yubikey_failed": "Failed to disable YubiKeys",
"txt_yubikey_disabled": "YubiKeys disabled",
"txt_yubikey_enabled": "YubiKey is enabled.",
"txt_yubikey_config_required": "Yubico validation is not configured",
"txt_yubikey_config_required_help": "Enter one YubiKey OTP first. NodeWarden will automatically request and save the instance Client ID and Secret key, then open the YubiKey setup form.",
"txt_otp_from_yubikey": "OTP from YubiKey",
"txt_please_input_yubikey_otp": "Please input YubiKey OTP",
"txt_yubikey_verify_failed": "YubiKey verification failed",
"txt_press_yubikey_to_authenticate": "Press your YubiKey to authenticate.",
"txt_yubikey_auto_configure": "Get and save automatically",
"txt_yubikey_validation_credentials": "Yubico validation credentials",
"txt_view": "View",
"txt_yubikey_config_updated": "Yubico validation credentials updated",
"txt_yubikey_config_update_failed": "Failed to update Yubico validation credentials",
"txt_yubikey_auto_config_failed": "Failed to get Yubico validation credentials",
"txt_yubikey_reconfigure_help": "Enter a fresh OTP to request and replace these credentials automatically.",
"txt_yubikey_auto_configure_again": "Get again automatically",
"txt_setting_coming_soon": "Coming soon.",
"txt_totp_manage_intro": "Scan the QR code or enter the key in your authenticator app, then enter the verification code.",
"txt_two_step_recovery_code_warning": "If you cannot access your two-step login provider, your one-time recovery code can be used to disable two-step login. Store the recovery code somewhere safe.",
@@ -860,6 +889,8 @@ const en: Record<string, string> = {
"txt_scope": "scope",
"txt_grant_type": "grant_type",
"txt_refresh": "Refresh",
"txt_refresh_status": "Refresh status",
"txt_load_failed": "Failed to load",
"txt_refresh_in_seconds_s": "Refresh in {seconds}s",
"txt_regenerate": "Regenerate",
"txt_registration_succeeded_please_sign_in": "Registration succeeded. Please sign in.",
+31
View File
@@ -27,6 +27,35 @@ const es: Record<string, string> = {
"txt_passkey_provider_help": "Usa una llave de seguridad compatible con FIDO2 o autenticación biométrica.",
"txt_yubico_otp_security_key": "Llave de seguridad Yubico OTP",
"txt_yubico_otp_security_key_help": "Usa un dispositivo YubiKey 4, 5 o NEO.",
"txt_yubikey_setup_intro": "Inserta tu YubiKey en un puerto USB. Selecciona el primer campo YubiKey vacío, toca el botón de la YubiKey y guarda el formulario.",
"txt_yubikey_plug_in": "Inserta tu YubiKey en un puerto USB.",
"txt_yubikey_select_empty_field": "Selecciona el primer campo YubiKey vacío.",
"txt_yubikey_touch_button": "Toca el botón de la YubiKey.",
"txt_yubikey_save_form": "Guarda el formulario.",
"txt_yubikey_x": "YubiKey {index}",
"txt_nfc_support": "Compatibilidad NFC",
"txt_yubikey_supports_nfc": "Una de mis llaves admite NFC.",
"txt_yubikey_supports_nfc_desc": "Si una de tus YubiKeys admite NFC, las apps móviles pueden avisarte cuando NFC esté disponible.",
"txt_disable_all_keys": "Desactivar todas las llaves",
"txt_yubikeys_updated": "YubiKeys actualizadas",
"txt_yubikey_update_failed": "No se pudieron actualizar las YubiKeys",
"txt_disable_yubikey_failed": "No se pudieron desactivar las YubiKeys",
"txt_yubikey_disabled": "YubiKeys desactivadas",
"txt_yubikey_enabled": "YubiKey activada.",
"txt_yubikey_config_required": "La validación de Yubico no está configurada",
"txt_yubikey_config_required_help": "Introduce primero un OTP de YubiKey. NodeWarden solicitará y guardará automáticamente el Client ID y la Secret key de la instancia, y luego abrirá el formulario de YubiKey.",
"txt_otp_from_yubikey": "OTP de YubiKey",
"txt_please_input_yubikey_otp": "Introduce el OTP de YubiKey",
"txt_yubikey_verify_failed": "No se pudo verificar la YubiKey",
"txt_press_yubikey_to_authenticate": "Pulsa tu YubiKey para autenticarte.",
"txt_yubikey_auto_configure": "Obtener y guardar automáticamente",
"txt_yubikey_validation_credentials": "Credenciales de validación de Yubico",
"txt_view": "Ver",
"txt_yubikey_config_updated": "Credenciales de validación de Yubico actualizadas",
"txt_yubikey_config_update_failed": "No se pudieron actualizar las credenciales de validación de Yubico",
"txt_yubikey_auto_config_failed": "No se pudieron obtener las credenciales de validación de Yubico",
"txt_yubikey_reconfigure_help": "Introduce un OTP nuevo para solicitar y reemplazar estas credenciales automáticamente.",
"txt_yubikey_auto_configure_again": "Obtener de nuevo automáticamente",
"txt_setting_coming_soon": "Próximamente.",
"txt_totp_manage_intro": "Escanea el código QR o introduce la clave en tu aplicación autenticadora, luego escribe el código de verificación.",
"txt_two_step_recovery_code_warning": "Si no puedes acceder a tu proveedor de inicio de sesión en dos pasos, tu código de recuperación de un solo uso puede desactivar el inicio de sesión en dos pasos. Guarda el código en un lugar seguro.",
@@ -860,6 +889,8 @@ const es: Record<string, string> = {
"txt_scope": "Ámbito",
"txt_grant_type": "Tipo de concesión",
"txt_refresh": "Actualizar",
"txt_refresh_status": "Actualizar estado",
"txt_load_failed": "No se pudo cargar",
"txt_refresh_in_seconds_s": "Actualizar en {seconds}s",
"txt_regenerate": "Regenerar",
"txt_registration_succeeded_please_sign_in": "Registro completado. Inicie sesión.",
+31
View File
@@ -28,6 +28,35 @@ const ru: Record<string, string> = {
"txt_passkey_provider_help": "Используйте FIDO2-совместимый ключ безопасности или биометрический аутентификатор.",
"txt_yubico_otp_security_key": "Ключ безопасности Yubico OTP",
"txt_yubico_otp_security_key_help": "Используйте устройство YubiKey 4, 5 или NEO.",
"txt_yubikey_setup_intro": "Вставьте YubiKey в USB-порт. Выберите первое пустое поле YubiKey ниже, коснитесь кнопки YubiKey и сохраните форму.",
"txt_yubikey_plug_in": "Вставьте YubiKey в USB-порт.",
"txt_yubikey_select_empty_field": "Выберите первое пустое поле YubiKey ниже.",
"txt_yubikey_touch_button": "Коснитесь кнопки YubiKey.",
"txt_yubikey_save_form": "Сохраните форму.",
"txt_yubikey_x": "YubiKey {index}",
"txt_nfc_support": "Поддержка NFC",
"txt_yubikey_supports_nfc": "Один из моих ключей поддерживает NFC.",
"txt_yubikey_supports_nfc_desc": "Если один из ваших YubiKey поддерживает NFC, мобильные приложения смогут подсказать вам, когда NFC доступен.",
"txt_disable_all_keys": "Отключить все ключи",
"txt_yubikeys_updated": "YubiKey обновлены",
"txt_yubikey_update_failed": "Не удалось обновить YubiKey",
"txt_disable_yubikey_failed": "Не удалось отключить YubiKey",
"txt_yubikey_disabled": "YubiKey отключены",
"txt_yubikey_enabled": "YubiKey включен.",
"txt_yubikey_config_required": "Проверка Yubico не настроена",
"txt_yubikey_config_required_help": "Сначала введите один OTP с YubiKey. NodeWarden автоматически запросит и сохранит Client ID и Secret key экземпляра, затем откроет форму настройки YubiKey.",
"txt_otp_from_yubikey": "OTP с YubiKey",
"txt_please_input_yubikey_otp": "Введите OTP с YubiKey",
"txt_yubikey_verify_failed": "Не удалось проверить YubiKey",
"txt_press_yubikey_to_authenticate": "Нажмите YubiKey для проверки.",
"txt_yubikey_auto_configure": "Получить и сохранить автоматически",
"txt_yubikey_validation_credentials": "Учетные данные проверки Yubico",
"txt_view": "Показать",
"txt_yubikey_config_updated": "Учетные данные проверки Yubico обновлены",
"txt_yubikey_config_update_failed": "Не удалось обновить учетные данные проверки Yubico",
"txt_yubikey_auto_config_failed": "Не удалось получить учетные данные проверки Yubico",
"txt_yubikey_reconfigure_help": "Введите новый OTP, чтобы автоматически запросить и заменить эти учетные данные.",
"txt_yubikey_auto_configure_again": "Получить снова автоматически",
"txt_setting_coming_soon": "Скоро появится.",
"txt_totp_manage_intro": "Отсканируйте QR-код или введите ключ в приложении-аутентификаторе, затем введите код проверки.",
"txt_two_step_recovery_code_warning": "Если вы не можете получить доступ к поставщику двухэтапного входа, одноразовый код восстановления можно использовать для отключения двухэтапного входа. Сохраните код в надежном месте.",
@@ -860,6 +889,8 @@ const ru: Record<string, string> = {
"txt_scope": "Область доступа",
"txt_grant_type": "Тип авторизации",
"txt_refresh": "Обновить",
"txt_refresh_status": "Обновить статус",
"txt_load_failed": "Не удалось загрузить",
"txt_refresh_in_seconds_s": "Обновить через {seconds} с.",
"txt_regenerate": "Регенерировать",
"txt_registration_succeeded_please_sign_in": "Регистрация прошла успешно. Пожалуйста, войдите в систему.",
+31
View File
@@ -27,6 +27,35 @@ const zhCN: Record<string, string> = {
"txt_passkey_provider_help": "使用兼容 FIDO2 的安全密钥或生物识别验证器。",
"txt_yubico_otp_security_key": "Yubico OTP 安全密钥",
"txt_yubico_otp_security_key_help": "使用 YubiKey 4、5 或 NEO 设备。",
"txt_yubikey_setup_intro": "将 YubiKey 插入计算机的 USB 端口。在下面选择第一个空的 YubiKey 输入字段。触摸 YubiKey 的按钮、保存。",
"txt_yubikey_plug_in": "将 YubiKey 插入计算机的 USB 端口",
"txt_yubikey_select_empty_field": "在下面选择第一个空的 YubiKey 输入字段",
"txt_yubikey_touch_button": "触摸 YubiKey 的按钮、保存",
"txt_yubikey_save_form": "保存",
"txt_yubikey_x": "YubiKey {index}",
"txt_nfc_support": "NFC 支持",
"txt_yubikey_supports_nfc": "我的某个密钥支持 NFC",
"txt_yubikey_supports_nfc_desc": "",
"txt_disable_all_keys": "停用全部密钥",
"txt_yubikeys_updated": "YubiKey 已更新",
"txt_yubikey_update_failed": "更新 YubiKey 失败",
"txt_disable_yubikey_failed": "停用 YubiKey 失败",
"txt_yubikey_disabled": "YubiKey 已停用",
"txt_yubikey_enabled": "YubiKey 已启用。",
"txt_yubikey_config_required": "尚未配置 Yubico 验证",
"txt_yubikey_config_required_help": "请先输入一次 YubiKey OTP。NodeWarden 会自动获取并保存实例级 Client ID 和 Secret key,成功后再进入 YubiKey 设置表单。",
"txt_otp_from_yubikey": "来自 YubiKey 的 OTP",
"txt_please_input_yubikey_otp": "请输入 YubiKey OTP",
"txt_yubikey_verify_failed": "YubiKey 验证失败",
"txt_press_yubikey_to_authenticate": "按下 YubiKey 进行验证。",
"txt_yubikey_auto_configure": "自动获取并保存",
"txt_yubikey_validation_credentials": "Yubico 验证凭据",
"txt_view": "查看",
"txt_yubikey_config_updated": "Yubico 验证凭据已更新",
"txt_yubikey_config_update_failed": "更新 Yubico 验证凭据失败",
"txt_yubikey_auto_config_failed": "获取 Yubico 验证凭据失败",
"txt_yubikey_reconfigure_help": "输入一个新的 OTP,可以重新自动获取并替换当前凭据。",
"txt_yubikey_auto_configure_again": "重新自动获取",
"txt_setting_coming_soon": "即将推出。",
"txt_totp_manage_intro": "扫描二维码或在验证器 App 中输入密钥,然后输入验证码。",
"txt_two_step_recovery_code_warning": "当您无法访问两步登录提供程序时,您的一次性恢复代码可用于停用两步登录。请将其妥善保管。",
@@ -860,6 +889,8 @@ const zhCN: Record<string, string> = {
"txt_scope": "权限范围",
"txt_grant_type": "授权类型",
"txt_refresh": "刷新",
"txt_refresh_status": "刷新状态",
"txt_load_failed": "加载失败",
"txt_refresh_in_seconds_s": "{seconds} 秒后刷新",
"txt_regenerate": "重新生成",
"txt_registration_succeeded_please_sign_in": "注册成功,请登录",
+31
View File
@@ -27,6 +27,35 @@ const zhTW: Record<string, string> = {
"txt_passkey_provider_help": "使用兼容 FIDO2 的安全密鑰或生物識別驗證器。",
"txt_yubico_otp_security_key": "Yubico OTP 安全密鑰",
"txt_yubico_otp_security_key_help": "使用 YubiKey 4、5 或 NEO 裝置。",
"txt_yubikey_setup_intro": "將 YubiKey 插入電腦的 USB 連接埠。在下方選擇第一個空的 YubiKey 輸入欄位,觸摸 YubiKey 按鈕,然後保存表單。",
"txt_yubikey_plug_in": "將 YubiKey 插入電腦的 USB 連接埠。",
"txt_yubikey_select_empty_field": "在下方選擇第一個空的 YubiKey 輸入欄位。",
"txt_yubikey_touch_button": "觸摸 YubiKey 按鈕。",
"txt_yubikey_save_form": "保存表單。",
"txt_yubikey_x": "YubiKey {index}",
"txt_nfc_support": "NFC 支援",
"txt_yubikey_supports_nfc": "我的某個密鑰支援 NFC。",
"txt_yubikey_supports_nfc_desc": "如果您的某個 YubiKey 支援 NFC,行動裝置偵測到 NFC 可用時會提示您。",
"txt_disable_all_keys": "停用全部密鑰",
"txt_yubikeys_updated": "YubiKey 已更新",
"txt_yubikey_update_failed": "更新 YubiKey 失敗",
"txt_disable_yubikey_failed": "停用 YubiKey 失敗",
"txt_yubikey_disabled": "YubiKey 已停用",
"txt_yubikey_enabled": "YubiKey 已啟用。",
"txt_yubikey_config_required": "尚未配置 Yubico 驗證",
"txt_yubikey_config_required_help": "請先輸入一次 YubiKey OTP。NodeWarden 會自動取得並保存實例級 Client ID 和 Secret key,成功後再進入 YubiKey 設定表單。",
"txt_otp_from_yubikey": "來自 YubiKey 的 OTP",
"txt_please_input_yubikey_otp": "請輸入 YubiKey OTP",
"txt_yubikey_verify_failed": "YubiKey 驗證失敗",
"txt_press_yubikey_to_authenticate": "按下 YubiKey 進行驗證。",
"txt_yubikey_auto_configure": "自動取得並保存",
"txt_yubikey_validation_credentials": "Yubico 驗證憑據",
"txt_view": "查看",
"txt_yubikey_config_updated": "Yubico 驗證憑據已更新",
"txt_yubikey_config_update_failed": "更新 Yubico 驗證憑據失敗",
"txt_yubikey_auto_config_failed": "取得 Yubico 驗證憑據失敗",
"txt_yubikey_reconfigure_help": "輸入一個新的 OTP,可以重新自動取得並替換目前憑據。",
"txt_yubikey_auto_configure_again": "重新自動取得",
"txt_setting_coming_soon": "即將推出。",
"txt_totp_manage_intro": "掃描二維碼或在驗證器 App 中輸入密鑰,然後輸入驗證碼。",
"txt_two_step_recovery_code_warning": "當您無法訪問兩步登入提供程序時,您的一次性恢復代碼可用於停用兩步登入。請將其妥善保管。",
@@ -860,6 +889,8 @@ const zhTW: Record<string, string> = {
"txt_scope": "權限範圍",
"txt_grant_type": "授權類型",
"txt_refresh": "刷新",
"txt_refresh_status": "刷新狀態",
"txt_load_failed": "載入失敗",
"txt_refresh_in_seconds_s": "{seconds} 秒後刷新",
"txt_regenerate": "重新生成",
"txt_registration_succeeded_please_sign_in": "註冊成功,請登錄",
+10
View File
@@ -15,6 +15,7 @@ export interface Profile {
name: string;
key: string;
masterPasswordHint?: string | null;
yubikeyEnabled?: boolean;
privateKey?: string | null;
publicKey?: string | null;
role: 'admin' | 'user';
@@ -295,6 +296,15 @@ export interface WebBootstrapResponse {
registrationInviteRequired?: boolean;
}
export interface YubiKeyOtpSettings {
enabled: boolean;
keys: [string, string, string, string, string];
nfc: boolean;
yubicoConfigured: boolean;
yubicoClientId: string;
yubicoSecretKey: string;
}
export interface TokenSuccess {
access_token: string;
refresh_token?: string;