class SSHKeyManager { constructor() { this.currentFlow = null; this.keys = []; this.filteredKeys = []; this.currentPage = 1; this.keysPerPage = 20; this.selectedKeys = new Set(); this.initializeEventListeners(); this.loadFlows(); } initializeEventListeners() { // Flow selection document.getElementById('flowSelect').addEventListener('change', (e) => { this.currentFlow = e.target.value; if (this.currentFlow) { this.loadKeys(); } else { this.clearTable(); } }); // Refresh button document.getElementById('refreshBtn').addEventListener('click', () => { this.loadFlows(); if (this.currentFlow) { this.loadKeys(); } }); // Add key button document.getElementById('addKeyBtn').addEventListener('click', () => { this.showAddKeyModal(); }); // Bulk delete button document.getElementById('bulkDeleteBtn').addEventListener('click', () => { this.deleteSelectedKeys(); }); // Search input document.getElementById('searchInput').addEventListener('input', (e) => { this.filterKeys(e.target.value); }); // Select all checkbox document.getElementById('selectAll').addEventListener('change', (e) => { this.toggleSelectAll(e.target.checked); }); // Pagination document.getElementById('prevPage').addEventListener('click', () => { this.changePage(this.currentPage - 1); }); document.getElementById('nextPage').addEventListener('click', () => { this.changePage(this.currentPage + 1); }); // Modal events this.initializeModalEvents(); } initializeModalEvents() { // Add key modal const addModal = document.getElementById('addKeyModal'); const addForm = document.getElementById('addKeyForm'); addForm.addEventListener('submit', (e) => { e.preventDefault(); this.addKey(); }); document.getElementById('cancelAdd').addEventListener('click', () => { this.hideModal('addKeyModal'); }); // View key modal document.getElementById('closeView').addEventListener('click', () => { this.hideModal('viewKeyModal'); }); document.getElementById('copyKey').addEventListener('click', () => { this.copyKeyToClipboard(); }); // Close modals when clicking on close button or outside document.querySelectorAll('.modal .close').forEach(closeBtn => { closeBtn.addEventListener('click', (e) => { this.hideModal(e.target.closest('.modal').id); }); }); document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { this.hideModal(modal.id); } }); }); } async loadFlows() { try { this.showLoading(); const response = await fetch('/api/flows'); if (!response.ok) throw new Error('Failed to load flows'); const flows = await response.json(); this.populateFlowSelector(flows); } catch (error) { this.showToast('Failed to load flows: ' + error.message, 'error'); } finally { this.hideLoading(); } } populateFlowSelector(flows) { const select = document.getElementById('flowSelect'); select.innerHTML = ''; flows.forEach(flow => { const option = document.createElement('option'); option.value = flow; option.textContent = flow; select.appendChild(option); }); } async loadKeys() { if (!this.currentFlow) return; try { this.showLoading(); const response = await fetch(`/${this.currentFlow}/keys`); if (!response.ok) throw new Error('Failed to load keys'); this.keys = await response.json(); this.filteredKeys = [...this.keys]; this.updateStats(); this.renderTable(); this.selectedKeys.clear(); this.updateBulkDeleteButton(); } catch (error) { this.showToast('Failed to load keys: ' + error.message, 'error'); this.clearTable(); } finally { this.hideLoading(); } } filterKeys(searchTerm) { if (!searchTerm.trim()) { this.filteredKeys = [...this.keys]; } else { const term = searchTerm.toLowerCase(); this.filteredKeys = this.keys.filter(key => key.server.toLowerCase().includes(term) || key.public_key.toLowerCase().includes(term) ); } this.currentPage = 1; this.renderTable(); } updateStats() { document.getElementById('totalKeys').textContent = this.keys.length; const uniqueServers = new Set(this.keys.map(key => key.server)); document.getElementById('uniqueServers').textContent = uniqueServers.size; } renderTable() { const tbody = document.getElementById('keysTableBody'); const noKeysMessage = document.getElementById('noKeysMessage'); if (this.filteredKeys.length === 0) { tbody.innerHTML = ''; noKeysMessage.style.display = 'block'; this.updatePagination(); return; } noKeysMessage.style.display = 'none'; const startIndex = (this.currentPage - 1) * this.keysPerPage; const endIndex = startIndex + this.keysPerPage; const pageKeys = this.filteredKeys.slice(startIndex, endIndex); tbody.innerHTML = pageKeys.map((key, index) => { const keyType = this.getKeyType(key.public_key); const keyPreview = this.getKeyPreview(key.public_key); const keyId = `${key.server}-${key.public_key}`; return ` ${this.escapeHtml(key.server)} ${keyType} ${keyPreview} `; }).join(''); // Add event listeners for checkboxes tbody.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const keyId = e.target.dataset.keyId; if (e.target.checked) { this.selectedKeys.add(keyId); } else { this.selectedKeys.delete(keyId); } this.updateBulkDeleteButton(); this.updateSelectAllCheckbox(); }); }); this.updatePagination(); } updatePagination() { const totalPages = Math.ceil(this.filteredKeys.length / this.keysPerPage); document.getElementById('pageInfo').textContent = `Page ${this.currentPage} of ${totalPages}`; document.getElementById('prevPage').disabled = this.currentPage <= 1; document.getElementById('nextPage').disabled = this.currentPage >= totalPages; } changePage(newPage) { const totalPages = Math.ceil(this.filteredKeys.length / this.keysPerPage); if (newPage >= 1 && newPage <= totalPages) { this.currentPage = newPage; this.renderTable(); } } toggleSelectAll(checked) { this.selectedKeys.clear(); if (checked) { this.filteredKeys.forEach(key => { const keyId = `${key.server}-${key.public_key}`; this.selectedKeys.add(keyId); }); } this.renderTable(); this.updateBulkDeleteButton(); } updateSelectAllCheckbox() { const selectAllCheckbox = document.getElementById('selectAll'); const visibleKeys = this.filteredKeys.length; const selectedVisibleKeys = this.filteredKeys.filter(key => this.selectedKeys.has(`${key.server}-${key.public_key}`) ).length; if (selectedVisibleKeys === 0) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = false; } else if (selectedVisibleKeys === visibleKeys) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = true; } else { selectAllCheckbox.indeterminate = true; } } updateBulkDeleteButton() { const bulkDeleteBtn = document.getElementById('bulkDeleteBtn'); bulkDeleteBtn.disabled = this.selectedKeys.size === 0; bulkDeleteBtn.textContent = this.selectedKeys.size > 0 ? `Delete Selected (${this.selectedKeys.size})` : 'Delete Selected'; } showAddKeyModal() { if (!this.currentFlow) { this.showToast('Please select a flow first', 'warning'); return; } document.getElementById('serverInput').value = ''; document.getElementById('keyInput').value = ''; this.showModal('addKeyModal'); } async addKey() { const server = document.getElementById('serverInput').value.trim(); const publicKey = document.getElementById('keyInput').value.trim(); if (!server || !publicKey) { this.showToast('Please fill in all fields', 'warning'); return; } if (!this.validateSSHKey(publicKey)) { this.showToast('Invalid SSH key format', 'error'); return; } try { this.showLoading(); const response = await fetch(`/${this.currentFlow}/keys`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify([{ server: server, public_key: publicKey }]) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || 'Failed to add key'); } this.hideModal('addKeyModal'); this.showToast('SSH key added successfully', 'success'); await this.loadKeys(); } catch (error) { this.showToast('Failed to add key: ' + error.message, 'error'); } finally { this.hideLoading(); } } viewKey(keyId) { const key = this.findKeyById(keyId); if (!key) return; document.getElementById('viewServer').textContent = key.server; document.getElementById('viewKey').value = key.public_key; this.showModal('viewKeyModal'); } async deleteKey(keyId) { if (!confirm('Are you sure you want to delete this SSH key?')) { return; } const key = this.findKeyById(keyId); if (!key) return; try { this.showLoading(); const response = await fetch(`/${this.currentFlow}/keys/${encodeURIComponent(key.server)}`, { method: 'DELETE' }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || 'Failed to delete key'); } this.showToast('SSH key deleted successfully', 'success'); await this.loadKeys(); } catch (error) { this.showToast('Failed to delete key: ' + error.message, 'error'); } finally { this.hideLoading(); } } async deleteSelectedKeys() { if (this.selectedKeys.size === 0) return; if (!confirm(`Are you sure you want to delete ${this.selectedKeys.size} selected SSH keys?`)) { return; } try { this.showLoading(); const deletePromises = Array.from(this.selectedKeys).map(keyId => { const key = this.findKeyById(keyId); if (!key) return Promise.resolve(); return fetch(`/${this.currentFlow}/keys/${encodeURIComponent(key.server)}`, { method: 'DELETE' }); }); await Promise.all(deletePromises); this.showToast(`${this.selectedKeys.size} SSH keys deleted successfully`, 'success'); await this.loadKeys(); } catch (error) { this.showToast('Failed to delete selected keys: ' + error.message, 'error'); } finally { this.hideLoading(); } } findKeyById(keyId) { return this.keys.find(key => `${key.server}-${key.public_key}` === keyId); } validateSSHKey(key) { const sshKeyRegex = /^(ssh-rsa|ssh-dss|ecdsa-sha2-nistp(256|384|521)|ssh-ed25519)\s+[A-Za-z0-9+/]+=*(\s+.*)?$/; return sshKeyRegex.test(key.trim()); } getKeyType(publicKey) { if (publicKey.startsWith('ssh-rsa')) return 'RSA'; if (publicKey.startsWith('ssh-ed25519')) return 'ED25519'; if (publicKey.startsWith('ecdsa-sha2-nistp')) return 'ECDSA'; if (publicKey.startsWith('ssh-dss')) return 'DSA'; return 'Unknown'; } getKeyPreview(publicKey) { const parts = publicKey.split(' '); if (parts.length >= 2) { const keyPart = parts[1]; if (keyPart.length > 20) { return keyPart.substring(0, 20) + '...'; } return keyPart; } return publicKey.substring(0, 20) + '...'; } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (m) => map[m]); } copyKeyToClipboard() { const keyTextarea = document.getElementById('viewKey'); keyTextarea.select(); keyTextarea.setSelectionRange(0, 99999); try { document.execCommand('copy'); this.showToast('SSH key copied to clipboard', 'success'); } catch (error) { this.showToast('Failed to copy to clipboard', 'error'); } } clearTable() { document.getElementById('keysTableBody').innerHTML = ''; document.getElementById('noKeysMessage').style.display = 'block'; document.getElementById('totalKeys').textContent = '0'; document.getElementById('uniqueServers').textContent = '0'; this.selectedKeys.clear(); this.updateBulkDeleteButton(); } showModal(modalId) { document.getElementById(modalId).style.display = 'block'; document.body.style.overflow = 'hidden'; } hideModal(modalId) { document.getElementById(modalId).style.display = 'none'; document.body.style.overflow = 'auto'; } showLoading() { document.getElementById('loadingOverlay').style.display = 'block'; } hideLoading() { document.getElementById('loadingOverlay').style.display = 'none'; } showToast(message, type = 'info') { const toastContainer = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); // Trigger animation setTimeout(() => toast.classList.add('show'), 100); // Remove toast after 4 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 4000); } } // Initialize the SSH Key Manager when the page loads document.addEventListener('DOMContentLoaded', () => { window.sshKeyManager = new SSHKeyManager(); });