feat: Add online clients tracking and display on dashboard and server views
This commit is contained in:
@@ -66,3 +66,4 @@ restore_local.php
|
|||||||
test_protocols.php
|
test_protocols.php
|
||||||
scripts/regen_qr.php
|
scripts/regen_qr.php
|
||||||
scripts/test_xray_install.sh
|
scripts/test_xray_install.sh
|
||||||
|
scripts/test_online.php
|
||||||
|
|||||||
+154
-6
@@ -525,16 +525,18 @@ class ServerMonitoring
|
|||||||
private function execSSH(string $cmd): ?string
|
private function execSSH(string $cmd): ?string
|
||||||
{
|
{
|
||||||
$host = $this->serverData['host'];
|
$host = $this->serverData['host'];
|
||||||
$port = $this->serverData['port'];
|
$port = (int)$this->serverData['port'];
|
||||||
$username = $this->serverData['username'];
|
$username = $this->serverData['username'];
|
||||||
$password = $this->serverData['password'];
|
$password = $this->serverData['password'];
|
||||||
|
|
||||||
|
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
|
||||||
$sshCmd = sprintf(
|
$sshCmd = sprintf(
|
||||||
'sshpass -p %s ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p %d %s@%s %s 2>/dev/null',
|
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
|
||||||
escapeshellarg($password),
|
$password,
|
||||||
$port,
|
$port,
|
||||||
escapeshellarg($username),
|
$sshOptions,
|
||||||
escapeshellarg($host),
|
$username,
|
||||||
|
$host,
|
||||||
escapeshellarg($cmd)
|
escapeshellarg($cmd)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -628,12 +630,158 @@ class ServerMonitoring
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block collected IPs
|
// Update blocking rules
|
||||||
if (!empty($ipsToBlock)) {
|
if (!empty($ipsToBlock)) {
|
||||||
|
// Block collected IPs (with -reset to replace existing rule)
|
||||||
$ipList = implode(' ', array_unique($ipsToBlock));
|
$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";
|
$blockCmd = "docker exec $xrayContainer xray api sib --server=127.0.0.1:10085 -outbound=blocked -inbound=vless-in -reset $ipList";
|
||||||
$this->execSSH($blockCmd);
|
$this->execSSH($blockCmd);
|
||||||
error_log("[Xray Enforcement] Blocked IPs: $ipList");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`);
|
||||||
@@ -265,9 +265,14 @@ Router::get('/dashboard', function () {
|
|||||||
// Get user's clients
|
// Get user's clients
|
||||||
$clients = VpnClient::listByUser($user['id']);
|
$clients = VpnClient::listByUser($user['id']);
|
||||||
|
|
||||||
|
// Get real-time online clients count from Xray API
|
||||||
|
$onlineData = ServerMonitoring::countOnlineClients();
|
||||||
|
|
||||||
View::render('dashboard.twig', [
|
View::render('dashboard.twig', [
|
||||||
'servers' => $servers,
|
'servers' => $servers,
|
||||||
'clients' => $clients,
|
'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', [
|
View::render('servers/view.twig', [
|
||||||
'server' => $serverData,
|
'server' => $serverData,
|
||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
@@ -852,6 +860,7 @@ Router::get('/servers/{id}', function ($params) {
|
|||||||
'server_protocols' => $serverProtocols,
|
'server_protocols' => $serverProtocols,
|
||||||
'selected_protocol_id' => $selectedProtocolId,
|
'selected_protocol_id' => $selectedProtocolId,
|
||||||
'available_protocols' => $availableProtocols,
|
'available_protocols' => $availableProtocols,
|
||||||
|
'online_logins' => $onlineLogins,
|
||||||
]);
|
]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
|
error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
|
||||||
|
|||||||
@@ -40,13 +40,13 @@
|
|||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
|
<div class="p-3 rounded-full bg-green-100 text-green-600">
|
||||||
<i class="fas fa-check-circle text-2xl"></i>
|
<i class="fas fa-wifi text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.active_clients') }}</p>
|
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.online_now') }}</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">
|
<p class="text-2xl font-bold text-gray-900">
|
||||||
{{ servers|filter(s => s.status == 'active')|length }}
|
{{ online_count|default(0) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -267,7 +267,7 @@
|
|||||||
{% for client in clients %}
|
{% for client in clients %}
|
||||||
<tr class="border-t">
|
<tr class="border-t">
|
||||||
<td class="px-6 py-4">{{ client.name }}</td>
|
<td class="px-6 py-4">{{ client.name }}</td>
|
||||||
<td class="px-6 py-4">{{ client.login|default('-') }}</td>
|
<td class="px-6 py-4">{{ client.name }}</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{% if client.protocol_name %}
|
{% if client.protocol_name %}
|
||||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
|
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
|
||||||
@@ -277,8 +277,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{% if client.status == 'active' %}
|
{% if client.name in online_logins %}
|
||||||
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span>
|
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
|
||||||
|
{% elseif client.status == 'active' %}
|
||||||
|
<span class="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user