diff --git a/files/fido2-ui-sdk.js b/files/fido2-ui-sdk.js index 4eca816..3affe6e 100644 --- a/files/fido2-ui-sdk.js +++ b/files/fido2-ui-sdk.js @@ -32,90 +32,1074 @@ 'my_devices': { 'en-US': 'My devices', 'zh-CN': '我的设备', - 'ja': 'マイデバイス' + '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': 'デバイスを追加' + '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': 'デバイス' + 'ja': 'デバイス', + 'es': 'Dispositivo', + 'de': 'Gerät', + 'fr': 'Appareil', + 'pt': 'Dispositivo', + 'ko': '장치', + 'ru': 'Устройство', + 'it': 'Dispositivo' }, 'title_time': { 'en-US': 'Registered time', 'zh-CN': '添加时间', - 'ja': '登録時間' + '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': '操作' + 'ja': '操作', + 'es': 'Acciones', + 'de': 'Aktionen', + 'fr': 'Actions', + 'pt': 'Ações', + 'ko': '작업', + 'ru': 'Действия', + 'it': 'Azioni' }, 'title_del': { 'en-US': 'Delete', 'zh-CN': '删除', - 'ja': '削除' + 'ja': '削除', + 'es': 'Eliminar', + 'de': 'Löschen', + 'fr': 'Supprimer', + 'pt': 'Excluir', + 'ko': '삭제', + 'ru': 'Удалить', + 'it': 'Elimina' }, 'title_logout': { 'en-US': 'Log out', 'zh-CN': '登出', - 'ja': 'ログアウト' + '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': 'デバイスがなし、追加してください。' + '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': 'デバイス登録完了' + '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': 'デバイスを削除しました' + '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': 'デバイスを削除しますか?' + '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セッションは正常です' + '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セッションは無効です' + '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': '閉じる' + 'ja': '閉じる', + 'es': 'Cerrar', + 'de': 'Schließen', + 'fr': 'Fermer', + 'pt': 'Fechar', + 'ko': '닫기', + 'ru': 'Закрыть', + 'it': 'Chiudi' }, 'title_welcome': { 'en-US': 'Welcome', 'zh-CN': '欢迎', - 'ja': 'ようこそ' + 'ja': 'ようこそ', + 'es': 'Bienvenido', + 'de': 'Willkommen', + 'fr': 'Bienvenue', + 'pt': 'Bem-vindo', + 'ko': '환영합니다', + 'ru': 'Добро пожаловать', + 'it': 'Benvenuto' }, 'btn_login': { 'en-US': 'Login', 'zh-CN': '重新登录', - 'ja': 'ログイン' + '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': 'セッション切れ、再ログインしてください' + '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: { + onFido2Success: null, + onFido2Error: 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 modal = document.createElement('div'); + modal.className = 'modal fade fido2-sdk-login-modal'; + modal.id = 'fido2LoginModal'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + + modal.innerHTML = this._getModalHTML(); + container.appendChild(modal); + this.modalElement = modal; + this.containerElement = container; + + this._debugLog('[Fido2Login] Modal element created'); + + 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() { + const theme = this.config.theme; + const features = this.config.features; + + return ` + + `; + }; + + Fido2Login.prototype._getBodyHTML = function() { + 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('#fido2MainBtn'); + if (mainBtn) { + mainBtn.addEventListener('click', () => { + if (self.mode === LoginMode.FIDO2) { + self._handleFido2Login(); + } else { + self._handlePasswordLogin(); + } + }); + } + + const toggleLink = container.querySelector('#fido2ToggleModeLink'); + if (toggleLink) { + toggleLink.addEventListener('click', () => { + this._debugLog('[Fido2Login] Toggle link clicked, current mode:', this.mode); + self._toggleMode(); + }); + } + + const userIdInput = container.querySelector('#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('#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', result.username, result.session); + this._closeUI(); + } else { + this._emit('fido2Error', { + 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', { + code: 'CANCELED', + message: this.i18n.getText('msg_fido2_canceled'), + originalError: error + }); + } else { + this._emit('fido2Error', { + 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(); + + 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', userId, 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('#fido2PasswordSection'); + const mainBtn = container.querySelector('#fido2MainBtn'); + const toggleLink = container.querySelector('#fido2ToggleModeLink'); + const titleEl = document.getElementById('fido2LoginModalTitle'); + const remainingEl = container.querySelector('#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 input = document.getElementById('fido2UserId'); + return input ? input.value.trim() : ''; + }; + + Fido2Login.prototype._getPasswordInput = function() { + const input = document.getElementById('fido2Password'); + return input ? input.value : ''; + }; + + Fido2Login.prototype._showError = function(message) { + const errorEl = document.getElementById('fido2LoginError'); + const hintEl = document.getElementById('fido2LoginHint'); + if (hintEl) hintEl.style.display = 'none'; + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + }; + + Fido2Login.prototype._hideError = function() { + const errorEl = document.getElementById('fido2LoginError'); + if (errorEl) errorEl.style.display = 'none'; + }; + + Fido2Login.prototype._showHint = function(message) { + const hintEl = document.getElementById('fido2LoginHint'); + if (hintEl) { + hintEl.textContent = message; + hintEl.style.display = 'block'; + } + }; + + Fido2Login.prototype._hideHint = function() { + const hintEl = document.getElementById('fido2LoginHint'); + if (hintEl) hintEl.style.display = 'none'; + }; + + Fido2Login.prototype._startLoading = function() { + const mainBtn = document.getElementById('fido2MainBtn'); + if (mainBtn) { + mainBtn.disabled = true; + mainBtn.dataset.originalText = mainBtn.textContent; + mainBtn.textContent = '...'; + } + }; + + Fido2Login.prototype._stopLoading = function() { + const mainBtn = document.getElementById('fido2MainBtn'); + 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('#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, ...args) { + const callback = this.config.callbacks['on' + event.charAt(0).toUpperCase() + event.slice(1)]; + if (typeof callback === 'function') { + callback.apply(this, args); + } + }; + + 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', result.username, result.session); + this._closeUI(); + } else { + this.state = LoginState.FIDO2; + this._emit('fido2Error', { + 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', { + code: 'CANCELED', + message: this.i18n.getText('msg_fido2_canceled'), + originalError: error + }); + } else { + this.state = LoginState.FIDO2; + this._emit('fido2Error', { + 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', { + 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('#fido2PasswordSection'); + const mainBtn = container.querySelector('#fido2MainBtn'); + const toggleLink = container.querySelector('#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('#fido2PasswordSection'); + const mainBtn = container.querySelector('#fido2MainBtn'); + const toggleLink = container.querySelector('#fido2ToggleModeLink'); + const titleEl = document.getElementById('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(); @@ -240,6 +1224,35 @@ 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); @@ -259,13 +1272,17 @@ DeviceManager.prototype.addDevice = async function(displayName) { try { - const sessionData = this.getSessionData(); - if (!sessionData) { - throw new Error('No session data available'); + const effectiveUserId = this.getEffectiveUserId(); + if (!effectiveUserId) { + throw new Error('No user ID available. Please login first.'); } - const userId = sessionData.uid; - const result = await registerFido2(userId, displayName || 'Device-' + userId, this.config.rpId); + 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') { await this.loadDevices(); @@ -282,6 +1299,16 @@ 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') { @@ -297,27 +1324,31 @@ } }; - DeviceManager.prototype.getSessionData = function() { - try { - const sessionText = sessionStorage.getItem('fido2_user_session'); - if (!sessionText) return null; - - return JSON.parse(sessionText); - } catch (error) { - return null; - } - }; - DeviceManager.prototype.getUserId = function() { - const sessionData = this.getSessionData(); - return sessionData ? sessionData.uid : null; + return this.getEffectiveUserId(); }; DeviceManager.prototype.checkSession = async function() { try { - this.sessionStatus = await validSession(this.config.rpId); - this.eventManager.emit('sessionStatusChanged', this.sessionStatus); - return this.sessionStatus; + const sessionUserId = this.getSessionUserId(); + + if (!sessionUserId) { + this.sessionStatus = false; + this.eventManager.emit('sessionStatusChanged', false); + return false; + } + + const validation = this.validateUserId(); + if (!validation.valid) { + this.sessionStatus = false; + this.eventManager.emit('sessionStatusChanged', false); + this.eventManager.emit('userMismatch', validation.error); + return false; + } + + this.sessionStatus = true; + this.eventManager.emit('sessionStatusChanged', true); + return true; } catch (error) { this.sessionStatus = false; this.eventManager.emit('sessionStatusChanged', false); @@ -444,7 +1475,8 @@ UIRenderer.prototype._getBodyHTML = function() { const features = this.config.features; - const userId = window.Fido2UIManager.deviceManager ? window.Fido2UIManager.deviceManager.getUserId() : ''; + const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null; + const userId = dm ? dm.getEffectiveUserId() : (this.config.userId || ''); let html = ''; @@ -454,13 +1486,6 @@ `; } - html += ``; - if (features.showAddButton) { html += ` - ${this.config.features.showAddButton ? ` + + + + +
+

📌 Step2: Device Management

+

Step1: Login first, then manage your FIDO2 devices

+ - -
- -
-

💻 Code Examples

-

Simplest integration:

-
- Fido2UIManager.renderDeviceManager({ - container: '#device-container', - mode: 'modal', - serverUrl: SERVER_URL -}); -
- -

With theme customization:

-
- Fido2UIManager.renderDeviceManager({ - container: '#device-container', - mode: 'modal', - serverUrl: SERVER_URL, - theme: { - primaryColor: '#ce59d9', - backgroundColor: '#f8f9fa' - }, - language: 'zh-CN' -}); -
-
- -
-

✨ Features

- +

Please login first to access device management

@@ -425,12 +507,74 @@ Clear Log
+ +
+

💻 Code Examples

+

Simplest integration:

+
+ Fido2UIManager.renderLogin({ + container: '#login-container', + mode: 'modal', + serverUrl: SERVER_URL +}); +
+ +

With theme customization:

+
+ Fido2UIManager.renderLogin({ + container: '#login-container', + mode: 'modal', + serverUrl: SERVER_URL, + language: 'ja', + theme: { + primaryColor: '#ce59d9', + backgroundColor: '#faf5ff' + }, + features: { + enablePasswordLogin: true, + autoShowPassword: true + } +}); +
+ +

Device Manager:

+
+ Fido2UIManager.renderDeviceManager({ + userId: currentUserId, + container: '#device-container', + mode: 'modal', + serverUrl: SERVER_URL, + language: 'ja' +}); +
+
+ +
+

✨ Features

+ +
+ +
+

+ + https://amipro.me/fido2_top.html + +

diff --git a/standalone-demo.html b/standalone-demo.html index da0fa58..539e891 100644 --- a/standalone-demo.html +++ b/standalone-demo.html @@ -442,7 +442,7 @@