|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
this.hideLoading(); |
|
|
|
|
|
console.log( |
|
|
'Virtual ISP Stack App initialized |
|
|
'); |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
|
|
|
document.querySelectorAll( |
|
|
'.nav-item |
|
|
').forEach(item => { |
|
|
item.addEventListener( |
|
|
'click |
|
|
', (e) => { |
|
|
const section = e.currentTarget.dataset.section; |
|
|
this.navigateToSection(section); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll( |
|
|
'.tab-btn |
|
|
').forEach(btn => { |
|
|
btn.addEventListener( |
|
|
'click |
|
|
', (e) => { |
|
|
const tab = e.currentTarget.dataset.tab; |
|
|
this.switchTab(tab); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll( |
|
|
'.close |
|
|
').forEach(btn => { |
|
|
btn.addEventListener( |
|
|
'click |
|
|
', (e) => { |
|
|
const modal = e.currentTarget.closest( |
|
|
'.modal |
|
|
'); |
|
|
this.closeModal(modal.id); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll( |
|
|
'.modal |
|
|
').forEach(modal => { |
|
|
modal.addEventListener( |
|
|
'click |
|
|
', (e) => { |
|
|
if (e.target === modal) { |
|
|
this.closeModal(modal.id); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById( |
|
|
'addRuleForm |
|
|
')?.addEventListener( |
|
|
'submit |
|
|
', (e) => { |
|
|
e.preventDefault(); |
|
|
this.addFirewallRule(); |
|
|
}); |
|
|
} |
|
|
|
|
|
setupNavigation() { |
|
|
|
|
|
this.navigateToSection( |
|
|
'dashboard |
|
|
'); |
|
|
} |
|
|
|
|
|
navigateToSection(section) { |
|
|
|
|
|
document.querySelectorAll( |
|
|
'.nav-item |
|
|
').forEach(item => { |
|
|
item.classList.remove( |
|
|
'active |
|
|
'); |
|
|
}); |
|
|
document.querySelector(`[data-section=\n'${section}\n']`).classList.add( |
|
|
'active |
|
|
'); |
|
|
|
|
|
|
|
|
document.querySelectorAll( |
|
|
'.content-section |
|
|
').forEach(sec => { |
|
|
sec.classList.remove( |
|
|
'active |
|
|
'); |
|
|
}); |
|
|
document.getElementById(section).classList.add( |
|
|
'active |
|
|
'); |
|
|
|
|
|
this.currentSection = section; |
|
|
|
|
|
|
|
|
this.loadSectionData(section); |
|
|
} |
|
|
|
|
|
switchTab(tab) { |
|
|
const container = event.target.closest( |
|
|
'.router-tabs |
|
|
'); |
|
|
|
|
|
|
|
|
container.querySelectorAll( |
|
|
'.tab-btn |
|
|
').forEach(btn => { |
|
|
btn.classList.remove( |
|
|
'active |
|
|
'); |
|
|
}); |
|
|
event.target.classList.add( |
|
|
'active |
|
|
'); |
|
|
|
|
|
|
|
|
container.querySelectorAll( |
|
|
'.tab-pane |
|
|
').forEach(pane => { |
|
|
pane.classList.remove( |
|
|
'active |
|
|
'); |
|
|
}); |
|
|
container.querySelector(`#${tab}`).classList.add( |
|
|
'active |
|
|
'); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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 |
|
|
'; |
|
|
|
|
|
|
|
|
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()); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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 |
|
|
'; |
|
|
|
|
|
|
|
|
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(); |
|
|
} 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(); |
|
|
} 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 |
|
|
') { |
|
|
|
|
|
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 |
|
|
'); |
|
|
|
|
|
|
|
|
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(); |
|
|
}); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|