Files
sample-site/files/fido2-ui-sdk.js
2026-01-18 21:48:19 +09:00

2031 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

(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;
};
UIRenderer.prototype.renderStandalone = function() {
if (this.config.container) {
const container = typeof this.config.container === 'string'
? document.querySelector(this.config.container)
: this.config.container;
if (container) {
container.innerHTML = this._getStandaloneBodyHTML();
this.containerElement = container;
this.themeManager.applyTheme(container);
this._bindEvents();
return container;
}
}
document.body.innerHTML = this._getStandaloneBodyHTML();
document.body.className = 'fido2-sdk-standalone';
this.containerElement = document.body;
this.themeManager.applyTheme(document.body);
this._bindEvents();
return document.body;
};
UIRenderer.prototype._getStandaloneBodyHTML = function() {
const theme = this.config.theme;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
const userId = dm ? dm.getEffectiveUserId() : (this.config.userId || '');
return `
<div class="container fido2-sdk-container">
<div class="card fido2-sdk-card">
<div class="card-header fido2-sdk-header d-flex justify-content-between align-items-center">
<div>
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo me-2" alt="Logo">` : ''}
<h4 class="mb-0 fido2-sdk-text">${this.i18n.getText('my_devices')}</h4>
</div>
${this.config.features.showSessionStatus ? '<span class="badge fido2-sdk-status-badge" id="fido2SessionStatus"></span>' : ''}
</div>
<div class="card-body fido2-sdk-body">
${this.config.features.showUserInfo && userId ? `
<div class="alert alert-info fido2-sdk-user-info fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>
` : ''}
${this.config.features.showAddButton ? `
<button type="button" class="btn btn-info mt-2 mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2AddDeviceBtn">
${this.i18n.getText('btn_add')}
</button>
` : ''}
<div class="table-responsive mt-2">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${this.config.features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="fido2DevicesList">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
};
UIRenderer.prototype._getStandaloneHTML = function() {
const theme = this.config.theme;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
const userId = dm ? dm.getEffectiveUserId() : (this.config.userId || '');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this.i18n.getText('my_devices')}</title>
<link rel="stylesheet" href="files/bootstrap.css">
<link rel="stylesheet" href="files/boxicons.css">
<link rel="stylesheet" href="files/fido2-ui-sdk.css">
</head>
<body class="fido2-sdk-standalone">
<div class="container fido2-sdk-container">
<div class="card fido2-sdk-card">
<div class="card-header fido2-sdk-header d-flex justify-content-between align-items-center">
<div>
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo me-2" alt="Logo">` : ''}
<h4 class="mb-0 fido2-sdk-text">${this.i18n.getText('my_devices')}</h4>
</div>
${this.config.features.showSessionStatus ? '<span class="badge fido2-sdk-status-badge" id="fido2SessionStatus"></span>' : ''}
</div>
<div class="card-body fido2-sdk-body">
${this.config.features.showUserInfo && userId ? `
<div class="alert alert-info fido2-sdk-user-info fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>
` : ''}
${this.config.features.showAddButton ? `
<button type="button" class="btn btn-info mt-2 mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2AddDeviceBtn">
${this.i18n.getText('btn_add')}
</button>
` : ''}
<div class="table-responsive mt-2">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${this.config.features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="fido2DevicesList">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
`;
};
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;
if (mode === 'standalone') {
manager.uiRenderer.renderStandalone();
} else {
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);