Spaces:
Paused
Paused
| /** | |
| * Virtual ISP Stack Frontend Application | |
| * Native JavaScript implementation for managing the Virtual ISP Stack | |
| */ | |
| class VirtualISPApp { | |
| constructor() { | |
| this.apiBase = | |
| '/api | |
| '; | |
| this.currentSection = | |
| 'dashboard | |
| '; | |
| this.refreshInterval = null; | |
| this.charts = {}; | |
| this.init(); | |
| } | |
| async init() { | |
| this.setupEventListeners(); | |
| this.setupNavigation(); | |
| this.setupCharts(); | |
| await this.loadInitialData(); | |
| this.startAutoRefresh(); | |
| // Hide loading overlay | |
| this.hideLoading(); | |
| console.log( | |
| 'Virtual ISP Stack App initialized | |
| '); | |
| } | |
| setupEventListeners() { | |
| // Navigation | |
| document.querySelectorAll( | |
| '.nav-item | |
| ').forEach(item => { | |
| item.addEventListener( | |
| 'click | |
| ', (e) => { | |
| const section = e.currentTarget.dataset.section; | |
| this.navigateToSection(section); | |
| }); | |
| }); | |
| // Tab buttons | |
| document.querySelectorAll( | |
| '.tab-btn | |
| ').forEach(btn => { | |
| btn.addEventListener( | |
| 'click | |
| ', (e) => { | |
| const tab = e.currentTarget.dataset.tab; | |
| this.switchTab(tab); | |
| }); | |
| }); | |
| // Modal close buttons | |
| document.querySelectorAll( | |
| '.close | |
| ').forEach(btn => { | |
| btn.addEventListener( | |
| 'click | |
| ', (e) => { | |
| const modal = e.currentTarget.closest( | |
| '.modal | |
| '); | |
| this.closeModal(modal.id); | |
| }); | |
| }); | |
| // Click outside modal to close | |
| document.querySelectorAll( | |
| '.modal | |
| ').forEach(modal => { | |
| modal.addEventListener( | |
| 'click | |
| ', (e) => { | |
| if (e.target === modal) { | |
| this.closeModal(modal.id); | |
| } | |
| }); | |
| }); | |
| // Form submissions | |
| document.getElementById( | |
| 'addRuleForm | |
| ')?.addEventListener( | |
| 'submit | |
| ', (e) => { | |
| e.preventDefault(); | |
| this.addFirewallRule(); | |
| }); | |
| } | |
| setupNavigation() { | |
| // Set initial active section | |
| this.navigateToSection( | |
| 'dashboard | |
| '); | |
| } | |
| navigateToSection(section) { | |
| // Update navigation | |
| document.querySelectorAll( | |
| '.nav-item | |
| ').forEach(item => { | |
| item.classList.remove( | |
| 'active | |
| '); | |
| }); | |
| document.querySelector(`[data-section=\n'${section}\n']`).classList.add( | |
| 'active | |
| '); | |
| // Update content | |
| document.querySelectorAll( | |
| '.content-section | |
| ').forEach(sec => { | |
| sec.classList.remove( | |
| 'active | |
| '); | |
| }); | |
| document.getElementById(section).classList.add( | |
| 'active | |
| '); | |
| this.currentSection = section; | |
| // Load section-specific data | |
| this.loadSectionData(section); | |
| } | |
| switchTab(tab) { | |
| const container = event.target.closest( | |
| '.router-tabs | |
| '); | |
| // Update tab buttons | |
| container.querySelectorAll( | |
| '.tab-btn | |
| ').forEach(btn => { | |
| btn.classList.remove( | |
| 'active | |
| '); | |
| }); | |
| event.target.classList.add( | |
| 'active | |
| '); | |
| // Update tab content | |
| container.querySelectorAll( | |
| '.tab-pane | |
| ').forEach(pane => { | |
| pane.classList.remove( | |
| 'active | |
| '); | |
| }); | |
| container.querySelector(`#${tab}`).classList.add( | |
| 'active | |
| '); | |
| // Load tab-specific data | |
| this.loadTabData(tab); | |
| } | |
| async loadInitialData() { | |
| try { | |
| await Promise.all([ | |
| this.loadSystemStatus(), | |
| this.loadDashboardData(), | |
| this.loadConfiguration() | |
| ]); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading initial data: | |
| ', error); | |
| this.showToast( | |
| 'Error loading initial data | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| async loadSectionData(section) { | |
| try { | |
| switch (section) { | |
| case | |
| 'dashboard | |
| ': | |
| await this.loadDashboardData(); | |
| break; | |
| case | |
| 'dhcp | |
| ': | |
| await this.loadDHCPData(); | |
| break; | |
| case | |
| 'nat | |
| ': | |
| await this.loadNATData(); | |
| break; | |
| case | |
| 'firewall | |
| ': | |
| await this.loadFirewallData(); | |
| break; | |
| case | |
| 'router | |
| ': | |
| await this.loadRouterData(); | |
| break; | |
| case | |
| 'bridge | |
| ': | |
| await this.loadBridgeData(); | |
| break; | |
| case | |
| 'sessions | |
| ': | |
| await this.loadSessionsData(); | |
| break; | |
| case | |
| 'logs | |
| ': | |
| await this.loadLogsData(); | |
| break; | |
| case | |
| 'vpn | |
| ': | |
| await this.loadVPNData(); | |
| break; | |
| case | |
| 'config | |
| ': | |
| await this.loadConfiguration(); | |
| break; | |
| } | |
| } catch (error) { | |
| console.error(`Error loading ${section} data:`, error); | |
| this.showToast(`Error loading ${section} data`, | |
| 'error | |
| '); | |
| } | |
| } | |
| async loadTabData(tab) { | |
| try { | |
| switch (tab) { | |
| case | |
| 'routes | |
| ': | |
| await this.loadRoutingTable(); | |
| break; | |
| case | |
| 'interfaces | |
| ': | |
| await this.loadInterfaces(); | |
| break; | |
| case | |
| 'arp | |
| ': | |
| await this.loadARPTable(); | |
| break; | |
| } | |
| } catch (error) { | |
| console.error(`Error loading ${tab} data:`, error); | |
| } | |
| } | |
| // API Methods | |
| async apiCall(endpoint, options = {}) { | |
| const url = `${this.apiBase}${endpoint}`; | |
| const defaultOptions = { | |
| headers: { | |
| 'Content-Type | |
| ': | |
| 'application/json | |
| ', | |
| }, | |
| }; | |
| const response = await fetch(url, { ...defaultOptions, ...options }); | |
| if (!response.ok) { | |
| throw new Error(`API call failed: ${response.status} ${response.statusText}`); | |
| } | |
| return await response.json(); | |
| } | |
| // System Status | |
| async loadSystemStatus() { | |
| try { | |
| const response = await this.apiCall( | |
| '/status | |
| '); | |
| this.updateSystemStatus(response.system_status); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading system status: | |
| ', error); | |
| this.updateSystemStatusOffline(); | |
| } | |
| } | |
| updateSystemStatus(status) { | |
| const indicator = document.getElementById( | |
| 'systemStatus | |
| '); | |
| const components = status.components; | |
| // Update header status | |
| const allOnline = Object.values(components).every(c => c === true); | |
| indicator.className = `status-indicator ${allOnline ? | |
| 'online | |
| ' : | |
| 'offline | |
| '}`; | |
| indicator.querySelector( | |
| 'span | |
| ').textContent = allOnline ? | |
| 'All Systems Online | |
| ' : | |
| 'System Issues | |
| '; | |
| // Update component status | |
| this.updateComponentStatus(components); | |
| } | |
| updateSystemStatusOffline() { | |
| const indicator = document.getElementById( | |
| 'systemStatus | |
| '); | |
| indicator.className = | |
| 'status-indicator offline | |
| '; | |
| indicator.querySelector( | |
| 'span | |
| ').textContent = | |
| 'System Offline | |
| '; | |
| } | |
| updateComponentStatus(components) { | |
| const container = document.getElementById( | |
| 'componentStatus | |
| '); | |
| container.innerHTML = | |
| '' | |
| ; | |
| Object.entries(components).forEach(([name, status]) => { | |
| const item = document.createElement( | |
| 'div | |
| '); | |
| item.className = `component-item ${status ? | |
| 'online | |
| ' : | |
| 'offline | |
| '}`; | |
| item.innerHTML = ` | |
| <span class=\n"component-name\n">${this.formatComponentName(name)}</span> | |
| <span class=\n"component-status-badge ${status ? \n'online\n' : \n'offline\n'}\n"> | |
| ${status ? | |
| 'Online | |
| ' : | |
| 'Offline | |
| '} | |
| </span> | |
| `; | |
| container.appendChild(item); | |
| }); | |
| } | |
| formatComponentName(name) { | |
| return name.replace(/_/g, | |
| ' | |
| ').replace(/\b\w/g, l => l.toUpperCase()); | |
| } | |
| // Dashboard Data | |
| async loadDashboardData() { | |
| try { | |
| const [statusResponse, statsResponse] = await Promise.all([ | |
| this.apiCall( | |
| '/status | |
| '), | |
| this.apiCall( | |
| '/stats | |
| ') | |
| ]); | |
| this.updateDashboardStats(statusResponse.system_status.stats); | |
| this.updateCharts(statsResponse.stats); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading dashboard data: | |
| ', error); | |
| } | |
| } | |
| updateDashboardStats(stats) { | |
| document.getElementById( | |
| 'dhcpLeaseCount | |
| ').textContent = stats.dhcp_leases || 0; | |
| document.getElementById( | |
| 'natSessionCount | |
| ').textContent = stats.nat_sessions || 0; | |
| document.getElementById( | |
| 'firewallRuleCount | |
| ').textContent = stats.firewall_rules || 0; | |
| document.getElementById( | |
| 'bridgeClientCount | |
| ').textContent = stats.bridge_clients || 0; | |
| } | |
| // DHCP Data | |
| async loadDHCPData() { | |
| try { | |
| const response = await this.apiCall( | |
| '/dhcp/leases | |
| '); | |
| this.updateDHCPTable(response.leases); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading DHCP data: | |
| ', error); | |
| this.updateDHCPTable([]); | |
| } | |
| } | |
| updateDHCPTable(leases) { | |
| const tbody = document.getElementById( | |
| 'dhcpTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| leases.forEach(lease => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| const remaining = Math.max(0, lease.lease_time - (Date.now() / 1000 - lease.lease_start)); | |
| row.innerHTML = ` | |
| <td>${lease.mac_address}</td> | |
| <td>${lease.ip_address}</td> | |
| <td>${this.formatDuration(lease.lease_time)}</td> | |
| <td>${this.formatDuration(remaining)}</td> | |
| <td><span class=\n"status-badge status-${lease.state.toLowerCase()}\n">${lease.state}</span></td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.releaseDHCPLease(\'${lease.mac_address}\')\n"> | |
| <i class=\n"fas fa-times\n"></i> Release | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async releaseDHCPLease(macAddress) { | |
| try { | |
| await this.apiCall(`/dhcp/leases/${macAddress}`, { method: | |
| 'DELETE | |
| ' }); | |
| this.showToast( | |
| 'DHCP lease released successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadDHCPData(); | |
| } catch (error) { | |
| console.error( | |
| 'Error releasing DHCP lease: | |
| ', error); | |
| this.showToast( | |
| 'Error releasing DHCP lease | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| // NAT Data | |
| async loadNATData() { | |
| try { | |
| const [sessionsResponse, statsResponse] = await Promise.all([ | |
| this.apiCall( | |
| '/nat/sessions | |
| '), | |
| this.apiCall( | |
| '/nat/stats | |
| ') | |
| ]); | |
| this.updateNATStats(statsResponse.stats); | |
| this.updateNATTable(sessionsResponse.sessions); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading NAT data: | |
| ', error); | |
| this.updateNATStats({}); | |
| this.updateNATTable([]); | |
| } | |
| } | |
| updateNATStats(stats) { | |
| document.getElementById( | |
| 'natActiveSessions | |
| ').textContent = stats.active_sessions || 0; | |
| document.getElementById( | |
| 'natPortUtilization | |
| ').textContent = | |
| `${Math.round((stats.ports_used / stats.total_ports) * 100) || 0}%`; | |
| document.getElementById( | |
| 'natBytesTranslated | |
| ').textContent = | |
| this.formatBytes(stats.bytes_translated || 0); | |
| } | |
| updateNATTable(sessions) { | |
| const tbody = document.getElementById( | |
| 'natTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| sessions.forEach(session => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${session.virtual_ip}:${session.virtual_port}</td> | |
| <td>${session.real_ip}:${session.real_port}</td> | |
| <td>${session.host_ip}:${session.host_port}</td> | |
| <td>${session.protocol}</td> | |
| <td>${this.formatDuration(session.duration)}</td> | |
| <td>${this.formatBytes(session.bytes_in)} / ${this.formatBytes(session.bytes_out)}</td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.closeNATSession(\'${session.session_id}\')\n"> | |
| <i class=\n"fas fa-times\n"></i> Close | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| // Firewall Data | |
| async loadFirewallData() { | |
| try { | |
| const [rulesResponse, logsResponse, statsResponse] = await Promise.all([ | |
| this.apiCall( | |
| '/firewall/rules | |
| '), | |
| this.apiCall( | |
| '/firewall/logs?limit=50 | |
| '), | |
| this.apiCall( | |
| '/firewall/stats | |
| ') | |
| ]); | |
| this.updateFirewallTable(rulesResponse.rules); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading firewall data: | |
| ', error); | |
| this.updateFirewallTable([]); | |
| } | |
| } | |
| updateFirewallTable(rules) { | |
| const tbody = document.getElementById( | |
| 'firewallTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| rules.forEach(rule => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${rule.priority}</td> | |
| <td>${rule.rule_id}</td> | |
| <td><span class=\n"status-badge status-${rule.action.toLowerCase()}\n">${rule.action}</span></td> | |
| <td>${rule.direction}</td> | |
| <td>${rule.source_ip || | |
| 'Any | |
| '}${rule.source_port ? | |
| ': | |
| ' + rule.source_port : | |
| '' | |
| }</td> | |
| <td>${rule.dest_ip || | |
| 'Any | |
| '}${rule.dest_port ? | |
| ': | |
| ' + rule.dest_port : | |
| '' | |
| }</td> | |
| <td>${rule.protocol || | |
| 'Any | |
| '} | |
| </td> | |
| <td>${rule.hit_count || 0}</td> | |
| <td><span class=\n"status-badge status-${rule.enabled ? \n'active\n' : \n'inactive\n'}\n">${rule.enabled ? | |
| 'Enabled | |
| ' : | |
| 'Disabled | |
| '}</span></td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.deleteFirewallRule(\'${rule.rule_id}\')\n"> | |
| <i class=\n"fas fa-trash\n"></i> Delete | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async deleteFirewallRule(ruleId) { | |
| try { | |
| await this.apiCall(`/firewall/rules/${ruleId}`, { method: | |
| 'DELETE | |
| ' }); | |
| this.showToast( | |
| 'Firewall rule deleted successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadFirewallData(); | |
| } catch (error) { | |
| console.error( | |
| 'Error deleting firewall rule: | |
| ', error); | |
| this.showToast( | |
| 'Error deleting firewall rule | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| // Router Data | |
| async loadRouterData() { | |
| await Promise.all([ | |
| this.loadRoutingTable(), | |
| this.loadInterfaces(), | |
| this.loadARPTable() | |
| ]); | |
| } | |
| async loadRoutingTable() { | |
| try { | |
| const response = await this.apiCall( | |
| '/router/routes | |
| '); | |
| this.updateRoutingTable(response.routes); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading routing table: | |
| ', error); | |
| this.updateRoutingTable([]); | |
| } | |
| } | |
| updateRoutingTable(routes) { | |
| const tbody = document.getElementById( | |
| 'routesTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| routes.forEach(route => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${route.destination}</td> | |
| <td>${route.gateway || | |
| 'Direct | |
| '}</td> | |
| <td>${route.interface}</td> | |
| <td>${route.metric}</td> | |
| <td>${route.type}</td> | |
| <td>${route.use_count || 0}</td> | |
| <td>${route.last_used ? new Date(route.last_used * 1000).toLocaleString() : | |
| 'Never | |
| '}</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async loadInterfaces() { | |
| try { | |
| const response = await this.apiCall( | |
| '/router/interfaces | |
| '); | |
| this.updateInterfacesTable(response.interfaces); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading interfaces: | |
| ', error); | |
| this.updateInterfacesTable([]); | |
| } | |
| } | |
| updateInterfacesTable(interfaces) { | |
| const tbody = document.getElementById( | |
| 'interfacesTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| interfaces.forEach(iface => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${iface.name}</td> | |
| <td>${iface.ip_address || | |
| 'N/A | |
| '}</td> | |
| <td>${iface.network || | |
| 'N/A | |
| '}</td> | |
| <td>${iface.mtu || | |
| 'N/A | |
| '}</td> | |
| <td><span class=\n"status-badge status-${iface.status.toLowerCase()}\n">${iface.status}</span></td> | |
| <td> | |
| <button class=\n"btn btn-secondary btn-sm\n" onclick=\n"app.toggleInterfaceStatus(\'${iface.name}\')\n"> | |
| <i class=\n"fas fa-toggle-${iface.status === | |
| 'Up | |
| ' ? | |
| 'on | |
| ' : | |
| 'off | |
| '}\n"></i> ${iface.status === | |
| 'Up | |
| ' ? | |
| 'Down | |
| ' : | |
| 'Up | |
| '} | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async toggleInterfaceStatus(interfaceName) { | |
| try { | |
| const response = await this.apiCall(`/router/interfaces/${interfaceName}/toggle`, { method: | |
| 'POST | |
| ' }); | |
| if (response.status === | |
| 'success | |
| ') { | |
| this.showToast(`Interface ${interfaceName} status toggled successfully`, | |
| 'success | |
| '); | |
| await this.loadInterfaces(); | |
| } else { | |
| this.showToast(`Failed to toggle interface ${interfaceName} status: ` + response.message, | |
| 'error | |
| '); | |
| } | |
| } catch (error) { | |
| console.error(`Error toggling interface ${interfaceName} status:`, error); | |
| this.showToast(`Error toggling interface ${interfaceName} status`, | |
| 'error | |
| '); | |
| } | |
| } | |
| async loadARPTable() { | |
| try { | |
| const response = await this.apiCall( | |
| '/router/arp | |
| '); | |
| this.updateARPTable(response.arp_entries); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading ARP table: | |
| ', error); | |
| this.updateARPTable([]); | |
| } | |
| } | |
| updateARPTable(arpEntries) { | |
| const tbody = document.getElementById( | |
| 'arpTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| arpEntries.forEach(entry => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${entry.ip_address}</td> | |
| <td>${entry.mac_address}</td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.deleteARPEntry(\'${entry.ip_address}\')\n"> | |
| <i class=\n"fas fa-trash\n"></i> Delete | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async deleteARPEntry(ipAddress) { | |
| try { | |
| await this.apiCall(`/router/arp/${ipAddress}`, { method: | |
| 'DELETE | |
| ' }); | |
| this.showToast( | |
| 'ARP entry deleted successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadARPTable(); | |
| } catch (error) { | |
| console.error( | |
| 'Error deleting ARP entry: | |
| ', error); | |
| this.showToast( | |
| 'Error deleting ARP entry | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| // Bridge Data | |
| async loadBridgeData() { | |
| try { | |
| const response = await this.apiCall( | |
| '/bridge/status | |
| '); | |
| this.updateBridgeStatus(response.status); | |
| this.updateBridgeClientsTable(response.clients); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading bridge data: | |
| ', error); | |
| this.updateBridgeStatusOffline(); | |
| this.updateBridgeClientsTable([]); | |
| } | |
| } | |
| updateBridgeStatus(status) { | |
| document.getElementById( | |
| 'websocketPort | |
| ').textContent = status.websocket_port; | |
| document.getElementById( | |
| 'websocketStatus | |
| ').textContent = status.websocket_status; | |
| document.getElementById( | |
| 'tcpPort | |
| ').textContent = status.tcp_port; | |
| document.getElementById( | |
| 'tcpStatus | |
| ').textContent = status.tcp_status; | |
| } | |
| updateBridgeStatusOffline() { | |
| document.getElementById( | |
| 'websocketPort | |
| ').textContent = | |
| 'N/A | |
| '; | |
| document.getElementById( | |
| 'websocketStatus | |
| ').textContent = | |
| 'Offline | |
| '; | |
| document.getElementById( | |
| 'tcpPort | |
| ').textContent = | |
| 'N/A | |
| '; | |
| document.getElementById( | |
| 'tcpStatus | |
| ').textContent = | |
| 'Offline | |
| '; | |
| } | |
| updateBridgeClientsTable(clients) { | |
| const tbody = document.getElementById( | |
| 'bridgeClientsTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| clients.forEach(client => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${client.client_id}</td> | |
| <td>${client.type}</td> | |
| <td>${client.remote_address}</td> | |
| <td>${new Date(client.connected_time * 1000).toLocaleString()}</td> | |
| <td>${this.formatBytes(client.packets_in)} / ${this.formatBytes(client.packets_out)}</td> | |
| <td>${this.formatBytes(client.bytes_in)} / ${this.formatBytes(client.bytes_out)}</td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.disconnectBridgeClient(\'${client.client_id}\')\n"> | |
| <i class=\n"fas fa-times\n"></i> Disconnect | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async disconnectBridgeClient(clientId) { | |
| try { | |
| await this.apiCall(`/bridge/clients/${clientId}`, { method: | |
| 'DELETE | |
| ' }); | |
| this.showToast( | |
| 'Bridge client disconnected successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadBridgeData(); | |
| } catch (error) { | |
| console.error( | |
| 'Error disconnecting bridge client: | |
| ', error); | |
| this.showToast( | |
| 'Error disconnecting bridge client | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| // Sessions Data | |
| async loadSessionsData() { | |
| try { | |
| const response = await this.apiCall( | |
| '/sessions | |
| '); | |
| this.updateSessionsTable(response.sessions); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading sessions data: | |
| ', error); | |
| this.updateSessionsTable([]); | |
| } | |
| } | |
| updateSessionsTable(sessions) { | |
| const tbody = document.getElementById( | |
| 'sessionsTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| sessions.forEach(session => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| row.innerHTML = ` | |
| <td>${session.session_id}</td> | |
| <td>${session.type}</td> | |
| <td>${session.state}</td> | |
| <td>${session.virtual_ip}:${session.virtual_port}</td> | |
| <td>${session.real_ip}:${session.real_port}</td> | |
| <td>${session.protocol}</td> | |
| <td>${this.formatDuration(session.duration)}</td> | |
| <td>${this.formatDuration(session.idle_time)}</td> | |
| <td>${JSON.stringify(session.metrics)}</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| // Logs Data | |
| async loadLogsData() { | |
| try { | |
| const response = await this.apiCall( | |
| '/logs | |
| '); | |
| this.updateLogsTable(response.logs); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading logs data: | |
| ', error); | |
| this.updateLogsTable([]); | |
| } | |
| } | |
| updateLogsTable(logs) { | |
| const container = document.getElementById( | |
| 'logContainer | |
| '); | |
| container.innerHTML = | |
| '' | |
| ; | |
| logs.forEach(log => { | |
| const entry = document.createElement( | |
| 'div | |
| '); | |
| entry.className = | |
| 'log-entry | |
| '; | |
| entry.innerHTML = ` | |
| <span class=\n"log-level ${log.level}\n">${log.level}</span> | |
| <div class=\n"log-content\n"> | |
| <div class=\n"log-timestamp\n">${new Date(log.timestamp * 1000).toLocaleString()}</div> | |
| <div class=\n"log-message\n">${log.message}</div> | |
| ${log.metadata ? `<div class=\n"log-metadata\n">${JSON.stringify(log.metadata)}</div>` : | |
| '' | |
| } | |
| </div> | |
| `; | |
| container.appendChild(entry); | |
| }); | |
| } | |
| async filterLogs() { | |
| const level = document.getElementById( | |
| 'logLevelFilter | |
| ').value; | |
| const search = document.getElementById( | |
| 'logSearch | |
| ').value; | |
| try { | |
| const response = await this.apiCall(`/logs?level=${level}&search=${search}`); | |
| this.updateLogsTable(response.logs); | |
| } catch (error) { | |
| console.error( | |
| 'Error filtering logs: | |
| ', error); | |
| this.showToast( | |
| 'Error filtering logs | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| async clearLogs() { | |
| try { | |
| await this.apiCall( | |
| '/logs/clear | |
| ', { method: | |
| 'POST | |
| ' }); | |
| this.showToast( | |
| 'Logs cleared successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadLogsData(); | |
| } catch (error) { | |
| console.error( | |
| 'Error clearing logs: | |
| ', error); | |
| this.showToast( | |
| 'Error clearing logs | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| // VPN Management Functions | |
| async loadVPNData() { | |
| try { | |
| await Promise.all([ | |
| this.loadVPNStatus(), | |
| this.loadVPNClients() | |
| ]); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading VPN data: | |
| ', error); | |
| } | |
| } | |
| async loadVPNStatus() { | |
| try { | |
| const response = await this.apiCall( | |
| '/openvpn/status | |
| '); | |
| this.updateVPNStatus(response.status); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading VPN status: | |
| ', error); | |
| this.updateVPNStatusOffline(); | |
| } | |
| } | |
| updateVPNStatus(status) { | |
| document.getElementById( | |
| 'vpnServerStatus | |
| ').textContent = status.is_running ? | |
| 'Running | |
| ' : | |
| 'Stopped | |
| '; | |
| document.getElementById( | |
| 'vpnServerIp | |
| ').textContent = status.server_ip || | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnServerPort | |
| ').textContent = status.server_port || | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnConnectedClients | |
| ').textContent = status.connected_clients || 0; | |
| document.getElementById( | |
| 'vpnUptime | |
| ').textContent = status.uptime ? this.formatDuration(status.uptime) : | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnBytesReceived | |
| ').textContent = this.formatBytes(status.total_bytes_received || 0); | |
| document.getElementById( | |
| 'vpnBytesSent | |
| ').textContent = this.formatBytes(status.total_bytes_sent || 0); | |
| // Update button states | |
| const startBtn = document.getElementById( | |
| 'startVpnBtn | |
| '); | |
| const stopBtn = document.getElementById( | |
| 'stopVpnBtn | |
| '); | |
| if (status.is_running) { | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| startBtn.classList.add( | |
| 'disabled | |
| '); | |
| stopBtn.classList.remove( | |
| 'disabled | |
| '); | |
| } else { | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| startBtn.classList.remove( | |
| 'disabled | |
| '); | |
| stopBtn.classList.add( | |
| 'disabled | |
| '); | |
| } | |
| } | |
| updateVPNStatusOffline() { | |
| document.getElementById( | |
| 'vpnServerStatus | |
| ').textContent = | |
| 'Unknown | |
| '; | |
| document.getElementById( | |
| 'vpnServerIp | |
| ').textContent = | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnServerPort | |
| ').textContent = | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnConnectedClients | |
| ').textContent = | |
| '0 | |
| '; | |
| document.getElementById( | |
| 'vpnUptime | |
| ').textContent = | |
| '- | |
| '; | |
| document.getElementById( | |
| 'vpnBytesReceived | |
| ').textContent = | |
| '0 | |
| '; | |
| document.getElementById( | |
| 'vpnBytesSent | |
| ').textContent = | |
| '0 | |
| '; | |
| // Enable both buttons when status is unknown | |
| const startBtn = document.getElementById( | |
| 'startVpnBtn | |
| '); | |
| const stopBtn = document.getElementById( | |
| 'stopVpnBtn | |
| '); | |
| startBtn.disabled = false; | |
| stopBtn.disabled = false; | |
| startBtn.classList.remove( | |
| 'disabled | |
| '); | |
| stopBtn.classList.remove( | |
| 'disabled | |
| '); | |
| } | |
| async loadVPNClients() { | |
| try { | |
| const response = await this.apiCall( | |
| '/openvpn/clients | |
| '); | |
| this.updateVPNClientsTable(response.clients); | |
| } catch (error) { | |
| console.error( | |
| 'Error loading VPN clients: | |
| ', error); | |
| this.updateVPNClientsTable([]); | |
| } | |
| } | |
| updateVPNClientsTable(clients) { | |
| const tbody = document.getElementById( | |
| 'vpnClientsTableBody | |
| '); | |
| tbody.innerHTML = | |
| '' | |
| ; | |
| clients.forEach(client => { | |
| const row = document.createElement( | |
| 'tr | |
| '); | |
| const connectedSince = new Date(client.connected_at * 1000).toLocaleString(); | |
| row.innerHTML = ` | |
| <td>${client.client_id}</td> | |
| <td>${client.common_name}</td> | |
| <td>${client.ip_address}</td> | |
| <td>${connectedSince}</td> | |
| <td>${this.formatBytes(client.bytes_received)}</td> | |
| <td>${this.formatBytes(client.bytes_sent)}</td> | |
| <td><span class=\n"status-badge status-${client.status.toLowerCase()}\n">${client.status}</span></td> | |
| <td> | |
| <button class=\n"btn btn-danger btn-sm\n" onclick=\n"app.disconnectVPNClient(\'${client.client_id}\')\n"> | |
| <i class=\n"fas fa-times\n"></i> Disconnect | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| async startVpnServer() { | |
| try { | |
| this.showLoading(); | |
| const response = await this.apiCall( | |
| '/openvpn/start | |
| ', { method: | |
| 'POST | |
| ' }); | |
| if (response.status === | |
| 'success | |
| ') { | |
| this.showToast( | |
| 'VPN server started successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadVPNStatus(); | |
| } else { | |
| this.showToast( | |
| 'Failed to start VPN server: | |
| ' + response.message, | |
| 'error | |
| '); | |
| } | |
| } catch (error) { | |
| console.error( | |
| 'Error starting VPN server: | |
| ', error); | |
| this.showToast( | |
| 'Error starting VPN server | |
| ', | |
| 'error | |
| '); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } | |
| async stopVpnServer() { | |
| try { | |
| this.showLoading(); | |
| const response = await this.apiCall( | |
| '/openvpn/stop | |
| ', { method: | |
| 'POST | |
| ' }); | |
| if (response.status === | |
| 'success | |
| ') { | |
| this.showToast( | |
| 'VPN server stopped successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadVPNStatus(); | |
| await this.loadVPNClients(); // Refresh clients list | |
| } else { | |
| this.showToast( | |
| 'Failed to stop VPN server: | |
| ' + response.message, | |
| 'error | |
| '); | |
| } | |
| } catch (error) { | |
| console.error( | |
| 'Error stopping VPN server: | |
| ', error); | |
| this.showToast( | |
| 'Error stopping VPN server | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| async disconnectVPNClient(clientId) { | |
| try { | |
| const response = await this.apiCall(`/openvpn/clients/${clientId}`, { method: | |
| 'DELETE | |
| ' }); | |
| if (response.status === | |
| 'success | |
| ') { | |
| this.showToast( | |
| 'VPN client disconnected successfully | |
| ', | |
| 'success | |
| '); | |
| await this.loadVPNClients(); | |
| await this.loadVPNStatus(); // Update client count | |
| } else { | |
| this.showToast( | |
| 'Failed to disconnect VPN client: | |
| ' + response.message, | |
| 'error | |
| '); | |
| } | |
| } catch (error) { | |
| console.error( | |
| 'Error disconnecting VPN client: | |
| ', error); | |
| this.showToast( | |
| 'Error disconnecting VPN client | |
| ', | |
| 'error | |
| '); | |
| } | |
| } | |
| async generateClientConfig() { | |
| try { | |
| const clientName = document.getElementById( | |
| 'clientName | |
| ').value; | |
| const serverIp = document.getElementById( | |
| 'serverIp | |
| ').value; | |
| if (!clientName || !serverIp) { | |
| this.showToast( | |
| 'Please fill in all fields | |
| ', | |
| 'error | |
| '); | |
| return; | |
| } | |
| this.showLoading(); | |
| const response = await this.apiCall( | |
| '/openvpn/generate-config | |
| ', { | |
| method: | |
| 'POST | |
| ', | |
| body: JSON.stringify({ | |
| client_name: clientName, | |
| server_ip: serverIp | |
| }) | |
| }); | |
| if (response.status === | |
| 'success | |
| ') { | |
| // Create and download the config file | |
| const blob = new Blob([response.config], { type: | |
| 'text/plain | |
| ' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement( | |
| 'a | |
| '); | |
| a.href = url; | |
| a.download = `${clientName}.ovpn`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| window.URL.revokeObjectURL(url); | |
| this.showToast( | |
| 'VPN client configuration generated and downloaded | |
| ', | |
| 'success | |
| '); | |
| this.closeModal( | |
| 'generateConfigModal | |
| '); | |
| // Clear form | |
| document.getElementById( | |
| 'generateConfigForm | |
| ').reset(); | |
| } else { | |
| this.showToast( | |
| 'Failed to generate VPN config: | |
| ' + response.message, | |
| 'error | |
| '); | |
| } | |
| } catch (error) { | |
| console.error( | |
| 'Error generating VPN config: | |
| ', error); | |
| this.showToast( | |
| 'Error generating VPN config | |
| ', | |
| 'error | |
| '); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } | |
| refreshVpnStatus() { | |
| this.loadVPNStatus(); | |
| } | |
| refreshVpnClients() { | |
| this.loadVPNClients(); | |
| } | |
| showGenerateConfigModal() { | |
| this.showModal( | |
| 'generateConfigModal | |
| '); | |
| } | |
| } | |
| document.addEventListener("DOMContentLoaded", () => { | |
| window.app = new VirtualISPApp(); | |
| }); | |
| // Global functions for direct HTML calls | |
| function refreshData() { | |
| app.loadInitialData(); | |
| } | |
| function refreshDHCPLeases() { | |
| app.loadDHCPData(); | |
| } | |
| function releaseDHCPLease(macAddress) { | |
| app.releaseDHCPLease(macAddress); | |
| } | |
| function refreshNATSessions() { | |
| app.loadNATData(); | |
| } | |
| function closeNATSession(sessionId) { | |
| app.closeNATSession(sessionId); | |
| } | |
| function showAddRuleModal() { | |
| app.showModal( | |
| 'addRuleModal | |
| '); | |
| } | |
| function refreshFirewallRules() { | |
| app.loadFirewallData(); | |
| } | |
| function deleteFirewallRule(ruleId) { | |
| app.deleteFirewallRule(ruleId); | |
| } | |
| function startVpnServer() { | |
| app.startVpnServer(); | |
| } | |
| function stopVpnServer() { | |
| app.stopVpnServer(); | |
| } | |
| function disconnectVPNClient(clientId) { | |
| app.disconnectVPNClient(clientId); | |
| } | |
| function generateClientConfig() { | |
| app.generateClientConfig(); | |
| } | |
| function refreshVpnStatus() { | |
| app.refreshVpnStatus(); | |
| } | |
| function refreshVpnClients() { | |
| app.refreshVpnClients(); | |
| } | |
| function showGenerateConfigModal() { | |
| app.showGenerateConfigModal(); | |
| } | |
| function filterLogs() { | |
| app.filterLogs(); | |
| } | |
| function searchLogs() { | |
| app.searchLogs(); | |
| } | |
| function clearLogs() { | |
| app.clearLogs(); | |
| } | |