(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);