Client with UI

This commit is contained in:
dqj
2026-01-17 18:29:04 +09:00
parent 44a35fb45a
commit db2b608606
7 changed files with 2705 additions and 5 deletions

View File

@@ -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;
}
}

276
files/fido2-ui-sdk.css Normal file
View File

@@ -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;
}

963
files/fido2-ui-sdk.js Normal file
View File

@@ -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 `
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content fido2-sdk-card">
<div class="modal-header fido2-sdk-header">
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo" alt="Logo">` : ''}
<h5 class="modal-title fido2-sdk-text">${this.i18n.getText('my_devices')}</h5>
${this.config.features.showSessionStatus ? '<span class="badge fido2-sdk-status-badge" id="fido2SessionStatus"></span>' : ''}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fido2-sdk-container">
${this._getBodyHTML()}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary fido2-sdk-btn" data-bs-dismiss="modal">${this.i18n.getText('btn_close')}</button>
</div>
</div>
</div>
`;
};
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 += `<div class="fido2-sdk-user-info mb-3 fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>`;
}
html += `<div id="fido2SessionAlert" class="alert alert-warning mb-3 fido2-sdk-text" style="display:none;">
${this.i18n.getText('msg_session_invalid')}
<button type="button" class="btn btn-sm btn-primary ms-2" id="fido2LoginBtn">
${this.i18n.getText('btn_login')}
</button>
</div>`;
if (features.showAddButton) {
html += `
<button type="button" class="btn btn-info mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2AddDeviceBtn">
${this.i18n.getText('btn_add')}
</button>
`;
}
html += `
<div class="table-responsive">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="fido2DevicesList">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
`;
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 = `
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
`;
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 += `
<tr>
<td class="fido2-sdk-text"><strong>${desc}</strong></td>
<td class="fido2-sdk-text">${date}</td>
${this.config.features.showDeleteButton ? `
<td>
<a href="javascript:void(0)"
class="text-danger"
data-fido2-action="delete"
data-device-id="${device.device_id}">
<i class="bx bx-trash"></i> ${this.i18n.getText('title_del')}
</a>
</td>
` : ''}
</tr>
`;
});
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 `
<div class="container fido2-sdk-container">
<div class="card fido2-sdk-card">
<div class="card-header fido2-sdk-header d-flex justify-content-between align-items-center">
<div>
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo me-2" alt="Logo">` : ''}
<h4 class="mb-0 fido2-sdk-text">${this.i18n.getText('my_devices')}</h4>
</div>
${this.config.features.showSessionStatus ? '<span class="badge fido2-sdk-status-badge" id="fido2SessionStatus"></span>' : ''}
</div>
<div class="card-body fido2-sdk-body">
${this.config.features.showUserInfo && userId ? `
<div class="alert alert-info fido2-sdk-user-info fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>
` : ''}
<div id="fido2SessionAlert" class="alert alert-warning mb-3 fido2-sdk-text" style="display:none;">
${this.i18n.getText('msg_session_invalid')}
<button type="button" class="btn btn-sm btn-primary ms-2" id="fido2LoginBtn">
${this.i18n.getText('btn_login')}
</button>
</div>
${this.config.features.showAddButton ? `
<button type="button" class="btn btn-info mt-2 mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2AddDeviceBtn">
${this.i18n.getText('btn_add')}
</button>
` : ''}
<div class="table-responsive mt-2">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${this.config.features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="fido2DevicesList">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
};
UIRenderer.prototype._getStandaloneHTML = function() {
const theme = this.config.theme;
const userId = window.Fido2UIManager.deviceManager ? window.Fido2UIManager.deviceManager.getUserId() : '';
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this.i18n.getText('my_devices')}</title>
<link rel="stylesheet" href="files/bootstrap.css">
<link rel="stylesheet" href="files/boxicons.css">
<link rel="stylesheet" href="files/fido2-ui-sdk.css">
</head>
<body class="fido2-sdk-standalone">
<div class="container fido2-sdk-container">
<div class="card fido2-sdk-card">
<div class="card-header fido2-sdk-header d-flex justify-content-between align-items-center">
<div>
${theme.logo ? `<img src="${theme.logo}" class="fido2-sdk-logo me-2" alt="Logo">` : ''}
<h4 class="mb-0 fido2-sdk-text">${this.i18n.getText('my_devices')}</h4>
</div>
${this.config.features.showSessionStatus ? '<span class="badge fido2-sdk-status-badge" id="fido2SessionStatus"></span>' : ''}
</div>
<div class="card-body fido2-sdk-body">
${this.config.features.showUserInfo && userId ? `
<div class="alert alert-info fido2-sdk-user-info fido2-sdk-text">
<strong>${this.i18n.getText('title_welcome')}:</strong> ${userId}
</div>
` : ''}
${this.config.features.showAddButton ? `
<button type="button" class="btn btn-info mt-2 mb-3 fido2-sdk-btn fido2-sdk-btn-primary" id="fido2AddDeviceBtn">
${this.i18n.getText('btn_add')}
</button>
` : ''}
<div class="table-responsive mt-2">
<table class="table table-striped fido2-sdk-table">
<thead>
<tr>
<th>${this.i18n.getText('title_device')}</th>
<th>${this.i18n.getText('title_time')}</th>
${this.config.features.showDeleteButton ? `<th>${this.i18n.getText('title_act')}</th>` : ''}
</tr>
</thead>
<tbody id="fido2DevicesList">
<tr>
<td colspan="3" class="text-center fido2-sdk-text">${this.i18n.getText('title_empty_list')}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
`;
};
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);