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 ` + + `; + }; + + UIRenderer.prototype._getBodyHTML = function() { + const features = this.config.features; + const userId = window.Fido2UIManager.deviceManager ? window.Fido2UIManager.deviceManager.getUserId() : ''; + + let html = ''; + + if (features.showUserInfo && userId) { + html += `
+ ${this.i18n.getText('title_welcome')}: ${userId} +
`; + } + + html += ``; + + if (features.showAddButton) { + html += ` + + `; + } + + html += ` +
+ + + + + + ${features.showDeleteButton ? `` : ''} + + + + + + + +
${this.i18n.getText('title_device')}${this.i18n.getText('title_time')}${this.i18n.getText('title_act')}
${this.i18n.getText('title_empty_list')}
+
+ `; + + return html; + }; + + UIRenderer.prototype._bindEvents = function() { + 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('#fido2AddDeviceBtn'); + if (addBtn) { + addBtn.replaceWith(addBtn.cloneNode(true)); + const newAddBtn = container.querySelector('#fido2AddDeviceBtn'); + newAddBtn.addEventListener('click', () => { + this.eventManager.emit('addDevice'); + }); + } + + const loginBtn = container.querySelector('#fido2LoginBtn'); + if (loginBtn) { + loginBtn.replaceWith(loginBtn.cloneNode(true)); + const newLoginBtn = container.querySelector('#fido2LoginBtn'); + newLoginBtn.addEventListener('click', () => { + this.eventManager.emit('login'); + }); + } + + 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 tbody = document.getElementById('fido2DevicesList'); + if (!tbody) return; + + if (!devices || devices.length === 0) { + tbody.innerHTML = ` + + ${this.i18n.getText('title_empty_list')} + + `; + return; + } + + let html = ''; + const dm = window.Fido2UIManager ? window.Fido2UIManager.deviceManager : null; + + devices.forEach(device => { + const desc = dm ? dm.parseDeviceDescription(device) : device.desc || 'Unknown Device'; + const date = new Date(device.registered_time).toLocaleString(); + + html += ` + + ${desc} + ${date} + ${this.config.features.showDeleteButton ? ` + + + ${this.i18n.getText('title_del')} + + + ` : ''} + + `; + }); + + tbody.innerHTML = html; + this._bindEvents(); + }; + + UIRenderer.prototype.updateSessionStatus = function(isValid) { + const badge = document.getElementById('fido2SessionStatus'); + const alertDiv = document.getElementById('fido2SessionAlert'); + const addBtn = document.getElementById('fido2AddDeviceBtn'); + + 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 (alertDiv) { + alertDiv.style.display = isValid ? 'none' : 'block'; + } + + if (addBtn) { + addBtn.disabled = !isValid; + } + }; + + 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 userId = window.Fido2UIManager.deviceManager ? window.Fido2UIManager.deviceManager.getUserId() : ''; + + return ` +
+
+
+
+ ${theme.logo ? `` : ''} +

${this.i18n.getText('my_devices')}

+
+ ${this.config.features.showSessionStatus ? '' : ''} +
+
+ ${this.config.features.showUserInfo && userId ? ` + + ` : ''} + + ${this.config.features.showAddButton ? ` + + ` : ''} +
+ + + + + + ${this.config.features.showDeleteButton ? `` : ''} + + + + + + + +
${this.i18n.getText('title_device')}${this.i18n.getText('title_time')}${this.i18n.getText('title_act')}
${this.i18n.getText('title_empty_list')}
+
+
+
+
+ `; + }; + + UIRenderer.prototype._getStandaloneHTML = function() { + const theme = this.config.theme; + const userId = window.Fido2UIManager.deviceManager ? window.Fido2UIManager.deviceManager.getUserId() : ''; + + return ` + + + + + + ${this.i18n.getText('my_devices')} + + + + + +
+
+
+
+ ${theme.logo ? `` : ''} +

${this.i18n.getText('my_devices')}

+
+ ${this.config.features.showSessionStatus ? '' : ''} +
+
+ ${this.config.features.showUserInfo && userId ? ` + + ` : ''} + ${this.config.features.showAddButton ? ` + + ` : ''} +
+ + + + + + ${this.config.features.showDeleteButton ? `` : ''} + + + + + + + +
${this.i18n.getText('title_device')}${this.i18n.getText('title_time')}${this.i18n.getText('title_act')}
${this.i18n.getText('title_empty_list')}
+
+
+
+
+ + + `; + }; + + 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.jQuery) { + throw new Error('jQuery is required. Please include jQuery before fido2-ui-sdk.js'); + } + + 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); + } + }; + + Fido2UIManager.prototype.renderDeviceManager = function(config) { + const manager = new Fido2UIManager(); + manager.init(config); + + 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); + + window.Fido2UIManager = 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 { + const isValid = await self.deviceManager.checkSession(); + if (!isValid) { + alert(self.i18n.getText('msg_session_status_fail') + ' ' + self.i18n.getText('btn_login')); + return; + } + await self.deviceManager.addDevice(); + self.uiRenderer.updateDevicesList(self.deviceManager.devices); + await self.deviceManager.checkSession(); + 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 { + const isValid = await self.deviceManager.checkSession(); + if (!isValid) { + alert(self.i18n.getText('msg_session_status_fail') + ' ' + self.i18n.getText('btn_login')); + return; + } + 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('login', async () => { + try { + const result = await authenticateFido2(null, self.config.rpId); + if (result.status === 'ok') { + await self.deviceManager.loadDevices(); + await self.deviceManager.checkSession(); + self.uiRenderer.updateDevicesList(self.deviceManager.devices); + } else { + alert(result.errorMessage || 'Login failed'); + } + } catch (error) { + console.error('Login error:', error); + alert(error.message || 'Login failed'); + } + }); + + 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.destroy = function() { + if (this.deviceManager) { + this.deviceManager.stopAutoRefresh(); + } + + if (this.uiRenderer) { + this.uiRenderer.cleanup(); + } + + this.initialized = false; + }; + + window.Fido2UIManager = new Fido2UIManager(); + window.Fido2UIManager.renderDeviceManager = Fido2UIManager.prototype.renderDeviceManager; + 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.destroy = function() { + if (window.Fido2UIManager && window.Fido2UIManager.destroy) { + window.Fido2UIManager.destroy(); + } + }; + + console.log('FIDO2 UI SDK v' + FIDO2_UI_VERSION + ' loaded'); +})(window); diff --git a/login.html b/login.html index e85d7dc..d79714c 100644 --- a/login.html +++ b/login.html @@ -62,7 +62,7 @@ // For stand alone: 'https://local.dqj-macpro.com' // For proxy: set 'https://mac-air-m2.dqj-home.com' --> - setFidoServerURL('https://mac-air-m2.dqj-home.com');//'https://fido2.amipro.me'); + setFidoServerURL('https://local.dqj-macpro.com');//'https://fido2.amipro.me'); const i18n_messages = new Map(); @@ -114,6 +114,42 @@ lang_map.set("ja", "パスワードレス ログイン"); i18n_messages.set("title_fido2_login", lang_map); + lang_map = new Map(); + lang_map.set("en-US", "This sample is production-oriented."); + lang_map.set("zh-CN", "此示例面向生产环境。"); + lang_map.set("ja", "このサンプルは本番指向です。"); + i18n_messages.set("msg_prod_note_title", lang_map); + + lang_map = new Map(); + lang_map.set("en-US", "Handles real-world browser differences"); + lang_map.set("zh-CN", "处理真实世界的浏览器差异"); + lang_map.set("ja", "実際のブラウザー差異に対応"); + i18n_messages.set("msg_prod_note_1", lang_map); + + lang_map = new Map(); + lang_map.set("en-US", "Uses recommended WebAuthn options"); + lang_map.set("zh-CN", "使用推荐的 WebAuthn 选项"); + lang_map.set("ja", "推奨される WebAuthn オプションを使用"); + i18n_messages.set("msg_prod_note_2", lang_map); + + lang_map = new Map(); + lang_map.set("en-US", "Mirrors production flow (RP ID, challenge, verification)"); + lang_map.set("zh-CN", "模拟生产流程(RP ID、质询、验证)"); + lang_map.set("ja", "本番フロー(RP ID、チャレンジ、検証)を再現"); + i18n_messages.set("msg_prod_note_3", lang_map); + + lang_map = new Map(); + lang_map.set("en-US", "Safe to use as a starting point"); + lang_map.set("zh-CN", "可安全作为起点使用"); + lang_map.set("ja", "スターターとして安全に利用可能"); + i18n_messages.set("msg_prod_note_4", lang_map); + + lang_map = new Map(); + lang_map.set("en-US", "15-minute integration guide"); + lang_map.set("zh-CN", "15分钟接入指南"); + lang_map.set("ja", "15分での導入ガイド"); + i18n_messages.set("msg_integration_link", lang_map); + window.onload = function() { logoutFido2UserSession(); @@ -173,6 +209,19 @@

Welcome to amiPro sample site!

Please sign-in to your account and start the adventure

+
+

This sample is production-oriented.

+ +

+ 30-minute integration guide +

+
+
diff --git a/modal-demo.html b/modal-demo.html new file mode 100644 index 0000000..a296b8a --- /dev/null +++ b/modal-demo.html @@ -0,0 +1,541 @@ + + + + + + FIDO2 UI SDK - Modal Demo + + + + + + + + + + + + + + + + + + + +
+
+

🔐 FIDO2 UI SDK - Modal Demo

+

Demonstrate how to use FIDO2 UI SDK to manage devices in a popup window

+
+ +
+

📌 Quick Start

+

Click the button below to open the device manager modal:

+ + +
+ +
+

💻 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

+
    +
  • Popup mode, stay on current page
  • +
  • Add/delete FIDO2 devices support
  • +
  • Real-time device list display
  • +
  • Session status monitoring
  • +
  • Theme color and style customization
  • +
  • Multi-language support (Chinese/English/Japanese)
  • +
  • Event callback system
  • +
+
+ +
+

📊 Event Log

+
+ +
+
+ +
+ + + + diff --git a/standalone-demo.html b/standalone-demo.html new file mode 100644 index 0000000..da0fa58 --- /dev/null +++ b/standalone-demo.html @@ -0,0 +1,534 @@ + + + + + + FIDO2 UI SDK - Standalone Demo + + + + + + + + + + + + + + + + + + + +
+
+

🔐 FIDO2 UI SDK - Standalone Demo

+

Demonstrate how to use FIDO2 UI SDK to manage devices in a standalone page

+
+ +
+

📌 What is Standalone Page Mode?

+

Standalone page mode renders a complete device management interface on the current page, suitable for scenarios requiring a dedicated device management page.

+
+ ⚠️ Note: +

Standalone page mode will replace the current page content. If your page already has a complete layout (like login.html or devices.html), it is recommended to use modal mode.

+
+
+ +
+

💻 Code Examples

+

Simplest standalone integration:

+
+ Fido2UIManager.renderDeviceManager({ + mode: 'standalone', + serverUrl: SERVER_URL +}); +
+ +

With theme customization:

+
+ Fido2UIManager.renderDeviceManager({ + mode: 'standalone', + serverUrl: SERVER_URL, + theme: { + logo: 'path/to/logo.png', + primaryColor: '#f5576c', + backgroundColor: '#ffffff' + }, + language: 'zh-CN' +}); +
+
+ +
+

✨ Features

+
    +
  • Standalone page with complete device management interface
  • +
  • Add/delete FIDO2 devices support
  • +
  • Real-time device list display
  • +
  • Session status monitoring
  • +
  • Theme color and style customization
  • +
  • Multi-language support (Chinese/English/Japanese)
  • +
  • Event callback system
  • +
  • Responsive design, mobile support
  • +
+
+ +
+

🚀 Demo

+

Click the buttons below to experience standalone page mode:

+ + + +
+ 💡 Tip: +

After clicking the button, the page will be replaced with the device management interface. To return to this demo page, please refresh the browser.

+
+
+ +
+

📖 Use Cases

+

Standalone page mode is suitable for the following scenarios:

+
    +
  • Standalone device management page (similar to existing devices.html)
  • +
  • User accesses device management directly via link
  • +
  • Device management as a standalone page feature
  • +
  • Need more screen space to display device information
  • +
+
+ + +
+ + + +