Spaces:
Paused
Paused
| // Dashboard JavaScript | |
| class VPNDashboard { | |
| constructor() { | |
| this.token = localStorage.getItem('auth_token'); | |
| this.currentUser = null; | |
| this.clients = []; | |
| this.serverStatus = {}; | |
| this.protocols = []; | |
| this.init(); | |
| } | |
| async init() { | |
| const token = localStorage.getItem("auth_token"); | |
| if (!token) { | |
| window.location.href = | |
| '/auth.html'; | |
| return; | |
| } | |
| this.token = token; | |
| try { | |
| await this.loadUserInfo(); | |
| await this.loadInitialData(); | |
| this.setupEventListeners(); | |
| this.startAutoRefresh(); | |
| } catch (error) { | |
| console.error("Dashboard initialization failed:", error); | |
| this.showAlert("error", "Failed to load dashboard", error.message); | |
| // If loading fails, it might be due to an invalid token, redirect to login | |
| localStorage.removeItem("auth_token"); | |
| localStorage.removeItem("refresh_token"); | |
| localStorage.removeItem("user_data"); | |
| window.location.href = "/auth.html"; | |
| } | |
| } async loadUserInfo() { | |
| try { | |
| const response = await this.apiCall("/api/profile"); | |
| this.currentUser = response.user; | |
| document.getElementById('userName').textContent = this.currentUser.username; | |
| } catch (error) { | |
| console.error('Failed to load user info:', error); | |
| throw error; | |
| } | |
| } | |
| async loadInitialData() { | |
| this.showLoading(true); | |
| try { | |
| await Promise.all([ | |
| this.loadClients(), | |
| this.loadServerStatus(), | |
| this.loadProtocols(), | |
| this.loadServerStatistics() | |
| ]); | |
| this.updateOverviewStats(); | |
| this.addActivity('Dashboard loaded successfully'); | |
| } catch (error) { | |
| console.error('Failed to load initial data:', error); | |
| this.showAlert('error', 'Failed to load data', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async loadClients() { | |
| try { | |
| const response = await this.apiCall('/api/vpn-clients'); | |
| this.clients = response.clients || []; | |
| this.renderClientsTable(); | |
| } catch (error) { | |
| console.error('Failed to load clients:', error); | |
| this.clients = []; | |
| } | |
| } | |
| async loadServerStatus() { | |
| try { | |
| const response = await this.apiCall('/api/server/status'); | |
| this.serverStatus = response.status || {}; | |
| this.serverInfo = response.server_info || {}; | |
| this.renderServerStatus(); | |
| } catch (error) { | |
| console.error('Failed to load server status:', error); | |
| this.serverStatus = {}; | |
| } | |
| } | |
| async loadProtocols() { | |
| try { | |
| const response = await fetch('/api/server/protocols'); | |
| const data = await response.json(); | |
| this.protocols = data.protocols || []; | |
| this.renderProtocols(); | |
| } catch (error) { | |
| console.error('Failed to load protocols:', error); | |
| this.protocols = []; | |
| } | |
| } | |
| async loadServerStatistics() { | |
| try { | |
| const response = await this.apiCall('/api/server/statistics'); | |
| this.statistics = response.statistics || {}; | |
| } catch (error) { | |
| console.error('Failed to load server statistics:', error); | |
| this.statistics = {}; | |
| } | |
| } | |
| updateOverviewStats() { | |
| const totalClients = this.clients.length; | |
| const runningServers = Object.values(this.serverStatus).filter(s => s.running).length; | |
| const serverLocation = this.serverInfo?.server_ip || 'Unknown'; | |
| document.getElementById('totalClients').textContent = totalClients; | |
| document.getElementById('runningServers').textContent = runningServers; | |
| document.getElementById('serverLocation').textContent = serverLocation; | |
| } | |
| renderClientsTable() { | |
| const tbody = document.getElementById('clientsTableBody'); | |
| if (this.clients.length === 0) { | |
| tbody.innerHTML = ` | |
| <tr> | |
| <td colspan="6" style="text-align: center; padding: 2rem; color: #666;"> | |
| <i class="fas fa-laptop" style="font-size: 2rem; margin-bottom: 1rem; display: block;"></i> | |
| No VPN clients found. Create your first client to get started. | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| tbody.innerHTML = this.clients.map(client => ` | |
| <tr> | |
| <td> | |
| <strong>${this.escapeHtml(client.client_name)}</strong> | |
| ${client.description ? `<br><small style="color: #666;">${this.escapeHtml(client.description)}</small>` : ''} | |
| </td> | |
| <td> | |
| <span class="protocol-badge ${client.protocol}"> | |
| ${client.protocol.toUpperCase()} | |
| </span> | |
| </td> | |
| <td> | |
| <span class="status-badge ${client.status}"> | |
| ${client.status} | |
| </span> | |
| </td> | |
| <td>${this.formatDate(client.created_at)}</td> | |
| <td>${client.last_connected ? this.formatDate(client.last_connected) : 'Never'}</td> | |
| <td> | |
| <div class="client-actions"> | |
| <button class="btn small primary" onclick="dashboard.downloadClientConfig(${client.id})"> | |
| <i class="fas fa-download"></i> | |
| </button> | |
| <button class="btn small secondary" onclick="dashboard.viewClientDetails(${client.id})"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| <button class="btn small danger" onclick="dashboard.revokeClient(${client.id})"> | |
| <i class="fas fa-ban"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| `).join(''); | |
| } | |
| renderServerStatus() { | |
| const protocols = ['openvpn', 'ikev2', 'wireguard']; | |
| protocols.forEach(protocol => { | |
| const status = this.serverStatus[protocol] || { running: false, client_count: 0, port: 0 }; | |
| const statusElement = document.getElementById(`${protocol}Status`); | |
| const clientsElement = document.getElementById(`${protocol}Clients`); | |
| const portElement = document.getElementById(`${protocol}Port`); | |
| if (statusElement) { | |
| statusElement.className = `server-status ${status.running ? 'online' : 'offline'}`; | |
| statusElement.innerHTML = ` | |
| <span class="status-dot"></span> | |
| ${status.running ? 'Online' : 'Offline'} | |
| `; | |
| } | |
| if (clientsElement) { | |
| clientsElement.textContent = status.client_count || 0; | |
| } | |
| if (portElement) { | |
| portElement.textContent = status.port || 0; | |
| } | |
| }); | |
| } | |
| renderProtocols() { | |
| const container = document.getElementById('protocolsGrid'); | |
| container.innerHTML = this.protocols.map(protocol => ` | |
| <div class="protocol-card"> | |
| <div class="protocol-header"> | |
| <div class="protocol-icon"> | |
| <i class="fas fa-network-wired"></i> | |
| </div> | |
| <h3>${protocol.name}</h3> | |
| </div> | |
| <p class="protocol-description">${protocol.description}</p> | |
| <div class="protocol-details"> | |
| <div class="protocol-detail"> | |
| <span>Port:</span> | |
| <span>${protocol.port}</span> | |
| </div> | |
| <div class="protocol-detail"> | |
| <span>Transport:</span> | |
| <span>${protocol.transport}</span> | |
| </div> | |
| </div> | |
| <div class="protocol-features"> | |
| <h4>Features:</h4> | |
| <ul> | |
| ${protocol.features.map(feature => `<li>${feature}</li>`).join('')} | |
| </ul> | |
| </div> | |
| <button class="btn primary" onclick="dashboard.createClientWithProtocol('${protocol.key}')"> | |
| Create ${protocol.name} Client | |
| </button> | |
| </div> | |
| `).join(''); | |
| } | |
| setupEventListeners() { | |
| // Close modals when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('modal')) { | |
| this.closeModal(e.target.id); | |
| } | |
| }); | |
| // Close user dropdown when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!e.target.closest('.user-menu')) { | |
| document.getElementById('userDropdown').classList.remove('show'); | |
| } | |
| }); | |
| } | |
| startAutoRefresh() { | |
| // Refresh server status every 30 seconds | |
| setInterval(() => { | |
| this.loadServerStatus(); | |
| }, 30000); | |
| // Refresh clients every 60 seconds | |
| setInterval(() => { | |
| this.loadClients(); | |
| }, 60000); | |
| } | |
| // API Helper | |
| async apiCall(endpoint, options = {}) { | |
| const defaultOptions = { | |
| headers: { | |
| 'Authorization': `Bearer ${this.token}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }; | |
| const response = await fetch(endpoint, { ...defaultOptions, ...options }); | |
| if (response.status === 401) { | |
| localStorage.removeItem('auth_token'); | |
| window.location.href = '/auth.html'; | |
| return; | |
| } | |
| if (!response.ok) { | |
| const error = await response.json().catch(() => ({ error: 'Network error' })); | |
| throw new Error(error.error || 'Request failed'); | |
| } | |
| return await response.json(); | |
| } | |
| // Navigation | |
| showSection(sectionId) { | |
| // Hide all sections | |
| document.querySelectorAll('.content-section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| // Remove active class from nav items | |
| document.querySelectorAll('.nav-item').forEach(item => { | |
| item.classList.remove('active'); | |
| }); | |
| // Show selected section | |
| document.getElementById(sectionId).classList.add('active'); | |
| // Add active class to nav item | |
| document.querySelector(`[data-section="${sectionId}"]`).classList.add('active'); | |
| // Load section-specific data | |
| this.loadSectionData(sectionId); | |
| } | |
| async loadSectionData(sectionId) { | |
| switch (sectionId) { | |
| case 'server-status': | |
| await this.loadServerStatus(); | |
| break; | |
| case 'vpn-clients': | |
| await this.loadClients(); | |
| break; | |
| case 'protocols': | |
| await this.loadProtocols(); | |
| break; | |
| } | |
| } | |
| // Client Management | |
| showCreateClientModal() { | |
| document.getElementById('createClientModal').classList.add('show'); | |
| document.getElementById('createClientForm').reset(); | |
| } | |
| createClientWithProtocol(protocol) { | |
| this.showCreateClientModal(); | |
| document.getElementById('clientProtocol').value = protocol; | |
| } | |
| async createVPNClient(event) { | |
| event.preventDefault(); | |
| const formData = new FormData(event.target); | |
| const clientData = { | |
| client_name: formData.get('clientName'), | |
| protocol: formData.get('protocol'), | |
| description: formData.get('description') | |
| }; | |
| try { | |
| this.showLoading(true); | |
| const response = await this.apiCall('/api/vpn-clients', { | |
| method: 'POST', | |
| body: JSON.stringify(clientData) | |
| }); | |
| this.showAlert('success', 'Client Created', 'VPN client created successfully'); | |
| this.closeModal('createClientModal'); | |
| await this.loadClients(); | |
| this.addActivity(`Created VPN client: ${clientData.client_name}`); | |
| } catch (error) { | |
| console.error('Failed to create client:', error); | |
| this.showAlert('error', 'Creation Failed', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async downloadClientConfig(clientId) { | |
| try { | |
| const response = await fetch(`/api/vpn-clients/${clientId}/download`, { | |
| headers: { | |
| 'Authorization': `Bearer ${this.token}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Download failed'); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `vpn-client-${clientId}.zip`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| this.addActivity(`Downloaded configuration for client ${clientId}`); | |
| } catch (error) { | |
| console.error('Download failed:', error); | |
| this.showAlert('error', 'Download Failed', error.message); | |
| } | |
| } | |
| async revokeClient(clientId) { | |
| if (!confirm('Are you sure you want to revoke this client? This action cannot be undone.')) { | |
| return; | |
| } | |
| try { | |
| await this.apiCall(`/api/vpn-clients/${clientId}/revoke`, { | |
| method: 'POST' | |
| }); | |
| this.showAlert('success', 'Client Revoked', 'Client access has been revoked'); | |
| await this.loadClients(); | |
| this.addActivity(`Revoked client ${clientId}`); | |
| } catch (error) { | |
| console.error('Failed to revoke client:', error); | |
| this.showAlert('error', 'Revocation Failed', error.message); | |
| } | |
| } | |
| // Server Management | |
| async startServer(protocol) { | |
| try { | |
| this.showLoading(true); | |
| const response = await this.apiCall(`/api/server/start/${protocol}`, { | |
| method: 'POST' | |
| }); | |
| this.showAlert('success', 'Server Started', response.message); | |
| await this.loadServerStatus(); | |
| this.addActivity(`Started ${protocol.toUpperCase()} server`); | |
| } catch (error) { | |
| console.error('Failed to start server:', error); | |
| this.showAlert('error', 'Start Failed', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async stopServer(protocol) { | |
| if (!confirm(`Are you sure you want to stop the ${protocol.toUpperCase()} server?`)) { | |
| return; | |
| } | |
| try { | |
| this.showLoading(true); | |
| const response = await this.apiCall(`/api/server/stop/${protocol}`, { | |
| method: 'POST' | |
| }); | |
| this.showAlert('success', 'Server Stopped', response.message); | |
| await this.loadServerStatus(); | |
| this.addActivity(`Stopped ${protocol.toUpperCase()} server`); | |
| } catch (error) { | |
| console.error('Failed to stop server:', error); | |
| this.showAlert('error', 'Stop Failed', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async restartServer(protocol) { | |
| if (!confirm(`Are you sure you want to restart the ${protocol.toUpperCase()} server?`)) { | |
| return; | |
| } | |
| try { | |
| this.showLoading(true); | |
| const response = await this.apiCall(`/api/server/restart/${protocol}`, { | |
| method: 'POST' | |
| }); | |
| this.showAlert('success', 'Server Restarted', response.message); | |
| await this.loadServerStatus(); | |
| this.addActivity(`Restarted ${protocol.toUpperCase()} server`); | |
| } catch (error) { | |
| console.error('Failed to restart server:', error); | |
| this.showAlert('error', 'Restart Failed', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async refreshServerStatus() { | |
| try { | |
| this.showLoading(true); | |
| await this.loadServerStatus(); | |
| this.showAlert('success', 'Status Refreshed', 'Server status updated'); | |
| } catch (error) { | |
| this.showAlert('error', 'Refresh Failed', error.message); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| async loadServerLogs() { | |
| const protocol = document.getElementById('logProtocol').value; | |
| try { | |
| const response = await this.apiCall(`/api/server/logs/${protocol}?lines=100`); | |
| const logsContainer = document.getElementById('logsContainer'); | |
| if (response.logs && response.logs.length > 0) { | |
| logsContainer.innerHTML = ` | |
| <div class="logs-content"> | |
| ${response.logs.join('\\n')} | |
| </div> | |
| `; | |
| } else { | |
| logsContainer.innerHTML = ` | |
| <div class="logs-content"> | |
| No logs available for ${protocol.toUpperCase()} | |
| </div> | |
| `; | |
| } | |
| // Scroll to bottom | |
| logsContainer.scrollTop = logsContainer.scrollHeight; | |
| } catch (error) { | |
| console.error('Failed to load logs:', error); | |
| this.showAlert('error', 'Logs Failed', error.message); | |
| } | |
| } | |
| // Filtering | |
| filterClients() { | |
| const protocolFilter = document.getElementById('protocolFilter').value; | |
| const statusFilter = document.getElementById('statusFilter').value; | |
| const searchFilter = document.getElementById('searchFilter').value.toLowerCase(); | |
| let filteredClients = this.clients; | |
| if (protocolFilter) { | |
| filteredClients = filteredClients.filter(client => client.protocol === protocolFilter); | |
| } | |
| if (statusFilter) { | |
| filteredClients = filteredClients.filter(client => client.status === statusFilter); | |
| } | |
| if (searchFilter) { | |
| filteredClients = filteredClients.filter(client => | |
| client.client_name.toLowerCase().includes(searchFilter) || | |
| (client.description && client.description.toLowerCase().includes(searchFilter)) | |
| ); | |
| } | |
| // Temporarily store original clients and render filtered | |
| const originalClients = this.clients; | |
| this.clients = filteredClients; | |
| this.renderClientsTable(); | |
| this.clients = originalClients; | |
| } | |
| // UI Helpers | |
| showModal(modalId) { | |
| document.getElementById(modalId).classList.add('show'); | |
| } | |
| closeModal(modalId) { | |
| document.getElementById(modalId).classList.remove('show'); | |
| } | |
| showLoading(show) { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| if (show) { | |
| overlay.classList.add('show'); | |
| } else { | |
| overlay.classList.remove('show'); | |
| } | |
| } | |
| showAlert(type, title, message) { | |
| const container = document.getElementById('alertContainer'); | |
| const alertId = 'alert-' + Date.now(); | |
| const alertHtml = ` | |
| <div class="alert ${type}" id="${alertId}"> | |
| <div class="alert-icon"> | |
| <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> | |
| </div> | |
| <div class="alert-content"> | |
| <div class="alert-title">${title}</div> | |
| <div class="alert-message">${message}</div> | |
| </div> | |
| <button class="alert-close" onclick="dashboard.closeAlert('${alertId}')"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| `; | |
| container.insertAdjacentHTML('beforeend', alertHtml); | |
| // Auto-remove after 5 seconds | |
| setTimeout(() => { | |
| this.closeAlert(alertId); | |
| }, 5000); | |
| } | |
| closeAlert(alertId) { | |
| const alert = document.getElementById(alertId); | |
| if (alert) { | |
| alert.remove(); | |
| } | |
| } | |
| addActivity(message) { | |
| const activityList = document.getElementById('activityList'); | |
| const activityHtml = ` | |
| <div class="activity-item"> | |
| <i class="fas fa-info-circle"></i> | |
| <span>${this.escapeHtml(message)}</span> | |
| <time>Just now</time> | |
| </div> | |
| `; | |
| activityList.insertAdjacentHTML('afterbegin', activityHtml); | |
| // Keep only last 10 activities | |
| const activities = activityList.querySelectorAll('.activity-item'); | |
| if (activities.length > 10) { | |
| activities[activities.length - 1].remove(); | |
| } | |
| } | |
| // Utility functions | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| formatDate(dateString) { | |
| if (!dateString) return 'Never'; | |
| const date = new Date(dateString); | |
| const now = new Date(); | |
| const diff = now - date; | |
| if (diff < 60000) return 'Just now'; | |
| if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`; | |
| if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`; | |
| return date.toLocaleDateString(); | |
| } | |
| // User menu | |
| toggleUserMenu() { | |
| document.getElementById('userDropdown').classList.toggle('show'); | |
| } | |
| showProfile() { | |
| this.showAlert('info', 'Profile', 'Profile management coming soon'); | |
| } | |
| showSettings() { | |
| this.showAlert('info', 'Settings', 'Settings management coming soon'); | |
| } | |
| logout() { | |
| localStorage.removeItem('auth_token'); | |
| window.location.href = '/auth.html'; | |
| } | |
| } | |
| // Global functions for onclick handlers | |
| let dashboard; | |
| function showSection(sectionId) { | |
| dashboard.showSection(sectionId); | |
| } | |
| function showCreateClientModal() { | |
| dashboard.showCreateClientModal(); | |
| } | |
| function createVPNClient(event) { | |
| dashboard.createVPNClient(event); | |
| } | |
| function closeModal(modalId) { | |
| dashboard.closeModal(modalId); | |
| } | |
| function toggleUserMenu() { | |
| dashboard.toggleUserMenu(); | |
| } | |
| function showProfile() { | |
| dashboard.showProfile(); | |
| } | |
| function showSettings() { | |
| dashboard.showSettings(); | |
| } | |
| function logout() { | |
| dashboard.logout(); | |
| } | |
| function startServer(protocol) { | |
| dashboard.startServer(protocol); | |
| } | |
| function stopServer(protocol) { | |
| dashboard.stopServer(protocol); | |
| } | |
| function restartServer(protocol) { | |
| dashboard.restartServer(protocol); | |
| } | |
| function refreshServerStatus() { | |
| dashboard.refreshServerStatus(); | |
| } | |
| function loadServerLogs() { | |
| dashboard.loadServerLogs(); | |
| } | |
| function filterClients() { | |
| dashboard.filterClients(); | |
| } | |
| // Initialize dashboard when page loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| dashboard = new VPNDashboard(); | |
| }); | |