From 7b845e952d255b8a2f513d38d8ca5425a7687f69 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Fri, 30 Jan 2026 21:07:30 +0300 Subject: [PATCH] feat: Add online clients tracking and display on dashboard and server views --- .gitignore | 1 + inc/ServerMonitoring.php | 160 +++++++++++++++++- .../055_dashboard_online_now_translation.sql | 9 + public/index.php | 9 + templates/dashboard.twig | 8 +- templates/servers/view.twig | 8 +- 6 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 migrations/055_dashboard_online_now_translation.sql diff --git a/.gitignore b/.gitignore index a493424..c23d763 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ restore_local.php test_protocols.php scripts/regen_qr.php scripts/test_xray_install.sh +scripts/test_online.php diff --git a/inc/ServerMonitoring.php b/inc/ServerMonitoring.php index bed5000..1e7e5a0 100644 --- a/inc/ServerMonitoring.php +++ b/inc/ServerMonitoring.php @@ -525,16 +525,18 @@ class ServerMonitoring private function execSSH(string $cmd): ?string { $host = $this->serverData['host']; - $port = $this->serverData['port']; + $port = (int)$this->serverData['port']; $username = $this->serverData['username']; $password = $this->serverData['password']; + $sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; $sshCmd = sprintf( - 'sshpass -p %s ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p %d %s@%s %s 2>/dev/null', - escapeshellarg($password), + "sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null", + $password, $port, - escapeshellarg($username), - escapeshellarg($host), + $sshOptions, + $username, + $host, escapeshellarg($cmd) ); @@ -628,12 +630,158 @@ class ServerMonitoring } } - // Block collected IPs + // Update blocking rules if (!empty($ipsToBlock)) { + // Block collected IPs (with -reset to replace existing rule) $ipList = implode(' ', array_unique($ipsToBlock)); $blockCmd = "docker exec $xrayContainer xray api sib --server=127.0.0.1:10085 -outbound=blocked -inbound=vless-in -reset $ipList"; $this->execSSH($blockCmd); error_log("[Xray Enforcement] Blocked IPs: $ipList"); + } else { + // No IPs to block - remove the blocking rule if it exists + $rmCmd = "docker exec $xrayContainer xray api rmrules --server=127.0.0.1:10085 sourceIpBlock 2>/dev/null || true"; + $this->execSSH($rmCmd); } } + + /** + * Count total online clients across all Xray servers + * Returns array with 'total' count and 'users' list + */ + public static function countOnlineClients(): array + { + $result = ['total' => 0, 'users' => []]; + + // Get all active servers + $servers = VpnServer::listAll(); + + foreach ($servers as $serverData) { + // Check if this is an Xray server + $containerName = $serverData['container_name'] ?? ''; + if (strpos($containerName, 'xray') === false) { + continue; + } + + // Build SSH command + $host = $serverData['host']; + $port = (int)($serverData['port'] ?? 22); + $username = $serverData['username'] ?? 'root'; + $password = $serverData['password'] ?? ''; + + $xrayContainer = $containerName ?: 'amnezia-xray'; + $cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085"; + + $sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5'; + $sshCmd = sprintf( + "sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null", + $password, + $port, + $sshOptions, + $username, + $host, + escapeshellarg($cmd) + ); + + $output = shell_exec($sshCmd); + if (!$output) { + continue; + } + + $data = json_decode($output, true); + if (!isset($data['users']) || !is_array($data['users'])) { + continue; + } + + foreach ($data['users'] as $user) { + // Parse format: "user>>>email>>>online" or object with email/count + if (is_string($user)) { + // Format: "user>>>olegtest3>>>online" + $parts = explode('>>>', $user); + if (count($parts) >= 2) { + $email = $parts[1]; + $result['total'] += 1; + $result['users'][] = [ + 'server_id' => $serverData['id'], + 'email' => $email, + 'count' => 1 + ]; + } + } else { + // Object format + $email = $user['email'] ?? 'unknown'; + $count = (int)($user['count'] ?? 1); + $result['total'] += $count; + $result['users'][] = [ + 'server_id' => $serverData['id'], + 'email' => $email, + 'count' => $count + ]; + } + } + } + + return $result; + } + + /** + * Get online clients for a specific server + * Returns array of online client logins/emails + */ + public static function getOnlineClientsForServer(array $serverData): array + { + $result = []; + + // Check if this is an Xray server + $containerName = $serverData['container_name'] ?? ''; + if (strpos($containerName, 'xray') === false) { + return $result; + } + + // Build SSH command + $host = $serverData['host']; + $port = (int)($serverData['port'] ?? 22); + $username = $serverData['username'] ?? 'root'; + $password = $serverData['password'] ?? ''; + + $xrayContainer = $containerName ?: 'amnezia-xray'; + $cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085"; + + $sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5'; + $sshCmd = sprintf( + "sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null", + $password, + $port, + $sshOptions, + $username, + $host, + escapeshellarg($cmd) + ); + + $output = shell_exec($sshCmd); + if (!$output) { + return $result; + } + + $data = json_decode($output, true); + if (!isset($data['users']) || !is_array($data['users'])) { + return $result; + } + + foreach ($data['users'] as $user) { + // Parse format: "user>>>email>>>online" + if (is_string($user)) { + $parts = explode('>>>', $user); + if (count($parts) >= 2) { + $result[] = $parts[1]; + } + } else { + $email = $user['email'] ?? null; + if ($email) { + $result[] = $email; + } + } + } + + return $result; + } } diff --git a/migrations/055_dashboard_online_now_translation.sql b/migrations/055_dashboard_online_now_translation.sql new file mode 100644 index 0000000..03505bb --- /dev/null +++ b/migrations/055_dashboard_online_now_translation.sql @@ -0,0 +1,9 @@ +-- Add translation for dashboard.online_now +INSERT INTO translations (`locale`, `category`, `key_name`, `translation`) VALUES +('en', 'dashboard', 'online_now', 'Online Now'), +('ru', 'dashboard', 'online_now', 'Сейчас онлайн'), +('es', 'dashboard', 'online_now', 'En línea ahora'), +('de', 'dashboard', 'online_now', 'Jetzt online'), +('fr', 'dashboard', 'online_now', 'En ligne maintenant'), +('zh', 'dashboard', 'online_now', '当前在线') +ON DUPLICATE KEY UPDATE `translation` = VALUES(`translation`); diff --git a/public/index.php b/public/index.php index 871925a..d8d3ac0 100644 --- a/public/index.php +++ b/public/index.php @@ -265,9 +265,14 @@ Router::get('/dashboard', function () { // Get user's clients $clients = VpnClient::listByUser($user['id']); + // Get real-time online clients count from Xray API + $onlineData = ServerMonitoring::countOnlineClients(); + View::render('dashboard.twig', [ 'servers' => $servers, 'clients' => $clients, + 'online_count' => $onlineData['total'], + 'online_users' => $onlineData['users'], ]); }); @@ -845,6 +850,9 @@ Router::get('/servers/{id}', function ($params) { } } + // Get online clients for this server (Xray) + $onlineLogins = ServerMonitoring::getOnlineClientsForServer($serverData); + View::render('servers/view.twig', [ 'server' => $serverData, 'clients' => $clients, @@ -852,6 +860,7 @@ Router::get('/servers/{id}', function ($params) { 'server_protocols' => $serverProtocols, 'selected_protocol_id' => $selectedProtocolId, 'available_protocols' => $availableProtocols, + 'online_logins' => $onlineLogins, ]); } catch (Exception $e) { error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine()); diff --git a/templates/dashboard.twig b/templates/dashboard.twig index 2e33e74..12e52f9 100644 --- a/templates/dashboard.twig +++ b/templates/dashboard.twig @@ -40,13 +40,13 @@
-
- +
+
-

{{ t('dashboard.active_clients') }}

+

{{ t('dashboard.online_now') }}

- {{ servers|filter(s => s.status == 'active')|length }} + {{ online_count|default(0) }}

diff --git a/templates/servers/view.twig b/templates/servers/view.twig index 39a63d9..6592140 100644 --- a/templates/servers/view.twig +++ b/templates/servers/view.twig @@ -267,7 +267,7 @@ {% for client in clients %} {{ client.name }} - {{ client.login|default('-') }} + {{ client.name }} {% if client.protocol_name %} {{ client.protocol_name }} @@ -277,8 +277,10 @@ {{ client.client_ip }} - {% if client.status == 'active' %} - {{ t('status.active') }} + {% if client.name in online_logins %} + Online + {% elseif client.status == 'active' %} + {{ t('status.active') }} {% else %} {{ t('status.disabled') }} {% endif %}