diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6cc14cf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,322 @@ +# FIDO2 UI SDK + +一个简单易用的 FIDO2 设备管理 UI 库,支持快速集成 FIDO2 认证设备管理功能。 + +## 特性 + +- 🚀 **快速集成** - 一行代码即可集成设备管理功能 +- 🎨 **主题定制** - 支持自定义 Logo、颜色、背景等 +- 🌐 **国际化** - 内置中文、英文、日文支持 +- 📱 **响应式** - 支持桌面端和移动端 +- 🎭 **双模式** - 支持弹出窗口和独立页面两种模式 +- 🎯 **事件回调** - 完整的事件系统,灵活处理各种操作 +- ♿ **向后兼容** - 完全兼容现有的对接方式 + +## 快速开始 + +### 1. 引入依赖 + +```html + + + + + + + + +``` + +### 2. 添加容器(仅模态框模式需要) + +```html +
+``` + +### 3. 调用 SDK + +```javascript +Fido2UIManager.renderDeviceManager({ + container: '#device-container', + mode: 'modal', + serverUrl: 'https://fido2.amipro.me' +}); +``` + +## 使用模式 + +### 模式 1: 弹出窗口 (推荐) + +弹出窗口模式在当前页面上显示模态框,用户操作完成后自动关闭。 + +```javascript +Fido2UIManager.renderDeviceManager({ + container: '#device-container', + mode: 'modal', + serverUrl: 'https://fido2.amipro.me' +}); +``` + +**适用场景:** +- 用户在设置页面需要管理设备 +- 不希望离开当前页面 +- 快速查看和管理设备 + +### 模式 2: 独立页面 + +独立页面模式会渲染完整的设备管理页面,适合作为独立的页面功能。 + +```javascript +Fido2UIManager.renderDeviceManager({ + mode: 'standalone', + serverUrl: 'https://fido2.amipro.me' +}); +``` + +**适用场景:** +- 需要独立的设备管理页面 +- 通过链接直接访问设备管理 +- 需要更多屏幕空间显示设备信息 + +## 配置选项 + +```javascript +Fido2UIManager.renderDeviceManager({ + // === 核心配置 === + serverUrl: 'https://fido2.amipro.me', // FIDO2 服务器地址 + mode: 'modal', // 'modal' | 'standalone' + + // === 容器配置 (仅 modal 模式需要) === + container: '#device-container', // 容器选择器或 DOM 元素 + + // === 主题定制 === + theme: { + logo: 'path/to/logo.png', // Logo URL + primaryColor: '#ce59d9', // 主色调 + backgroundColor: '#ffffff', // 背景色 + textColor: '#333333', // 文字颜色 + borderRadius: '8px', // 圆角 + }, + + // === 国际化 === + language: 'zh-CN', // 'en-US' | 'zh-CN' | 'ja' + customI18n: { // 自定义翻译 + 'my_devices': { 'zh-CN': '我的设备' }, + // ... + }, + + // === 功能开关 === + features: { + showAddButton: true, + showDeleteButton: true, + showUserInfo: true, + showSessionStatus: true, + }, + + // === 事件回调 === + callbacks: { + onInit: () => {}, // 初始化完成 + onDeviceAdded: (device) => {}, // 设备添加成功 + onDeviceDeleted: (deviceId) => {}, // 设备删除成功 + onDeviceListLoaded: (devices) => {}, // 设备列表加载完成 + onError: (error) => {}, // 错误发生 + onClose: () => {}, // 窗口关闭 (仅 modal 模式) + }, + + // === 其他 === + rpId: null, // Relying Party ID + autoRefresh: true, // 自动刷新设备列表 + refreshInterval: 5000, // 刷新间隔(ms) +}); +``` + +## 完整示例 + +### 示例 1: 基础集成 + +```html + + + + + + + + + + + + + + + + +``` + +### 示例 2: 带主题定制 + +```javascript +Fido2UIManager.renderDeviceManager({ + container: '#device-container', + mode: 'modal', + serverUrl: 'https://fido2.amipro.me', + theme: { + logo: '/assets/my-logo.png', + primaryColor: '#0066cc', + backgroundColor: '#f8f9fa', + textColor: '#333333', + borderRadius: '12px' + }, + language: 'zh-CN' +}); +``` + +### 示例 3: 带事件回调 + +```javascript +Fido2UIManager.renderDeviceManager({ + container: '#device-container', + mode: 'modal', + serverUrl: 'https://fido2.amipro.me', + callbacks: { + onInit: function(manager) { + console.log('设备管理器初始化完成'); + }, + onDeviceAdded: function(device) { + console.log('设备添加成功:', device); + // 刷新用户界面或其他逻辑 + }, + onDeviceDeleted: function(deviceId) { + console.log('设备删除成功:', deviceId); + }, + onDeviceListLoaded: function(devices) { + console.log('设备列表加载完成:', devices.length, '个设备'); + }, + onError: function(error) { + console.error('发生错误:', error); + }, + onClose: function() { + console.log('窗口关闭'); + } + } +}); +``` + +### 示例 4: 自定义翻译 + +```javascript +Fido2UIManager.renderDeviceManager({ + container: '#device-container', + mode: 'modal', + serverUrl: 'https://fido2.amipro.me', + language: 'zh-CN', + customI18n: { + 'my_devices': { + 'zh-CN': '我的 FIDO2 设备' + }, + 'btn_add': { + 'zh-CN': '添加新设备' + } + } +}); +``` + +## API 方法 + +### renderDeviceManager(config) + +渲染设备管理器。 + +**参数:** +- `config` (Object) - 配置对象 + +**返回:** +- (Object) - 设备管理器实例 + +### close() + +关闭模态框(仅 modal 模式)。 + +```javascript +Fido2UIManager.close(); +``` + +### refresh() + +刷新设备列表和会话状态。 + +```javascript +Fido2UIManager.refresh(); +``` + +### destroy() + +销毁设备管理器实例。 + +```javascript +Fido2UIManager.destroy(); +``` + +## 事件系统 + +SDK 提供以下事件: + +| 事件名称 | 触发时机 | 回调参数 | +|---------|---------|---------| +| `init` | 初始化完成 | (manager) | +| `deviceAdded` | 设备添加成功 | (device) | +| `deviceDeleted` | 设备删除成功 | (deviceId) | +| `deviceListLoaded` | 设备列表加载完成 | (devices) | +| `sessionStatusChanged` | 会话状态改变 | (isValid) | +| `error` | 发生错误 | (error) | +| `close` | 窗口关闭 | 无 | + +## 向后兼容性 + +SDK 完全兼容现有的对接方式: + +- ✅ `devices.html` - 继续使用现有逻辑,无需修改 +- ✅ `login.html` - 继续使用现有逻辑,无需修改 +- ✅ `dfido2-lib.js` - 核心库保持不变 +- ✅ 所有 CSS 文件 - 保持原样 + +## 浏览器支持 + +- Chrome 67+ +- Firefox 60+ +- Safari 13+ +- Edge 18+ + +## 依赖 + +- jQuery 3.x +- Bootstrap 5.x +- dfido2-lib.js(核心 FIDO2 库) + +## 演示 + +- [模态框模式演示](modal-demo.html) +- [独立页面模式演示](standalone-demo.html) + +## 注意事项 + +1. 确保 `dfido2-lib.js` 在 `fido2-ui-sdk.js` 之前加载 +2. Modal 模式需要提供有效的容器选择器 +3. 独立页面模式会替换当前页面的内容 +4. 确保浏览器支持 WebAuthn API + +## 许可证 + +[根据您的项目许可证填写] + +## 联系方式 + +如有问题或建议,请联系 [您的联系方式] diff --git a/files/dfido2-lib.js b/files/dfido2-lib.js index bc4b590..d48598b 100644 --- a/files/dfido2-lib.js +++ b/files/dfido2-lib.js @@ -78,6 +78,9 @@ async function listUserDevicesFido2(rpId = null) { const resp = await response.json(); if ('ok' === resp.status && resp.session === session_data.session) { return {status:'ok', devices:resp.devices} + } else if (resp.errorMessage && resp.errorMessage.includes('No user session')) { + sessionStorage.removeItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION); + return {status:'ok', devices:[]} } else { return {status:'failed', errorMessage: resp.errorMessage} } @@ -374,8 +377,8 @@ function makePublicKey(attOptsResp) { rp: attOptsResp.rp, user: { id: _stringToArrayBuffer(attOptsResp.user.id), //_base64ToArrayBuffer(_fromBase64URL(attOptsResp.user.id)), - name: attOptsResp.user.name, - displayName: attOptsResp.user.displayName, + name: attOptsResp.user.name || attOptsResp.user.id || 'user', + displayName: attOptsResp.user.displayName || attOptsResp.user.name || attOptsResp.user.id || 'User', }, pubKeyCredParams: attOptsResp.pubKeyCredParams, timeout: attOptsResp.timeout, @@ -387,6 +390,10 @@ function makePublicKey(attOptsResp) { async function doAttestation(username, displayName, rpId, userVerification = 'preferred') { var process_time_limit = Number.MAX_SAFE_INTEGER + if (window._fido2_pending_request) { + return {status:'failed', errorMessage: 'Fido2LibErr106:A request is already pending'}; + } + window._fido2_pending_request = true; try { const attestationOptions = { username: username, @@ -472,13 +479,19 @@ async function doAttestation(username, displayName, rpId, userVerification = 'pr errRtn.errCode = fido2LibErrCodes.user_canceled } }else errRtn.errCode = fido2LibErrCodes.unknown - + return errRtn; + } finally { + window._fido2_pending_request = false; } } async function doAssertion(username = null, rpId = null, userVerification = 'preferred') { var process_time_limit = Number.MAX_SAFE_INTEGER + if (window._fido2_pending_request) { + return {status:'failed', errorMessage: 'Fido2LibErr106:A request is already pending'}; + } + window._fido2_pending_request = true; try { let authnOptions; /* @@ -584,8 +597,10 @@ async function doAssertion(username = null, rpId = null, userVerification = 'pre errRtn.errCode = fido2LibErrCodes.user_canceled } }else errRtn.errCode = fido2LibErrCodes.unknown - + return errRtn; + } finally { + window._fido2_pending_request = false; } } diff --git a/files/fido2-ui-sdk.css b/files/fido2-ui-sdk.css new file mode 100644 index 0000000..67905ff --- /dev/null +++ b/files/fido2-ui-sdk.css @@ -0,0 +1,276 @@ +.fido2-sdk-modal .modal-dialog { + max-width: 800px; +} + +.fido2-sdk-modal .modal-content { + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); +} + +.fido2-sdk-card { + transition: all 0.3s ease; +} + +.fido2-sdk-header { + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + display: flex; + align-items: center; + gap: 12px; +} + +.fido2-sdk-logo { + max-height: 40px; + width: auto; +} + +.fido2-sdk-container { + padding: 24px; +} + +.fido2-sdk-body { + padding: 24px; +} + +.fido2-sdk-text { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.fido2-sdk-table { + width: 100%; + margin-top: 16px; +} + +.fido2-sdk-table th { + font-weight: 600; + color: #333; + padding: 12px 16px; + border-bottom: 2px solid #dee2e6; +} + +.fido2-sdk-table td { + padding: 12px 16px; + vertical-align: middle; +} + +.fido2-sdk-btn { + padding: 10px 24px; + font-weight: 500; + border: none; + border-radius: 6px; + transition: all 0.2s ease; + cursor: pointer; +} + +.fido2-sdk-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.fido2-sdk-btn:active { + transform: translateY(0); +} + +.fido2-sdk-btn-primary { + color: white !important; +} + +.fido2-sdk-status-badge { + font-size: 12px; + padding: 6px 12px; + border-radius: 20px; + font-weight: 500; +} + +.fido2-sdk-user-info { + padding: 12px 16px; + border-radius: 6px; + background-color: #e7f1ff; + border-left: 4px solid #0d6efd; +} + +.fido2-sdk-standalone { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 40px 20px; +} + +.fido2-sdk-standalone .container { + max-width: 1000px; +} + +.fido2-sdk-standalone .card { + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + border: none; +} + +.fido2-sdk-standalone .card-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 2px solid #dee2e6; +} + +.fido2-sdk-standalone .card-body { + background-color: #ffffff; +} + +.fido2-sdk-table .text-danger { + color: #dc3545 !important; + text-decoration: none; + transition: all 0.2s ease; + white-space: nowrap; +} + +.fido2-sdk-table .text-danger:hover { + color: #c82333 !important; + text-decoration: underline; +} + +.fido2-sdk-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +@media (max-width: 768px) { + .fido2-sdk-modal .modal-dialog { + margin: 10px; + max-width: calc(100% - 20px); + } + + .fido2-sdk-standalone { + padding: 20px 10px; + } + + .fido2-sdk-table th, + .fido2-sdk-table td { + padding: 8px 12px; + font-size: 14px; + } + + .fido2-sdk-btn { + padding: 8px 16px; + font-size: 14px; + } + + .fido2-sdk-logo { + max-height: 30px; + } +} + +.fido2-sdk-loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: fido2-sdk-spin 1s ease-in-out infinite; + margin-right: 8px; +} + +@keyframes fido2-sdk-spin { + to { transform: rotate(360deg); } +} + +.fido2-sdk-device-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #e7f1ff; + color: #0d6efd; + margin-right: 8px; +} + +.fido2-sdk-empty-state { + text-align: center; + padding: 40px 20px; + color: #6c757d; +} + +.fido2-sdk-empty-state i { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.fido2-sdk-empty-state p { + margin: 0; + font-size: 16px; +} + +.fido2-sdk-tooltip { + position: relative; + display: inline-block; + cursor: help; +} + +.fido2-sdk-tooltip::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 8px 12px; + background-color: #333; + color: #fff; + font-size: 12px; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; + z-index: 1000; +} + +.fido2-sdk-tooltip:hover::after { + opacity: 1; + visibility: visible; +} + +.fido2-sdk-btn-group { + display: flex; + gap: 8px; +} + +.fido2-sdk-btn-group .btn { + flex: 1; +} + +#fido2AddDeviceBtn { + margin-top: 12px; +} + +.fido2-sdk-alert { + padding: 12px 16px; + border-radius: 6px; + margin-bottom: 16px; + border-left: 4px solid; +} + +.fido2-sdk-alert-success { + background-color: #d4edda; + border-color: #28a745; + color: #155724; +} + +.fido2-sdk-alert-error { + background-color: #f8d7da; + border-color: #dc3545; + color: #721c24; +} + +.fido2-sdk-alert-info { + background-color: #d1ecf1; + border-color: #17a2b8; + color: #0c5460; +} + +.fido2-sdk-alert-warning { + background-color: #fff3cd; + border-color: #ffc107; + color: #856404; +} diff --git a/files/fido2-ui-sdk.js b/files/fido2-ui-sdk.js new file mode 100644 index 0000000..4eca816 --- /dev/null +++ b/files/fido2-ui-sdk.js @@ -0,0 +1,963 @@ +(function(window) { + 'use strict'; + + 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': 'マイデバイス' + }, + 'btn_add': { + 'en-US': 'Add device', + 'zh-CN': '添加设备', + 'ja': 'デバイスを追加' + }, + 'title_device': { + 'en-US': 'Device', + 'zh-CN': '设备', + 'ja': 'デバイス' + }, + 'title_time': { + 'en-US': 'Registered time', + 'zh-CN': '添加时间', + 'ja': '登録時間' + }, + 'title_act': { + 'en-US': 'Actions', + 'zh-CN': '操作', + 'ja': '操作' + }, + 'title_del': { + 'en-US': 'Delete', + 'zh-CN': '删除', + 'ja': '削除' + }, + 'title_logout': { + 'en-US': 'Log out', + 'zh-CN': '登出', + 'ja': 'ログアウト' + }, + 'title_empty_list': { + 'en-US': 'No devices, please add.', + 'zh-CN': '无设备,请添加。', + 'ja': 'デバイスがなし、追加してください。' + }, + 'msg_register_ok': { + 'en-US': 'Device registered successfully', + 'zh-CN': '添加设备成功', + 'ja': 'デバイス登録完了' + }, + 'msg_deldev_ok': { + 'en-US': 'Device deleted successfully', + 'zh-CN': '设备删除成功', + 'ja': 'デバイスを削除しました' + }, + 'msg_confirm_deldev': { + 'en-US': 'Do you want to delete this device?', + 'zh-CN': '确认删除此设备吗?', + 'ja': 'デバイスを削除しますか?' + }, + 'msg_session_status_ok': { + 'en-US': 'FIDO2 session is valid', + 'zh-CN': 'FIDO2会话正常', + 'ja': 'FIDO2セッションは正常です' + }, + 'msg_session_status_fail': { + 'en-US': 'FIDO2 session is invalid', + 'zh-CN': 'FIDO2会话无效', + 'ja': 'FIDO2セッションは無効です' + }, + 'btn_close': { + 'en-US': 'Close', + 'zh-CN': '关闭', + 'ja': '閉じる' + }, + 'title_welcome': { + 'en-US': 'Welcome', + 'zh-CN': '欢迎', + 'ja': 'ようこそ' + }, + 'btn_login': { + 'en-US': 'Login', + 'zh-CN': '重新登录', + 'ja': 'ログイン' + }, + 'msg_session_invalid': { + 'en-US': 'Session expired, please login again', + 'zh-CN': '会话已过期,请重新登录', + 'ja': 'セッション切れ、再ログインしてください' + } + }; + + 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.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 sessionData = this.getSessionData(); + if (!sessionData) { + throw new Error('No session data available'); + } + + const userId = sessionData.uid; + const result = await registerFido2(userId, displayName || 'Device-' + userId, this.config.rpId); + + if (result.status === 'ok') { + 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 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.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; + }; + + DeviceManager.prototype.checkSession = async function() { + try { + this.sessionStatus = await validSession(this.config.rpId); + 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 modal = document.createElement('div'); + modal.className = 'modal fade fido2-sdk-modal'; + modal.id = 'fido2SdkModal'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + + modal.innerHTML = this._getModalHTML(); + container.appendChild(modal); + this.modalElement = modal; + this.containerElement = container; + + 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(); + + return modal; + }; + + UIRenderer.prototype._getModalHTML = function() { + const theme = this.config.theme; + + return ` +| ${this.i18n.getText('title_device')} | +${this.i18n.getText('title_time')} | + ${features.showDeleteButton ? `${this.i18n.getText('title_act')} | ` : ''} +
|---|---|---|
| ${this.i18n.getText('title_empty_list')} | +||
| ${this.i18n.getText('title_device')} | +${this.i18n.getText('title_time')} | + ${this.config.features.showDeleteButton ? `${this.i18n.getText('title_act')} | ` : ''} +
|---|---|---|
| ${this.i18n.getText('title_empty_list')} | +||
| ${this.i18n.getText('title_device')} | +${this.i18n.getText('title_time')} | + ${this.config.features.showDeleteButton ? `${this.i18n.getText('title_act')} | ` : ''} +
|---|---|---|
| ${this.i18n.getText('title_empty_list')} | +||
Please sign-in to your account and start the adventure
+This sample is production-oriented.
+