1896 lines
62 KiB
JavaScript
1896 lines
62 KiB
JavaScript
(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,
|
||
},
|
||
callbacks: {},
|
||
rpId: null,
|
||
autoRefresh: true,
|
||
refreshInterval: 5000,
|
||
};
|
||
|
||
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'
|
||
},
|
||
'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'
|
||
}
|
||
};
|
||
|
||
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: error.message || this.i18n.getText('msg_fido2_failed'),
|
||
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._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;
|
||
}
|
||
|
||
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) {
|
||
throw new Error('No user ID available. Please login first.');
|
||
}
|
||
|
||
const validation = this.validateUserId();
|
||
if (!validation.valid) {
|
||
throw new Error(validation.error);
|
||
}
|
||
|
||
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 {
|
||
throw new Error(result.errorMessage || 'Failed to add device');
|
||
}
|
||
} catch (error) {
|
||
this.eventManager.emit('error', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
DeviceManager.prototype.deleteDevice = async function(deviceId) {
|
||
try {
|
||
const effectiveUserId = this.getEffectiveUserId();
|
||
if (!effectiveUserId) {
|
||
throw new Error('No user ID available. Please login first.');
|
||
}
|
||
|
||
const validation = this.validateUserId();
|
||
if (!validation.valid) {
|
||
throw new Error(validation.error);
|
||
}
|
||
|
||
const result = await delUserDeviceFido2(deviceId, this.config.rpId);
|
||
|
||
if (result.status === 'ok') {
|
||
await this.loadDevices();
|
||
this.eventManager.emit('deviceDeleted', deviceId);
|
||
return result;
|
||
} else {
|
||
throw new Error(result.errorMessage || 'Failed to delete device');
|
||
}
|
||
} catch (error) {
|
||
this.eventManager.emit('error', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
DeviceManager.prototype.getUserId = function() {
|
||
return this.getEffectiveUserId();
|
||
};
|
||
|
||
DeviceManager.prototype.checkSession = async function() {
|
||
try {
|
||
// Simply validate session with server, same as devices.html does
|
||
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.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;
|
||
|
||
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">${this.i18n.getText('my_devices')}</h5>
|
||
${this.config.features.showSessionStatus ? `<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">
|
||
<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 || '');
|
||
|
||
let html = '';
|
||
|
||
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');
|
||
});
|
||
}
|
||
|
||
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.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._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(error.message || self.i18n.getText('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(error.message || self.i18n.getText('error'));
|
||
}
|
||
});
|
||
|
||
this.eventManager.on('deviceListLoaded', (devices) => {
|
||
self.uiRenderer.updateDevicesList(devices);
|
||
});
|
||
|
||
this.eventManager.on('sessionStatusChanged', (isValid) => {
|
||
self.uiRenderer.updateSessionStatus(isValid);
|
||
});
|
||
|
||
this.eventManager.on('error', (error) => {
|
||
console.error('FIDO2 SDK Error:', error);
|
||
});
|
||
};
|
||
|
||
Fido2UIManager.prototype._loadInitialData = async function() {
|
||
try {
|
||
await this.deviceManager.loadDevices();
|
||
await this.deviceManager.checkSession();
|
||
} catch (error) {
|
||
console.error('Initial data load error:', error);
|
||
}
|
||
};
|
||
|
||
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.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.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();
|
||
}
|
||
};
|
||
|
||
console.log('FIDO2 UI SDK v' + FIDO2_UI_VERSION + ' loaded');
|
||
})(window);
|