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
|
||||
scripts/regen_qr.php
|
||||
scripts/test_xray_install.sh
|
||||
scripts/test_online.php
|
||||
|
||||
+154
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
$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());
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
|
||||
<i class="fas fa-check-circle text-2xl"></i>
|
||||
<div class="p-3 rounded-full bg-green-100 text-green-600">
|
||||
<i class="fas fa-wifi text-2xl"></i>
|
||||
</div>
|
||||
<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">
|
||||
{{ servers|filter(s => s.status == 'active')|length }}
|
||||
{{ online_count|default(0) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
{% for client in clients %}
|
||||
<tr class="border-t">
|
||||
<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">
|
||||
{% if client.protocol_name %}
|
||||
<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 class="px-6 py-4">{{ client.client_ip }}</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if client.status == 'active' %}
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span>
|
||||
{% if client.name in online_logins %}
|
||||
<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 %}
|
||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user