(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: 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 ` `; }; 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 += ` `; html += `
`; html += ` `; html += `
`; html += `
`; if (features.enablePasswordLogin) { html += `
${this.i18n.getText('link_use_password')}
`; } if (features.showRemainingAttempts && this.mode === LoginMode.PASSWORD) { const remaining = this.maxAttempts - this.attemptCount; html += `
${this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining)}
`; } 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 = `${this.i18n.getText('msg_remaining_attempts').replace('{n}', remaining)}`; 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 ` `; }; 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 += `
${this.i18n.getText('title_welcome')}: ${userId}
`; } if (features.showAddButton) { html += ` `; } html += `
${features.showDeleteButton ? `` : ''}
${this.i18n.getText('title_device')} ${this.i18n.getText('title_time')}${this.i18n.getText('title_act')}
${this.i18n.getText('title_empty_list')}
`; 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 = ` ${this.i18n.getText('title_empty_list')} `; 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 += ` ${desc} ${date} ${this.config.features.showDeleteButton ? ` ${this.i18n.getText('title_del')} ` : ''} `; }); 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);