Add traffic limit functionality
- Add traffic_limit field to vpn_clients table (migration 007) - Add traffic limit management methods in VpnClient class - Add API endpoints: set-traffic-limit, traffic-limit-status, overlimit - Add UI for setting limits when creating clients - Add traffic limit column in clients table with visual indicators - Add automatic traffic limit check via cron (bin/check_traffic_limits.php) - Add translations for traffic limits (6 languages) - Update README with traffic limit documentation
This commit is contained in:
+2
-1
@@ -36,8 +36,9 @@ COPY apache.conf /etc/apache2/sites-available/000-default.conf
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 755 /var/www/html/public
|
||||
|
||||
# Setup cron for client expiration check (runs every hour)
|
||||
# Setup cron for client expiration and traffic limit checks (runs every hour)
|
||||
RUN echo "0 * * * * www-data cd /var/www/html && /usr/local/bin/php bin/check_expired_clients.php >> /var/log/cron.log 2>&1" > /etc/cron.d/amnezia-cron \
|
||||
&& echo "0 * * * * www-data cd /var/www/html && /usr/local/bin/php bin/check_traffic_limits.php >> /var/log/cron.log 2>&1" >> /etc/cron.d/amnezia-cron \
|
||||
&& chmod 0644 /etc/cron.d/amnezia-cron \
|
||||
&& crontab /etc/cron.d/amnezia-cron \
|
||||
&& touch /var/log/cron.log
|
||||
|
||||
@@ -6,13 +6,14 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
|
||||
|
||||
- VPN server deployment via SSH
|
||||
- Client configuration management with **expiration dates**
|
||||
- **Traffic limits** for clients with automatic enforcement
|
||||
- **Server backup and restore** functionality
|
||||
- Traffic statistics monitoring
|
||||
- QR code generation for mobile apps
|
||||
- Multi-language interface (English, Russian, Spanish, German, French, Chinese)
|
||||
- REST API with JWT authentication
|
||||
- User authentication and access control
|
||||
- **Automatic client expiration check** via cron
|
||||
- **Automatic client expiration and traffic limit checks** via cron
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -64,8 +65,9 @@ JWT_SECRET=your-secret-key-change-this
|
||||
1. Open server details
|
||||
2. Enter client name
|
||||
3. **Select expiration period** (optional, default: never expires)
|
||||
4. Click Create Client
|
||||
5. Download config or scan QR code
|
||||
4. **Select traffic limit** (optional, default: unlimited)
|
||||
5. Click Create Client
|
||||
6. Download config or scan QR code
|
||||
|
||||
### Manage Client Expiration
|
||||
|
||||
@@ -86,6 +88,29 @@ curl http://localhost:8082/api/clients/expiring?days=7 \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### Manage Traffic Limits
|
||||
|
||||
Set and monitor traffic limits via UI or API:
|
||||
```bash
|
||||
# Set traffic limit (10 GB = 10737418240 bytes)
|
||||
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"limit_bytes": 10737418240}'
|
||||
|
||||
# Remove traffic limit (set to unlimited)
|
||||
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"limit_bytes": null}'
|
||||
|
||||
# Check traffic limit status
|
||||
curl http://localhost:8082/api/clients/123/traffic-limit-status \
|
||||
-H "Authorization: Bearer <token>"
|
||||
|
||||
# Get clients over traffic limit
|
||||
curl http://localhost:8082/api/clients/overlimit \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### Server Backups
|
||||
|
||||
Create and restore backups via UI or API:
|
||||
@@ -118,6 +143,20 @@ Run manually:
|
||||
docker compose exec web php /var/www/html/bin/check_expired_clients.php
|
||||
```
|
||||
|
||||
### Automatic Traffic Limit Check
|
||||
|
||||
**Runs automatically in Docker container** every hour to disable clients that exceeded their traffic limit.
|
||||
|
||||
Check cron logs:
|
||||
```bash
|
||||
docker compose exec web tail -f /var/log/cron.log
|
||||
```
|
||||
|
||||
Run manually:
|
||||
```bash
|
||||
docker compose exec web php /var/www/html/bin/check_traffic_limits.php
|
||||
```
|
||||
|
||||
### API Authentication
|
||||
|
||||
Get JWT token:
|
||||
@@ -167,6 +206,10 @@ POST /api/clients/{id}/extend - Extend client expiration
|
||||
Parameters: days (int)
|
||||
GET /api/clients/expiring - Get clients expiring soon
|
||||
Parameters: days (default: 7)
|
||||
POST /api/clients/{id}/set-traffic-limit - Set client traffic limit
|
||||
Parameters: limit_bytes (int or null for unlimited)
|
||||
GET /api/clients/{id}/traffic-limit-status - Get traffic limit status
|
||||
GET /api/clients/overlimit - Get clients over traffic limit
|
||||
```
|
||||
|
||||
### Backups
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Check and disable clients that exceeded their traffic limit
|
||||
* Run this script via cron every hour
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../inc/Config.php';
|
||||
require_once __DIR__ . '/../inc/DB.php';
|
||||
require_once __DIR__ . '/../inc/VpnClient.php';
|
||||
require_once __DIR__ . '/../inc/VpnServer.php';
|
||||
|
||||
// Load config
|
||||
Config::load(__DIR__ . '/../.env');
|
||||
|
||||
echo '[' . date('Y-m-d H:i:s') . '] Checking for clients over traffic limit...' . PHP_EOL;
|
||||
|
||||
try {
|
||||
$disabled = VpnClient::disableClientsOverLimit();
|
||||
|
||||
if ($disabled > 0) {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] Disabled ' . $disabled . ' client(s) that exceeded traffic limit' . PHP_EOL;
|
||||
} else {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] No clients over traffic limit' . PHP_EOL;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -825,5 +825,117 @@ class VpnClient {
|
||||
$diff = strtotime($this->data['expires_at']) - time();
|
||||
return (int)floor($diff / 86400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set traffic limit for client
|
||||
*
|
||||
* @param int|null $limitBytes Traffic limit in bytes (NULL = unlimited)
|
||||
* @return bool Success
|
||||
*/
|
||||
public function setTrafficLimit(?int $limitBytes): bool {
|
||||
if (!$this->data) {
|
||||
throw new Exception('Client not loaded');
|
||||
}
|
||||
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('UPDATE vpn_clients SET traffic_limit = ? WHERE id = ?');
|
||||
$result = $stmt->execute([$limitBytes, $this->clientId]);
|
||||
|
||||
if ($result) {
|
||||
$this->data['traffic_limit'] = $limitBytes;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total traffic used (sent + received)
|
||||
*
|
||||
* @return int Total traffic in bytes
|
||||
*/
|
||||
public function getTotalTraffic(): int {
|
||||
if (!$this->data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)($this->data['traffic_sent'] ?? 0) + (int)($this->data['traffic_received'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client has exceeded traffic limit
|
||||
*
|
||||
* @return bool True if over limit
|
||||
*/
|
||||
public function isOverLimit(): bool {
|
||||
if (!$this->data || $this->data['traffic_limit'] === null) {
|
||||
return false; // No limit set
|
||||
}
|
||||
|
||||
$totalTraffic = $this->getTotalTraffic();
|
||||
return $totalTraffic >= (int)$this->data['traffic_limit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get traffic limit status
|
||||
*
|
||||
* @return array Status info
|
||||
*/
|
||||
public function getTrafficLimitStatus(): array {
|
||||
$totalTraffic = $this->getTotalTraffic();
|
||||
$limit = $this->data['traffic_limit'] ?? null;
|
||||
|
||||
return [
|
||||
'total_traffic' => $totalTraffic,
|
||||
'traffic_limit' => $limit,
|
||||
'is_unlimited' => $limit === null,
|
||||
'is_over_limit' => $this->isOverLimit(),
|
||||
'percentage_used' => $limit ? min(100, round(($totalTraffic / $limit) * 100, 2)) : 0,
|
||||
'remaining' => $limit ? max(0, $limit - $totalTraffic) : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all clients that exceeded their traffic limit
|
||||
*
|
||||
* @return array List of client IDs over limit
|
||||
*/
|
||||
public static function getClientsOverLimit(): array {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->query('
|
||||
SELECT id, name, traffic_sent, traffic_received, traffic_limit
|
||||
FROM vpn_clients
|
||||
WHERE traffic_limit IS NOT NULL
|
||||
AND (traffic_sent + traffic_received) >= traffic_limit
|
||||
AND status = "active"
|
||||
ORDER BY id
|
||||
');
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all clients that exceeded their traffic limit
|
||||
*
|
||||
* @return int Number of clients disabled
|
||||
*/
|
||||
public static function disableClientsOverLimit(): int {
|
||||
$clients = self::getClientsOverLimit();
|
||||
$disabled = 0;
|
||||
|
||||
foreach ($clients as $clientData) {
|
||||
try {
|
||||
$client = new VpnClient($clientData['id']);
|
||||
if ($client->revoke()) {
|
||||
$disabled++;
|
||||
error_log("Client {$clientData['name']} (ID: {$clientData['id']}) disabled: traffic limit exceeded");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to disable client {$clientData['id']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $disabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -205,6 +205,9 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('en', 'clients.sync_stats', 'Sync Stats'),
|
||||
('en', 'clients.title', 'Clients'),
|
||||
('en', 'clients.traffic', 'Traffic'),
|
||||
('en', 'clients.traffic_limit', 'Traffic Limit'),
|
||||
('en', 'clients.unlimited', 'Unlimited'),
|
||||
('en', 'clients.overlimit', 'Over Limit'),
|
||||
('en', 'backups.title', 'Server Backups'),
|
||||
('en', 'backups.create', 'Create Backup'),
|
||||
('en', 'backups.restore', 'Restore'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Russian translations
|
||||
-- This migration adds Russian language translations
|
||||
-- RU translations
|
||||
-- This migration adds RU language translations
|
||||
|
||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||
('ru', 'auth.email', 'Email'),
|
||||
@@ -32,6 +32,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('ru', 'clients.never', 'Никогда'),
|
||||
('ru', 'clients.never_expires', 'Бессрочно'),
|
||||
('ru', 'clients.no_clients', 'Пока нет клиентов'),
|
||||
('ru', 'clients.overlimit', 'Превышен лимит'),
|
||||
('ru', 'clients.qr_code', 'QR-код'),
|
||||
('ru', 'clients.received', 'Получено'),
|
||||
('ru', 'clients.restore', 'Восстановить'),
|
||||
@@ -43,6 +44,8 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('ru', 'clients.sync_stats', 'Синхронизировать статистику'),
|
||||
('ru', 'clients.title', 'Клиенты'),
|
||||
('ru', 'clients.traffic', 'Трафик'),
|
||||
('ru', 'clients.traffic_limit', 'Лимит трафика'),
|
||||
('ru', 'clients.unlimited', 'Безлимитно'),
|
||||
('ru', 'common.days', 'дней'),
|
||||
('ru', 'dashboard.active_clients', 'Активные клиенты'),
|
||||
('ru', 'dashboard.add_first_server', 'Добавить первый сервер'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Spanish translations
|
||||
-- This migration adds Spanish language translations
|
||||
-- ES translations
|
||||
-- This migration adds ES language translations
|
||||
|
||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||
('es', 'auth.email', 'Correo electrónico'),
|
||||
@@ -32,6 +32,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('es', 'clients.never', 'Nunca'),
|
||||
('es', 'clients.never_expires', 'Nunca vence'),
|
||||
('es', 'clients.no_clients', 'Aún no hay clientes'),
|
||||
('es', 'clients.overlimit', 'Límite excedido'),
|
||||
('es', 'clients.qr_code', 'Código QR'),
|
||||
('es', 'clients.received', 'Recibido'),
|
||||
('es', 'clients.restore', 'Restaurar'),
|
||||
@@ -43,6 +44,8 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('es', 'clients.sync_stats', 'Sincronizar estadísticas'),
|
||||
('es', 'clients.title', 'Clientes'),
|
||||
('es', 'clients.traffic', 'Tráfico'),
|
||||
('es', 'clients.traffic_limit', 'Límite de tráfico'),
|
||||
('es', 'clients.unlimited', 'Ilimitado'),
|
||||
('es', 'common.days', 'días'),
|
||||
('es', 'dashboard.active_clients', 'Clientes activos'),
|
||||
('es', 'dashboard.add_first_server', 'Agregar primer servidor'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- German translations
|
||||
-- This migration adds German language translations
|
||||
-- DE translations
|
||||
-- This migration adds DE language translations
|
||||
|
||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||
('de', 'auth.email', 'E-Mail'),
|
||||
@@ -32,6 +32,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('de', 'clients.never', 'Niemals'),
|
||||
('de', 'clients.never_expires', 'Läuft nie ab'),
|
||||
('de', 'clients.no_clients', 'Noch keine Kunden'),
|
||||
('de', 'clients.overlimit', 'Limit überschritten'),
|
||||
('de', 'clients.qr_code', 'QR-Code'),
|
||||
('de', 'clients.received', 'Empfangen'),
|
||||
('de', 'clients.restore', 'Wiederherstellen'),
|
||||
@@ -43,6 +44,8 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('de', 'clients.sync_stats', 'Statistiken synchronisieren'),
|
||||
('de', 'clients.title', 'Clients'),
|
||||
('de', 'clients.traffic', 'Datenverkehr'),
|
||||
('de', 'clients.traffic_limit', 'Traffic-Limit'),
|
||||
('de', 'clients.unlimited', 'Unbegrenzt'),
|
||||
('de', 'common.days', 'Tage'),
|
||||
('de', 'dashboard.active_clients', 'Aktive Clients'),
|
||||
('de', 'dashboard.add_first_server', 'Ersten Server hinzufügen'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- French translations
|
||||
-- This migration adds French language translations
|
||||
-- FR translations
|
||||
-- This migration adds FR language translations
|
||||
|
||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||
('fr', 'auth.email', 'Email'),
|
||||
@@ -32,6 +32,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('fr', 'clients.never', 'Jamais'),
|
||||
('fr', 'clients.never_expires', 'N''expire jamais'),
|
||||
('fr', 'clients.no_clients', 'Pas encore de clients'),
|
||||
('fr', 'clients.overlimit', 'Limite dépassée'),
|
||||
('fr', 'clients.qr_code', 'Code QR'),
|
||||
('fr', 'clients.received', 'Reçu'),
|
||||
('fr', 'clients.restore', 'Restaurer'),
|
||||
@@ -43,6 +44,8 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('fr', 'clients.sync_stats', 'Synchroniser les statistiques'),
|
||||
('fr', 'clients.title', 'Clients'),
|
||||
('fr', 'clients.traffic', 'Trafic'),
|
||||
('fr', 'clients.traffic_limit', 'Limite de trafic'),
|
||||
('fr', 'clients.unlimited', 'Illimité'),
|
||||
('fr', 'common.days', 'jours'),
|
||||
('fr', 'dashboard.active_clients', 'Clients actifs'),
|
||||
('fr', 'dashboard.add_first_server', 'Ajouter le premier serveur'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Chinese translations
|
||||
-- This migration adds Chinese language translations
|
||||
-- ZH translations
|
||||
-- This migration adds ZH language translations
|
||||
|
||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||
('zh', 'auth.email', '邮箱'),
|
||||
@@ -32,6 +32,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('zh', 'clients.never', '从不'),
|
||||
('zh', 'clients.never_expires', '永不过期'),
|
||||
('zh', 'clients.no_clients', '还没有客户'),
|
||||
('zh', 'clients.overlimit', '超出限制'),
|
||||
('zh', 'clients.qr_code', '二维码'),
|
||||
('zh', 'clients.received', '已接收'),
|
||||
('zh', 'clients.restore', '恢复'),
|
||||
@@ -43,6 +44,8 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('zh', 'clients.sync_stats', '同步统计'),
|
||||
('zh', 'clients.title', '客户端'),
|
||||
('zh', 'clients.traffic', '流量'),
|
||||
('zh', 'clients.traffic_limit', '流量限制'),
|
||||
('zh', 'clients.unlimited', '无限制'),
|
||||
('zh', 'common.days', '天'),
|
||||
('zh', 'dashboard.active_clients', '活跃客户端'),
|
||||
('zh', 'dashboard.add_first_server', '添加第一个服务器'),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add traffic limit field to vpn_clients table
|
||||
-- This migration adds traffic limit functionality to clients
|
||||
|
||||
ALTER TABLE vpn_clients
|
||||
ADD COLUMN traffic_limit BIGINT UNSIGNED NULL COMMENT 'Traffic limit in bytes (NULL = unlimited)' AFTER traffic_received,
|
||||
ADD INDEX idx_traffic_limit (traffic_limit);
|
||||
@@ -396,6 +396,7 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
||||
$serverId = (int)$params['id'];
|
||||
$clientName = trim($_POST['name'] ?? '');
|
||||
$expiresInDays = !empty($_POST['expires_in_days']) ? (int)$_POST['expires_in_days'] : null;
|
||||
$trafficLimitGb = !empty($_POST['traffic_limit_gb']) ? (float)$_POST['traffic_limit_gb'] : null;
|
||||
|
||||
if (empty($clientName)) {
|
||||
redirect('/servers/' . $serverId . '?error=Client+name+is+required');
|
||||
@@ -415,6 +416,14 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
||||
}
|
||||
|
||||
$clientId = VpnClient::create($serverId, $user['id'], $clientName, $expiresInDays);
|
||||
|
||||
// Set traffic limit if specified
|
||||
if ($trafficLimitGb !== null) {
|
||||
$client = new VpnClient($clientId);
|
||||
$limitBytes = (int)($trafficLimitGb * 1073741824); // Convert GB to bytes
|
||||
$client->setTrafficLimit($limitBytes);
|
||||
}
|
||||
|
||||
redirect('/clients/' . $clientId);
|
||||
} catch (Exception $e) {
|
||||
redirect('/servers/' . $serverId . '?error=' . urlencode($e->getMessage()));
|
||||
@@ -1322,6 +1331,110 @@ Router::get('/api/clients/expiring', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Set client traffic limit
|
||||
Router::post('/api/clients/{id}/set-traffic-limit', function ($params) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
if (!$user) return;
|
||||
|
||||
$clientId = (int)$params['id'];
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
// limit_bytes can be null (unlimited) or positive integer
|
||||
$limitBytes = isset($data['limit_bytes']) ? (int)$data['limit_bytes'] : null;
|
||||
|
||||
if ($limitBytes !== null && $limitBytes < 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'limit_bytes must be positive or null for unlimited']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new VpnClient($clientId);
|
||||
$clientData = $client->getData();
|
||||
|
||||
// Check ownership
|
||||
if ($clientData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$client->setTrafficLimit($limitBytes);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'limit_bytes' => $limitBytes,
|
||||
'limit_gb' => $limitBytes ? round($limitBytes / 1073741824, 2) : null
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
|
||||
// Check client traffic limit status
|
||||
Router::get('/api/clients/{id}/traffic-limit-status', function ($params) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
if (!$user) return;
|
||||
|
||||
$clientId = (int)$params['id'];
|
||||
|
||||
try {
|
||||
$client = new VpnClient($clientId);
|
||||
$clientData = $client->getData();
|
||||
|
||||
// Check ownership
|
||||
if ($clientData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$status = $client->getTrafficLimitStatus();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => $status
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
|
||||
// Get clients over traffic limit
|
||||
Router::get('/api/clients/overlimit', function () {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
if (!$user) return;
|
||||
|
||||
try {
|
||||
$clients = VpnClient::getClientsOverLimit();
|
||||
|
||||
// Filter by user if not admin
|
||||
if ($user['role'] !== 'admin') {
|
||||
$clients = array_filter($clients, function($c) use ($user) {
|
||||
return $c['user_id'] == $user['id'];
|
||||
});
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'clients' => array_values($clients),
|
||||
'count' => count($clients)
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* SETTINGS ROUTES
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,21 @@
|
||||
<option value="365">365 {{ t('common.days') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.traffic_limit') }}</label>
|
||||
<select name="traffic_limit_gb" class="w-full px-3 py-2 border rounded">
|
||||
<option value="" selected>{{ t('clients.unlimited') }}</option>
|
||||
<option value="1">1 GB</option>
|
||||
<option value="5">5 GB</option>
|
||||
<option value="10">10 GB</option>
|
||||
<option value="25">25 GB</option>
|
||||
<option value="50">50 GB</option>
|
||||
<option value="100">100 GB</option>
|
||||
<option value="250">250 GB</option>
|
||||
<option value="500">500 GB</option>
|
||||
<option value="1000">1000 GB (1 TB)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded w-full" id="createClientBtn">
|
||||
<span id="createClientText">{{ t('form.create') }}</span>
|
||||
<i class="fas fa-spinner fa-spin" id="createClientSpinner" style="display:none;"></i>
|
||||
@@ -66,6 +81,7 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.status') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.expiration') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.traffic') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.traffic_limit') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.last_handshake') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.actions') }}</th>
|
||||
</tr>
|
||||
@@ -105,12 +121,34 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div class="text-gray-600">
|
||||
↑ {{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
↑ {{ (client.traffic_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
↓ {{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
↓ {{ (client.traffic_received|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
{% if client.traffic_limit %}
|
||||
{% set total_traffic = (client.traffic_sent|default(0) + client.traffic_received|default(0)) %}
|
||||
{% set limit_gb = (client.traffic_limit / 1073741824)|number_format(2) %}
|
||||
{% set used_gb = (total_traffic / 1073741824)|number_format(2) %}
|
||||
{% set percentage = ((total_traffic / client.traffic_limit) * 100)|round %}
|
||||
|
||||
{% if percentage >= 100 %}
|
||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ t('clients.overlimit') }}
|
||||
</span>
|
||||
{% elseif percentage >= 80 %}
|
||||
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
|
||||
{{ used_gb }} / {{ limit_gb }} GB ({{ percentage }}%)
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600">{{ used_gb }} / {{ limit_gb }} GB</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-400">{{ t('clients.unlimited') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
{% if client.last_handshake %}
|
||||
<span class="text-gray-600">{{ client.last_handshake }}</span>
|
||||
|
||||
Reference in New Issue
Block a user