Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multi-Proxy SSH Manager</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/xterm-addon-attach@0.8.0/dist/xterm-addon-attach.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/dist/xterm-addon-fit.min.js"></script> | |
| <style> | |
| .terminal { | |
| padding: 10px; | |
| height: 100%; | |
| } | |
| .connection-status { | |
| transition: all 0.3s ease; | |
| } | |
| .tab-active { | |
| border-bottom: 3px solid #3b82f6; | |
| color: #3b82f6; | |
| font-weight: 600; | |
| } | |
| #terminal-container { | |
| height: calc(100vh - 180px); | |
| } | |
| .password-field { | |
| position: relative; | |
| } | |
| .password-toggle { | |
| position: absolute; | |
| right: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| cursor: pointer; | |
| color: #6b7280; | |
| } | |
| .server-card { | |
| transition: all 0.2s ease; | |
| } | |
| .server-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .server-card.active { | |
| border-left: 4px solid #3b82f6; | |
| } | |
| .animate-pulse { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <div class="container mx-auto px-4 py-6 max-w-7xl"> | |
| <div class="bg-white rounded-xl shadow-lg overflow-hidden"> | |
| <!-- Header --> | |
| <div class="bg-indigo-600 text-white px-6 py-4 flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-server text-2xl"></i> | |
| <h1 class="text-2xl font-bold">Multi-Proxy SSH Manager</h1> | |
| </div> | |
| <div id="connection-status" class="connection-status bg-gray-500 text-white px-3 py-1 rounded-full text-sm flex items-center"> | |
| <span class="w-3 h-3 rounded-full bg-current mr-2"></span> | |
| <span>Disconnected</span> | |
| </div> | |
| </div> | |
| <!-- Tabs --> | |
| <div class="border-b border-gray-200"> | |
| <div class="flex px-6"> | |
| <button id="servers-tab" class="tab-active px-4 py-3 text-sm font-medium">Servers</button> | |
| <button id="new-server-tab" class="px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700">New Server</button> | |
| <button id="settings-tab" class="px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700">Settings</button> | |
| </div> | |
| </div> | |
| <!-- Servers Panel --> | |
| <div id="servers-panel" class="p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-lg font-medium text-gray-900">Your Proxy Servers</h2> | |
| <div class="relative"> | |
| <input type="text" id="server-search" placeholder="Search servers..." class="pl-8 pr-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <div id="servers-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| <!-- Server cards will be added here dynamically --> | |
| <div class="text-center py-10 text-gray-500" id="no-servers-message"> | |
| <i class="fas fa-server text-4xl mb-3"></i> | |
| <p>No servers configured yet</p> | |
| <button id="add-first-server" class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| <i class="fas fa-plus mr-2"></i> Add Your First Server | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- New Server Panel --> | |
| <div id="new-server-panel" class="p-6 hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-lg font-medium text-gray-900">Add New Proxy Server</h2> | |
| <button id="auto-generate-btn" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| <i class="fas fa-bolt mr-1"></i> Auto Generate | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <div class="mb-4"> | |
| <label for="server-name" class="block text-sm font-medium text-gray-700">Server Name</label> | |
| <input type="text" id="server-name" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="My Production Server"> | |
| </div> | |
| <div class="space-y-4"> | |
| <h3 class="text-md font-medium text-gray-900">SSH Server Details</h3> | |
| <div> | |
| <label for="ssh-host" class="block text-sm font-medium text-gray-700">Host</label> | |
| <input type="text" id="ssh-host" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="example.com"> | |
| </div> | |
| <div> | |
| <label for="ssh-port" class="block text-sm font-medium text-gray-700">Port</label> | |
| <input type="number" id="ssh-port" value="22" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| </div> | |
| <div> | |
| <label for="ssh-username" class="block text-sm font-medium text-gray-700">Username</label> | |
| <input type="text" id="ssh-username" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="user"> | |
| </div> | |
| <div class="password-field"> | |
| <label for="ssh-password" class="block text-sm font-medium text-gray-700">Password</label> | |
| <input type="password" id="ssh-password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="••••••••"> | |
| <span class="password-toggle" id="toggle-ssh-password"> | |
| <i class="far fa-eye"></i> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="text-md font-medium text-gray-900 mb-4">Proxy Server Details</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="proxy-type" class="block text-sm font-medium text-gray-700">Proxy Type</label> | |
| <select id="proxy-type" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| <option value="http">HTTP/HTTPS</option> | |
| <option value="socks4">SOCKS4</option> | |
| <option value="socks5">SOCKS5</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="proxy-host" class="block text-sm font-medium text-gray-700">Proxy Host</label> | |
| <input type="text" id="proxy-host" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="proxy.example.com"> | |
| </div> | |
| <div> | |
| <label for="proxy-port" class="block text-sm font-medium text-gray-700">Proxy Port</label> | |
| <input type="number" id="proxy-port" value="8080" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| </div> | |
| <div> | |
| <label for="proxy-username" class="block text-sm font-medium text-gray-700">Proxy Username (optional)</label> | |
| <input type="text" id="proxy-username" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| </div> | |
| <div class="password-field"> | |
| <label for="proxy-password" class="block text-sm font-medium text-gray-700">Proxy Password (optional)</label> | |
| <input type="password" id="proxy-password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| <span class="password-toggle" id="toggle-proxy-password"> | |
| <i class="far fa-eye"></i> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-end space-x-3"> | |
| <button id="cancel-add-server" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| Cancel | |
| </button> | |
| <button id="save-server" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| <i class="fas fa-save mr-2"></i> Save Server | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Settings Panel --> | |
| <div id="settings-panel" class="p-6 hidden"> | |
| <h2 class="text-lg font-medium text-gray-900 mb-4">Application Settings</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="terminal-theme" class="block text-sm font-medium text-gray-700">Terminal Theme</label> | |
| <select id="terminal-theme" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> | |
| <option value="default">Default (Dark)</option> | |
| <option value="light">Light</option> | |
| <option value="solarized-dark">Solarized Dark</option> | |
| <option value="solarized-light">Solarized Light</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="terminal-font" class="block text-sm font-medium text-gray-700">Terminal Font Size</label> | |
| <input type="range" id="terminal-font" min="10" max="24" value="14" class="mt-1 block w-full"> | |
| <div class="text-sm text-gray-500 mt-1" id="font-size-display">Font Size: 14px</div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex items-center h-5"> | |
| <input id="bell-sound" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"> | |
| </div> | |
| <div class="ml-3 text-sm"> | |
| <label for="bell-sound" class="font-medium text-gray-700">Enable Terminal Bell Sound</label> | |
| <p class="text-gray-500">Play sound when terminal bell character is received</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-8"> | |
| <h3 class="text-md font-medium text-gray-900 mb-2">Data Management</h3> | |
| <div class="space-y-2"> | |
| <button id="export-servers" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| <i class="fas fa-file-export mr-2"></i> Export Server Configurations | |
| </button> | |
| <button id="import-servers" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| <i class="fas fa-file-import mr-2"></i> Import Server Configurations | |
| </button> | |
| <input type="file" id="import-file" class="hidden" accept=".json"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Terminal Container --> | |
| <div id="terminal-container" class="bg-black hidden"> | |
| <div id="terminal" class="terminal"></div> | |
| </div> | |
| </div> | |
| <div class="mt-4 text-center text-sm text-gray-500"> | |
| <p>Multi-Proxy SSH Manager v1.1 - All connections are client-side only</p> | |
| </div> | |
| </div> | |
| <!-- Server Details Modal --> | |
| <div id="server-details-modal" class="fixed inset-0 z-50 hidden overflow-y-auto"> | |
| <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 transition-opacity" aria-hidden="true"> | |
| <div class="absolute inset-0 bg-gray-500 opacity-75"></div> | |
| </div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="sm:flex sm:items-start"> | |
| <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-100 sm:mx-0 sm:h-10 sm:w-10"> | |
| <i class="fas fa-server text-indigo-600"></i> | |
| </div> | |
| <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> | |
| <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">Server Details</h3> | |
| <div class="mt-4"> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Server Name</p> | |
| <p id="modal-server-name" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Status</p> | |
| <p id="modal-server-status" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">SSH Host</p> | |
| <p id="modal-ssh-host" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">SSH Port</p> | |
| <p id="modal-ssh-port" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Proxy Type</p> | |
| <p id="modal-proxy-type" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Proxy Host</p> | |
| <p id="modal-proxy-host" class="mt-1 text-sm text-gray-900"></p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | |
| <button type="button" id="connect-server" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Connect | |
| </button> | |
| <button type="button" id="edit-server" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Edit | |
| </button> | |
| <button type="button" id="delete-server" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Delete | |
| </button> | |
| <button type="button" id="close-modal" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> | |
| Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // IndexedDB Database Manager | |
| class DatabaseManager { | |
| constructor() { | |
| this.dbName = 'SSHProxyManager'; | |
| this.dbVersion = 1; | |
| this.storeName = 'servers'; | |
| this.db = null; | |
| } | |
| async init() { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open(this.dbName, this.dbVersion); | |
| request.onerror = (event) => { | |
| console.error('Database error:', event.target.error); | |
| reject(event.target.error); | |
| }; | |
| request.onsuccess = (event) => { | |
| this.db = event.target.result; | |
| resolve(); | |
| }; | |
| request.onupgradeneeded = (event) => { | |
| const db = event.target.result; | |
| if (!db.objectStoreNames.contains(this.storeName)) { | |
| const store = db.createObjectStore(this.storeName, { keyPath: 'id' }); | |
| // Create indexes for searching | |
| store.createIndex('name', 'name', { unique: false }); | |
| store.createIndex('sshHost', 'sshHost', { unique: false }); | |
| store.createIndex('status', 'status', { unique: false }); | |
| } | |
| }; | |
| }); | |
| } | |
| async getAllServers() { | |
| return new Promise((resolve, reject) => { | |
| if (!this.db) { | |
| reject(new Error('Database not initialized')); | |
| return; | |
| } | |
| const transaction = this.db.transaction([this.storeName], 'readonly'); | |
| const store = transaction.objectStore(this.storeName); | |
| const request = store.getAll(); | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async saveServer(server) { | |
| return new Promise((resolve, reject) => { | |
| const transaction = this.db.transaction([this.storeName], 'readwrite'); | |
| const store = transaction.objectStore(this.storeName); | |
| const request = store.put(server); | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async deleteServer(serverId) { | |
| return new Promise((resolve, reject) => { | |
| const transaction = this.db.transaction([this.storeName], 'readwrite'); | |
| const store = transaction.objectStore(this.storeName); | |
| const request = store.delete(serverId); | |
| request.onsuccess = () => resolve(); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async searchServers(query) { | |
| return new Promise((resolve, reject) => { | |
| const transaction = this.db.transaction([this.storeName], 'readonly'); | |
| const store = transaction.objectStore(this.storeName); | |
| const nameIndex = store.index('name'); | |
| const request = nameIndex.getAll(); | |
| request.onsuccess = () => { | |
| const servers = request.result; | |
| const results = servers.filter(server => | |
| server.name.toLowerCase().includes(query.toLowerCase()) || | |
| server.sshHost.toLowerCase().includes(query.toLowerCase()) | |
| ); | |
| resolve(results); | |
| }; | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async updateServerStatus(serverId, status) { | |
| return new Promise((resolve, reject) => { | |
| const transaction = this.db.transaction([this.storeName], 'readwrite'); | |
| const store = transaction.objectStore(this.storeName); | |
| const request = store.get(serverId); | |
| request.onsuccess = () => { | |
| const server = request.result; | |
| if (server) { | |
| server.status = status; | |
| server.lastStateChange = new Date().toISOString(); | |
| const updateRequest = store.put(server); | |
| updateRequest.onsuccess = () => resolve(server); | |
| updateRequest.onerror = () => reject(updateRequest.error); | |
| } else { | |
| reject(new Error('Server not found')); | |
| } | |
| }; | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| } | |
| // Initialize database manager | |
| const dbManager = new DatabaseManager(); | |
| let servers = []; // Keep this for compatibility with existing code | |
| // Server management | |
| let currentServerId = null; | |
| let term; | |
| let socket; | |
| let fitAddon; | |
| // Add connection state management | |
| const ConnectionState = { | |
| DISCONNECTED: 'disconnected', | |
| CONNECTING: 'connecting', | |
| CONNECTED: 'connected', | |
| ERROR: 'error' | |
| }; | |
| // Add session management | |
| class SSHSession { | |
| constructor(server) { | |
| this.server = server; | |
| this.term = null; | |
| this.socket = null; | |
| this.connectionStartTime = null; | |
| this.lastActivity = null; | |
| } | |
| start() { | |
| this.connectionStartTime = new Date(); | |
| this.updateActivity(); | |
| // Initialize terminal and connection | |
| } | |
| updateActivity() { | |
| this.lastActivity = new Date(); | |
| } | |
| isIdle(timeoutMinutes = 30) { | |
| if (!this.lastActivity) return false; | |
| const idleTime = (new Date() - this.lastActivity) / 1000 / 60; | |
| return idleTime > timeoutMinutes; | |
| } | |
| } | |
| // Add terminal history and command completion | |
| class TerminalHistory { | |
| constructor(maxSize = 100) { | |
| this.history = []; | |
| this.maxSize = maxSize; | |
| this.position = -1; | |
| } | |
| add(command) { | |
| if (command && command.trim()) { | |
| this.history.unshift(command); | |
| if (this.history.length > this.maxSize) { | |
| this.history.pop(); | |
| } | |
| } | |
| this.position = -1; | |
| } | |
| previous() { | |
| if (this.position < this.history.length - 1) { | |
| this.position++; | |
| return this.history[this.position]; | |
| } | |
| return null; | |
| } | |
| next() { | |
| if (this.position > 0) { | |
| this.position--; | |
| return this.history[this.position]; | |
| } else if (this.position === 0) { | |
| this.position = -1; | |
| return ''; | |
| } | |
| return null; | |
| } | |
| } | |
| // Initialize the app | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| try { | |
| await dbManager.init(); | |
| servers = await dbManager.getAllServers(); | |
| renderServers(); | |
| setupEventListeners(); | |
| } catch (error) { | |
| console.error('Failed to initialize database:', error); | |
| showNotification('Failed to load server configurations', 'red'); | |
| } | |
| }); | |
| // Tab switching | |
| function setupEventListeners() { | |
| document.getElementById('servers-tab').addEventListener('click', () => { | |
| document.getElementById('servers-panel').classList.remove('hidden'); | |
| document.getElementById('new-server-panel').classList.add('hidden'); | |
| document.getElementById('settings-panel').classList.add('hidden'); | |
| document.getElementById('servers-tab').classList.add('tab-active'); | |
| document.getElementById('new-server-tab').classList.remove('tab-active'); | |
| document.getElementById('settings-tab').classList.remove('tab-active'); | |
| }); | |
| document.getElementById('new-server-tab').addEventListener('click', () => { | |
| document.getElementById('servers-panel').classList.add('hidden'); | |
| document.getElementById('new-server-panel').classList.remove('hidden'); | |
| document.getElementById('settings-panel').classList.add('hidden'); | |
| document.getElementById('servers-tab').classList.remove('tab-active'); | |
| document.getElementById('new-server-tab').classList.add('tab-active'); | |
| document.getElementById('settings-tab').classList.remove('tab-active'); | |
| resetForm(); | |
| }); | |
| document.getElementById('settings-tab').addEventListener('click', () => { | |
| document.getElementById('servers-panel').classList.add('hidden'); | |
| document.getElementById('new-server-panel').classList.add('hidden'); | |
| document.getElementById('settings-panel').classList.remove('hidden'); | |
| document.getElementById('servers-tab').classList.remove('tab-active'); | |
| document.getElementById('new-server-tab').classList.remove('tab-active'); | |
| document.getElementById('settings-tab').classList.add('tab-active'); | |
| }); | |
| // Font size slider | |
| document.getElementById('terminal-font').addEventListener('input', (e) => { | |
| const size = e.target.value; | |
| document.getElementById('font-size-display').textContent = `Font Size: ${size}px`; | |
| if (term) { | |
| term.setOption('fontSize', size); | |
| } | |
| }); | |
| // Password toggle functionality | |
| function setupPasswordToggle(toggleId, inputId) { | |
| const toggle = document.getElementById(toggleId); | |
| const input = document.getElementById(inputId); | |
| toggle.addEventListener('click', () => { | |
| if (input.type === 'password') { | |
| input.type = 'text'; | |
| toggle.innerHTML = '<i class="far fa-eye-slash"></i>'; | |
| } else { | |
| input.type = 'password'; | |
| toggle.innerHTML = '<i class="far fa-eye"></i>'; | |
| } | |
| }); | |
| } | |
| setupPasswordToggle('toggle-ssh-password', 'ssh-password'); | |
| setupPasswordToggle('toggle-proxy-password', 'proxy-password'); | |
| // Terminal theme change | |
| document.getElementById('terminal-theme').addEventListener('change', updateTerminalTheme); | |
| // Add first server button | |
| document.getElementById('add-first-server').addEventListener('click', () => { | |
| document.getElementById('servers-tab').click(); | |
| document.getElementById('new-server-tab').click(); | |
| }); | |
| // Save server button | |
| document.getElementById('save-server').addEventListener('click', saveServer); | |
| // Cancel add server | |
| document.getElementById('cancel-add-server').addEventListener('click', () => { | |
| document.getElementById('servers-tab').click(); | |
| }); | |
| // Auto generate credentials | |
| document.getElementById('auto-generate-btn').addEventListener('click', autoGenerateCredentials); | |
| // Server search | |
| document.getElementById('server-search').addEventListener('input', (e) => { | |
| debouncedSearch(e.target.value); | |
| }); | |
| // Modal buttons | |
| const modalButtons = { | |
| 'connect-server': connectToServer, | |
| 'edit-server': editServer, | |
| 'delete-server': deleteServer, | |
| 'close-modal': closeModal | |
| }; | |
| // Add event listeners to modal buttons | |
| Object.entries(modalButtons).forEach(([id, handler]) => { | |
| const button = document.getElementById(id); | |
| if (button) { | |
| button.addEventListener('click', handler); | |
| } | |
| }); | |
| // Add keyboard event listener for modal | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| closeModal(); | |
| } | |
| }); | |
| // Add click event listener for modal backdrop | |
| const modal = document.getElementById('server-details-modal'); | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) { | |
| closeModal(); | |
| } | |
| }); | |
| // Data management | |
| document.getElementById('export-servers').addEventListener('click', exportServers); | |
| document.getElementById('import-servers').addEventListener('click', () => { | |
| document.getElementById('import-file').click(); | |
| }); | |
| document.getElementById('import-file').addEventListener('change', importServers); | |
| // Add accessibility | |
| addAccessibility(); | |
| } | |
| // Render server cards | |
| function renderServers() { | |
| const container = document.getElementById('servers-container'); | |
| const noServersMessage = document.getElementById('no-servers-message'); | |
| if (servers.length === 0) { | |
| noServersMessage.classList.remove('hidden'); | |
| container.innerHTML = ''; | |
| container.appendChild(noServersMessage); | |
| return; | |
| } | |
| noServersMessage.classList.add('hidden'); | |
| container.innerHTML = ''; | |
| servers.forEach(server => { | |
| const card = document.createElement('div'); | |
| card.className = 'server-card bg-white rounded-lg border border-gray-200 p-4 cursor-pointer'; | |
| card.dataset.id = server.id; | |
| card.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="server-name text-lg font-medium text-gray-900">${server.name}</h3> | |
| <p class="text-sm text-gray-500 mt-1">${server.sshHost}:${server.sshPort}</p> | |
| </div> | |
| <span class="px-2 py-1 text-xs rounded-full ${server.status === 'connected' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}"> | |
| ${server.status || 'disconnected'} | |
| </span> | |
| </div> | |
| <div class="mt-4 flex justify-between items-center"> | |
| <div> | |
| <p class="text-xs text-gray-500">Proxy: ${server.proxyType.toUpperCase()}</p> | |
| <p class="text-xs text-gray-500">${server.proxyHost}:${server.proxyPort}</p> | |
| </div> | |
| <button class="details-btn p-1 text-gray-400 hover:text-gray-500"> | |
| <i class="fas fa-ellipsis-h"></i> | |
| </button> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| // Add click event to card | |
| card.addEventListener('click', (e) => { | |
| if (!e.target.classList.contains('details-btn') && !e.target.closest('.details-btn')) { | |
| showServerDetails(server.id); | |
| } | |
| }); | |
| // Add click event to details button | |
| const detailsBtn = card.querySelector('.details-btn'); | |
| detailsBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| showServerDetails(server.id); | |
| }); | |
| }); | |
| } | |
| // Show server details modal | |
| function showServerDetails(serverId) { | |
| const server = servers.find(s => s.id === serverId); | |
| if (!server) return; | |
| currentServerId = serverId; | |
| // Update modal content | |
| document.getElementById('modal-server-name').textContent = server.name; | |
| document.getElementById('modal-server-status').textContent = server.status || 'disconnected'; | |
| document.getElementById('modal-ssh-host').textContent = server.sshHost; | |
| document.getElementById('modal-ssh-port').textContent = server.sshPort; | |
| document.getElementById('modal-proxy-type').textContent = server.proxyType.toUpperCase(); | |
| document.getElementById('modal-proxy-host').textContent = `${server.proxyHost}:${server.proxyPort}`; | |
| // Update connect button based on status | |
| const connectBtn = document.getElementById('connect-server'); | |
| if (server.status === 'connected') { | |
| connectBtn.innerHTML = '<i class="fas fa-power-off mr-2"></i> Disconnect'; | |
| connectBtn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); | |
| connectBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| } else { | |
| connectBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Connect'; | |
| connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700'); | |
| } | |
| // Show modal | |
| document.getElementById('server-details-modal').classList.remove('hidden'); | |
| } | |
| // Close modal | |
| function closeModal() { | |
| document.getElementById('server-details-modal').classList.add('hidden'); | |
| currentServerId = null; | |
| } | |
| // Connect to server | |
| function connectToServer() { | |
| if (!currentServerId) return; | |
| const server = servers.find(s => s.id === currentServerId); | |
| if (!server) return; | |
| if (server.status === 'connected') { | |
| // Disconnect | |
| disconnectFromServer(server.id); | |
| } else { | |
| // Connect | |
| const connectBtn = document.getElementById('connect-server'); | |
| connectBtn.disabled = true; | |
| connectBtn.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-2"></i> Connecting...'; | |
| // Update status | |
| updateConnectionState(server.id, ConnectionState.CONNECTING); | |
| // Simulate connection (in a real app, this would connect to your backend) | |
| setTimeout(() => { | |
| updateConnectionState(server.id, ConnectionState.CONNECTED); | |
| connectBtn.innerHTML = '<i class="fas fa-power-off mr-2"></i> Disconnect'; | |
| connectBtn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700'); | |
| connectBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.disabled = false; | |
| // Initialize terminal if not already done | |
| if (!term) { | |
| initTerminal(); | |
| } | |
| // Write welcome message to terminal | |
| term.writeln(`\x1b[32mConnected to ${server.sshHost} through ${server.proxyType.toUpperCase()} proxy\x1b[0m`); | |
| term.writeln('\x1b[33mThis is a simulation - in a real app, this would be a live SSH session\x1b[0m'); | |
| term.writeln(''); | |
| // Prompt | |
| term.write('$ '); | |
| // Handle user input (simulated) | |
| term.onData(data => { | |
| term.write(data); | |
| if (data === '\r') { | |
| term.write('\r\n$ '); | |
| } | |
| }); | |
| // Update connection status in header | |
| const statusElement = document.getElementById('connection-status'); | |
| statusElement.className = 'connection-status bg-green-500 text-white px-3 py-1 rounded-full text-sm flex items-center'; | |
| statusElement.innerHTML = '<span class="w-3 h-3 rounded-full bg-current mr-2"></span><span>Connected to ' + server.name + '</span>'; | |
| // Show terminal | |
| document.getElementById('terminal-container').classList.remove('hidden'); | |
| // Close modal after successful connection | |
| closeModal(); | |
| }, 1500); | |
| } | |
| } | |
| // Disconnect from server | |
| function disconnectFromServer(serverId) { | |
| const server = servers.find(s => s.id === serverId); | |
| if (!server) return; | |
| updateConnectionState(serverId, ConnectionState.DISCONNECTED); | |
| // Update modal | |
| const connectBtn = document.getElementById('connect-server'); | |
| connectBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Connect'; | |
| connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700'); | |
| // Update connection status in header | |
| const statusElement = document.getElementById('connection-status'); | |
| statusElement.className = 'connection-status bg-gray-500 text-white px-3 py-1 rounded-full text-sm flex items-center'; | |
| statusElement.innerHTML = '<span class="w-3 h-3 rounded-full bg-current mr-2"></span><span>Disconnected</span>'; | |
| // Hide terminal container | |
| document.getElementById('terminal-container').classList.add('hidden'); | |
| // In a real app, close the connection here | |
| if (term) { | |
| term.writeln('\r\n\x1b[31mConnection closed\x1b[0m'); | |
| term = null; | |
| } | |
| // Close modal | |
| closeModal(); | |
| // Show notification | |
| showNotification('Disconnected from server', 'blue'); | |
| } | |
| // Edit server | |
| function editServer() { | |
| if (!currentServerId) return; | |
| const server = servers.find(s => s.id === currentServerId); | |
| if (!server) return; | |
| // Fill form with server data | |
| document.getElementById('server-name').value = server.name; | |
| document.getElementById('ssh-host').value = server.sshHost; | |
| document.getElementById('ssh-port').value = server.sshPort; | |
| document.getElementById('ssh-username').value = server.sshUsername; | |
| document.getElementById('ssh-password').value = server.sshPassword || ''; | |
| document.getElementById('proxy-type').value = server.proxyType; | |
| document.getElementById('proxy-host').value = server.proxyHost; | |
| document.getElementById('proxy-port').value = server.proxyPort; | |
| document.getElementById('proxy-username').value = server.proxyUsername || ''; | |
| document.getElementById('proxy-password').value = server.proxyPassword || ''; | |
| // Switch to edit mode | |
| document.getElementById('new-server-tab').click(); | |
| const saveBtn = document.getElementById('save-server'); | |
| saveBtn.dataset.mode = 'edit'; | |
| saveBtn.dataset.id = server.id; | |
| // Close modal | |
| closeModal(); | |
| // Show notification | |
| showNotification('Editing server configuration', 'blue'); | |
| } | |
| // Delete server | |
| async function deleteServer() { | |
| if (!currentServerId) return; | |
| const server = servers.find(s => s.id === currentServerId); | |
| if (!server) return; | |
| if (confirm(`Are you sure you want to delete the server "${server.name}"?`)) { | |
| try { | |
| if (server.status === 'connected') { | |
| disconnectFromServer(server.id); | |
| } | |
| // Delete from IndexedDB | |
| await dbManager.deleteServer(currentServerId); | |
| // Update local array | |
| servers = servers.filter(s => s.id !== currentServerId); | |
| renderServers(); | |
| closeModal(); | |
| showNotification('Server configuration deleted', 'green'); | |
| } catch (error) { | |
| console.error('Failed to delete server:', error); | |
| showNotification('Failed to delete server configuration', 'red'); | |
| } | |
| } | |
| } | |
| // Save server | |
| async function saveServer() { | |
| const mode = this.dataset.mode; | |
| const serverId = this.dataset.id; | |
| const server = { | |
| name: document.getElementById('server-name').value.trim(), | |
| sshHost: document.getElementById('ssh-host').value.trim(), | |
| sshPort: document.getElementById('ssh-port').value.trim(), | |
| sshUsername: document.getElementById('ssh-username').value.trim(), | |
| sshPassword: document.getElementById('ssh-password').value, | |
| proxyType: document.getElementById('proxy-type').value, | |
| proxyHost: document.getElementById('proxy-host').value.trim(), | |
| proxyPort: document.getElementById('proxy-port').value.trim(), | |
| proxyUsername: document.getElementById('proxy-username').value.trim(), | |
| proxyPassword: document.getElementById('proxy-password').value, | |
| status: 'disconnected', | |
| lastConnected: null | |
| }; | |
| // Validate required fields | |
| if (!server.name || !server.sshHost || !server.sshUsername || !server.proxyHost) { | |
| showNotification('Please fill in all required fields', 'red'); | |
| return; | |
| } | |
| try { | |
| // Encrypt sensitive data | |
| const sensitiveData = { | |
| sshPassword: server.sshPassword, | |
| proxyPassword: server.proxyPassword | |
| }; | |
| server.encryptedCredentials = await encryptCredentials(sensitiveData); | |
| delete server.sshPassword; | |
| delete server.proxyPassword; | |
| if (mode === 'edit') { | |
| server.id = serverId; | |
| } else { | |
| server.id = Date.now().toString(); | |
| } | |
| // Save to IndexedDB | |
| await dbManager.saveServer(server); | |
| // Update local array | |
| if (mode === 'edit') { | |
| const index = servers.findIndex(s => s.id === serverId); | |
| if (index !== -1) { | |
| servers[index] = server; | |
| } | |
| } else { | |
| servers.push(server); | |
| } | |
| renderServers(); | |
| document.getElementById('servers-tab').click(); | |
| showNotification(`Server ${mode === 'edit' ? 'updated' : 'added'} successfully`, 'green'); | |
| resetForm(); | |
| } catch (error) { | |
| console.error('Failed to save server:', error); | |
| showNotification('Failed to save server configuration', 'red'); | |
| } | |
| } | |
| // Reset form | |
| function resetForm() { | |
| document.getElementById('server-name').value = ''; | |
| document.getElementById('ssh-host').value = ''; | |
| document.getElementById('ssh-port').value = '22'; | |
| document.getElementById('ssh-username').value = ''; | |
| document.getElementById('ssh-password').value = ''; | |
| document.getElementById('proxy-type').value = 'http'; | |
| document.getElementById('proxy-host').value = ''; | |
| document.getElementById('proxy-port').value = '8080'; | |
| document.getElementById('proxy-username').value = ''; | |
| document.getElementById('proxy-password').value = ''; | |
| // Remove edit mode if exists | |
| const saveBtn = document.getElementById('save-server'); | |
| if (saveBtn.dataset.mode) { | |
| delete saveBtn.dataset.mode; | |
| delete saveBtn.dataset.id; | |
| } | |
| } | |
| // Auto generate credentials | |
| function autoGenerateCredentials() { | |
| // Generate random username (adjective + noun) | |
| const adjectives = ['happy', 'quick', 'silent', 'clever', 'brave', 'gentle', 'jolly', 'lucky', 'merry', 'proud']; | |
| const nouns = ['panda', 'tiger', 'eagle', 'dolphin', 'koala', 'raven', 'fox', 'wolf', 'bear', 'owl']; | |
| const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]; | |
| const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; | |
| const randomNumber = Math.floor(Math.random() * 100); | |
| const username = `${randomAdjective}_${randomNoun}${randomNumber}`; | |
| // Generate random password (12 characters) | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; | |
| let password = ''; | |
| for (let i = 0; i < 12; i++) { | |
| password += chars.charAt(Math.floor(Math.random() * chars.length)); | |
| } | |
| // Set the values | |
| document.getElementById('server-name').value = `${randomAdjective} ${randomNoun} server`; | |
| document.getElementById('ssh-host').value = 'demo.webssh-proxy.com'; | |
| document.getElementById('ssh-port').value = '22'; | |
| document.getElementById('ssh-username').value = username; | |
| document.getElementById('ssh-password').value = password; | |
| document.getElementById('proxy-host').value = 'proxy.demo.webssh-proxy.com'; | |
| // Show notification | |
| showNotification('Demo credentials generated! These will work for 1 hour.', 'blue'); | |
| } | |
| // Save servers to localStorage | |
| function saveServers() { | |
| localStorage.setItem('sshProxyServers', JSON.stringify(servers)); | |
| } | |
| // Update server status | |
| async function updateConnectionState(serverId, state, error = null) { | |
| try { | |
| const updatedServer = await dbManager.updateServerStatus(serverId, state); | |
| const index = servers.findIndex(s => s.id === serverId); | |
| if (index !== -1) { | |
| servers[index] = updatedServer; | |
| } | |
| renderServers(); | |
| updateUIForConnectionState(state, error); | |
| } catch (error) { | |
| console.error('Failed to update server status:', error); | |
| showNotification('Failed to update server status', 'red'); | |
| } | |
| } | |
| // Export servers | |
| async function exportServers() { | |
| try { | |
| const allServers = await dbManager.getAllServers(); | |
| const dataStr = JSON.stringify(allServers, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
| const exportName = 'ssh-proxy-servers-' + new Date().toISOString().slice(0, 10) + '.json'; | |
| const linkElement = document.createElement('a'); | |
| linkElement.setAttribute('href', dataUri); | |
| linkElement.setAttribute('download', exportName); | |
| linkElement.click(); | |
| showNotification('Server configurations exported successfully', 'green'); | |
| } catch (error) { | |
| console.error('Failed to export servers:', error); | |
| showNotification('Failed to export server configurations', 'red'); | |
| } | |
| } | |
| // Import servers | |
| async function importServers(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = async (e) => { | |
| try { | |
| const importedServers = JSON.parse(e.target.result); | |
| if (Array.isArray(importedServers) && importedServers.length > 0) { | |
| // Save each server to IndexedDB | |
| for (const server of importedServers) { | |
| await dbManager.saveServer(server); | |
| } | |
| // Update local array | |
| servers = await dbManager.getAllServers(); | |
| renderServers(); | |
| showNotification(`Imported ${importedServers.length} server configuration(s)`, 'green'); | |
| } else { | |
| showNotification('No valid server configurations found', 'yellow'); | |
| } | |
| } catch (error) { | |
| console.error('Failed to import servers:', error); | |
| showNotification('Failed to import server configurations', 'red'); | |
| } | |
| event.target.value = ''; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Show notification | |
| function showNotification(message, color) { | |
| const colors = { | |
| green: 'bg-green-500', | |
| blue: 'bg-blue-500', | |
| yellow: 'bg-yellow-500', | |
| red: 'bg-red-500' | |
| }; | |
| const notification = document.createElement('div'); | |
| notification.className = `fixed bottom-4 right-4 ${colors[color] || 'bg-gray-500'} text-white px-4 py-2 rounded-md shadow-lg flex items-center animate-pulse`; | |
| notification.innerHTML = ` | |
| <i class="fas fa-${color === 'green' ? 'check-circle' : color === 'red' ? 'exclamation-circle' : 'info-circle'} mr-2"></i> | |
| <span>${message}</span> | |
| `; | |
| document.body.appendChild(notification); | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| notification.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => notification.remove(), 300); | |
| }, 3000); | |
| } | |
| // Terminal initialization | |
| function initTerminal() { | |
| // Create terminal | |
| term = new Terminal({ | |
| cursorBlink: true, | |
| fontSize: parseInt(document.getElementById('terminal-font').value), | |
| theme: { | |
| background: '#000000', | |
| foreground: '#ffffff' | |
| } | |
| }); | |
| // Load theme | |
| updateTerminalTheme(); | |
| // Addons | |
| fitAddon = new FitAddon.FitAddon(); | |
| term.loadAddon(fitAddon); | |
| // Open terminal in container | |
| const terminalContainer = document.getElementById('terminal-container'); | |
| terminalContainer.classList.remove('hidden'); | |
| term.open(document.getElementById('terminal')); | |
| // Fit terminal to container | |
| fitAddon.fit(); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| fitAddon.fit(); | |
| }); | |
| } | |
| function updateTerminalTheme() { | |
| if (!term) return; | |
| const theme = document.getElementById('terminal-theme').value; | |
| let themeConfig; | |
| switch(theme) { | |
| case 'light': | |
| themeConfig = { | |
| background: '#ffffff', | |
| foreground: '#000000', | |
| cursor: '#000000', | |
| selection: 'rgba(0, 0, 0, 0.3)' | |
| }; | |
| break; | |
| case 'solarized-dark': | |
| themeConfig = { | |
| background: '#002b36', | |
| foreground: '#839496', | |
| cursor: '#839496', | |
| selection: 'rgba(0, 43, 54, 0.5)' | |
| }; | |
| break; | |
| case 'solarized-light': | |
| themeConfig = { | |
| background: '#fdf6e3', | |
| foreground: '#657b83', | |
| cursor: '#657b83', | |
| selection: 'rgba(253, 246, 227, 0.5)' | |
| }; | |
| break; | |
| default: // default dark | |
| themeConfig = { | |
| background: '#000000', | |
| foreground: '#ffffff', | |
| cursor: '#ffffff', | |
| selection: 'rgba(255, 255, 255, 0.3)' | |
| }; | |
| } | |
| term.setOption('theme', themeConfig); | |
| } | |
| // Enhanced encryption using WebCrypto API | |
| async function encryptCredentials(data) { | |
| const encoder = new TextEncoder(); | |
| const key = await window.crypto.subtle.generateKey( | |
| { | |
| name: "AES-GCM", | |
| length: 256 | |
| }, | |
| true, | |
| ["encrypt", "decrypt"] | |
| ); | |
| const iv = window.crypto.getRandomValues(new Uint8Array(12)); | |
| const encoded = encoder.encode(JSON.stringify(data)); | |
| const encrypted = await window.crypto.subtle.encrypt( | |
| { | |
| name: "AES-GCM", | |
| iv: iv | |
| }, | |
| key, | |
| encoded | |
| ); | |
| return { | |
| encrypted: Array.from(new Uint8Array(encrypted)), | |
| iv: Array.from(iv), | |
| key: await exportKey(key) | |
| }; | |
| } | |
| // Enhanced error handling | |
| function handleConnectionError(error, serverId) { | |
| const errorTypes = { | |
| NETWORK_ERROR: 'network', | |
| AUTH_ERROR: 'authentication', | |
| PROXY_ERROR: 'proxy', | |
| TIMEOUT: 'timeout' | |
| }; | |
| const errorMessages = { | |
| [errorTypes.NETWORK_ERROR]: 'Network connection failed', | |
| [errorTypes.AUTH_ERROR]: 'Authentication failed', | |
| [errorTypes.PROXY_ERROR]: 'Proxy connection failed', | |
| [errorTypes.TIMEOUT]: 'Connection timed out' | |
| }; | |
| updateConnectionState(serverId, ConnectionState.ERROR, { | |
| type: error.type, | |
| message: errorMessages[error.type] || 'Unknown error occurred', | |
| timestamp: new Date().toISOString(), | |
| details: error.details | |
| }); | |
| showNotification(errorMessages[error.type] || 'Connection error', 'red'); | |
| } | |
| // Add debouncing for search | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| const debouncedSearch = debounce(async (searchTerm) => { | |
| try { | |
| const results = await dbManager.searchServers(searchTerm); | |
| const serverCards = document.querySelectorAll('.server-card'); | |
| serverCards.forEach(card => { | |
| const serverId = card.dataset.id; | |
| const found = results.some(server => server.id === serverId); | |
| card.classList.toggle('hidden', !found); | |
| }); | |
| } catch (error) { | |
| console.error('Failed to search servers:', error); | |
| showNotification('Failed to search servers', 'red'); | |
| } | |
| }, 300); | |
| // Add accessibility | |
| function addAccessibility() { | |
| // Add ARIA labels | |
| const serverCards = document.querySelectorAll('.server-card'); | |
| serverCards.forEach(card => { | |
| card.setAttribute('role', 'button'); | |
| card.setAttribute('tabindex', '0'); | |
| card.setAttribute('aria-label', `Connect to ${card.querySelector('.server-name').textContent}`); | |
| // Add keyboard navigation | |
| card.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| card.click(); | |
| } | |
| }); | |
| }); | |
| } | |
| // Add input validation | |
| const ValidationRules = { | |
| serverName: { | |
| minLength: 3, | |
| maxLength: 50, | |
| pattern: /^[a-zA-Z0-9\s-_]+$/ | |
| }, | |
| host: { | |
| pattern: /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ | |
| }, | |
| port: { | |
| min: 1, | |
| max: 65535 | |
| } | |
| }; | |
| function validateServerData(server) { | |
| const errors = []; | |
| if (!ValidationRules.serverName.pattern.test(server.name)) { | |
| errors.push('Invalid server name format'); | |
| } | |
| if (!ValidationRules.host.pattern.test(server.sshHost)) { | |
| errors.push('Invalid SSH host format'); | |
| } | |
| const port = parseInt(server.sshPort); | |
| if (isNaN(port) || port < ValidationRules.port.min || port > ValidationRules.port.max) { | |
| errors.push('Invalid port number'); | |
| } | |
| return errors; | |
| } | |
| </script> | |
| </body> | |
| </html> |