diff --git a/files/dfido2-lib.js b/files/dfido2-lib.js
index e1cb5e2..f4a68be 100644
--- a/files/dfido2-lib.js
+++ b/files/dfido2-lib.js
@@ -1,3 +1,12 @@
+/**
+ *
+ * @file dfido2-lib.js
+ * @description FIDO2 library of amipro FIDO2 Server
+ * @version 2025-12-12
+ * @author Amipro Co., Ltd. (https://www.amipro.me/)
+ * @license Copyright (c) Amipro Co., Ltd. All rights reserved.
+ */
+
const DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION = 'fido2_user_session'
const DFIDO2_LIB_LOCALSTG_NAME_REGISTERED = 'dfido2_lib_registered'
const DFIDO2_LIB_LOCALSTG_NAME_SVR_URL = 'dfido2_lib_svr_url'
diff --git a/files/fido2-ui-sdk.js b/files/fido2-ui-sdk.js
index 42cf6d2..06cec11 100644
--- a/files/fido2-ui-sdk.js
+++ b/files/fido2-ui-sdk.js
@@ -1,3 +1,12 @@
+/**
+ *
+ * @file fido2-ui-sdk.js
+ * @description FIDO2 UI SDK of amipro FIDO2 Server
+ * @version 2025-12-12
+ * @author Amipro Co., Ltd. (https://www.amipro.me/)
+ * @license Copyright (c) Amipro Co., Ltd. All rights reserved.
+ */
+
(function(window) {
'use strict';
@@ -34,11 +43,13 @@
showDeleteButton: true,
showUserInfo: true,
showSessionStatus: true,
+ showRecoveryButton: true,
},
callbacks: {},
rpId: null,
autoRefresh: true,
refreshInterval: 5000,
+ recoverySessionId: null,
};
const DEFAULT_I18N = {
@@ -449,6 +460,78 @@
'ko': '비밀번호를 입력하세요',
'ru': 'Пожалуйста, введите пароль',
'it': 'Per favore inserisci password'
+ },
+ 'title_recovery': {
+ 'en-US': 'Device Recovery',
+ 'zh-CN': '设备恢复',
+ 'ja': 'デバイス回復',
+ 'es': 'Recuperación de dispositivo',
+ 'de': 'Gerätewiederherstellung',
+ 'fr': 'Récupération de dispositif',
+ 'pt': 'Recuperação de dispositivo',
+ 'ko': '장치 복구',
+ 'ru': 'Восстановление устройства',
+ 'it': 'Ripristino dispositivo'
+ },
+ 'msg_recovery_success': {
+ 'en-US': 'Device recovered successfully',
+ 'zh-CN': '设备恢复成功',
+ 'ja': 'デバイス回復完了',
+ 'es': 'Dispositivo recuperado exitosamente',
+ 'de': 'Gerät erfolgreich wiederhergestellt',
+ 'fr': 'Appareil récupéré avec succès',
+ 'pt': 'Dispositivo recuperado com sucesso',
+ 'ko': '장치 복구 완료',
+ 'ru': 'Устройство успешно восстановлено',
+ 'it': 'Dispositivo ripristinato con successo'
+ },
+ 'msg_recovery_failed': {
+ 'en-US': 'Recovery failed',
+ 'zh-CN': '恢复失败',
+ 'ja': '回復失敗',
+ 'es': 'Recuperación fallida',
+ 'de': 'Wiederherstellung fehlgeschlagen',
+ 'fr': 'Échec de la récupération',
+ 'pt': 'Falha na recuperação',
+ 'ko': '복구 실패',
+ 'ru': 'Ошибка восстановления',
+ 'it': 'Ripristino fallito'
+ },
+ 'msg_recovery_user_found': {
+ 'en-US': 'Recovery session verified for user: {user}',
+ 'zh-CN': '已验证恢复会话,用户: {user}',
+ 'ja': 'ユーザー {user} の回復セッションが確認されました',
+ 'es': 'Sesión de recuperación verificada para el usuario: {user}',
+ 'de': 'Wiederherstellungssitzung für Benutzer verifiziert: {user}',
+ 'fr': 'Session de récupération vérifiée pour l\'utilisateur: {user}',
+ 'pt': 'Sessão de recuperação verificada para o usuário: {user}',
+ 'ko': '사용자 {user}의 복구 세션 확인됨',
+ 'ru': 'Сессия восстановления подтверждена для пользователя: {user}',
+ 'it': 'Sessione di ripristino verificata per l\'utente: {user}'
+ },
+ 'btn_start_recovery': {
+ 'en-US': 'Start Recovery',
+ 'zh-CN': '开始恢复',
+ 'ja': '回復を開始',
+ 'es': 'Iniciar recuperación',
+ 'de': 'Wiederherstellung starten',
+ 'fr': 'Démarrer la récupération',
+ 'pt': 'Iniciar recuperação',
+ 'ko': '복구 시작',
+ 'ru': 'Начать восстановление',
+ 'it': 'Avvia ripristino'
+ },
+ 'btn_cancel': {
+ 'en-US': 'Cancel',
+ 'zh-CN': '取消',
+ 'ja': 'キャンセル',
+ 'es': 'Cancelar',
+ 'de': 'Abbrechen',
+ 'fr': 'Annuler',
+ 'pt': 'Cancelar',
+ 'ko': '취소',
+ 'ru': 'Отмена',
+ 'it': 'Annulla'
}
};
@@ -1286,6 +1369,8 @@
this.devices = [];
this.refreshTimer = null;
this.sessionStatus = false;
+ this.recoveryMode = false;
+ this.recoveryUser = null;
}
DeviceManager.prototype.getSessionUserId = function() {
@@ -1395,7 +1480,6 @@
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);
@@ -1407,6 +1491,112 @@
}
};
+ DeviceManager.prototype.getRegistrationUser = async function(regSessionId) {
+ try {
+ const response = await fetch(getServerUrl() + '/reg/username', {
+ method: 'POST',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ session_id: regSessionId })
+ });
+ const resp = await response.json();
+ return resp;
+ } catch (err) {
+ console.log(err);
+ return { status: 'failed', errorMessage: err.message };
+ }
+ };
+
+ DeviceManager.prototype.startRecovery = async function(recoverySessionId) {
+ try {
+ if (!recoverySessionId) {
+ throw new Error('Recovery session ID is required');
+ }
+
+ const result = await this.getRegistrationUser(recoverySessionId);
+
+ if (result && result.status === 'ok' && result.username) {
+ this.recoveryMode = true;
+ this.recoveryUser = {
+ username: result.username,
+ displayName: result.displayname || result.username,
+ sessionId: recoverySessionId
+ };
+
+ this.config.userId = result.username;
+ this.eventManager.emit('recoveryStarted', this.recoveryUser);
+ return {
+ success: true,
+ user: this.recoveryUser
+ };
+ } else {
+ throw new Error(result.errorMessage || 'Invalid recovery session');
+ }
+ } catch (error) {
+ this.eventManager.emit('error', error);
+ throw error;
+ }
+ };
+
+ DeviceManager.prototype.finishRecovery = async function(displayName) {
+ try {
+ if (!this.recoveryMode || !this.recoveryUser) {
+ throw new Error('Not in recovery mode');
+ }
+
+ const effectiveDisplayName = displayName || this.recoveryUser.displayName || 'RecoveredDevice-' + this.recoveryUser.username;
+
+ const result = await registerFido2(this.recoveryUser.username, effectiveDisplayName, this.config.rpId);
+
+ if (result.status === 'ok') {
+ const wasRecoveryMode = this.recoveryMode;
+ const recoveredUser = this.recoveryUser;
+
+ this.recoveryMode = false;
+ this.recoveryUser = null;
+
+ await this.loadDevices();
+ this.eventManager.emit('deviceAdded', result);
+ this.eventManager.emit('recoveryCompleted', {
+ user: recoveredUser,
+ device: result
+ });
+
+ return {
+ success: true,
+ user: recoveredUser,
+ device: result
+ };
+ } else {
+ throw new Error(result.errorMessage || 'Failed to add device during recovery');
+ }
+ } catch (error) {
+ this.eventManager.emit('error', error);
+ throw error;
+ }
+ };
+
+ DeviceManager.prototype.cancelRecovery = function() {
+ if (this.recoveryMode) {
+ const canceledUser = this.recoveryUser;
+ this.recoveryMode = false;
+ this.recoveryUser = null;
+ this.eventManager.emit('recoveryCanceled', canceledUser);
+ return true;
+ }
+ return false;
+ };
+
+ DeviceManager.prototype.isInRecoveryMode = function() {
+ return this.recoveryMode;
+ };
+
+ DeviceManager.prototype.getRecoveryUser = function() {
+ return this.recoveryUser;
+ };
+
DeviceManager.prototype.parseDeviceDescription = function(device) {
if (device.desc && device.desc.length > 0) {
return device.desc;
@@ -1510,20 +1700,24 @@
UIRenderer.prototype._getModalHTML = function(uniqueId, sessionStatusId, addBtnId, devicesListId) {
const theme = this.config.theme;
+ const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
+ const isRecoveryMode = dm ? dm.isInRecoveryMode() : false;
+ const modalTitle = isRecoveryMode ? this.i18n.getText('title_recovery') : this.i18n.getText('my_devices');
return `
${this._getBodyHTML(uniqueId, addBtnId, devicesListId)}
@@ -1535,41 +1729,57 @@
const features = this.config.features;
const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null;
const userId = dm ? dm.getEffectiveUserId() : (this.config.userId || '');
+ const isRecoveryMode = dm ? dm.isInRecoveryMode() : false;
+ const recoveryUser = dm ? dm.getRecoveryUser() : null;
let html = '';
- if (features.showUserInfo && userId) {
- html += `
- ${this.i18n.getText('title_welcome')}: ${userId}
-
`;
- }
-
- if (features.showAddButton) {
+ if (isRecoveryMode && recoveryUser) {
html += `
-