Files
sample-site/files/fido2-ui-sdk.js
2026-02-05 10:22:07 +09:00

2703 lines
96 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
*
* @file fido2-ui-sdk.js
* @description FIDO2 UI SDK of amipro FIDO2 Server
* @version 2025-12-12
* @author Amipro Co., Ltd. (https://www.amipro.me/)
* @license Copyright (c) Amipro Co., Ltd. All rights reserved.
*/
(function(window) {
'use strict';
// Check required dependencies
if (typeof authenticateFido2 === 'undefined' ||
typeof registerFido2 === 'undefined' ||
typeof listUserDevicesFido2 === 'undefined' ||
typeof delUserDeviceFido2 === 'undefined' ||
typeof setFidoServerURL === 'undefined' ||
typeof logoutFido2UserSession === 'undefined') {
throw new Error(
'FIDO2 UI SDK requires dfido2-lib.js to be loaded first. ' +
'Please add: <script src="files/dfido2-lib.js"></script> before fido2-ui-sdk.js'
);
}
const FIDO2_UI_VERSION = '1.0.0';
const DEFAULT_CONFIG = {
serverUrl: 'https://fido2.amipro.me',
mode: 'modal',
container: null,
theme: {
logo: null,
primaryColor: '#696cff',
backgroundColor: '#ffffff',
textColor: '#333333',
borderRadius: '8px',
},
language: 'zh-CN',
customI18n: {},
features: {
showAddButton: true,
showDeleteButton: true,
showUserInfo: true,
showSessionStatus: true,
showRecoveryButton: true,
},
callbacks: {},
rpId: null,
autoRefresh: true,
refreshInterval: 5000,
recoverySessionId: null,
};
const DEFAULT_I18N = {
'my_devices': {
'en-US': 'My devices',
'zh-CN': '我的设备',
'ja': 'マイデバイス',
'es': 'Mis dispositivos',
'de': 'Meine Geräte',
'fr': 'Mes appareils',
'pt': 'Meus dispositivos',
'ko': '내 장치',
'ru': 'Мои устройства',
'it': 'I miei dispositivi'
},
'btn_add': {
'en-US': 'Add device',
'zh-CN': '添加设备',
'ja': 'デバイスを追加',
'es': 'Añadir dispositivo',
'de': 'Gerät hinzufügen',
'fr': 'Ajouter un appareil',
'pt': 'Adicionar dispositivo',
'ko': '장치 추가',
'ru': 'Добавить устройство',
'it': 'Aggiungi dispositivo'
},
'title_device': {
'en-US': 'Device',
'zh-CN': '设备',
'ja': 'デバイス',
'es': 'Dispositivo',
'de': 'Gerät',
'fr': 'Appareil',
'pt': 'Dispositivo',
'ko': '장치',
'ru': 'Устройство',
'it': 'Dispositivo'
},
'title_time': {
'en-US': 'Registered time',
'zh-CN': '添加时间',
'ja': '登録時間',
'es': 'Hora de registro',
'de': 'Registrierungszeit',
'fr': 'Heure d\'inscription',
'pt': 'Hora do registro',
'ko': '등록 시간',
'ru': 'Время регистрации',
'it': 'Ora di registrazione'
},
'title_act': {
'en-US': 'Actions',
'zh-CN': '操作',
'ja': '操作',
'es': 'Acciones',
'de': 'Aktionen',
'fr': 'Actions',
'pt': 'Ações',
'ko': '작업',
'ru': 'Действия',
'it': 'Azioni'
},
'title_del': {
'en-US': 'Delete',
'zh-CN': '删除',
'ja': '削除',
'es': 'Eliminar',
'de': 'Löschen',
'fr': 'Supprimer',
'pt': 'Excluir',
'ko': '삭제',
'ru': 'Удалить',
'it': 'Elimina'
},
'title_logout': {
'en-US': 'Log out',
'zh-CN': '登出',
'ja': 'ログアウト',
'es': 'Cerrar sesión',
'de': 'Abmelden',
'fr': 'Déconnexion',
'pt': 'Sair',
'ko': '로그아웃',
'ru': 'Выйти',
'it': 'Disconnetti'
},
'title_empty_list': {
'en-US': 'No devices, please add.',
'zh-CN': '无设备,请添加。',
'ja': 'デバイスがなし、追加してください。',
'es': 'Sin dispositivos, por favor añada.',
'de': 'Keine Geräte, bitte hinzufügen.',
'fr': 'Pas d\'appareils, s\'il vous plaît ajouter.',
'pt': 'Sem dispositivos, por favor adicione.',
'ko': '장치가 없습니다. 추가해 주세요.',
'ru': 'Нет устройств, пожалуйста добавьте.',
'it': 'Nessun dispositivo, si prega di aggiungere.'
},
'msg_register_ok': {
'en-US': 'Device registered successfully',
'zh-CN': '添加设备成功',
'ja': 'デバイス登録完了',
'es': 'Dispositivo registrado exitosamente',
'de': 'Gerät erfolgreich registriert',
'fr': 'Appareil enregistré avec succès',
'pt': 'Dispositivo registrado com sucesso',
'ko': '장치 등록 완료',
'ru': 'Устройство успешно зарегистрировано',
'it': 'Dispositivo registrato con successo'
},
'msg_deldev_ok': {
'en-US': 'Device deleted successfully',
'zh-CN': '设备删除成功',
'ja': 'デバイスを削除しました',
'es': 'Dispositivo eliminado exitosamente',
'de': 'Gerät erfolgreich gelöscht',
'fr': 'Appareil supprimé avec succès',
'pt': 'Dispositivo excluído com sucesso',
'ko': '장치 삭제 완료',
'ru': 'Устройство успешно удалено',
'it': 'Dispositivo eliminato con successo'
},
'msg_confirm_deldev': {
'en-US': 'Do you want to delete this device?',
'zh-CN': '确认删除此设备吗?',
'ja': 'デバイスを削除しますか?',
'es': '¿Desea eliminar este dispositivo?',
'de': 'Möchten Sie dieses Gerät löschen?',
'fr': 'Voulez-vous supprimer cet appareil?',
'pt': 'Deseja excluir este dispositivo?',
'ko': '이 장치를 삭제하시겠습니까?',
'ru': 'Хотите удалить это устройство?',
'it': 'Vuoi eliminare questo dispositivo?'
},
'msg_session_status_ok': {
'en-US': 'FIDO2 session is valid',
'zh-CN': 'FIDO2会话正常',
'ja': 'FIDO2セッションは正常です',
'es': 'La sesión FIDO2 es válida',
'de': 'FIDO2-Sitzung ist gültig',
'fr': 'La session FIDO2 est valide',
'pt': 'A sessão FIDO2 é válida',
'ko': 'FIDO2 세션이 정상입니다',
'ru': 'Сессия FIDO2 действительна',
'it': 'La sessione FIDO2 è valida'
},
'msg_session_status_fail': {
'en-US': 'FIDO2 session is invalid',
'zh-CN': 'FIDO2会话无效',
'ja': 'FIDO2セッションは無効です',
'es': 'La sesión FIDO2 no es válida',
'de': 'FIDO2-Sitzung ist ungültig',
'fr': 'La session FIDO2 n\'est pas valide',
'pt': 'A sessão FIDO2 é inválida',
'ko': 'FIDO2 세션이 무효합니다',
'ru': 'Сессия FIDO2 недействительна',
'it': 'La sessione FIDO2 non è valida'
},
'btn_close': {
'en-US': 'Close',
'zh-CN': '关闭',
'ja': '閉じる',
'es': 'Cerrar',
'de': 'Schließen',
'fr': 'Fermer',
'pt': 'Fechar',
'ko': '닫기',
'ru': 'Закрыть',
'it': 'Chiudi'
},
'title_welcome': {
'en-US': 'Welcome',
'zh-CN': '欢迎',
'ja': 'ようこそ',
'es': 'Bienvenido',
'de': 'Willkommen',
'fr': 'Bienvenue',
'pt': 'Bem-vindo',
'ko': '환영합니다',
'ru': 'Добро пожаловать',
'it': 'Benvenuto'
},
'btn_login': {
'en-US': 'Login',
'zh-CN': '重新登录',
'ja': 'ログイン',
'es': 'Iniciar sesión',
'de': 'Anmelden',
'fr': 'Connexion',
'pt': 'Login',
'ko': '로그인',
'ru': 'Войти',
'it': 'Accedi'
},
'msg_session_invalid': {
'en-US': 'Session expired, please login again',
'zh-CN': '会话已过期,请重新登录',
'ja': 'セッション切れ、再ログインしてください',
'es': 'Sesión vencida, por favor inicie sesión nuevamente',
'de': 'Sitzung abgelaufen, bitte erneut anmelden',
'fr': 'Session expirée, veuillez vous connecter à nouveau',
'pt': 'Sessão expirada, por favor faça login novamente',
'ko': '세션이 만료되었습니다. 다시 로그인해 주세요.',
'ru': 'Сессия истекла, пожалуйста войдите снова',
'it': 'Sessione scaduta, si prega di accedere nuovamente'
},
'title_login': {
'en-US': 'Login',
'zh-CN': '登录',
'ja': 'ログイン',
'es': 'Iniciar sesión',
'de': 'Anmelden',
'fr': 'Connexion',
'pt': 'Login',
'ko': '로그인',
'ru': 'Войти',
'it': 'Accedi'
},
'title_password_login': {
'en-US': 'Password Login',
'zh-CN': '密码登录',
'ja': 'パスワードログイン',
'es': 'Iniciar sesión con contraseña',
'de': 'Anmeldung mit Passwort',
'fr': 'Connexion par mot de passe',
'pt': 'Login com senha',
'ko': '비밀번호 로그인',
'ru': 'Вход по паролю',
'it': 'Accesso con password'
},
'btn_fido2_login': {
'en-US': 'Use Passkey',
'zh-CN': '使用Passkey登录',
'ja': 'Passkeyを使用',
'es': 'Usar Passkey',
'de': 'Passkey verwenden',
'fr': 'Utiliser Passkey',
'pt': 'Usar Passkey',
'ko': 'Passkey 사용',
'ru': 'Использовать Passkey',
'it': 'Usa Passkey'
},
'btn_password_login': {
'en-US': 'Password Login',
'zh-CN': '密码登录',
'ja': 'パスワードログイン',
'es': 'Iniciar sesión con contraseña',
'de': 'Anmeldung mit Passwort',
'fr': 'Connexion par mot de passe',
'pt': 'Login com senha',
'ko': '비밀번호 로그인',
'ru': 'Вход по паролю',
'it': 'Accesso con password'
},
'link_use_password': {
'en-US': 'Use password instead',
'zh-CN': '使用密码登录',
'ja': 'パスワードを使用',
'es': 'Usar contraseña',
'de': 'Passwort verwenden',
'fr': 'Utiliser mot de passe',
'pt': 'Usar senha',
'ko': '비밀번호 사용',
'ru': 'Использовать пароль',
'it': 'Usa password'
},
'link_use_fido2': {
'en-US': 'Use Passkey instead',
'zh-CN': '使用Passkey登录',
'ja': 'Passkeyを使用',
'es': 'Usar Passkey',
'de': 'Passkey verwenden',
'fr': 'Utiliser Passkey',
'pt': 'Usar Passkey',
'ko': 'Passkey 사용',
'ru': 'Использовать Passkey',
'it': 'Usa Passkey'
},
'placeholder_user_id': {
'en-US': 'Enter User ID',
'zh-CN': '请输入用户ID',
'ja': 'ユーザーIDを入力',
'es': 'Ingrese ID de usuario',
'de': 'Benutzer-ID eingeben',
'fr': 'Entrez l\'ID utilisateur',
'pt': 'Digite o ID do usuário',
'ko': '사용자 ID 입력',
'ru': 'Введите ID пользователя',
'it': 'Inserisci ID utente'
},
'placeholder_password': {
'en-US': 'Enter Password',
'zh-CN': '请输入密码',
'ja': 'パスワードを入力',
'es': 'Ingrese contraseña',
'de': 'Passwort eingeben',
'fr': 'Entrez le mot de passe',
'pt': 'Digite a senha',
'ko': '비밀번호 입력',
'ru': 'Введите пароль',
'it': 'Inserisci password'
},
'msg_remaining_attempts': {
'en-US': 'Remaining attempts: {n}',
'zh-CN': '剩余尝试次数: {n}',
'ja': '残り{n}回',
'es': 'Intentos restantes: {n}',
'de': 'Verbleibende Versuche: {n}',
'fr': 'Tentatives restantes: {n}',
'pt': 'Tentativas restantes: {n}',
'ko': '남은 시도: {n}',
'ru': 'Осталось попыток: {n}',
'it': 'Tentativi rimanenti: {n}'
},
'msg_password_exhausted': {
'en-US': 'Too many failed attempts, login closed',
'zh-CN': '尝试次数过多,登录已关闭',
'ja': '試行回数超過、ログイン閉鎖',
'es': 'Demasiados intentos fallidos, inicio de sesión cerrado',
'de': 'Zu viele fehlgeschlagene Versuche, Anmeldung geschlossen',
'fr': 'Trop de tentatives échouées, connexion fermée',
'pt': 'Muitas tentativas falharam, login fechado',
'ko': '시도 횟수 초과, 로그인 종료',
'ru': 'Слишком много неудачных попыток, вход закрыт',
'it': 'Troppi tentativi falliti, accesso chiuso'
},
'title_password_exhausted': {
'en-US': 'Login Failed',
'zh-CN': '登录失败',
'ja': 'ログイン失敗',
'es': 'Error de inicio de sesión',
'de': 'Anmeldung fehlgeschlagen',
'fr': 'Échec de la connexion',
'pt': 'Falha no login',
'ko': '로그인 실패',
'ru': 'Ошибка входа',
'it': 'Accesso fallito'
},
'msg_no_registration': {
'en-US': 'No device registered',
'zh-CN': '暂未注册设备',
'ja': 'デバイス未登録',
'es': 'No hay dispositivo registrado',
'de': 'Kein Gerät registriert',
'fr': 'Aucun appareil enregistré',
'pt': 'Nenhum dispositivo registrado',
'ko': '등록된 장치 없음',
'ru': 'Устройство не зарегистрировано',
'it': 'Nessun dispositivo registrato'
},
'msg_fido2_failed': {
'en-US': 'Passkey login failed',
'zh-CN': 'Passkey登录失败',
'ja': 'Passkeyログイン失敗',
'es': 'Error de inicio de sesión con Passkey',
'de': 'Passkey-Anmeldung fehlgeschlagen',
'fr': 'Échec de la connexion Passkey',
'pt': 'Falha no login com Passkey',
'ko': 'Passkey 로그인 실패',
'ru': 'Ошибка входа Passkey',
'it': 'Accesso Passkey fallito'
},
'msg_fido2_canceled': {
'en-US': 'Login was canceled',
'zh-CN': '登录已取消',
'ja': 'ログインキャンセル',
'es': 'Inicio de sesión cancelado',
'de': 'Anmeldung abgebrochen',
'fr': 'Connexion annulée',
'pt': 'Login cancelado',
'ko': '로그인 취소됨',
'ru': 'Вход отменён',
'it': 'Accesso annullato'
},
'error_invalid_state': {
'en-US': 'The authenticator contains credentials that are already registered with this website.',
'zh-CN': '您的认证器包含已在此网站注册的凭据。',
'ja': 'この認証デバイスには、すでにこの网站に登録されている憑拠が含まれています。',
'es': 'El autenticador contiene credenciales que ya están registradas en este sitio web.',
'de': 'Der Authenticator enthält Anmeldeinformationen, die bereits auf dieser Website registriert sind.',
'fr': 'L\'authentificateur contient des identifiants déjà enregistrés sur ce site Web.',
'pt': 'O autenticador contém credenciais já registradas neste site.',
'ko': '인증기에 이미 이 웹사이트에 등록된 자격 증명이 포함되어 있습니다.',
'ru': 'Аутентификатор содержит учетные данные, уже зарегистрированные на этом веб-сайте.',
'it': 'L\'autenticatore contiene credenziali già registrate su questo sito web.'
},
'error_not_allowed': {
'en-US': 'The operation was not allowed.',
'zh-CN': '操作不被允许。',
'ja': '操作が許可されませんでした。',
'es': 'La operación no fue permitida.',
'de': 'Der Vorgang wurde nicht zugelassen.',
'fr': 'L\'opération n\'a pas été autorisée.',
'pt': 'A operação não foi permitida.',
'ko': '작업이 허용되지 않았습니다.',
'ru': 'Операция не была разрешена.',
'it': 'L\'operazione non è stata consentita.'
},
'error_abort': {
'en-US': 'The operation was aborted.',
'zh-CN': '操作已中止。',
'ja': '操作が中止されました。',
'es': 'La operación fue abortada.',
'de': 'Der Vorgang wurde abgebrochen.',
'fr': 'L\'opération a été abandonnée.',
'pt': 'A operação foi abortada.',
'ko': '작업이 중단되었습니다.',
'ru': 'Операция была прервана.',
'it': 'L\'operazione è stata interrotta.'
},
'error_not_supported': {
'en-US': 'This operation is not supported on your device.',
'zh-CN': '您的设备不支持此操作。',
'ja': 'この機器ではこの操作はサポートされていません。',
'es': 'Esta operación no es compatible con su dispositivo.',
'de': 'Dieser Vorgang wird auf Ihrem Gerät nicht unterstützt.',
'fr': 'Cette opération n\'est pas prise en charge sur votre appareil.',
'pt': 'Esta operação não é suportada no seu dispositivo.',
'ko': '이 작업은 해당 장치에서 지원되지 않습니다.',
'ru': 'Эта операция не поддерживается на вашем устройстве.',
'it': 'Questa operazione non è supportata sul tuo dispositivo.'
},
'error_security': {
'en-US': 'A security error occurred.',
'zh-CN': '发生安全错误。',
'ja': 'セキュリティエラーが発生しました。',
'es': 'Ocurrió un error de seguridad.',
'de': 'Ein Sicherheitsfehler ist aufgetreten.',
'fr': 'Une erreur de sécurité s\'est produite.',
'pt': 'Ocorreu um erro de segurança.',
'ko': '보안 오류가 발생했습니다.',
'ru': 'Произошла ошибка безопасности.',
'it': 'Si è verificato un errore di sicurezza.'
},
'error_network': {
'en-US': 'A network error occurred.',
'zh-CN': '发生网络错误。',
'ja': 'ネットワークエラーが発生しました。',
'es': 'Ocurrió un error de red.',
'de': 'Ein Netzwerkfehler ist aufgetreten.',
'fr': 'Une erreur de réseau s\'est produite.',
'pt': 'Ocorreu um erro de rede.',
'ko': '네트워크 오류가 발생했습니다.',
'ru': 'Произошла ошибка сети.',
'it': 'Si è verificato un errore di rete.'
},
'error_constraint': {
'en-US': 'The operation failed due to a constraint violation.',
'zh-CN': '由于约束冲突,操作失败。',
'ja': '制約違反のため操作に失敗しました。',
'es': 'La operación falló debido a una violación de restricción.',
'de': 'Der Vorgang ist aufgrund einer Einschränkungsverletzung fehlgeschlagen.',
'fr': 'L\'opération a échoué en raison d\'une violation de contrainte.',
'pt': 'A operação falhou devido a uma violação de restrição.',
'ko': '제약 조건 위반으로 인해 작업이 실패했습니다.',
'ru': 'Операция не удалась из-за нарушения ограничения.',
'it': 'L\'operazione non riuscita a causa di una violazione del vincolo.'
},
'error_not_readable': {
'en-US': 'Could not read the credential.',
'zh-CN': '无法读取凭据。',
'ja': '憑拠を読み取れませんでした。',
'es': 'No se pudo leer la credencial.',
'de': 'Die Anmeldeinformation konnte nicht gelesen werden.',
'fr': 'Impossible de lire les identifiants.',
'pt': 'Não foi possível ler a credencial.',
'ko': '자격 증명을 읽을 수 없습니다.',
'ru': 'Не удалось прочитать учетные данные.',
'it': 'Impossibile leggere le credenziali.'
},
'error_encoding': {
'en-US': 'The data format is invalid.',
'zh-CN': '数据格式无效。',
'ja': 'データ形式が無効です。',
'es': 'El formato de datos no es válido.',
'de': 'Das Datenformat ist ungültig.',
'fr': 'Le format de données n\'est pas valide.',
'pt': 'O formato de dados é inválido.',
'ko': '데이터 형식이 잘못되었습니다.',
'ru': 'Формат данных недействителен.',
'it': 'Il formato dati non è valido.'
},
'error_data': {
'en-US': 'A data error occurred.',
'zh-CN': '发生数据错误。',
'ja': 'データエラーが発生しました。',
'es': 'Ocurrió un error de datos.',
'de': 'Ein Datenfehler ist aufgetreten.',
'fr': 'Une erreur de données s\'est produite.',
'pt': 'Ocorreu um erro de dados.',
'ko': '데이터 오류가 발생했습니다.',
'ru': 'Произошла ошибка данных.',
'it': 'Si è verificato un errore nei dati.'
},
'error_timeout': {
'en-US': 'The operation timed out.',
'zh-CN': '操作超时。',
'ja': '操作がタイムアウトしました。',
'es': 'La operación caducó.',
'de': 'Der Vorgang ist abgelaufen.',
'fr': 'L\'opération a expiré.',
'pt': 'A operação expirou.',
'ko': '작업 시간이 초과되었습니다.',
'ru': 'Операция истекла.',
'it': 'L\'operazione è scaduta.'
},
'error_system': {
'en-US': 'A system error occurred.',
'zh-CN': '发生系统错误。',
'ja': 'システムエラーが発生しました。',
'es': 'Ocurrió un error del sistema.',
'de': 'Ein Systemfehler ist aufgetreten.',
'fr': 'Une erreur système s\'est produite.',
'pt': 'Ocorreu um erro do sistema.',
'ko': '시스템 오류가 발생했습니다.',
'ru': 'Произошла системная ошибка.',
'it': 'Si è verificato un errore di sistema.'
},
'error_browser_not_supported': {
'en-US': 'Your browser does not support FIDO2/WebAuthn.',
'zh-CN': '您的浏览器不支持FIDO2/WebAuthn。',
'ja': 'お使いのブラウザは FIDO2/WebAuthn をサポートしていません。',
'es': 'Su navegador no es compatible con FIDO2/WebAuthn.',
'de': 'Ihr Browser unterstützt FIDO2/WebAuthn nicht.',
'fr': 'Votre navigateur ne prend pas en charge FIDO2/WebAuthn.',
'pt': 'Seu navegador não suporta FIDO2/WebAuthn.',
'ko': '브라우저가 FIDO2/WebAuthn을 지원하지 않습니다.',
'ru': 'Ваш браузер не поддерживает FIDO2/WebAuthn.',
'it': 'Il tuo browser non supporta FIDO2/WebAuthn.'
},
'error_fido2lib_101': {
'en-US': 'Your browser does not support FIDO2/WebAuthn.',
'zh-CN': '您的浏览器不支持FIDO2/WebAuthn。',
'ja': 'お使いのブラウザは FIDO2/WebAuthn をサポートしていません。',
'es': 'Su navegador no es compatible con FIDO2/WebAuthn.',
'de': 'Ihr Browser unterstützt FIDO2/WebAuthn nicht.',
'fr': 'Votre navigateur ne prend pas en charge FIDO2/WebAuthn.',
'pt': 'Seu navegador não suporta FIDO2/WebAuthn.',
'ko': '브라우저가 FIDO2/WebAuthn을 지원하지 않습니다.',
'ru': 'Ваш браузер не поддерживает FIDO2/WebAuthn.',
'it': 'Il tuo browser non supporta FIDO2/WebAuthn.'
},
'error_fido2lib_102': {
'en-US': 'The operation was canceled by the user.',
'zh-CN': '操作已被用户取消。',
'ja': 'ユーザーによって操作がキャンセルされました。',
'es': 'La operación fue cancelada por el usuario.',
'de': 'Der Vorgang wurde vom Benutzer abgebrochen.',
'fr': 'L\'opération a été annulée par l\'utilisateur.',
'pt': 'A operação foi cancelada pelo usuário.',
'ko': '작업이 사용자에 의해 취소되었습니다.',
'ru': 'Операция была отменена пользователем.',
'it': 'L\'operazione è stata annullata dall\'utente.'
},
'error_fido2lib_103': {
'en-US': 'The operation timed out.',
'zh-CN': '操作超时。',
'ja': '操作がタイムアウトしました。',
'es': 'La operación caducó.',
'de': 'Der Vorgang ist abgelaufen.',
'fr': 'L\'opération a expiré.',
'pt': 'A operação expirou.',
'ko': '작업 시간이 초과되었습니다.',
'ru': 'Операция истекла.',
'it': 'L\'operazione è scaduta.'
},
'error_fido2lib_105': {
'en-US': 'The authenticator contains credentials that are already registered with this website.',
'zh-CN': '您的认证器包含已在此网站注册的凭据。',
'ja': 'この認証デバイスには、すでにこの网站に登録されている憑拠が含まれています。',
'es': 'El autenticador contiene credenciales que ya están registradas en este sitio web.',
'de': 'Der Authenticator enthält Anmeldeinformationen, die bereits auf dieser Website registriert sind.',
'fr': 'L\'authentificateur contient des identifiants déjà enregistrés sur ce site Web.',
'pt': 'O autenticador contém credenciais já registradas neste site.',
'ko': '인증기에 이미 이 웹사이트에 등록된 자격 증명이 포함되어 있습니다.',
'ru': 'Аутентификатор содержит учетные данные, уже зарегистрированные на этом веб-сайте.',
'it': 'L\'autenticatore contiene credenziali già registrate su questo sito web.'
},
'error_fido2lib_106': {
'en-US': 'Another request is already in progress.',
'zh-CN': '另一个请求正在进行中。',
'ja': '別のリクエストがすでに進行中です。',
'es': 'Otra solicitud ya está en progreso.',
'de': 'Eine andere Anfrage wird bereits verarbeitet.',
'fr': 'Une autre demande est déjà en cours.',
'pt': 'Outra solicitação já está em andamento.',
'ko': '다른 요청이 이미 진행 중입니다.',
'ru': 'Другой запрос уже выполняется.',
'it': 'Un\'altra richiesta è già in corso.'
},
'error_svr_101': {
'en-US': 'Unregistered enterprise authenticator.',
'zh-CN': '未注册的企业认证器。',
'ja': '登録されていないエンタープライズ認証デバイス。',
'es': 'Autenticador empresarial no registrado.',
'de': 'Nicht registrierter Enterprise-Authenticator.',
'fr': 'Authentificateur d\'entreprise non enregistré.',
'pt': 'Autenticador empresarial não registrado.',
'ko': '등록되지 않은 엔터프라이즈 인증기.',
'ru': 'Незарегистрированный корпоративный аутентификатор.',
'it': 'Autenticatore aziendale non registrato.'
},
'error_svr_102': {
'en-US': 'Cannot authenticate with a unique device bound key from a different device.',
'zh-CN': '无法使用来自其他设备的唯一设备绑定密钥进行身份验证。',
'ja': '別のデバイスからの一意のデバイスバインドキーで認証できません。',
'es': 'No se puede autenticar con una clave de vinculación de dispositivo única desde un dispositivo diferente.',
'de': 'Authentifizierung mit einem eindeutigen Gerätebindungsschlüssel von einem anderen Gerät nicht möglich.',
'fr': 'Impossible de s\'authentifier avec une clé de liaison d\'appareil unique à partir d\'un autre appareil.',
'pt': 'Não é possível autenticar com uma chave de vinculação de dispositivo exclusiva de um dispositivo diferente.',
'ko': '다른 장치의 고유 장치 바인딩 키로 인증할 수 없습니다.',
'ru': 'Невозможно аутентифицироваться с ключом привязки уникального устройства с другого устройства.',
'it': 'Impossibile autenticarsi con una chiave di associazione dispositivo univoca da un dispositivo diverso.'
},
'error_svr_103': {
'en-US': 'Unable to verify signature.',
'zh-CN': '无法验证签名。',
'ja': '署名を認証できません。',
'es': 'No se puede verificar la firma.',
'de': 'Signatur kann nicht verifiziert werden.',
'fr': 'Impossible de vérifier la signature.',
'pt': 'Não foi possível verificar a assinatura.',
'ko': '서명을 확인할 수 없습니다.',
'ru': 'Не удалось проверить подпись.',
'it': 'Impossibile verificare la firma.'
},
'error_svr_104': {
'en-US': 'Key not found.',
'zh-CN': '认证Key未找到。',
'ja': 'キーが見つかりません!',
'es': 'Clave no encontrada.',
'de': 'Schlüssel nicht gefunden.',
'fr': 'Clé non trouvée.',
'pt': 'Chave não encontrada.',
'ko': '키를 찾을 수 없습니다.',
'ru': 'Ключ не найден.',
'it': 'Chiave non trovata.'
},
'error_svr_105': {
'en-US': 'Username does not exist.',
'zh-CN': '用户名不存在。',
'ja': 'ユーザー名は存在しません!',
'es': 'El nombre de usuario no existe.',
'de': 'Benutzername existiert nicht.',
'fr': 'Le nom d\'utilisateur n\'existe pas.',
'pt': 'O nome de usuário não existe.',
'ko': '사용자 이름이 존재하지 않습니다.',
'ru': 'Имя пользователя не существует.',
'it': 'Il nome utente non esiste.'
},
'error_svr_106': {
'en-US': 'Unique Device ID is required.',
'zh-CN': '需要Unique Device ID。',
'ja': '固有のデバイスIDが必要です',
'es': 'Se requiere ID de dispositivo único.',
'de': 'Eindeutige Geräte-ID erforderlich.',
'fr': 'L\'ID d\'appareil unique est requis.',
'pt': 'É necessário ID de dispositivo exclusivo.',
'ko': '고유 장치 ID가 필요합니다.',
'ru': 'Требуется уникальный идентификатор устройства.',
'it': 'È richiesto l\'ID dispositivo univoco.'
},
'error_svr_120': {
'en-US': 'User has reached the device limit.',
'zh-CN': '用户已达到设备限制数。',
'ja': 'ユーザーはデバイスの制限数に達しました!',
'es': 'El usuario ha alcanzado el límite de dispositivos.',
'de': 'Benutzer hat das Gerätelimit erreicht.',
'fr': 'L\'utilisateur a atteint la limite d\'appareils.',
'pt': 'O usuário atingiu o limite de dispositivos.',
'ko': '사용자가 장치 제한에 도달했습니다.',
'ru': 'Пользователь достиг предела устройств.',
'it': 'L\'utente ha raggiunto il limite del dispositivo.'
},
'error': {
'en-US': 'An error occurred.',
'zh-CN': '发生错误。',
'ja': 'エラーが発生しました。',
'es': 'Ocurrió un error.',
'de': 'Ein Fehler ist aufgetreten.',
'fr': 'Une erreur s\'est produite.',
'pt': 'Ocorreu um erro.',
'ko': '오류가 발생했습니다.',
'ru': 'Произошла ошибка.',
'it': 'Si è verificato un errore.'
},
'msg_autofail_hint': {
'en-US': 'Auto login failed, please enter User ID',
'zh-CN': '自动登录失败请输入用户ID',
'ja': '自動ログイン失敗、ユーザーIDを入力してください',
'es': 'Error de inicio de sesión automático, ingrese ID de usuario',
'de': 'Automatische Anmeldung fehlgeschlagen, bitte Benutzer-ID eingeben',
'fr': 'Échec de la connexion automatique, entrez l\'ID utilisateur',
'pt': 'Login automático falhou, digite o ID do usuário',
'ko': '자동 로그인 실패, 사용자 ID 입력하세요',
'ru': 'Автоматический вход не удался, введите ID пользователя',
'it': 'Accesso automatico fallito, inserisci ID utente'
},
'msg_invalid_user_id': {
'en-US': 'Please enter User ID',
'zh-CN': '请输入用户ID',
'ja': 'ユーザーIDを入力してください',
'es': 'Por favor ingrese ID de usuario',
'de': 'Bitte Benutzer-ID eingeben',
'fr': 'Veuillez entrer l\'ID utilisateur',
'pt': 'Por favor digite o ID do usuário',
'ko': '사용자 ID를 입력하세요',
'ru': 'Пожалуйста, введите ID пользователя',
'it': 'Per favore inserisci ID utente'
},
'msg_invalid_password': {
'en-US': 'Please enter Password',
'zh-CN': '请输入密码',
'ja': 'パスワードを入力してください',
'es': 'Por favor ingrese contraseña',
'de': 'Bitte Passwort eingeben',
'fr': 'Veuillez entrer le mot de passe',
'pt': 'Por favor digite a senha',
'ko': '비밀번호를 입력하세요',
'ru': 'Пожалуйста, введите пароль',
'it': 'Per favore inserisci password'
},
'title_recovery': {
'en-US': 'Device Recovery',
'zh-CN': '设备恢复',
'ja': 'デバイス回復',
'es': 'Recuperación de dispositivo',
'de': 'Gerätewiederherstellung',
'fr': 'Récupération de dispositif',
'pt': 'Recuperação de dispositivo',
'ko': '장치 복구',
'ru': 'Восстановление устройства',
'it': 'Ripristino dispositivo'
},
'msg_recovery_success': {
'en-US': 'Device recovered successfully',
'zh-CN': '设备恢复成功',
'ja': 'デバイス回復完了',
'es': 'Dispositivo recuperado exitosamente',
'de': 'Gerät erfolgreich wiederhergestellt',
'fr': 'Appareil récupéré avec succès',
'pt': 'Dispositivo recuperado com sucesso',
'ko': '장치 복구 완료',
'ru': 'Устройство успешно восстановлено',
'it': 'Dispositivo ripristinato con successo'
},
'msg_recovery_failed': {
'en-US': 'Recovery failed',
'zh-CN': '恢复失败',
'ja': '回復失敗',
'es': 'Recuperación fallida',
'de': 'Wiederherstellung fehlgeschlagen',
'fr': 'Échec de la récupération',
'pt': 'Falha na recuperação',
'ko': '복구 실패',
'ru': 'Ошибка восстановления',
'it': 'Ripristino fallito'
},
'msg_recovery_user_found': {
'en-US': 'Recovery session verified for user: {user}',
'zh-CN': '已验证恢复会话,用户: {user}',
'ja': 'ユーザー {user} の回復セッションが確認されました',
'es': 'Sesión de recuperación verificada para el usuario: {user}',
'de': 'Wiederherstellungssitzung für Benutzer verifiziert: {user}',
'fr': 'Session de récupération vérifiée pour l\'utilisateur: {user}',
'pt': 'Sessão de recuperação verificada para o usuário: {user}',
'ko': '사용자 {user}의 복구 세션 확인됨',
'ru': 'Сессия восстановления подтверждена для пользователя: {user}',
'it': 'Sessione di ripristino verificata per l\'utente: {user}'
},
'btn_start_recovery': {
'en-US': 'Start Recovery',
'zh-CN': '开始恢复',
'ja': '回復を開始',
'es': 'Iniciar recuperación',
'de': 'Wiederherstellung starten',
'fr': 'Démarrer la récupération',
'pt': 'Iniciar recuperação',
'ko': '복구 시작',
'ru': 'Начать восстановление',
'it': 'Avvia ripristino'
},
'btn_cancel': {
'en-US': 'Cancel',
'zh-CN': '取消',
'ja': 'キャンセル',
'es': 'Cancelar',
'de': 'Abbrechen',
'fr': 'Annuler',
'pt': 'Cancelar',
'ko': '취소',
'ru': 'Отмена',
'it': 'Annulla'
}
};
const LOGIN_DEFAULT_CONFIG = {
serverUrl: 'https://fido2.amipro.me',
container: null,
theme: {
logo: null,
primaryColor: '#696cff',
backgroundColor: '#ffffff',
textColor: '#333333',
borderRadius: '8px',
},
language: 'zh-CN',
customI18n: {},
features: {
autoAuth: true,
enablePasswordLogin: true,
autoShowPassword: false,
maxPasswordAttempts: 3,
showRemainingAttempts: true,
},
callbacks: {
onLoginSuccess: null,
onLoginError: null,
onPasswordLogin: null,
onPasswordExhausted: null,
onLoginClosed: null,
},
rpId: null,
};
const LoginMode = {
FIDO2: 'fido2',
PASSWORD: 'password'
};
const LoginState = {
LOADING: 'loading',
FIDO2: 'fido2',
PASSWORD: 'password',
CLOSED: 'closed'
};
function Fido2Login(config) {
this.config = Object.assign({}, LOGIN_DEFAULT_CONFIG, config);
this.i18n = new I18nManager(this.config);
this.themeManager = new ThemeManager(this.config);
this.eventManager = new EventManager();
this.state = LoginState.LOADING;
this.mode = LoginMode.FIDO2;
this.attemptCount = 0;
this.maxAttempts = this.config.features.maxPasswordAttempts || 3;
this.modalElement = null;
this.containerElement = null;
this.bootstrapModal = null;
this.initialized = false;
if (this.config.serverUrl) {
setFidoServerURL(this.config.serverUrl);
}
}
Fido2Login.prototype._createModal = function() {
const container = typeof this.config.container === 'string'
? document.querySelector(this.config.container)
: this.config.container;
this._debugLog('[Fido2Login] _createModal container:', this.config.container);
this._debugLog('[Fido2Login] Container element:', container);
if (!container) {
throw new Error('Container not found: ' + this.config.container);
}
container.innerHTML = '';
const uniqueId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const modalId = 'fido2LoginModal_' + uniqueId;
const titleId = 'fido2LoginModalTitle_' + uniqueId;
const errorId = 'fido2LoginError_' + uniqueId;
const hintId = 'fido2LoginHint_' + uniqueId;
const userIdId = 'fido2UserId_' + uniqueId;
const passwordId = 'fido2Password_' + uniqueId;
const passwordSectionId = 'fido2PasswordSection_' + uniqueId;
const mainBtnId = 'fido2MainBtn_' + uniqueId;
const toggleLinkId = 'fido2ToggleModeLink_' + uniqueId;
const modal = document.createElement('div');
modal.className = 'modal fade fido2-sdk-login-modal';
modal.id = modalId;
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = this._getModalHTML(uniqueId, titleId, errorId, hintId, userIdId, passwordId, passwordSectionId, mainBtnId, toggleLinkId);
container.appendChild(modal);
this.modalElement = modal;
this.containerElement = container;
this._uniqueId = uniqueId;
this._debugLog('[Fido2Login] Modal element created with ID:', modalId);
this.themeManager.applyTheme(modal);
this.bootstrapModal = new window.bootstrap.Modal(modal, {
backdrop: true,
keyboard: true
});
this._debugLog('[Fido2Login] BootstrapModal instance created');
modal.addEventListener('hidden.bs.modal', () => {
this.state = LoginState.CLOSED;
this._emit('loginClosed');
this.cleanup();
});
this._bindEvents();
return modal;
};
Fido2Login.prototype._getModalHTML = function(uniqueId, titleId, errorId, hintId, userIdId, passwordId, passwordSectionId, mainBtnId, toggleLinkId) {
const theme = this.config.theme;
const features = this.config.features;
return `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content fido2-sdk-card">
<div class="modal-header fido2-sdk-header">
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo me-2" alt="Logo">` : ''}
<h5 class="modal-title fido2-sdk-text" id="${titleId}">${this.i18n.getText('title_login')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fido2-sdk-container">
${this._getBodyHTML(uniqueId, errorId, hintId, userIdId, passwordId, passwordSectionId, mainBtnId, toggleLinkId)}
</div>
</div>
</div>
`;
};
Fido2Login.prototype._getBodyHTML = function(uniqueId, errorId, hintId, userIdId, passwordId, passwordSectionId, mainBtnId, toggleLinkId) {
const features = this.config.features;
const theme = this.config.theme;
let html = '';
html += `
<div id="${errorId}" class="alert alert-danger mb-3" style="display:none;"></div>
<div id="${hintId}" class="alert alert-info mb-3" style="display:none;"></div>
`;
html += `
<div class="mb-3">
<label for="${userIdId}" class="form-label fido2-sdk-text">${this.i18n.getText('placeholder_user_id')}</label>
<input type="text" class="form-control" id="${userIdId}" placeholder="${this.i18n.getText('placeholder_user_id')}">
</div>
`;
html += `
<div class="mb-3" id="${passwordSectionId}" style="display:none;">
<label for="${passwordId}" class="form-label fido2-sdk-text">${this.i18n.getText('placeholder_password')}</label>
<input type="password" class="form-control" id="${passwordId}" placeholder="${this.i18n.getText('placeholder_password')}">
</div>
`;
html += `
<div class="d-grid gap-2">
<button type="button" class="btn btn-primary fido2-sdk-btn fido2-sdk-btn-primary" id="${mainBtnId}">
${this.i18n.getText('btn_fido2_login')}
</button>
</div>
`;
html += `<hr class="my-4">`;
if (features.enablePasswordLogin) {
html += `
<div class="text-center">
<a href="javascript:void(0)" class="fido2-sdk-link" id="${toggleLinkId}">${this.i18n.getText('link_use_password')}</a>
</div>
`;
}
if (features.showRemainingAttempts && this.mode === LoginMode.PASSWORD) {
const remaining = this.maxAttempts - this.attemptCount;
html += `
<div class="text-center mt-3">
<small class="text-muted fido2-sdk-text" id="fido2RemainingAttempts_${uniqueId}">
${this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining)}
</small>
</div>
`;
}
return html;
};
Fido2Login.prototype._bindEvents = function() {
const self = this;
const container = this.modalElement || this.containerElement;
if (!container) return;
const mainBtn = container.querySelector('[id^="fido2MainBtn_"]');
if (mainBtn) {
mainBtn.addEventListener('click', () => {
if (self.mode === LoginMode.FIDO2) {
self._handleFido2Login();
} else {
self._handlePasswordLogin();
}
});
}
const toggleLink = container.querySelector('[id^="fido2ToggleModeLink_"]');
if (toggleLink) {
toggleLink.addEventListener('click', () => {
this._debugLog('[Fido2Login] Toggle link clicked, current mode:', self.mode);
self._toggleMode();
});
}
const userIdInput = container.querySelector('[id^="fido2UserId_"]');
if (userIdInput) {
userIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
if (self.mode === LoginMode.FIDO2) {
self._handleFido2Login();
} else {
self._handlePasswordLogin();
}
}
});
}
const passwordInput = container.querySelector('[id^="fido2Password_"]');
if (passwordInput) {
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
self._handlePasswordLogin();
}
});
}
};
Fido2Login.prototype._handleFido2Login = function() {
const userId = this._getUserIdInput();
if (!userId) {
this._showError(this.i18n.getText('msg_invalid_user_id'));
return;
}
this._hideError();
this._startLoading();
authenticateFido2(userId, this.config.rpId).then(result => {
this._stopLoading();
if (result.status === 'ok') {
this.state = LoginState.CLOSED;
this._emit('fido2Success', {
method: 'fido2',
username: result.username,
session: result.session
});
this._closeUI();
} else {
this._emit('fido2Error', {
method: 'fido2',
code: 'AUTH_FAILED',
message: result.errorMessage || this.i18n.getText('msg_fido2_failed'),
originalError: result
});
}
}).catch(error => {
this._stopLoading();
if (error.name === 'AbortError' || (error.message && error.message.toLowerCase().includes('canceled'))) {
this._emit('fido2Error', {
method: 'fido2',
code: 'CANCELED',
message: this.i18n.getText('msg_fido2_canceled'),
originalError: error
});
} else {
this._emit('fido2Error', {
method: 'fido2',
code: 'AUTH_FAILED',
message: error.message || this.i18n.getText('msg_fido2_failed'),
originalError: error
});
}
});
};
Fido2Login.prototype._handlePasswordLogin = function() {
const userId = this._getUserIdInput();
const password = this._getPasswordInput();
if (!userId) {
this._showError(this.i18n.getText('msg_invalid_user_id'));
return;
}
if (!password) {
this._showError(this.i18n.getText('msg_invalid_password'));
return;
}
this._hideError();
this._startLoading();
// Clear any existing FIDO2 session before password login
if (typeof logoutFido2UserSession === 'function') {
logoutFido2UserSession();
}
const passwordCallback = this.config.callbacks.onPasswordLogin;
if (typeof passwordCallback !== 'function') {
this._stopLoading();
console.error('onPasswordLogin callback is not defined');
return;
}
Promise.resolve(passwordCallback(userId, password)).then(success => {
this._stopLoading();
if (success) {
this.state = LoginState.CLOSED;
this._emit('fido2Success', {
method: 'password',
username: userId,
session: null
});
this._closeUI();
} else {
this.attemptCount++;
if (this.attemptCount >= this.maxAttempts) {
this._closeUI();
this._emit('passwordExhausted', userId, this.attemptCount, this.maxAttempts);
} else {
this._updateRemainingAttempts();
const remaining = this.maxAttempts - this.attemptCount;
this._showError(this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining));
}
}
}).catch(error => {
this._stopLoading();
this.attemptCount++;
if (this.attemptCount >= this.maxAttempts) {
this._closeUI();
this._emit('passwordExhausted', userId, this.attemptCount, this.maxAttempts);
} else {
this._updateRemainingAttempts();
const remaining = this.maxAttempts - this.attemptCount;
this._showError(this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining));
}
});
};
Fido2Login.prototype._toggleMode = function() {
const container = this.modalElement || this.containerElement;
if (!container) return;
const passwordSection = container.querySelector('[id^="fido2PasswordSection_"]');
const mainBtn = container.querySelector('[id^="fido2MainBtn_"]');
const toggleLink = container.querySelector('[id^="fido2ToggleModeLink_"]');
const titleEl = container.querySelector('[id^="fido2LoginModalTitle_"]');
const remainingEl = container.querySelector('[id^="fido2RemainingAttempts_"]');
if (this.mode === LoginMode.FIDO2) {
this.mode = LoginMode.PASSWORD;
if (passwordSection) passwordSection.style.display = 'block';
if (mainBtn) mainBtn.textContent = this.i18n.getText('btn_password_login');
if (toggleLink) toggleLink.textContent = this.i18n.getText('link_use_fido2');
if (titleEl) titleEl.textContent = this.i18n.getText('title_password_login');
this.attemptCount = 0;
const remaining = this.maxAttempts - this.attemptCount;
if (remainingEl) {
remainingEl.textContent = this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining);
} else if (this.config.features.showRemainingAttempts) {
const remainingContainer = document.createElement('div');
remainingContainer.className = 'text-center mt-3';
remainingContainer.innerHTML = `<small class="text-muted fido2-sdk-text" id="fido2RemainingAttempts_${this._uniqueId}">${this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining)}</small>`;
toggleLink.parentNode.parentNode.insertBefore(remainingContainer, toggleLink.parentNode.nextSibling);
}
} else {
this.mode = LoginMode.FIDO2;
if (passwordSection) passwordSection.style.display = 'none';
if (mainBtn) mainBtn.textContent = this.i18n.getText('btn_fido2_login');
if (toggleLink) toggleLink.textContent = this.i18n.getText('link_use_password');
if (titleEl) titleEl.textContent = this.i18n.getText('title_login');
if (remainingEl) {
remainingEl.parentNode.remove();
}
}
this._hideError();
};
Fido2Login.prototype._getUserIdInput = function() {
const container = this.modalElement || this.containerElement;
const input = container ? container.querySelector('[id^="fido2UserId_"]') : null;
return input ? input.value.trim() : '';
};
Fido2Login.prototype._getPasswordInput = function() {
const container = this.modalElement || this.containerElement;
const input = container ? container.querySelector('[id^="fido2Password_"]') : null;
return input ? input.value : '';
};
Fido2Login.prototype._showError = function(message) {
const container = this.modalElement || this.containerElement;
const errorEl = container ? container.querySelector('[id^="fido2LoginError_"]') : null;
const hintEl = container ? container.querySelector('[id^="fido2LoginHint_"]') : null;
if (hintEl) hintEl.style.display = 'none';
if (errorEl) {
errorEl.textContent = message;
errorEl.style.display = 'block';
}
};
Fido2Login.prototype._hideError = function() {
const container = this.modalElement || this.containerElement;
const errorEl = container ? container.querySelector('[id^="fido2LoginError_"]') : null;
if (errorEl) errorEl.style.display = 'none';
};
Fido2Login.prototype._showHint = function(message) {
const container = this.modalElement || this.containerElement;
const hintEl = container ? container.querySelector('[id^="fido2LoginHint_"]') : null;
if (hintEl) {
hintEl.textContent = message;
hintEl.style.display = 'block';
}
};
Fido2Login.prototype._hideHint = function() {
const container = this.modalElement || this.containerElement;
const hintEl = container ? container.querySelector('[id^="fido2LoginHint_"]') : null;
if (hintEl) hintEl.style.display = 'none';
};
Fido2Login.prototype._startLoading = function() {
const container = this.modalElement || this.containerElement;
const mainBtn = container ? container.querySelector('[id^="fido2MainBtn_"]') : null;
if (mainBtn) {
mainBtn.disabled = true;
mainBtn.dataset.originalText = mainBtn.textContent;
mainBtn.textContent = '...';
}
};
Fido2Login.prototype._stopLoading = function() {
const container = this.modalElement || this.containerElement;
const mainBtn = container ? container.querySelector('[id^="fido2MainBtn_"]') : null;
if (mainBtn) {
mainBtn.disabled = false;
mainBtn.textContent = mainBtn.dataset.originalText ||
(this.mode === LoginMode.FIDO2 ? this.i18n.getText('btn_fido2_login') : this.i18n.getText('btn_password_login'));
}
};
Fido2Login.prototype._updateRemainingAttempts = function() {
const container = this.modalElement || this.containerElement;
if (!container) return;
const remainingEl = container.querySelector('[id^="fido2RemainingAttempts_"]');
if (remainingEl) {
const remaining = this.maxAttempts - this.attemptCount;
remainingEl.textContent = this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining);
}
};
Fido2Login.prototype._emit = function(event, data) {
const callbackName = 'on' + event.charAt(0).toUpperCase() + event.slice(1);
const callback = this.config.callbacks[callbackName];
if (typeof callback === 'function') {
if (event === 'fido2Success') {
callback(data.username, data.session, data);
} else if (event === 'fido2Error') {
callback(data, data.originalError);
} else {
callback(data);
}
}
};
Fido2Login.prototype._closeUI = function() {
if (this.bootstrapModal) {
this.bootstrapModal.hide();
} else if (this.modalElement) {
const bootstrapModal = window.bootstrap.Modal.getInstance(this.modalElement);
if (bootstrapModal) {
bootstrapModal.hide();
}
}
};
Fido2Login.prototype.show = function() {
this._debugLog('[Fido2Login] show() called, initialized:', this.initialized);
if (this.initialized) {
this._debugLog('[Fido2Login] Already initialized, showing modal');
if (this.bootstrapModal) {
this.bootstrapModal.show();
}
return;
}
this._debugLog('[Fido2Login] Creating modal...');
this._createModal();
this._initAutoAuth();
this.initialized = true;
this._debugLog('[Fido2Login] Modal created, showing...');
if (this.bootstrapModal) {
this._debugLog('[Fido2Login] bootstrapModal.show() called');
this.bootstrapModal.show();
}
};
Fido2Login.prototype._initAutoAuth = function() {
const self = this;
const features = this.config.features;
this._debugLog('[Fido2Login] _initAutoAuth called');
this._debugLog('[Fido2Login] features.autoAuth:', features.autoAuth);
this._debugLog('[Fido2Login] canTryAutoAuthentication():', canTryAutoAuthentication());
this._debugLog('[Fido2Login] dfido2_lib_registered:', localStorage.getItem('dfido2_lib_registered'));
if (features.autoAuth && canTryAutoAuthentication()) {
this.state = LoginState.LOADING;
this._showHint(this.i18n.getText('msg_autofail_hint'));
this._debugLog('[Fido2Login] Starting auto Fido2 authentication...');
authenticateFido2(null, this.config.rpId).then(result => {
this._debugLog('[Fido2Login] authenticateFido2 result:', result);
this._hideHint();
if (result.status === 'ok') {
this.state = LoginState.CLOSED;
this._emit('fido2Success', {
method: 'fido2',
username: result.username,
session: result.session
});
this._closeUI();
} else {
this.state = LoginState.FIDO2;
this._emit('fido2Error', {
method: 'fido2',
code: 'AUTH_FAILED',
message: result.errorMessage || this.i18n.getText('msg_fido2_failed'),
originalError: result
});
this._updateUIForFido2Mode();
}
}).catch(error => {
this._debugLog('[Fido2Login] authenticateFido2 error:', error);
this._hideHint();
if (error.name === 'AbortError' || (error.message && error.message.toLowerCase().includes('canceled'))) {
this.state = LoginState.FIDO2;
this._emit('fido2Error', {
method: 'fido2',
code: 'CANCELED',
message: this.i18n.getText('msg_fido2_canceled'),
originalError: error
});
} else {
this.state = LoginState.FIDO2;
this._emit('fido2Error', {
method: 'fido2',
code: 'AUTH_FAILED',
message: this._getWebAuthnErrorMessage(error),
originalError: error
});
}
this._updateUIForFido2Mode();
});
} else if (!canTryAutoAuthentication()) {
this._debugLog('[Fido2Login] No registration, going to password mode');
if (features.enablePasswordLogin) {
this.state = LoginState.PASSWORD;
this.mode = LoginMode.PASSWORD;
this._updateUIForPasswordMode();
} else {
this.state = LoginState.FIDO2;
this._emit('fido2Error', {
method: 'fido2',
code: 'NO_REGISTRATION',
message: this.i18n.getText('msg_no_registration'),
originalError: null
});
}
} else {
this._debugLog('[Fido2Login] autoAuth disabled or other condition, going to fido2 mode');
this.state = LoginState.FIDO2;
this._updateUIForFido2Mode();
}
};
Fido2Login.prototype._getWebAuthnErrorI18nKey = function(error) {
if (!error || !error.name) return null;
const errorNameMap = {
'InvalidStateError': 'error_invalid_state',
'NotAllowedError': 'error_not_allowed',
'AbortError': 'error_abort',
'NotSupportedError': 'error_not_supported',
'SecurityError': 'error_security',
'NetworkError': 'error_network',
'ConstraintError': 'error_constraint',
'NotReadableError': 'error_not_readable',
'EncodingError': 'error_encoding',
'DataError': 'error_data',
'TimeoutError': 'error_timeout'
};
return errorNameMap[error.name] || null;
};
Fido2Login.prototype._getWebAuthnErrorMessage = function(error) {
if (!error) return this.i18n.getText('msg_fido2_failed');
const i18nKey = this._getWebAuthnErrorI18nKey(error);
if (i18nKey && this.i18n.getText(i18nKey)) {
return this.i18n.getText(i18nKey);
}
return error.message || this.i18n.getText('msg_fido2_failed');
};
Fido2Login.prototype._debugLog = function() {
const args = Array.prototype.slice.call(arguments);
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
if (typeof window.fido2LoginDebug === 'function') {
window.fido2LoginDebug(message);
}
console.log.apply(console, args);
};
Fido2Login.prototype._updateUIForFido2Mode = function() {
const container = this.modalElement || this.containerElement;
if (!container) return;
const passwordSection = container.querySelector('[id^="fido2PasswordSection_"]');
const mainBtn = container.querySelector('[id^="fido2MainBtn_"]');
const toggleLink = container.querySelector('[id^="fido2ToggleModeLink_"]');
if (passwordSection) passwordSection.style.display = 'none';
if (mainBtn) mainBtn.textContent = this.i18n.getText('btn_fido2_login');
if (toggleLink && this.config.features.enablePasswordLogin) {
toggleLink.textContent = this.i18n.getText('link_use_password');
toggleLink.style.display = 'inline';
} else if (toggleLink) {
toggleLink.style.display = 'none';
}
};
Fido2Login.prototype._updateUIForPasswordMode = function() {
const container = this.modalElement || this.containerElement;
if (!container) return;
const passwordSection = container.querySelector('[id^="fido2PasswordSection_"]');
const mainBtn = container.querySelector('[id^="fido2MainBtn_"]');
const toggleLink = container.querySelector('[id^="fido2ToggleModeLink_"]');
const titleEl = container.querySelector('[id^="fido2LoginModalTitle_"]');
if (passwordSection) passwordSection.style.display = 'block';
if (mainBtn) mainBtn.textContent = this.i18n.getText('btn_password_login');
if (toggleLink) toggleLink.textContent = this.i18n.getText('link_use_fido2');
if (titleEl) titleEl.textContent = this.i18n.getText('title_password_login');
this._updateRemainingAttempts();
};
Fido2Login.prototype.hide = function() {
this._closeUI();
};
Fido2Login.prototype.destroy = function() {
this.state = LoginState.CLOSED;
if (this.bootstrapModal) {
this.bootstrapModal.dispose();
}
this.cleanup();
};
Fido2Login.prototype.cleanup = function() {
if (this.modalElement) {
this.themeManager.cleanup(this.modalElement);
if (this.modalElement.parentNode) {
this.modalElement.parentNode.removeChild(this.modalElement);
}
this.modalElement = null;
}
this.containerElement = null;
this.bootstrapModal = null;
};
Fido2Login.prototype.setMode = function(mode) {
if (mode === LoginMode.PASSWORD && this.mode !== LoginMode.PASSWORD) {
this.mode = LoginMode.PASSWORD;
this.attemptCount = 0;
this._updateUIForPasswordMode();
} else if (mode === LoginMode.FIDO2 && this.mode !== LoginMode.FIDO2) {
this.mode = LoginMode.FIDO2;
this._updateUIForFido2Mode();
}
};
Fido2Login.prototype.getUserId = function() {
return this._getUserIdInput();
};
Fido2Login.prototype.getState = function() {
return this.state;
};
Fido2Login.prototype.getMode = function() {
return this.mode;
};
Fido2Login.prototype.getAttemptCount = function() {
return this.attemptCount;
};
Fido2Login.prototype.getRemainingAttempts = function() {
return this.maxAttempts - this.attemptCount;
};
Fido2Login.prototype.resetPasswordAttempts = function() {
this.attemptCount = 0;
this._updateRemainingAttempts();
};
function I18nManager(config) {
this.config = config;
this.messages = new Map();
for (let key in DEFAULT_I18N) {
this.messages.set(key, new Map());
for (let lang in DEFAULT_I18N[key]) {
this.messages.get(key).set(lang, DEFAULT_I18N[key][lang]);
}
}
if (config.customI18n) {
for (let key in config.customI18n) {
if (!this.messages.has(key)) {
this.messages.set(key, new Map());
}
for (let lang in config.customI18n[key]) {
this.messages.get(key).set(lang, config.customI18n[key][lang]);
}
}
}
}
I18nManager.prototype.getText = function(key) {
const lang = this.config.language || window.navigator.language || 'en-US';
const keyMap = this.messages.get(key);
if (!keyMap) return key;
let text = keyMap.get(lang);
if (!text) text = keyMap.get('en-US');
return text || key;
};
function EventManager() {
this.callbacks = {};
}
EventManager.prototype.on = function(event, callback) {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
this.callbacks[event].push(callback);
};
EventManager.prototype.off = function(event, callback) {
if (!this.callbacks[event]) return;
const index = this.callbacks[event].indexOf(callback);
if (index > -1) {
this.callbacks[event].splice(index, 1);
}
};
EventManager.prototype.emit = function(event, data) {
if (!this.callbacks[event]) return;
this.callbacks[event].forEach(function(callback) {
callback(data);
});
};
function ThemeManager(config) {
this.config = config;
}
ThemeManager.prototype.applyTheme = function(element) {
if (!element) return;
const theme = this.config.theme;
const styleId = 'fido2-ui-sdk-theme-' + Date.now();
let css = '';
if (theme.primaryColor) {
css += '.fido2-sdk-btn-primary { background-color: ' + theme.primaryColor + '!important; border-color: ' + theme.primaryColor + '!important; }';
css += '.fido2-sdk-header { border-bottom: 2px solid ' + theme.primaryColor + '!important; }';
}
if (theme.backgroundColor) {
css += '.fido2-sdk-container { background-color: ' + theme.backgroundColor + '!important; }';
}
if (theme.textColor) {
css += '.fido2-sdk-text { color: ' + theme.textColor + '!important; }';
}
if (theme.borderRadius) {
css += '.fido2-sdk-card, .fido2-sdk-btn { border-radius: ' + theme.borderRadius + '!important; }';
}
if (css) {
let style = document.getElementById(styleId);
if (style) {
style.parentNode.removeChild(style);
}
style = document.createElement('style');
style.id = styleId;
style.textContent = css;
document.head.appendChild(style);
}
element.dataset.themeStyleId = styleId;
};
ThemeManager.prototype.cleanup = function(element) {
if (!element) return;
const styleId = element.dataset.themeStyleId;
if (styleId) {
const style = document.getElementById(styleId);
if (style) {
style.parentNode.removeChild(style);
}
}
};
function DeviceManager(config, i18n, eventManager) {
this.config = config;
this.i18n = i18n;
this.eventManager = eventManager;
this.devices = [];
this.refreshTimer = null;
this.sessionStatus = false;
this.recoveryMode = false;
this.recoveryUser = null;
}
DeviceManager.prototype.getSessionUserId = function() {
try {
const sessionText = sessionStorage.getItem('fido2_user_session');
if (!sessionText) return null;
const sessionData = JSON.parse(sessionText);
return sessionData.uid || null;
} catch (error) {
return null;
}
};
DeviceManager.prototype.getEffectiveUserId = function() {
const sessionUserId = this.getSessionUserId();
if (sessionUserId) {
return sessionUserId;
}
return this.config.userId || null;
};
DeviceManager.prototype.validateUserId = function() {
const sessionUserId = this.getSessionUserId();
const inputUserId = this.config.userId;
if (sessionUserId && inputUserId && sessionUserId !== inputUserId) {
return { valid: false, error: 'User ID mismatch: session user is ' + sessionUserId };
}
return { valid: true };
};
DeviceManager.prototype.loadDevices = async function() {
try {
const result = await listUserDevicesFido2(this.config.rpId);
if (result.status === 'ok') {
this.devices = result.devices || [];
this.eventManager.emit('deviceListLoaded', this.devices);
return this.devices;
} else {
throw new Error(result.errorMessage || 'Failed to load devices');
}
} catch (error) {
this.eventManager.emit('error', error);
throw error;
}
};
DeviceManager.prototype.addDevice = async function(displayName) {
try {
const effectiveUserId = this.getEffectiveUserId();
if (!effectiveUserId) {
const err = new Error('No user ID available. Please login first.');
this.eventManager.emit('error', err);
throw err;
}
const validation = this.validateUserId();
if (!validation.valid) {
const err = new Error(validation.error);
this.eventManager.emit('error', err);
throw err;
}
const result = await registerFido2(effectiveUserId, displayName || 'Device-' + effectiveUserId, this.config.rpId);
if (result.status === 'ok') {
// registerFido2 automatically creates/updates session in sessionStorage
await this.loadDevices();
this.eventManager.emit('deviceAdded', result);
return result;
} else {
// Create error with original properties preserved
const err = new Error(result.errorMessage || 'Failed to add device');
if (result.name) err.name = result.name;
if (result.errCode) err.errCode = result.errCode;
this.eventManager.emit('error', err);
throw err;
}
} catch (error) {
if (!error.name) {
error.name = 'DeviceManagerError';
}
this.eventManager.emit('error', error);
throw error;
}
};
DeviceManager.prototype.deleteDevice = async function(deviceId) {
try {
const effectiveUserId = this.getEffectiveUserId();
if (!effectiveUserId) {
const err = new Error('No user ID available. Please login first.');
this.eventManager.emit('error', err);
throw err;
}
const validation = this.validateUserId();
if (!validation.valid) {
const err = new Error(validation.error);
this.eventManager.emit('error', err);
throw err;
}
const result = await delUserDeviceFido2(deviceId, this.config.rpId);
if (result.status === 'ok') {
await this.loadDevices();
this.eventManager.emit('deviceDeleted', deviceId);
return result;
} else {
// Create error with original properties preserved
const err = new Error(result.errorMessage || 'Failed to delete device');
if (result.name) err.name = result.name;
if (result.errCode) err.errCode = result.errCode;
this.eventManager.emit('error', err);
throw err;
}
} catch (error) {
if (!error.name) {
error.name = 'DeviceManagerError';
}
this.eventManager.emit('error', error);
throw error;
}
};
DeviceManager.prototype.getUserId = function() {
return this.getEffectiveUserId();
};
DeviceManager.prototype.checkSession = async function() {
try {
const sessionOk = await validSession(this.config.rpId);
this.sessionStatus = !!sessionOk;
this.eventManager.emit('sessionStatusChanged', this.sessionStatus);
return this.sessionStatus;
} catch (error) {
this.sessionStatus = false;
this.eventManager.emit('sessionStatusChanged', false);
return false;
}
};
DeviceManager.prototype.getRegistrationUser = async function(regSessionId) {
try {
const response = await fetch(getServerUrl() + '/reg/username', {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ session_id: regSessionId })
});
const resp = await response.json();
return resp;
} catch (err) {
console.log(err);
return { status: 'failed', errorMessage: err.message };
}
};
DeviceManager.prototype.startRecovery = async function(recoverySessionId) {
try {
if (!recoverySessionId) {
throw new Error('Recovery session ID is required');
}
const result = await this.getRegistrationUser(recoverySessionId);
if (result && result.status === 'ok' && result.username) {
this.recoveryMode = true;
this.recoveryUser = {
username: result.username,
displayName: result.displayname || result.username,
sessionId: recoverySessionId
};
this.config.userId = result.username;
this.eventManager.emit('recoveryStarted', this.recoveryUser);
return {
success: true,
user: this.recoveryUser
};
} else {
throw new Error(result.errorMessage || 'Invalid recovery session');
}
} catch (error) {
this.eventManager.emit('error', error);
throw error;
}
};
DeviceManager.prototype.finishRecovery = async function(displayName) {
try {
if (!this.recoveryMode || !this.recoveryUser) {
throw new Error('Not in recovery mode');
}
const effectiveDisplayName = displayName || this.recoveryUser.displayName || 'RecoveredDevice-' + this.recoveryUser.username;
const result = await registerFido2(this.recoveryUser.username, effectiveDisplayName, this.config.rpId);
if (result.status === 'ok') {
const wasRecoveryMode = this.recoveryMode;
const recoveredUser = this.recoveryUser;
this.recoveryMode = false;
this.recoveryUser = null;
await this.loadDevices();
this.eventManager.emit('deviceAdded', result);
this.eventManager.emit('recoveryCompleted', {
user: recoveredUser,
device: result
});
return {
success: true,
user: recoveredUser,
device: result
};
} else {
throw new Error(result.errorMessage || 'Failed to add device during recovery');
}
} catch (error) {
this.eventManager.emit('error', error);
throw error;
}
};
DeviceManager.prototype.cancelRecovery = function() {
if (this.recoveryMode) {
const canceledUser = this.recoveryUser;
this.recoveryMode = false;
this.recoveryUser = null;
this.eventManager.emit('recoveryCanceled', canceledUser);
return true;
}
return false;
};
DeviceManager.prototype.isInRecoveryMode = function() {
return this.recoveryMode;
};
DeviceManager.prototype.getRecoveryUser = function() {
return this.recoveryUser;
};
DeviceManager.prototype.parseDeviceDescription = function(device) {
if (device.desc && device.desc.length > 0) {
return device.desc;
}
try {
if (device.userAgent && typeof UAParser !== 'undefined') {
const parser = new UAParser(device.userAgent);
if (parser.getOS().name) {
return parser.getDevice().model + ',' + parser.getOS().name + ',' + parser.getBrowser().name;
}
}
return device.userAgent || 'Unknown Device';
} catch (error) {
return 'Unknown Device';
}
};
DeviceManager.prototype.startAutoRefresh = function() {
if (!this.config.autoRefresh) return;
this.stopAutoRefresh();
this.refreshTimer = setInterval(async () => {
try {
await this.loadDevices();
await this.checkSession();
} catch (error) {
console.error('Auto refresh error:', error);
}
}, this.config.refreshInterval);
};
DeviceManager.prototype.stopAutoRefresh = function() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
};
function UIRenderer(config, i18n, themeManager, eventManager) {
this.config = config;
this.i18n = i18n;
this.themeManager = themeManager;
this.eventManager = eventManager;
this.modalElement = null;
this.containerElement = null;
}
UIRenderer.prototype.renderModal = function() {
if (!this.config.container) {
throw new Error('Container is required for modal mode');
}
const container = typeof this.config.container === 'string'
? document.querySelector(this.config.container)
: this.config.container;
if (!container) {
throw new Error('Container not found: ' + this.config.container);
}
container.innerHTML = '';
const uniqueId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const modalId = 'fido2SdkModal_' + uniqueId;
const sessionStatusId = 'fido2SessionStatus_' + uniqueId;
const addBtnId = 'fido2AddDeviceBtn_' + uniqueId;
const devicesListId = 'fido2DevicesList_' + uniqueId;
const modal = document.createElement('div');
modal.className = 'modal fade fido2-sdk-modal';
modal.id = modalId;
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = this._getModalHTML(uniqueId, sessionStatusId, addBtnId, devicesListId);
container.appendChild(modal);
this.modalElement = modal;
this.containerElement = container;
this._uniqueId = uniqueId;
this.themeManager.applyTheme(modal);
const bootstrapModal = new window.bootstrap.Modal(modal, {
backdrop: true,
keyboard: true
});
bootstrapModal.show();
modal.addEventListener('hidden.bs.modal', () => {
this.eventManager.emit('close');
this.cleanup();
});
this._bindEvents(uniqueId, addBtnId, devicesListId);
return modal;
};
UIRenderer.prototype._getModalHTML = function(uniqueId, sessionStatusId, addBtnId, devicesListId) {
const theme = this.config.theme;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
const isRecoveryMode = dm ? dm.isInRecoveryMode() : false;
const modalTitle = isRecoveryMode ? this.i18n.getText('title_recovery') : this.i18n.getText('my_devices');
return `
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content fido2-sdk-card">
<div class="modal-header fido2-sdk-header">
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo" alt="Logo">` : ''}
<h5 class="modal-title fido2-sdk-text">${modalTitle}</h5>
${this.config.features.showSessionStatus && !isRecoveryMode ? `<span class="badge fido2-sdk-status-badge" id="${sessionStatusId}"></span>` : ''}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fido2-sdk-container">
${this._getBodyHTML(uniqueId, addBtnId, devicesListId)}
</div>
<div class="modal-footer">
${isRecoveryMode ? `<button type="button" class="btn btn-secondary fido2-sdk-btn" id="fido2CancelRecoveryBtn_${uniqueId}">${this.i18n.getText('btn_cancel')}</button>` : ''}
<button type="button" class="btn btn-secondary fido2-sdk-btn" data-bs-dismiss="modal">${this.i18n.getText('btn_close')}</button>
</div>
</div>
</div>
`;
};
UIRenderer.prototype._getBodyHTML = function(uniqueId, addBtnId, devicesListId) {
const features = this.config.features;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
const userId = dm ? dm.getEffectiveUserId() : (this.config.userId || '');
const isRecoveryMode = dm ? dm.isInRecoveryMode() : false;
const recoveryUser = dm ? dm.getRecoveryUser() : null;
let html = '';
if (isRecoveryMode && recoveryUser) {
html += `
<div class="alert alert-success mb-3 fido2-sdk-text">
<strong>${this.i18n.getText('msg_recovery_user_found').replace('{user}', recoveryUser.displayName || recoveryUser.username)}</strong>
</div>
`;
html += `
<button type="button" class="btn btn-success mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2StartRecoveryBtn_${uniqueId}">
${this.i18n.getText('btn_start_recovery')}
</button>
`;
} else {
if (features.showUserInfo && userId) {
html += `<div class="fido2-sdk-user-info mb-3 fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>`;
}
if (features.showAddButton) {
html += `
<button type="button" class="btn btn-info mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="${addBtnId}">
${this.i18n.getText('btn_add')}
</button>
`;
}
html += `
<div class="table-responsive">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="${devicesListId}">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
`;
}
return html;
};
UIRenderer.prototype._bindEvents = function(uniqueId, addBtnId, devicesListId) {
const container = this.modalElement || this.containerElement;
if (!container) return;
container.querySelectorAll('[data-fido2-action="delete"]').forEach(btn => {
btn.replaceWith(btn.cloneNode(true));
});
const addBtn = container.querySelector('#' + addBtnId);
if (addBtn) {
addBtn.replaceWith(addBtn.cloneNode(true));
const newAddBtn = container.querySelector('#' + addBtnId);
newAddBtn.addEventListener('click', () => {
this.eventManager.emit('addDevice');
});
}
const startRecoveryBtn = container.querySelector('[id^="fido2StartRecoveryBtn_"]');
if (startRecoveryBtn) {
startRecoveryBtn.replaceWith(startRecoveryBtn.cloneNode(true));
const newStartRecoveryBtn = container.querySelector('[id^="fido2StartRecoveryBtn_"]');
if (newStartRecoveryBtn) {
newStartRecoveryBtn.addEventListener('click', () => {
this.eventManager.emit('startRecovery');
});
}
}
const cancelRecoveryBtn = container.querySelector('[id^="fido2CancelRecoveryBtn_"]');
if (cancelRecoveryBtn) {
cancelRecoveryBtn.replaceWith(cancelRecoveryBtn.cloneNode(true));
const newCancelRecoveryBtn = container.querySelector('[id^="fido2CancelRecoveryBtn_"]');
if (newCancelRecoveryBtn) {
newCancelRecoveryBtn.addEventListener('click', () => {
this.eventManager.emit('cancelRecovery');
});
}
}
container.querySelectorAll('[data-fido2-action="delete"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const deviceId = btn.dataset.deviceId;
if (deviceId && confirm(this.i18n.getText('msg_confirm_deldev'))) {
this.eventManager.emit('deleteDevice', deviceId);
}
});
});
};
UIRenderer.prototype.updateDevicesList = function(devices) {
const container = this.modalElement || this.containerElement;
const tbody = container ? container.querySelector('[id^="fido2DevicesList_"]') : null;
if (!tbody) return;
if (!devices || devices.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
`;
return;
}
let html = '';
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
devices.forEach(device => {
const desc = dm ? dm.parseDeviceDescription(device) : device.desc || 'Unknown Device';
const date = new Date(device.registered_time).toLocaleString();
html += `
<tr>
<td class="fido2-sdk-text"><strong>${desc}</strong></td>
<td class="fido2-sdk-text">${date}</td>
${this.config.features.showDeleteButton ? `
<td>
<a href="javascript:void(0)"
class="text-danger"
data-fido2-action="delete"
data-device-id="${device.device_id}">
<i class="bx bx-trash"></i> ${this.i18n.getText('title_del')}
</a>
</td>
` : ''}
</tr>
`;
});
tbody.innerHTML = html;
this._bindEvents(this._uniqueId, 'fido2AddDeviceBtn_' + this._uniqueId, 'fido2DevicesList_' + this._uniqueId);
};
UIRenderer.prototype.updateSessionStatus = function(isValid) {
const container = this.modalElement || this.containerElement;
const badge = container ? container.querySelector('[id^="fido2SessionStatus_"]') : null;
const addBtn = container ? container.querySelector('[id^="fido2AddDeviceBtn_"]') : null;
if (badge) {
if (isValid) {
badge.className = 'badge bg-success fido2-sdk-status-badge';
badge.textContent = this.i18n.getText('msg_session_status_ok');
} else {
badge.className = 'badge bg-danger fido2-sdk-status-badge';
badge.textContent = this.i18n.getText('msg_session_status_fail');
}
}
if (addBtn) {
addBtn.disabled = false;
}
};
UIRenderer.prototype.updateRecoveryUI = function() {
const container = this.modalElement || this.containerElement;
if (!container) return;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
if (!dm) return;
const isRecoveryMode = dm.isInRecoveryMode();
const uniqueId = this._uniqueId;
const titleEl = container.querySelector('.modal-title');
if (titleEl) {
titleEl.textContent = isRecoveryMode ? this.i18n.getText('title_recovery') : this.i18n.getText('my_devices');
}
const sessionBadge = container.querySelector('[id^="fido2SessionStatus_"]');
if (sessionBadge) {
sessionBadge.style.display = isRecoveryMode ? 'none' : 'inline';
}
const modalBody = container.querySelector('.modal-body');
if (modalBody) {
modalBody.innerHTML = this._getBodyHTML(uniqueId, 'fido2AddDeviceBtn_' + uniqueId, 'fido2DevicesList_' + uniqueId);
}
const modalFooter = container.querySelector('.modal-footer');
if (modalFooter) {
if (isRecoveryMode) {
let cancelBtn = modalFooter.querySelector('[id^="fido2CancelRecoveryBtn_"]');
if (!cancelBtn) {
cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'btn btn-secondary fido2-sdk-btn';
cancelBtn.id = 'fido2CancelRecoveryBtn_' + uniqueId;
cancelBtn.textContent = this.i18n.getText('btn_cancel');
modalFooter.insertBefore(cancelBtn, modalFooter.firstChild);
}
} else {
const cancelBtn = modalFooter.querySelector('[id^="fido2CancelRecoveryBtn_"]');
if (cancelBtn) {
cancelBtn.remove();
}
}
}
this._bindEvents(uniqueId, 'fido2AddDeviceBtn_' + uniqueId, 'fido2DevicesList_' + uniqueId);
};
UIRenderer.prototype.close = function() {
if (this.modalElement) {
const bootstrapModal = window.bootstrap.Modal.getInstance(this.modalElement);
if (bootstrapModal) {
bootstrapModal.hide();
}
}
};
UIRenderer.prototype.cleanup = function() {
if (this.modalElement) {
this.themeManager.cleanup(this.modalElement);
if (this.modalElement.parentNode) {
this.modalElement.parentNode.removeChild(this.modalElement);
}
this.modalElement = null;
}
this.containerElement = null;
};
function Fido2UIManager() {
this.config = null;
this.i18n = null;
this.eventManager = null;
this.themeManager = null;
this.deviceManager = null;
this.uiRenderer = null;
this.initialized = false;
}
Fido2UIManager.prototype.init = function(userConfig) {
if (this.initialized) {
console.warn('Fido2UIManager already initialized');
return this;
}
if (!window.bootstrap) {
throw new Error('Bootstrap is required. Please include Bootstrap JS before fido2-ui-sdk.js');
}
this.config = Object.assign({}, DEFAULT_CONFIG, userConfig);
this.i18n = new I18nManager(this.config);
this.eventManager = new EventManager();
this.themeManager = new ThemeManager(this.config);
this.deviceManager = new DeviceManager(this.config, this.i18n, this.eventManager);
this.uiRenderer = new UIRenderer(this.config, this.i18n, this.themeManager, this.eventManager);
if (this.config.serverUrl) {
setFidoServerURL(this.config.serverUrl);
}
this._setupCallbacks();
this.initialized = true;
return this;
};
Fido2UIManager.prototype._setupCallbacks = function() {
const callbacks = this.config.callbacks;
if (callbacks.onInit) {
this.eventManager.on('init', callbacks.onInit);
}
if (callbacks.onDeviceAdded) {
this.eventManager.on('deviceAdded', callbacks.onDeviceAdded);
}
if (callbacks.onDeviceDeleted) {
this.eventManager.on('deviceDeleted', callbacks.onDeviceDeleted);
}
if (callbacks.onDeviceListLoaded) {
this.eventManager.on('deviceListLoaded', callbacks.onDeviceListLoaded);
}
if (callbacks.onError) {
this.eventManager.on('error', callbacks.onError);
}
if (callbacks.onClose) {
this.eventManager.on('close', callbacks.onClose);
}
if (callbacks.onUserMismatch) {
this.eventManager.on('userMismatch', callbacks.onUserMismatch);
}
};
Fido2UIManager.prototype.renderDeviceManager = function(config) {
const manager = new Fido2UIManager();
manager.init(config);
window.Fido2UIManager = manager;
const mode = manager.config.mode;
manager.uiRenderer.renderModal();
manager._bindInternalEvents();
manager._loadInitialData();
manager.deviceManager.startAutoRefresh();
manager.eventManager.emit('init', manager);
return manager;
};
Fido2UIManager.prototype._getI18nErrorMessage = function(error, defaultKey) {
if (!error) return this.i18n.getText(defaultKey || 'error');
const errorMessage = error.message || '';
const errorName = error.name || '';
// Check WebAuthn DOM error name first
if (errorName && this._getWebAuthnErrorI18nKey) {
const i18nKey = this._getWebAuthnErrorI18nKey(error);
if (i18nKey && this.i18n.getText(i18nKey)) {
return this.i18n.getText(i18nKey);
}
}
// Check for Fido2LibErr codes in error message
if (errorMessage.includes('Fido2LibErr')) {
const match = errorMessage.match(/Fido2LibErr(\d+):/);
if (match) {
const i18nKey = 'error_fido2lib_' + match[1];
if (this.i18n.getText(i18nKey)) {
return this.i18n.getText(i18nKey);
}
}
}
// Check for SvrErr codes in error message
if (errorMessage.includes('SvrErr')) {
const match = errorMessage.match(/SvrErr(\d+):/);
if (match) {
const i18nKey = 'error_svr_' + match[1];
if (this.i18n.getText(i18nKey)) {
return this.i18n.getText(i18nKey);
}
}
}
return errorMessage || this.i18n.getText(defaultKey || 'error');
};
Fido2UIManager.prototype._getWebAuthnErrorI18nKey = function(error) {
if (!error || !error.name) return null;
const errorNameMap = {
'InvalidStateError': 'error_invalid_state',
'NotAllowedError': 'error_not_allowed',
'AbortError': 'error_abort',
'NotSupportedError': 'error_not_supported',
'SecurityError': 'error_security',
'NetworkError': 'error_network',
'ConstraintError': 'error_constraint',
'NotReadableError': 'error_not_readable',
'EncodingError': 'error_encoding',
'DataError': 'error_data',
'TimeoutError': 'error_timeout'
};
return errorNameMap[error.name] || null;
};
Fido2UIManager.prototype._bindInternalEvents = function() {
const self = this;
this.eventManager.on('addDevice', async () => {
const addBtn = document.getElementById('fido2AddDeviceBtn');
if (addBtn) addBtn.disabled = true;
try {
await self.deviceManager.addDevice();
self.uiRenderer.updateDevicesList(self.deviceManager.devices);
// Re-check session after device list is updated
await self.deviceManager.checkSession();
self.uiRenderer.updateSessionStatus(self.deviceManager.sessionStatus);
alert(self.i18n.getText('msg_register_ok'));
} catch (error) {
console.error('Add device error:', error);
alert(self._getI18nErrorMessage(error, 'error'));
} finally {
if (addBtn) addBtn.disabled = false;
}
});
this.eventManager.on('deleteDevice', async (deviceId) => {
try {
await self.deviceManager.deleteDevice(deviceId);
self.uiRenderer.updateDevicesList(self.deviceManager.devices);
await self.deviceManager.checkSession();
alert(self.i18n.getText('msg_deldev_ok'));
} catch (error) {
console.error('Delete device error:', error);
alert(self._getI18nErrorMessage(error, 'error'));
}
});
this.eventManager.on('deviceListLoaded', (devices) => {
self.uiRenderer.updateDevicesList(devices);
});
this.eventManager.on('sessionStatusChanged', (isValid) => {
self.uiRenderer.updateSessionStatus(isValid);
});
this.eventManager.on('startRecovery', async () => {
const recoveryBtn = document.querySelector('[id^="fido2StartRecoveryBtn_"]');
if (recoveryBtn) recoveryBtn.disabled = true;
try {
const result = await self.deviceManager.finishRecovery();
if (result.success) {
alert(self.i18n.getText('msg_recovery_success'));
self.uiRenderer.updateDevicesList(self.deviceManager.devices);
await self.deviceManager.checkSession();
self.uiRenderer.updateSessionStatus(self.deviceManager.sessionStatus);
}
} catch (error) {
console.error('Recovery error:', error);
alert(error.message || self.i18n.getText('msg_recovery_failed'));
} finally {
if (recoveryBtn) recoveryBtn.disabled = false;
}
});
this.eventManager.on('cancelRecovery', async () => {
self.deviceManager.cancelRecovery();
self.close();
});
this.eventManager.on('recoveryStarted', (user) => {
console.log('Recovery started for user:', user);
self.uiRenderer.updateRecoveryUI();
});
this.eventManager.on('recoveryCompleted', (result) => {
console.log('Recovery completed:', result);
});
this.eventManager.on('recoveryCanceled', (user) => {
console.log('Recovery canceled for user:', user);
});
this.eventManager.on('error', (error) => {
console.error('FIDO2 SDK Error:', error);
});
};
Fido2UIManager.prototype._loadInitialData = async function() {
try {
if (this.config.recoverySessionId) {
await this.deviceManager.startRecovery(this.config.recoverySessionId);
this.uiRenderer.cleanup();
this.uiRenderer.renderModal();
this._bindInternalEvents();
} else {
await this.deviceManager.loadDevices();
await this.deviceManager.checkSession();
}
} catch (error) {
console.error('Initial data load error:', error);
if (this.config.recoverySessionId) {
alert(error.message || this.i18n.getText('msg_recovery_failed'));
this.close();
}
}
};
Fido2UIManager.prototype.close = function() {
if (this.uiRenderer) {
this.uiRenderer.close();
}
};
Fido2UIManager.prototype.refresh = async function() {
if (this.deviceManager) {
await this.deviceManager.loadDevices();
await this.deviceManager.checkSession();
}
};
Fido2UIManager.prototype.logout = function() {
if (typeof logoutFido2UserSession === 'function') {
logoutFido2UserSession();
}
this.eventManager.emit('logout');
};
Fido2UIManager.prototype.destroy = function() {
if (this.deviceManager) {
this.deviceManager.stopAutoRefresh();
}
if (this.uiRenderer) {
this.uiRenderer.cleanup();
}
this.initialized = false;
};
Fido2UIManager.prototype.renderDeviceManagerWithRecovery = function(config) {
const recoveryConfig = Object.assign({}, config, {
recoverySessionId: config.recoverySessionId || config.recoverySessionId
});
return this.renderDeviceManager(recoveryConfig);
};
Fido2UIManager.prototype.renderFromUrlParams = function(config) {
const url = new URL(window.location.href);
const params = url.searchParams;
const regSessionId = params.get('rid');
if (regSessionId) {
return this.renderDeviceManagerWithRecovery(Object.assign({}, config, {
recoverySessionId: regSessionId
}));
} else {
return this.renderDeviceManager(config);
}
};
Fido2UIManager.prototype.startRecovery = function(recoverySessionId, displayName) {
return this.deviceManager.finishRecovery(displayName);
};
Fido2UIManager.prototype.isRecoveryMode = function() {
return this.deviceManager ? this.deviceManager.isInRecoveryMode() : false;
};
Fido2UIManager.prototype.getRecoveryUser = function() {
return this.deviceManager ? this.deviceManager.getRecoveryUser() : null;
};
Fido2UIManager.prototype.renderLogin = function(config) {
if (!window.bootstrap) {
throw new Error('Bootstrap is required. Please include Bootstrap JS before fido2-ui-sdk.js');
}
const login = new Fido2Login(config);
login.show();
return login;
};
window.Fido2UIManager = new Fido2UIManager();
window.Fido2UIManager.renderDeviceManager = Fido2UIManager.prototype.renderDeviceManager;
window.Fido2UIManager.renderLogin = Fido2UIManager.prototype.renderLogin;
window.Fido2UIManager.renderDeviceManagerWithRecovery = Fido2UIManager.prototype.renderDeviceManagerWithRecovery;
window.Fido2UIManager.renderFromUrlParams = Fido2UIManager.prototype.renderFromUrlParams;
window.Fido2UIManager.close = function() {
if (window.Fido2UIManager && window.Fido2UIManager.close) {
window.Fido2UIManager.close();
}
};
window.Fido2UIManager.refresh = function() {
if (window.Fido2UIManager && window.Fido2UIManager.refresh) {
window.Fido2UIManager.refresh();
}
};
window.Fido2UIManager.logout = function() {
if (window.Fido2UIManager && window.Fido2UIManager.logout) {
window.Fido2UIManager.logout();
}
};
window.Fido2UIManager.destroy = function() {
if (window.Fido2UIManager && window.Fido2UIManager.destroy) {
window.Fido2UIManager.destroy();
}
};
window.Fido2UIManager.isRecoveryMode = function() {
if (window.Fido2UIManager && window.Fido2UIManager.isRecoveryMode) {
return window.Fido2UIManager.isRecoveryMode();
}
return false;
};
window.Fido2UIManager.getRecoveryUser = function() {
if (window.Fido2UIManager && window.Fido2UIManager.getRecoveryUser) {
return window.Fido2UIManager.getRecoveryUser();
}
return null;
};
window.Fido2UIManager.startRecovery = function(recoverySessionId, displayName) {
if (window.Fido2UIManager && window.Fido2UIManager.startRecovery) {
return window.Fido2UIManager.startRecovery(recoverySessionId, displayName);
}
return Promise.reject(new Error('Recovery not initialized'));
};
console.log('FIDO2 UI SDK v' + FIDO2_UI_VERSION + ' loaded');
})(window);