A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 875 lines 31 kB view raw
1class HSMTokenManager { 2 constructor(baseUrl = '') { 3 this.baseUrl = baseUrl; 4 this.apiPath = '/api/v1'; 5 this.storageKey = 'hsm-token'; 6 this.expiryKey = 'hsm-token-expiry'; 7 this.cachedToken = null; 8 this.tokenExpiry = null; 9 this.refreshPromise = null; 10 this.loadCachedToken(); 11 } 12 13 loadCachedToken() { 14 const token = localStorage.getItem(this.storageKey); 15 const expiry = localStorage.getItem(this.expiryKey); 16 17 if (token && expiry) { 18 this.cachedToken = token; 19 this.tokenExpiry = new Date(expiry); 20 } 21 } 22 23 isTokenValid() { 24 if (!this.cachedToken || !this.tokenExpiry) { 25 return false; 26 } 27 28 // Consider token invalid if it expires within 5 minutes 29 const bufferTime = 5 * 60 * 1000; // 5 minutes in milliseconds 30 return new Date() < (new Date(this.tokenExpiry.getTime() - bufferTime)); 31 } 32 33 async getValidToken() { 34 // Return cached token if still valid 35 if (this.isTokenValid()) { 36 return this.cachedToken; 37 } 38 39 // If already refreshing, wait for that promise 40 if (this.refreshPromise) { 41 return await this.refreshPromise; 42 } 43 44 // Start token refresh 45 this.refreshPromise = this.refreshToken(); 46 47 try { 48 const token = await this.refreshPromise; 49 return token; 50 } finally { 51 this.refreshPromise = null; 52 } 53 } 54 55 async refreshToken() { 56 try { 57 // First try to get a K8s token automatically (if kubectl is configured) 58 let k8sToken = await this.getK8sToken(); 59 60 if (!k8sToken) { 61 // Prompt user for token 62 k8sToken = await this.promptForK8sToken(); 63 } 64 65 // Exchange K8s token for HSM JWT 66 const response = await fetch(`${this.baseUrl}${this.apiPath}/auth/token`, { 67 method: 'POST', 68 headers: { 69 'Content-Type': 'application/json' 70 }, 71 body: JSON.stringify({ k8s_token: k8sToken }) 72 }); 73 74 if (!response.ok) { 75 const errorData = await response.json(); 76 throw new Error(errorData.error || `HTTP ${response.status}`); 77 } 78 79 const data = await response.json(); 80 81 // Cache the new token 82 this.cachedToken = data.token; 83 this.tokenExpiry = new Date(data.expires_at); 84 85 localStorage.setItem(this.storageKey, this.cachedToken); 86 localStorage.setItem(this.expiryKey, this.tokenExpiry.toISOString()); 87 88 return this.cachedToken; 89 } catch (error) { 90 console.error('Token refresh failed:', error); 91 this.clearToken(); 92 throw error; 93 } 94 } 95 96 async getK8sToken() { 97 // This would work if the web UI had access to kubectl context 98 // For now, we'll return null to trigger user prompt 99 return null; 100 } 101 102 async promptForK8sToken() { 103 return new Promise((resolve, reject) => { 104 // Create modal dialog 105 const modal = this.createTokenModal(); 106 document.body.appendChild(modal); 107 108 // Focus on input 109 const input = modal.querySelector('#tokenInput'); 110 const submitBtn = modal.querySelector('#submitToken'); 111 const cancelBtn = modal.querySelector('#cancelToken'); 112 113 input.focus(); 114 115 const cleanup = () => { 116 document.body.removeChild(modal); 117 }; 118 119 submitBtn.onclick = () => { 120 const token = input.value.trim(); 121 if (token) { 122 cleanup(); 123 resolve(token); 124 } else { 125 alert('Please enter a valid token'); 126 } 127 }; 128 129 cancelBtn.onclick = () => { 130 cleanup(); 131 reject(new Error('Authentication cancelled by user')); 132 }; 133 134 // Submit on Enter 135 input.onkeydown = (e) => { 136 if (e.key === 'Enter') { 137 submitBtn.click(); 138 } 139 }; 140 }); 141 } 142 143 createTokenModal() { 144 const modal = document.createElement('div'); 145 modal.className = 'auth-modal'; 146 modal.innerHTML = ` 147 <div class="auth-modal-content"> 148 <h2>🔐 Authentication Required</h2> 149 <p>The HSM Secrets API requires authentication. Please provide a Kubernetes service account token.</p> 150 151 <div class="auth-instructions"> 152 <p><strong>To get a token, run this command:</strong></p> 153 <code>kubectl create token hsm-web-ui-sa --duration=8h</code> 154 <p><small>Replace <code>hsm-web-ui-sa</code> with your service account name</small></p> 155 </div> 156 157 <div class="form-group"> 158 <label for="tokenInput">Service Account Token:</label> 159 <textarea id="tokenInput" placeholder="Paste your Kubernetes service account token here..." rows="4"></textarea> 160 </div> 161 162 <div class="auth-actions"> 163 <button id="submitToken" class="btn">Login</button> 164 <button id="cancelToken" class="btn btn-secondary">Cancel</button> 165 </div> 166 </div> 167 `; 168 return modal; 169 } 170 171 clearToken() { 172 this.cachedToken = null; 173 this.tokenExpiry = null; 174 localStorage.removeItem(this.storageKey); 175 localStorage.removeItem(this.expiryKey); 176 } 177 178 getTokenInfo() { 179 if (!this.cachedToken || !this.tokenExpiry) { 180 return { authenticated: false }; 181 } 182 183 return { 184 authenticated: true, 185 expiresAt: this.tokenExpiry, 186 valid: this.isTokenValid() 187 }; 188 } 189} 190 191class HSMSecretsAPI { 192 constructor(baseUrl = '') { 193 this.baseUrl = baseUrl; 194 this.apiPath = '/api/v1'; 195 this.tokenManager = new HSMTokenManager(baseUrl); 196 } 197 198 async request(path, options = {}) { 199 const url = `${this.baseUrl}${this.apiPath}${path}`; 200 201 // Skip authentication for health and auth endpoints 202 const skipAuth = path.includes('/health') || path.includes('/auth/token'); 203 204 const headers = { 205 'Content-Type': 'application/json', 206 ...options.headers 207 }; 208 209 // Add authentication header if not skipping auth 210 if (!skipAuth) { 211 try { 212 const token = await this.tokenManager.getValidToken(); 213 headers['Authorization'] = `Bearer ${token}`; 214 } catch (error) { 215 throw new Error(`Authentication failed: ${error.message}`); 216 } 217 } 218 219 const config = { 220 headers, 221 ...options 222 }; 223 224 try { 225 const response = await fetch(url, config); 226 const data = await response.json(); 227 228 if (!response.ok) { 229 // Handle authentication errors specifically 230 if (response.status === 401) { 231 this.tokenManager.clearToken(); 232 throw new Error('Authentication failed. Please login again.'); 233 } 234 throw new Error(data.error?.message || `HTTP ${response.status}`); 235 } 236 237 return data; 238 } catch (error) { 239 console.error('API Request failed:', error); 240 throw error; 241 } 242 } 243 244 async getHealth() { 245 return this.request('/health'); 246 } 247 248 async listSecrets(page = 1, pageSize = 100) { 249 return this.request(`/hsm/secrets?page=${page}&page_size=${pageSize}`); 250 } 251 252 async getSecret(secretName) { 253 return this.request(`/hsm/secrets/${encodeURIComponent(secretName)}`); 254 } 255 256 async getDeviceStatus() { 257 return this.request('/hsm/status'); 258 } 259 260 async getDeviceInfo() { 261 return this.request('/hsm/info'); 262 } 263 264 async createSecret(secretName, data, metadata = null) { 265 const requestBody = { data }; 266 if (metadata) { 267 requestBody.metadata = metadata; 268 } 269 return this.request(`/hsm/secrets/${encodeURIComponent(secretName)}`, { 270 method: 'POST', 271 body: JSON.stringify(requestBody) 272 }); 273 } 274 275 async deleteSecret(secretName) { 276 return this.request(`/hsm/secrets/${encodeURIComponent(secretName)}`, { 277 method: 'DELETE' 278 }); 279 } 280} 281 282class HSMSecretsUI { 283 constructor() { 284 this.api = new HSMSecretsAPI(); 285 this.secrets = []; 286 this.init(); 287 } 288 289 init() { 290 this.kvPairCounter = 0; 291 this.labelPairCounter = 0; 292 this.setupEventListeners(); 293 this.loadInitialData(); 294 this.initializeCreateForm(); 295 this.updateAuthStatus(); 296 297 // Update auth status every 30 seconds 298 setInterval(() => this.updateAuthStatus(), 30000); 299 } 300 301 initializeCreateForm() { 302 // Add initial empty key-value pair to the form 303 this.addKeyValuePair(); 304 // Add initial empty label pair to the metadata form 305 this.addLabelPair(); 306 } 307 308 setupEventListeners() { 309 const createForm = document.getElementById('createForm'); 310 createForm.addEventListener('submit', (e) => this.handleCreateSecret(e)); 311 } 312 313 async loadInitialData() { 314 await this.checkAPIHealth(); 315 await this.loadDeviceStatus(); 316 await this.loadSecrets(); 317 } 318 319 async checkAPIHealth() { 320 try { 321 const health = await this.api.getHealth(); 322 const statusElement = document.getElementById('apiStatus'); 323 const deviceCountElement = document.getElementById('deviceCount'); 324 325 if (health.success && health.data.status === 'healthy') { 326 statusElement.textContent = '✅ Healthy'; 327 statusElement.style.color = '#22543d'; 328 } else { 329 statusElement.textContent = '⚠️ Degraded'; 330 statusElement.style.color = '#dd6b20'; 331 } 332 333 // Update device count if available 334 if (deviceCountElement && health.data.activeNodes !== undefined) { 335 deviceCountElement.textContent = health.data.activeNodes; 336 } 337 } catch (error) { 338 const statusElement = document.getElementById('apiStatus'); 339 statusElement.textContent = '❌ Error'; 340 statusElement.style.color = '#c53030'; 341 console.error('Health check failed:', error); 342 } 343 } 344 345 async loadDeviceStatus() { 346 const statusElement = document.getElementById('deviceStatus'); 347 statusElement.innerHTML = '<div class="loading">Loading device status...</div>'; 348 349 try { 350 const [statusResponse, infoResponse] = await Promise.all([ 351 this.api.getDeviceStatus(), 352 this.api.getDeviceInfo() 353 ]); 354 355 const devices = statusResponse.data.devices || {}; 356 const deviceInfos = infoResponse.data.deviceInfos || {}; 357 const totalDevices = statusResponse.data.totalDevices || 0; 358 359 this.renderDeviceStatus(devices, deviceInfos, totalDevices); 360 } catch (error) { 361 this.showError(statusElement, `Failed to load device status: ${error.message}`); 362 } 363 } 364 365 renderDeviceStatus(devices, deviceInfos, totalDevices) { 366 const statusElement = document.getElementById('deviceStatus'); 367 368 if (totalDevices === 0) { 369 statusElement.innerHTML = '<p style="text-align: center; color: #666; padding: 20px;">No HSM devices found.</p>'; 370 return; 371 } 372 373 const deviceItems = Object.entries(devices).map(([deviceName, isConnected]) => { 374 const info = deviceInfos[deviceName]; 375 const statusIcon = isConnected ? '🟢' : '🔴'; 376 const statusText = isConnected ? 'Connected' : 'Disconnected'; 377 378 return ` 379 <div class="device-item ${isConnected ? 'connected' : 'disconnected'}"> 380 <div class="device-header"> 381 <span class="device-name">${statusIcon} ${this.escapeHtml(deviceName)}</span> 382 <span class="device-status-badge">${statusText}</span> 383 </div> 384 ${info ? ` 385 <div class="device-details"> 386 <div class="device-info"> 387 <span>Manufacturer: ${this.escapeHtml(info.manufacturer || 'Unknown')}</span> 388 <span>Model: ${this.escapeHtml(info.model || 'Unknown')}</span> 389 <span>Serial: ${this.escapeHtml(info.serialNumber || 'Unknown')}</span> 390 </div> 391 </div> 392 ` : ''} 393 </div> 394 `; 395 }).join(''); 396 397 statusElement.innerHTML = deviceItems; 398 } 399 400 async loadSecrets() { 401 const listElement = document.getElementById('secretsList'); 402 listElement.innerHTML = '<div class="loading">Loading secrets...</div>'; 403 404 try { 405 const response = await this.api.listSecrets(); 406 this.secrets = response.data.secrets || []; 407 408 document.getElementById('totalSecrets').textContent = this.secrets.length; 409 410 this.renderSecretsList(); 411 } catch (error) { 412 this.showError(listElement, `Failed to load secrets: ${error.message}`); 413 } 414 } 415 416 renderSecretsList() { 417 const listElement = document.getElementById('secretsList'); 418 419 if (this.secrets.length === 0) { 420 listElement.innerHTML = '<p style="text-align: center; color: #666; padding: 20px;">No secrets found. Create your first secret!</p>'; 421 return; 422 } 423 424 listElement.innerHTML = this.secrets.map(secretName => ` 425 <div class="secret-item"> 426 <div class="secret-name">🔐 ${this.escapeHtml(secretName)}</div> 427 <div class="secret-actions"> 428 <button class="btn btn-secondary" onclick="ui.viewSecret('${this.escapeHtml(secretName)}')"> 429 👁️ View 430 </button> 431 <button class="btn btn-danger" onclick="ui.deleteSecret('${this.escapeHtml(secretName)}')"> 432 🗑️ Delete 433 </button> 434 </div> 435 </div> 436 `).join(''); 437 } 438 439 async viewSecret(secretName) { 440 const viewSection = document.getElementById('viewSection'); 441 const viewMessage = document.getElementById('viewMessage'); 442 const detailsElement = document.getElementById('secretDetails'); 443 444 viewSection.style.display = 'block'; 445 viewMessage.innerHTML = ''; 446 detailsElement.innerHTML = '<div class="loading">Loading secret details...</div>'; 447 448 // Scroll to view section 449 viewSection.scrollIntoView({ behavior: 'smooth' }); 450 451 try { 452 const response = await this.api.getSecret(secretName); 453 const secretData = response.data; 454 455 // Convert byte arrays to strings for display 456 const displayData = {}; 457 if (secretData.data) { 458 for (const [key, value] of Object.entries(secretData.data)) { 459 // Handle byte arrays by converting to string 460 if (Array.isArray(value)) { 461 displayData[key] = String.fromCharCode.apply(null, value); 462 } else { 463 displayData[key] = value; 464 } 465 } 466 } 467 468 const deviceBadge = secretData.deviceCount > 1 ? 469 `<span class="device-badge multi-device">${secretData.deviceCount} devices</span>` : 470 `<span class="device-badge single-device">1 device</span>`; 471 472 detailsElement.innerHTML = ` 473 <h3>Secret: ${this.escapeHtml(secretName)} ${deviceBadge}</h3> 474 <div class="secret-metadata"> 475 <div class="metadata-item"> 476 <strong>Path:</strong> ${this.escapeHtml(secretData.path || secretName)} 477 </div> 478 <div class="metadata-item"> 479 <strong>Checksum:</strong> ${this.escapeHtml(secretData.checksum || 'N/A')} 480 </div> 481 <div class="metadata-item"> 482 <strong>Keys:</strong> ${Object.keys(displayData).length} 483 </div> 484 ${secretData.deviceCount ? ` 485 <div class="metadata-item"> 486 <strong>Device Count:</strong> ${secretData.deviceCount} 487 </div> 488 ` : ''} 489 </div> 490 <div class="secret-data"> 491 <strong>Data:</strong> 492 <div class="json-preview">${this.escapeHtml(JSON.stringify(displayData, null, 2))}</div> 493 </div> 494 `; 495 } catch (error) { 496 this.showError(viewMessage, `Failed to load secret: ${error.message}`); 497 detailsElement.innerHTML = ''; 498 } 499 } 500 501 async deleteSecret(secretName) { 502 if (!confirm(`Are you sure you want to delete the secret "${secretName}"? This action cannot be undone.`)) { 503 return; 504 } 505 506 try { 507 await this.api.deleteSecret(secretName); 508 this.showSuccess(null, `Secret "${secretName}" deleted successfully!`); 509 await this.loadSecrets(); // Refresh after deletion 510 } catch (error) { 511 this.showError(null, `Failed to delete secret: ${error.message}`); 512 } 513 } 514 515 showCreateForm() { 516 document.getElementById('createSection').style.display = 'block'; 517 document.getElementById('secretName').focus(); 518 document.getElementById('createSection').scrollIntoView({ behavior: 'smooth' }); 519 } 520 521 hideCreateForm() { 522 document.getElementById('createSection').style.display = 'none'; 523 document.getElementById('createForm').reset(); 524 document.getElementById('createMessage').innerHTML = ''; 525 526 // Reset key-value pairs to single empty pair 527 const kvPairs = document.getElementById('kvPairs'); 528 kvPairs.innerHTML = ''; 529 this.kvPairCounter = 0; 530 this.addKeyValuePair(); // Add one empty pair 531 532 // Reset label pairs and advanced section 533 const labelPairs = document.getElementById('labelPairs'); 534 labelPairs.innerHTML = ''; 535 this.labelPairCounter = 0; 536 this.addLabelPair(); // Add one empty label pair 537 538 // Close advanced section 539 const advancedContent = document.getElementById('advancedContent'); 540 const advancedToggle = document.querySelector('.advanced-toggle'); 541 advancedContent.classList.remove('show'); 542 advancedToggle.classList.remove('expanded'); 543 } 544 545 hideViewSection() { 546 document.getElementById('viewSection').style.display = 'none'; 547 document.getElementById('viewMessage').innerHTML = ''; 548 } 549 550 addKeyValuePair(key = '', value = '') { 551 const kvPairs = document.getElementById('kvPairs'); 552 553 const pairId = this.kvPairCounter++; 554 const pairDiv = document.createElement('div'); 555 pairDiv.className = 'kv-pair'; 556 pairDiv.id = `kvPair${pairId}`; 557 558 pairDiv.innerHTML = ` 559 <input type="text" name="key${pairId}" placeholder="Key (e.g., api_key)" value="${this.escapeHtml(key)}" required> 560 <input type="text" name="value${pairId}" placeholder="Value" value="${this.escapeHtml(value)}" required> 561 <button type="button" class="btn btn-remove btn-small" onclick="ui.removeKeyValuePair('kvPair${pairId}')" title="Remove this key-value pair"> 562563 </button> 564 `; 565 566 kvPairs.appendChild(pairDiv); 567 568 // Focus on the key input for new pairs (but not during initial load) 569 if (!key && kvPairs.children.length > 1) { 570 pairDiv.querySelector('input[name^="key"]').focus(); 571 } 572 } 573 574 removeKeyValuePair(pairId) { 575 const kvPairs = document.getElementById('kvPairs'); 576 const pairElement = document.getElementById(pairId); 577 578 // Don't allow removing the last pair 579 if (kvPairs.children.length <= 1) { 580 return; 581 } 582 583 if (pairElement) { 584 pairElement.remove(); 585 } 586 } 587 588 collectKeyValuePairs() { 589 const kvPairs = document.getElementById('kvPairs'); 590 const pairs = kvPairs.querySelectorAll('.kv-pair'); 591 const data = {}; 592 593 for (const pair of pairs) { 594 const keyInput = pair.querySelector('input[name^="key"]'); 595 const valueInput = pair.querySelector('input[name^="value"]'); 596 597 if (keyInput && valueInput) { 598 const key = keyInput.value.trim(); 599 const value = valueInput.value.trim(); 600 601 if (key && value) { 602 data[key] = value; 603 } 604 } 605 } 606 607 return data; 608 } 609 610 toggleAdvanced() { 611 const content = document.getElementById('advancedContent'); 612 const toggle = document.querySelector('.advanced-toggle'); 613 614 content.classList.toggle('show'); 615 toggle.classList.toggle('expanded'); 616 } 617 618 addLabelPair(key = '', value = '') { 619 const labelPairs = document.getElementById('labelPairs'); 620 621 const pairId = this.labelPairCounter++; 622 const pairDiv = document.createElement('div'); 623 pairDiv.className = 'tag-pair'; 624 pairDiv.id = `labelPair${pairId}`; 625 626 pairDiv.innerHTML = ` 627 <input type="text" name="labelKey${pairId}" placeholder="Label key (e.g., app, environment)" value="${this.escapeHtml(key)}"> 628 <input type="text" name="labelValue${pairId}" placeholder="Label value (e.g., backend, production)" value="${this.escapeHtml(value)}"> 629 <button type="button" class="btn btn-remove btn-small" onclick="ui.removeLabelPair('labelPair${pairId}')" title="Remove this label"> 630631 </button> 632 `; 633 634 labelPairs.appendChild(pairDiv); 635 636 // Focus on the key input for new pairs (but not during initial load) 637 if (!key && labelPairs.children.length > 1) { 638 pairDiv.querySelector('input[name^="labelKey"]').focus(); 639 } 640 } 641 642 removeLabelPair(pairId) { 643 const labelPairs = document.getElementById('labelPairs'); 644 const pairElement = document.getElementById(pairId); 645 646 // Don't allow removing the last pair 647 if (labelPairs.children.length <= 1) { 648 return; 649 } 650 651 if (pairElement) { 652 pairElement.remove(); 653 } 654 } 655 656 collectLabelPairs() { 657 const labelPairs = document.getElementById('labelPairs'); 658 const pairs = labelPairs.querySelectorAll('.tag-pair'); 659 const labels = {}; 660 661 for (const pair of pairs) { 662 const keyInput = pair.querySelector('input[name^="labelKey"]'); 663 const valueInput = pair.querySelector('input[name^="labelValue"]'); 664 665 if (keyInput && valueInput) { 666 const key = keyInput.value.trim(); 667 const value = valueInput.value.trim(); 668 669 if (key && value) { 670 labels[key] = value; 671 } 672 } 673 } 674 675 return labels; 676 } 677 678 collectMetadata() { 679 const description = document.getElementById('metadataDescription').value.trim(); 680 const format = document.getElementById('metadataFormat').value.trim(); 681 const dataType = document.getElementById('metadataDataType').value.trim(); 682 const source = document.getElementById('metadataSource').value.trim(); 683 const labels = this.collectLabelPairs(); 684 685 // Only return metadata if at least one field is filled 686 if (!description && !format && !dataType && !source && Object.keys(labels).length === 0) { 687 return null; 688 } 689 690 const metadata = {}; 691 if (description) metadata.description = description; 692 if (format) metadata.format = format; 693 if (dataType) metadata.data_type = dataType; 694 if (source) metadata.source = source; 695 if (Object.keys(labels).length > 0) metadata.labels = labels; 696 697 // Add creation timestamp 698 metadata.created_at = new Date().toISOString(); 699 700 return metadata; 701 } 702 703 async handleCreateSecret(event) { 704 event.preventDefault(); 705 706 const messageElement = document.getElementById('createMessage'); 707 const formData = new FormData(event.target); 708 const secretName = formData.get('secretName').trim(); 709 710 messageElement.innerHTML = ''; 711 712 // Validate inputs 713 if (!secretName) { 714 this.showError(messageElement, 'Secret name is required'); 715 return; 716 } 717 718 // Collect key-value pairs 719 const secretData = this.collectKeyValuePairs(); 720 721 if (Object.keys(secretData).length === 0) { 722 this.showError(messageElement, 'At least one key-value pair is required'); 723 return; 724 } 725 726 // Collect metadata if any is provided 727 const metadata = this.collectMetadata(); 728 729 // Validate key names (no spaces, no special chars except underscore) 730 for (const key of Object.keys(secretData)) { 731 if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) { 732 this.showError(messageElement, `Invalid key "${key}". Keys must start with a letter and contain only letters, numbers, and underscores.`); 733 return; 734 } 735 } 736 737 // Get submit button and store original text 738 const submitBtn = event.target.querySelector('button[type="submit"]'); 739 const originalText = submitBtn.textContent; 740 741 try { 742 // Show loading state 743 submitBtn.textContent = 'Creating...'; 744 submitBtn.disabled = true; 745 746 await this.api.createSecret(secretName, secretData, metadata); 747 748 this.showSuccess(messageElement, `Secret "${secretName}" created successfully!`); 749 750 // Reset form and refresh list 751 event.target.reset(); 752 await this.loadSecrets(); // Refresh after creation 753 754 // Hide form after a delay 755 setTimeout(() => this.hideCreateForm(), 2000); 756 757 } catch (error) { 758 this.showError(messageElement, `Failed to create secret: ${error.message}`); 759 } finally { 760 // Restore button state 761 submitBtn.textContent = originalText; 762 submitBtn.disabled = false; 763 } 764 } 765 766 async refreshSecrets() { 767 await this.loadSecrets(); 768 } 769 770 async refreshDeviceStatus() { 771 await this.loadDeviceStatus(); 772 } 773 774 async refreshAll() { 775 await this.loadInitialData(); 776 this.updateAuthStatus(); 777 } 778 779 updateAuthStatus() { 780 const authInfo = this.api.tokenManager.getTokenInfo(); 781 const authElement = document.getElementById('authStatus'); 782 783 if (authElement) { 784 if (authInfo.authenticated && authInfo.valid) { 785 const expiresIn = Math.floor((authInfo.expiresAt - new Date()) / (1000 * 60)); // minutes 786 authElement.innerHTML = `✅ Authenticated (expires in ${expiresIn}m)`; 787 authElement.className = 'auth-status authenticated'; 788 } else if (authInfo.authenticated && !authInfo.valid) { 789 authElement.innerHTML = `⚠️ Token Expired`; 790 authElement.className = 'auth-status expired'; 791 } else { 792 authElement.innerHTML = `❌ Not Authenticated`; 793 authElement.className = 'auth-status not-authenticated'; 794 } 795 } 796 } 797 798 logout() { 799 if (confirm('Are you sure you want to logout? You will need to provide a new token to continue using the HSM API.')) { 800 this.api.tokenManager.clearToken(); 801 this.updateAuthStatus(); 802 this.showSuccess(null, 'Logged out successfully. You will be prompted for authentication on your next API request.'); 803 } 804 } 805 806 showError(element, message) { 807 const errorHTML = `<div class="error">❌ ${this.escapeHtml(message)}</div>`; 808 if (element) { 809 element.innerHTML = errorHTML; 810 } else { 811 // Show at top of page 812 const container = document.querySelector('.container'); 813 const existingError = container.querySelector('.error'); 814 if (existingError) { 815 existingError.remove(); 816 } 817 container.insertAdjacentHTML('afterbegin', errorHTML); 818 819 // Remove after 5 seconds 820 setTimeout(() => { 821 const errorEl = container.querySelector('.error'); 822 if (errorEl) errorEl.remove(); 823 }, 5000); 824 } 825 } 826 827 showSuccess(element, message) { 828 const successHTML = `<div class="success">✅ ${this.escapeHtml(message)}</div>`; 829 if (element) { 830 element.innerHTML = successHTML; 831 } else { 832 // Show at top of page 833 const container = document.querySelector('.container'); 834 const existingSuccess = container.querySelector('.success'); 835 if (existingSuccess) { 836 existingSuccess.remove(); 837 } 838 container.insertAdjacentHTML('afterbegin', successHTML); 839 840 // Remove after 5 seconds 841 setTimeout(() => { 842 const successEl = container.querySelector('.success'); 843 if (successEl) successEl.remove(); 844 }, 5000); 845 } 846 } 847 848 escapeHtml(unsafe) { 849 return unsafe 850 .replace(/&/g, "&amp;") 851 .replace(/</g, "&lt;") 852 .replace(/>/g, "&gt;") 853 .replace(/"/g, "&quot;") 854 .replace(/'/g, "&#039;"); 855 } 856} 857 858// Global functions for onclick handlers 859let ui; 860 861window.addEventListener('DOMContentLoaded', () => { 862 ui = new HSMSecretsUI(); 863 // Expose ui object globally for onclick handlers 864 window.ui = ui; 865}); 866 867// Expose functions globally for onclick handlers 868window.refreshSecrets = () => ui.refreshSecrets(); 869window.refreshDeviceStatus = () => ui.refreshDeviceStatus(); 870window.refreshAll = () => ui.refreshAll(); 871window.showCreateForm = () => ui.showCreateForm(); 872window.hideCreateForm = () => ui.hideCreateForm(); 873window.hideViewSection = () => ui.hideViewSection(); 874window.logout = () => ui.logout(); 875