Add multilingual support with translations for German, Russian, French, and Chinese
Added time limits and backup functions for servers
This commit is contained in:
@@ -48,6 +48,11 @@ FLUSH PRIVILEGES;
|
|||||||
|
|
||||||
USE amnezia_panel;
|
USE amnezia_panel;
|
||||||
SOURCE migrations/001_init.sql;
|
SOURCE migrations/001_init.sql;
|
||||||
|
SOURCE migrations/002_translations_ru.sql;
|
||||||
|
SOURCE migrations/003_translations_es.sql;
|
||||||
|
SOURCE migrations/004_translations_de.sql;
|
||||||
|
SOURCE migrations/005_translations_fr.sql;
|
||||||
|
SOURCE migrations/006_translations_zh.sql;
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Update Database Config**
|
6. **Update Database Config**
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- VPN server deployment via SSH
|
- VPN server deployment via SSH
|
||||||
- Client configuration management
|
- Client configuration management with **expiration dates**
|
||||||
|
- **Server backup and restore** functionality
|
||||||
- Traffic statistics monitoring
|
- Traffic statistics monitoring
|
||||||
- QR code generation for mobile apps
|
- QR code generation for mobile apps
|
||||||
- Multi-language interface (English, Russian, Spanish, German, French, Chinese)
|
- Multi-language interface (English, Russian, Spanish, German, French, Chinese)
|
||||||
- REST API with JWT authentication
|
- REST API with JWT authentication
|
||||||
- User authentication and access control
|
- User authentication and access control
|
||||||
|
- **Automatic client expiration check** via cron
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -61,8 +63,54 @@ JWT_SECRET=your-secret-key-change-this
|
|||||||
|
|
||||||
1. Open server details
|
1. Open server details
|
||||||
2. Enter client name
|
2. Enter client name
|
||||||
3. Click Create Client
|
3. **Select expiration period** (optional, default: never expires)
|
||||||
4. Download config or scan QR code
|
4. Click Create Client
|
||||||
|
5. Download config or scan QR code
|
||||||
|
|
||||||
|
### Manage Client Expiration
|
||||||
|
|
||||||
|
Set expiration via UI or API:
|
||||||
|
```bash
|
||||||
|
# Set specific date
|
||||||
|
curl -X POST http://localhost:8082/api/clients/123/set-expiration \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{"expires_at": "2025-12-31 23:59:59"}'
|
||||||
|
|
||||||
|
# Extend by 30 days
|
||||||
|
curl -X POST http://localhost:8082/api/clients/123/extend \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{"days": 30}'
|
||||||
|
|
||||||
|
# Get expiring clients (within 7 days)
|
||||||
|
curl http://localhost:8082/api/clients/expiring?days=7 \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Backups
|
||||||
|
|
||||||
|
Create and restore backups via UI or API:
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
curl -X POST http://localhost:8082/api/servers/1/backup \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
# List backups
|
||||||
|
curl http://localhost:8082/api/servers/1/backups \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
# Restore from backup
|
||||||
|
curl -X POST http://localhost:8082/api/servers/1/restore \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-d '{"backup_id": 123}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Client Expiration Check
|
||||||
|
|
||||||
|
Run via cron to auto-disable expired clients:
|
||||||
|
```bash
|
||||||
|
# Add to crontab (runs every hour)
|
||||||
|
0 * * * * docker compose exec web php /var/www/html/bin/check_expired_clients.php
|
||||||
|
```
|
||||||
|
|
||||||
### API Authentication
|
### API Authentication
|
||||||
|
|
||||||
@@ -103,10 +151,25 @@ GET /api/clients - List all clients
|
|||||||
GET /api/clients/{id}/details - Get client details with stats, config and QR code
|
GET /api/clients/{id}/details - Get client details with stats, config and QR code
|
||||||
GET /api/clients/{id}/qr - Get client QR code
|
GET /api/clients/{id}/qr - Get client QR code
|
||||||
POST /api/clients/create - Create new client (returns config and QR code)
|
POST /api/clients/create - Create new client (returns config and QR code)
|
||||||
Parameters: server_id, name
|
Parameters: server_id, name, expires_in_days (optional)
|
||||||
POST /api/clients/{id}/revoke - Revoke client access
|
POST /api/clients/{id}/revoke - Revoke client access
|
||||||
POST /api/clients/{id}/restore - Restore client access
|
POST /api/clients/{id}/restore - Restore client access
|
||||||
DELETE /api/clients/{id}/delete - Delete client by ID
|
DELETE /api/clients/{id}/delete - Delete client by ID
|
||||||
|
POST /api/clients/{id}/set-expiration - Set client expiration date
|
||||||
|
Parameters: expires_at (Y-m-d H:i:s or null)
|
||||||
|
POST /api/clients/{id}/extend - Extend client expiration
|
||||||
|
Parameters: days (int)
|
||||||
|
GET /api/clients/expiring - Get clients expiring soon
|
||||||
|
Parameters: days (default: 7)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
```
|
||||||
|
POST /api/servers/{id}/backup - Create server backup
|
||||||
|
GET /api/servers/{id}/backups - List server backups
|
||||||
|
POST /api/servers/{id}/restore - Restore from backup
|
||||||
|
Parameters: backup_id
|
||||||
|
DELETE /api/backups/{id} - Delete backup
|
||||||
```
|
```
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
@@ -133,7 +196,7 @@ inc/ - Core classes
|
|||||||
JWT.php - Token auth
|
JWT.php - Token auth
|
||||||
QrUtil.php - QR code generation
|
QrUtil.php - QR code generation
|
||||||
templates/ - Twig templates
|
templates/ - Twig templates
|
||||||
migrations/ - SQL migrations
|
migrations/ - SQL migrations (executed in alphabetical order)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"name": "NL Server1",
|
||||||
|
"host": "62.204.42.184",
|
||||||
|
"port": 22,
|
||||||
|
"vpn_port": 64966,
|
||||||
|
"vpn_subnet": "10.8.1.0\/24",
|
||||||
|
"container_name": "amnezia-awg",
|
||||||
|
"server_public_key": "AFgZ21KZdOmopdpg5EP6g7rroGFpPOlGNXVZ8sh7JFY=",
|
||||||
|
"preshared_key": "QdeZLyd5\/eZoC8Iz5MAGu6lrEX2dp04Rq6+s2UhefnA=",
|
||||||
|
"awg_params": "{\"H1\": 700691355, \"H2\": 416510699, \"H3\": 1626278027, \"H4\": 1657157387, \"Jc\": 3, \"S1\": 117, \"S2\": 177, \"Jmax\": 50, \"Jmin\": 10}"
|
||||||
|
},
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "oleg",
|
||||||
|
"client_ip": "10.8.1.1",
|
||||||
|
"public_key": "U3N6bUhl+Wfqe7dB+Rn9vu1A16nz2cz\/9WgFiJoHMS0=",
|
||||||
|
"private_key": "gMjQq1E8vD76ZEuA+si\/Xd5FiA1mkSBjUsjc8d4+lFw=",
|
||||||
|
"preshared_key": "QdeZLyd5\/eZoC8Iz5MAGu6lrEX2dp04Rq6+s2UhefnA=",
|
||||||
|
"config": "[Interface]\nPrivateKey = gMjQq1E8vD76ZEuA+si\/Xd5FiA1mkSBjUsjc8d4+lFw=\nAddress = 10.8.1.1\/32\nDNS = 1.1.1.1, 1.0.0.1\nJc = 3\nJmin = 10\nJmax = 50\nS1 = 117\nS2 = 177\nH1 = 700691355\nH2 = 416510699\nH3 = 1626278027\nH4 = 1657157387\n\n[Peer]\nPublicKey = AFgZ21KZdOmopdpg5EP6g7rroGFpPOlGNXVZ8sh7JFY=\nPresharedKey = QdeZLyd5\/eZoC8Iz5MAGu6lrEX2dp04Rq6+s2UhefnA=\nEndpoint = 62.204.42.184:64966\nAllowedIPs = 0.0.0.0\/0, ::\/0\nPersistentKeepalive = 25\n",
|
||||||
|
"status": "active",
|
||||||
|
"expires_at": null,
|
||||||
|
"created_at": "2025-11-08 05:46:15"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backup_date": "2025-11-08 06:06:24",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Check and disable expired VPN clients
|
||||||
|
* Run this script via cron to automatically disable clients that have expired
|
||||||
|
*
|
||||||
|
* Example cron: 0 * * * * /usr/bin/php /var/www/html/bin/check_expired_clients.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require __DIR__ . '/../inc/Config.php';
|
||||||
|
require __DIR__ . '/../inc/DB.php';
|
||||||
|
require __DIR__ . '/../inc/VpnClient.php';
|
||||||
|
require __DIR__ . '/../inc/VpnServer.php';
|
||||||
|
|
||||||
|
// Load environment
|
||||||
|
Config::load(__DIR__ . '/../.env');
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Checking for expired clients...\n";
|
||||||
|
|
||||||
|
// Disable all expired clients
|
||||||
|
$count = VpnClient::disableExpiredClients();
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Disabled {$count} expired client(s)\n";
|
||||||
|
} else {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] No expired clients found\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report expiring clients (within 7 days)
|
||||||
|
$expiring = VpnClient::getExpiringClients(7);
|
||||||
|
|
||||||
|
if (count($expiring) > 0) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] " . count($expiring) . " client(s) expiring soon:\n";
|
||||||
|
foreach ($expiring as $client) {
|
||||||
|
$daysLeft = (int)floor((strtotime($client['expires_at']) - time()) / 86400);
|
||||||
|
echo " - {$client['name']} ({$client['email']}) expires in {$daysLeft} day(s)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] ERROR: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -176,6 +176,7 @@ class SettingsController {
|
|||||||
|
|
||||||
$service = $_POST['service'] ?? '';
|
$service = $_POST['service'] ?? '';
|
||||||
$apiKey = trim($_POST['api_key'] ?? '');
|
$apiKey = trim($_POST['api_key'] ?? '');
|
||||||
|
$skipTest = isset($_POST['skip_test']); // Allow saving without testing
|
||||||
|
|
||||||
if (empty($service) || empty($apiKey)) {
|
if (empty($service) || empty($apiKey)) {
|
||||||
View::render('settings.twig', [
|
View::render('settings.twig', [
|
||||||
@@ -185,22 +186,20 @@ class SettingsController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate OpenRouter key format
|
// Test the API key (unless skip_test is set)
|
||||||
if ($service === 'openrouter' && !preg_match('/^sk-or-v1-[a-zA-Z0-9]{64,}$/', $apiKey)) {
|
if ($service === 'openrouter' && !$skipTest) {
|
||||||
View::render('settings.twig', [
|
|
||||||
'error' => $this->translator->translate('settings.error_invalid_key'),
|
|
||||||
'translation_stats' => $this->getTranslationStats()
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the API key
|
|
||||||
if ($service === 'openrouter') {
|
|
||||||
$testResult = $this->testOpenRouterKey($apiKey);
|
$testResult = $this->testOpenRouterKey($apiKey);
|
||||||
if (!$testResult['success']) {
|
if (!$testResult['success']) {
|
||||||
|
// If rate limited, suggest saving without test
|
||||||
|
$errorMsg = $this->translator->translate('settings.error_key_test') . ': ' . $testResult['error'];
|
||||||
|
if (strpos($testResult['error'], '429') !== false || strpos($testResult['error'], 'Rate limit') !== false) {
|
||||||
|
$errorMsg .= ' - You can save without testing by checking "Skip validation"';
|
||||||
|
}
|
||||||
|
|
||||||
View::render('settings.twig', [
|
View::render('settings.twig', [
|
||||||
'error' => $this->translator->translate('settings.error_key_test') . ': ' . $testResult['error'],
|
'error' => $errorMsg,
|
||||||
'translation_stats' => $this->getTranslationStats()
|
'translation_stats' => $this->getTranslationStats(),
|
||||||
|
'openrouter_key' => ''
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,35 +209,32 @@ class SettingsController {
|
|||||||
$saved = $this->translator->saveApiKey($service, $apiKey);
|
$saved = $this->translator->saveApiKey($service, $apiKey);
|
||||||
|
|
||||||
if ($saved) {
|
if ($saved) {
|
||||||
View::render('settings.twig', [
|
$_SESSION['settings_success'] = $this->translator->translate('settings.key_saved');
|
||||||
'success' => $this->translator->translate('settings.key_saved'),
|
header('Location: /settings#api');
|
||||||
'translation_stats' => $this->getTranslationStats(),
|
exit;
|
||||||
'openrouter_key' => '' // Don't show the saved key
|
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
View::render('settings.twig', [
|
$_SESSION['settings_error'] = $this->translator->translate('message.error');
|
||||||
'error' => $this->translator->translate('message.error'),
|
header('Location: /settings#api');
|
||||||
'translation_stats' => $this->getTranslationStats()
|
exit;
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function testOpenRouterKey($apiKey) {
|
private function testOpenRouterKey($apiKey) {
|
||||||
// Test with a simple translation request
|
// Test with a simple request to check API key validity
|
||||||
$url = 'https://openrouter.ai/api/v1/chat/completions';
|
$url = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
$data = [
|
$data = [
|
||||||
'model' => 'google/gemini-2.0-flash-exp:free',
|
'model' => 'openai/gpt-4o-mini',
|
||||||
'messages' => [
|
'messages' => [
|
||||||
['role' => 'user', 'content' => 'Translate "test" to Spanish. Reply only with the translation.']
|
['role' => 'user', 'content' => 'Reply with: OK']
|
||||||
],
|
],
|
||||||
'temperature' => 0.3,
|
'max_tokens' => 5
|
||||||
'max_tokens' => 10
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'Authorization: Bearer ' . $apiKey,
|
'Authorization: Bearer ' . $apiKey,
|
||||||
@@ -248,19 +244,55 @@ class SettingsController {
|
|||||||
|
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($httpCode === 200) {
|
// Handle cURL errors
|
||||||
|
if ($curlError) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Network error: ' . $curlError
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
$result = json_decode($response, true);
|
$result = json_decode($response, true);
|
||||||
if (isset($result['choices'][0]['message']['content'])) {
|
|
||||||
|
// Success - got a valid response
|
||||||
|
if ($httpCode === 200 && isset($result['choices'][0]['message'])) {
|
||||||
return ['success' => true];
|
return ['success' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract error message from various formats
|
||||||
|
$errorMsg = 'Unknown error';
|
||||||
|
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
if (is_string($result['error'])) {
|
||||||
|
$errorMsg = $result['error'];
|
||||||
|
} elseif (isset($result['error']['message'])) {
|
||||||
|
$errorMsg = $result['error']['message'];
|
||||||
|
} elseif (isset($result['error']['code'])) {
|
||||||
|
$errorMsg = 'Error code: ' . $result['error']['code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HTTP code if not 200
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
$errorMsg .= ' (HTTP ' . $httpCode . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common error messages user-friendly translations
|
||||||
|
if (strpos($errorMsg, 'No auth credentials') !== false || $httpCode === 401) {
|
||||||
|
$errorMsg = 'Invalid API key or authentication failed';
|
||||||
|
} elseif (strpos($errorMsg, 'insufficient_quota') !== false || strpos($errorMsg, 'quota') !== false) {
|
||||||
|
$errorMsg = 'API quota exceeded or no credits available';
|
||||||
|
} elseif (strpos($errorMsg, 'rate_limit') !== false) {
|
||||||
|
$errorMsg = 'Rate limit exceeded, try again later';
|
||||||
}
|
}
|
||||||
|
|
||||||
$error = json_decode($response, true);
|
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => $error['error']['message'] ?? 'Unknown error (HTTP ' . $httpCode . ')'
|
'error' => $errorMsg
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ services:
|
|||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: amnezia-panel-db
|
container_name: amnezia-panel-db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-authentication-plugin=mysql_native_password
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -15,6 +16,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql
|
||||||
- ./migrations:/docker-entrypoint-initdb.d
|
- ./migrations:/docker-entrypoint-initdb.d
|
||||||
|
- ./my.cnf:/etc/mysql/conf.d/my.cnf:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class DB {
|
|||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
];
|
];
|
||||||
self::$pdo = new PDO($dsn, $user, $pass, $options);
|
self::$pdo = new PDO($dsn, $user, $pass, $options);
|
||||||
|
|
||||||
|
// Explicitly set UTF-8 encoding for connection
|
||||||
|
self::$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||||
|
|
||||||
return self::$pdo;
|
return self::$pdo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-7
@@ -196,11 +196,11 @@ class Translator {
|
|||||||
* Translate text using AI with model fallback
|
* Translate text using AI with model fallback
|
||||||
*/
|
*/
|
||||||
private static function translateWithAI(string $text, string $targetLanguage): ?string {
|
private static function translateWithAI(string $text, string $targetLanguage): ?string {
|
||||||
// Try multiple free models for reliability
|
// Use reliable paid models with fallback
|
||||||
$models = [
|
$models = [
|
||||||
'google/gemini-2.0-flash-exp:free',
|
'anthropic/claude-3.5-sonnet',
|
||||||
'meta-llama/llama-3.2-3b-instruct:free',
|
'openai/gpt-4o-mini',
|
||||||
'qwen/qwen-2-7b-instruct:free'
|
'google/gemini-pro-1.5'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
@@ -350,9 +350,10 @@ class Translator {
|
|||||||
foreach ($missingKeys as $key => $value) {
|
foreach ($missingKeys as $key => $value) {
|
||||||
if (self::autoTranslate($targetLang, $key, $value)) {
|
if (self::autoTranslate($targetLang, $key, $value)) {
|
||||||
$stats['translated']++;
|
$stats['translated']++;
|
||||||
usleep(500000); // 500ms delay between requests
|
sleep(3); // 3 second delay between requests to avoid rate limits
|
||||||
} else {
|
} else {
|
||||||
$stats['failed']++;
|
$stats['failed']++;
|
||||||
|
sleep(2); // Also delay on failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,8 +391,9 @@ class Translator {
|
|||||||
$jsonTexts = json_encode($textsForJson, JSON_UNESCAPED_UNICODE);
|
$jsonTexts = json_encode($textsForJson, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
$models = [
|
$models = [
|
||||||
'google/gemini-2.0-flash-exp:free',
|
'anthropic/claude-3.5-sonnet',
|
||||||
'meta-llama/llama-3.2-3b-instruct:free'
|
'openai/gpt-4o-mini',
|
||||||
|
'google/gemini-pro-1.5'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
|
|||||||
+145
-4
@@ -30,8 +30,14 @@ class VpnClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new VPN client
|
* Create new VPN client
|
||||||
|
*
|
||||||
|
* @param int $serverId Server ID
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @param string $name Client name
|
||||||
|
* @param int|null $expiresInDays Days until expiration (null = never expires)
|
||||||
|
* @return int Client ID
|
||||||
*/
|
*/
|
||||||
public static function create(int $serverId, int $userId, string $name): int {
|
public static function create(int $serverId, int $userId, string $name, ?int $expiresInDays = null): int {
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
|
|
||||||
// Get server data
|
// Get server data
|
||||||
@@ -69,11 +75,14 @@ class VpnClient {
|
|||||||
// Generate QR code
|
// Generate QR code
|
||||||
$qrCode = self::generateQRCode($config);
|
$qrCode = self::generateQRCode($config);
|
||||||
|
|
||||||
|
// Calculate expiration date
|
||||||
|
$expiresAt = $expiresInDays ? date('Y-m-d H:i:s', strtotime("+{$expiresInDays} days")) : null;
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
$stmt = $pdo->prepare('
|
$stmt = $pdo->prepare('
|
||||||
INSERT INTO vpn_clients
|
INSERT INTO vpn_clients
|
||||||
(server_id, user_id, name, client_ip, public_key, private_key, preshared_key, config, qr_code, status)
|
(server_id, user_id, name, client_ip, public_key, private_key, preshared_key, config, qr_code, status, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
');
|
');
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
@@ -86,7 +95,8 @@ class VpnClient {
|
|||||||
$serverData['preshared_key'],
|
$serverData['preshared_key'],
|
||||||
$config,
|
$config,
|
||||||
$qrCode,
|
$qrCode,
|
||||||
'active'
|
'active',
|
||||||
|
$expiresAt
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (int)$pdo->lastInsertId();
|
return (int)$pdo->lastInsertId();
|
||||||
@@ -685,4 +695,135 @@ class VpnClient {
|
|||||||
|
|
||||||
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
|
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set client expiration date
|
||||||
|
*
|
||||||
|
* @param int $clientId Client ID
|
||||||
|
* @param string|null $expiresAt Expiration date (Y-m-d H:i:s) or null for never expires
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function setExpiration(int $clientId, ?string $expiresAt): bool {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('UPDATE vpn_clients SET expires_at = ? WHERE id = ?');
|
||||||
|
return $stmt->execute([$expiresAt, $clientId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend client expiration by days
|
||||||
|
*
|
||||||
|
* @param int $clientId Client ID
|
||||||
|
* @param int $days Days to extend
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function extendExpiration(int $clientId, int $days): bool {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
|
||||||
|
// Get current expiration
|
||||||
|
$stmt = $pdo->prepare('SELECT expires_at FROM vpn_clients WHERE id = ?');
|
||||||
|
$stmt->execute([$clientId]);
|
||||||
|
$client = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new expiration from current or now
|
||||||
|
$baseDate = $client['expires_at'] ? strtotime($client['expires_at']) : time();
|
||||||
|
$newExpiration = date('Y-m-d H:i:s', strtotime("+{$days} days", $baseDate));
|
||||||
|
|
||||||
|
return self::setExpiration($clientId, $newExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get clients expiring soon
|
||||||
|
*
|
||||||
|
* @param int $days Check for clients expiring within N days
|
||||||
|
* @return array List of expiring clients
|
||||||
|
*/
|
||||||
|
public static function getExpiringClients(int $days = 7): array {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT c.*, s.name as server_name, s.host, u.name as user_name, u.email
|
||||||
|
FROM vpn_clients c
|
||||||
|
JOIN vpn_servers s ON c.server_id = s.id
|
||||||
|
JOIN users u ON c.user_id = u.id
|
||||||
|
WHERE c.expires_at IS NOT NULL
|
||||||
|
AND c.expires_at <= DATE_ADD(NOW(), INTERVAL ? DAY)
|
||||||
|
AND c.expires_at > NOW()
|
||||||
|
AND c.status = "active"
|
||||||
|
ORDER BY c.expires_at ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$days]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get expired clients
|
||||||
|
*
|
||||||
|
* @return array List of expired clients
|
||||||
|
*/
|
||||||
|
public static function getExpiredClients(): array {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->query('
|
||||||
|
SELECT c.*, s.name as server_name, s.host
|
||||||
|
FROM vpn_clients c
|
||||||
|
JOIN vpn_servers s ON c.server_id = s.id
|
||||||
|
WHERE c.expires_at IS NOT NULL
|
||||||
|
AND c.expires_at <= NOW()
|
||||||
|
AND c.status = "active"
|
||||||
|
ORDER BY c.expires_at DESC
|
||||||
|
');
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable expired clients automatically
|
||||||
|
*
|
||||||
|
* @return int Number of clients disabled
|
||||||
|
*/
|
||||||
|
public static function disableExpiredClients(): int {
|
||||||
|
$expiredClients = self::getExpiredClients();
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($expiredClients as $clientData) {
|
||||||
|
try {
|
||||||
|
$client = new self($clientData['id']);
|
||||||
|
$client->revoke();
|
||||||
|
$count++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Failed to disable expired client {$clientData['id']}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is expired
|
||||||
|
*
|
||||||
|
* @return bool True if expired
|
||||||
|
*/
|
||||||
|
public function isExpired(): bool {
|
||||||
|
if (!$this->data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data['expires_at'] !== null && strtotime($this->data['expires_at']) <= time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get days until expiration
|
||||||
|
*
|
||||||
|
* @return int|null Days until expiration (negative if expired, null if never expires)
|
||||||
|
*/
|
||||||
|
public function getDaysUntilExpiration(): ?int {
|
||||||
|
if (!$this->data || $this->data['expires_at'] === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = strtotime($this->data['expires_at']) - time();
|
||||||
|
return (int)floor($diff / 86400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,4 +446,266 @@ BASH;
|
|||||||
public function getData(): ?array {
|
public function getData(): ?array {
|
||||||
return $this->data;
|
return $this->data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup of server configuration and all clients
|
||||||
|
*
|
||||||
|
* @param int $userId User who creates the backup
|
||||||
|
* @param string $backupType Type: 'manual' or 'automatic'
|
||||||
|
* @return int Backup ID
|
||||||
|
*/
|
||||||
|
public function createBackup(int $userId, string $backupType = 'manual'): int {
|
||||||
|
if (!$this->data) {
|
||||||
|
throw new Exception('Server not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$backupName = 'backup_' . $this->serverId . '_' . date('Y-m-d_His') . '.json';
|
||||||
|
$backupDir = '/var/www/html/backups';
|
||||||
|
$backupPath = $backupDir . '/' . $backupName;
|
||||||
|
|
||||||
|
// Create backups directory if not exists
|
||||||
|
if (!is_dir($backupDir)) {
|
||||||
|
mkdir($backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all clients for this server
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT id, name, client_ip, public_key, private_key, preshared_key,
|
||||||
|
config, status, expires_at, created_at
|
||||||
|
FROM vpn_clients
|
||||||
|
WHERE server_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$this->serverId]);
|
||||||
|
$clients = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Prepare backup data
|
||||||
|
$backupData = [
|
||||||
|
'server' => [
|
||||||
|
'name' => $this->data['name'],
|
||||||
|
'host' => $this->data['host'],
|
||||||
|
'port' => $this->data['port'],
|
||||||
|
'vpn_port' => $this->data['vpn_port'],
|
||||||
|
'vpn_subnet' => $this->data['vpn_subnet'],
|
||||||
|
'container_name' => $this->data['container_name'],
|
||||||
|
'server_public_key' => $this->data['server_public_key'],
|
||||||
|
'preshared_key' => $this->data['preshared_key'],
|
||||||
|
'awg_params' => $this->data['awg_params'],
|
||||||
|
],
|
||||||
|
'clients' => $clients,
|
||||||
|
'backup_date' => date('Y-m-d H:i:s'),
|
||||||
|
'version' => '1.0'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Write backup to file
|
||||||
|
$json = json_encode($backupData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
file_put_contents($backupPath, $json);
|
||||||
|
|
||||||
|
$backupSize = filesize($backupPath);
|
||||||
|
|
||||||
|
// Insert backup record
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO server_backups
|
||||||
|
(server_id, backup_name, backup_path, backup_size, clients_count, backup_type, status, created_by)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$this->serverId,
|
||||||
|
$backupName,
|
||||||
|
$backupPath,
|
||||||
|
$backupSize,
|
||||||
|
count($clients),
|
||||||
|
$backupType,
|
||||||
|
'completed',
|
||||||
|
$userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Mark backup as failed
|
||||||
|
if (isset($stmt)) {
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO server_backups
|
||||||
|
(server_id, backup_name, backup_path, backup_type, status, error_message, created_by)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$this->serverId,
|
||||||
|
$backupName,
|
||||||
|
$backupPath,
|
||||||
|
$backupType,
|
||||||
|
'failed',
|
||||||
|
$e->getMessage(),
|
||||||
|
$userId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all backups for this server
|
||||||
|
*
|
||||||
|
* @return array List of backups
|
||||||
|
*/
|
||||||
|
public function listBackups(): array {
|
||||||
|
if (!$this->data) {
|
||||||
|
throw new Exception('Server not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT b.*, u.name as created_by_name, u.email as created_by_email
|
||||||
|
FROM server_backups b
|
||||||
|
LEFT JOIN users u ON b.created_by = u.id
|
||||||
|
WHERE b.server_id = ?
|
||||||
|
ORDER BY b.created_at DESC
|
||||||
|
');
|
||||||
|
$stmt->execute([$this->serverId]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore server from backup
|
||||||
|
* Note: This only restores client configurations to database
|
||||||
|
* Server must already be deployed
|
||||||
|
*
|
||||||
|
* @param int $backupId Backup ID
|
||||||
|
* @return array Restoration results
|
||||||
|
*/
|
||||||
|
public function restoreBackup(int $backupId): array {
|
||||||
|
if (!$this->data) {
|
||||||
|
throw new Exception('Server not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->data['status'] !== 'active') {
|
||||||
|
throw new Exception('Server must be active to restore backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::conn();
|
||||||
|
|
||||||
|
// Get backup record
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM server_backups WHERE id = ? AND server_id = ?');
|
||||||
|
$stmt->execute([$backupId, $this->serverId]);
|
||||||
|
$backup = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$backup) {
|
||||||
|
throw new Exception('Backup not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($backup['backup_path'])) {
|
||||||
|
throw new Exception('Backup file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read backup data
|
||||||
|
$backupData = json_decode(file_get_contents($backup['backup_path']), true);
|
||||||
|
|
||||||
|
if (!$backupData || !isset($backupData['clients'])) {
|
||||||
|
throw new Exception('Invalid backup format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$restored = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($backupData['clients'] as $clientData) {
|
||||||
|
try {
|
||||||
|
// Check if client already exists by IP
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM vpn_clients WHERE server_id = ? AND client_ip = ?');
|
||||||
|
$stmt->execute([$this->serverId, $clientData['client_ip']]);
|
||||||
|
$existing = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$errors[] = "Client {$clientData['name']} already exists";
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert client
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO vpn_clients
|
||||||
|
(server_id, user_id, name, client_ip, public_key, private_key, preshared_key,
|
||||||
|
config, status, expires_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$this->serverId,
|
||||||
|
$this->data['user_id'],
|
||||||
|
$clientData['name'],
|
||||||
|
$clientData['client_ip'],
|
||||||
|
$clientData['public_key'],
|
||||||
|
$clientData['private_key'],
|
||||||
|
$clientData['preshared_key'],
|
||||||
|
$clientData['config'],
|
||||||
|
'disabled', // Restore as disabled for safety
|
||||||
|
$clientData['expires_at']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add client to server container
|
||||||
|
VpnClient::addClientToServer($this->data, $clientData['public_key'], $clientData['client_ip']);
|
||||||
|
|
||||||
|
$restored++;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
$errors[] = "Failed to restore {$clientData['name']}: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true, // Always success if process completed
|
||||||
|
'restored' => $restored,
|
||||||
|
'failed' => $failed,
|
||||||
|
'total' => count($backupData['clients']),
|
||||||
|
'errors' => $errors,
|
||||||
|
'message' => $restored > 0 ? "Restored $restored clients" : "No clients restored"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete backup
|
||||||
|
*
|
||||||
|
* @param int $backupId Backup ID
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public static function deleteBackup(int $backupId): bool {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
|
||||||
|
// Get backup path
|
||||||
|
$stmt = $pdo->prepare('SELECT backup_path FROM server_backups WHERE id = ?');
|
||||||
|
$stmt->execute([$backupId]);
|
||||||
|
$backup = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$backup) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
if (file_exists($backup['backup_path'])) {
|
||||||
|
unlink($backup['backup_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete record
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM server_backups WHERE id = ?');
|
||||||
|
return $stmt->execute([$backupId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backup by ID
|
||||||
|
*
|
||||||
|
* @param int $backupId Backup ID
|
||||||
|
* @return array|null Backup data
|
||||||
|
*/
|
||||||
|
public static function getBackup(int $backupId): ?array {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM server_backups WHERE id = ?');
|
||||||
|
$stmt->execute([$backupId]);
|
||||||
|
return $stmt->fetch() ?: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,13 @@ CREATE TABLE IF NOT EXISTS vpn_clients (
|
|||||||
last_handshake TIMESTAMP NULL COMMENT 'Last successful WireGuard handshake',
|
last_handshake TIMESTAMP NULL COMMENT 'Last successful WireGuard handshake',
|
||||||
last_sync_at TIMESTAMP NULL COMMENT 'Last time stats were synced from server',
|
last_sync_at TIMESTAMP NULL COMMENT 'Last time stats were synced from server',
|
||||||
status ENUM('active', 'disabled') DEFAULT 'active',
|
status ENUM('active', 'disabled') DEFAULT 'active',
|
||||||
|
expires_at TIMESTAMP NULL COMMENT 'Client expiration date (NULL = never expires)',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
INDEX idx_server_id (server_id),
|
INDEX idx_server_id (server_id),
|
||||||
INDEX idx_user_id (user_id),
|
INDEX idx_user_id (user_id),
|
||||||
INDEX idx_status (status),
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_expires_at (expires_at),
|
||||||
INDEX idx_last_handshake (last_handshake),
|
INDEX idx_last_handshake (last_handshake),
|
||||||
UNIQUE KEY unique_server_client_ip (server_id, client_ip),
|
UNIQUE KEY unique_server_client_ip (server_id, client_ip),
|
||||||
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
|
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
|
||||||
@@ -137,6 +139,26 @@ CREATE TABLE IF NOT EXISTS api_keys (
|
|||||||
INDEX idx_active (is_active)
|
INDEX idx_active (is_active)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Server Backups table
|
||||||
|
CREATE TABLE IF NOT EXISTS server_backups (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
server_id INT UNSIGNED NOT NULL,
|
||||||
|
backup_name VARCHAR(255) NOT NULL COMMENT 'Backup file name',
|
||||||
|
backup_path VARCHAR(500) NOT NULL COMMENT 'Path to backup file',
|
||||||
|
backup_size BIGINT UNSIGNED DEFAULT 0 COMMENT 'Backup file size in bytes',
|
||||||
|
clients_count INT UNSIGNED DEFAULT 0 COMMENT 'Number of clients in backup',
|
||||||
|
backup_type ENUM('manual', 'automatic') DEFAULT 'manual',
|
||||||
|
status ENUM('creating', 'completed', 'failed') DEFAULT 'creating',
|
||||||
|
error_message TEXT NULL COMMENT 'Error message if backup failed',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by INT UNSIGNED NULL COMMENT 'User who created the backup',
|
||||||
|
INDEX idx_server_id (server_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert default admin user
|
-- Insert default admin user
|
||||||
INSERT IGNORE INTO users (email, password_hash, name, role, status)
|
INSERT IGNORE INTO users (email, password_hash, name, role, status)
|
||||||
VALUES ('admin@amnez.ia', '$2y$10$SKEI6ogiWr2gsSG/nELLp.JcfpGhxsDLAAI7gdtTOI3ELz4zJzzPG', 'Administrator', 'admin', 'active');
|
VALUES ('admin@amnez.ia', '$2y$10$SKEI6ogiWr2gsSG/nELLp.JcfpGhxsDLAAI7gdtTOI3ELz4zJzzPG', 'Administrator', 'admin', 'active');
|
||||||
@@ -160,8 +182,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'auth.register', 'Register'),
|
('en', 'auth.register', 'Register'),
|
||||||
('en', 'clients.actions', 'Actions'),
|
('en', 'clients.actions', 'Actions'),
|
||||||
('en', 'clients.add', 'Add Client'),
|
('en', 'clients.add', 'Add Client'),
|
||||||
|
('en', 'clients.create', 'Create Client'),
|
||||||
('en', 'clients.delete', 'Delete'),
|
('en', 'clients.delete', 'Delete'),
|
||||||
|
('en', 'clients.delete_confirm', 'Delete this client permanently?'),
|
||||||
('en', 'clients.download_config', 'Download Config'),
|
('en', 'clients.download_config', 'Download Config'),
|
||||||
|
('en', 'clients.expiration', 'Expiration'),
|
||||||
|
('en', 'clients.expired', 'Expired'),
|
||||||
|
('en', 'clients.never', 'Never'),
|
||||||
|
('en', 'clients.never_expires', 'Never expires'),
|
||||||
|
('en', 'clients.no_clients', 'No clients yet'),
|
||||||
('en', 'clients.ip', 'IP Address'),
|
('en', 'clients.ip', 'IP Address'),
|
||||||
('en', 'clients.last_handshake', 'Last Handshake'),
|
('en', 'clients.last_handshake', 'Last Handshake'),
|
||||||
('en', 'clients.name', 'Client Name'),
|
('en', 'clients.name', 'Client Name'),
|
||||||
@@ -169,12 +198,25 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'clients.received', 'Received'),
|
('en', 'clients.received', 'Received'),
|
||||||
('en', 'clients.restore', 'Restore'),
|
('en', 'clients.restore', 'Restore'),
|
||||||
('en', 'clients.revoke', 'Revoke'),
|
('en', 'clients.revoke', 'Revoke'),
|
||||||
|
('en', 'clients.revoke_confirm', 'Revoke access for this client?'),
|
||||||
('en', 'clients.sent', 'Sent'),
|
('en', 'clients.sent', 'Sent'),
|
||||||
('en', 'clients.server', 'Server'),
|
('en', 'clients.server', 'Server'),
|
||||||
('en', 'clients.status', 'Status'),
|
('en', 'clients.status', 'Status'),
|
||||||
('en', 'clients.sync_stats', 'Sync Stats'),
|
('en', 'clients.sync_stats', 'Sync Stats'),
|
||||||
('en', 'clients.title', 'Clients'),
|
('en', 'clients.title', 'Clients'),
|
||||||
('en', 'clients.traffic', 'Traffic'),
|
('en', 'clients.traffic', 'Traffic'),
|
||||||
|
('en', 'backups.title', 'Server Backups'),
|
||||||
|
('en', 'backups.create', 'Create Backup'),
|
||||||
|
('en', 'backups.restore', 'Restore'),
|
||||||
|
('en', 'backups.no_backups', 'No backups yet'),
|
||||||
|
('en', 'backups.create_confirm', 'Create backup of all clients on this server?'),
|
||||||
|
('en', 'backups.restore_confirm', 'Restore clients from this backup? Existing clients will not be affected.'),
|
||||||
|
('en', 'backups.delete_confirm', 'Delete this backup permanently?'),
|
||||||
|
('en', 'backups.created_success', 'Backup created successfully'),
|
||||||
|
('en', 'backups.restored_success', 'Restored'),
|
||||||
|
('en', 'backups.deleted_success', 'Backup deleted successfully'),
|
||||||
|
('en', 'backups.login_required', 'Please login via API to manage backups'),
|
||||||
|
('en', 'common.days', 'days'),
|
||||||
('en', 'dashboard.active_clients', 'Active Clients'),
|
('en', 'dashboard.active_clients', 'Active Clients'),
|
||||||
('en', 'dashboard.add_first_server', 'Add First Server'),
|
('en', 'dashboard.add_first_server', 'Add First Server'),
|
||||||
('en', 'dashboard.get_started', 'Get started by adding your first VPN server'),
|
('en', 'dashboard.get_started', 'Get started by adding your first VPN server'),
|
||||||
@@ -188,6 +230,7 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'dashboard.welcome', 'Welcome to Amnezia VPN Management Panel'),
|
('en', 'dashboard.welcome', 'Welcome to Amnezia VPN Management Panel'),
|
||||||
('en', 'form.cancel', 'Cancel'),
|
('en', 'form.cancel', 'Cancel'),
|
||||||
('en', 'form.close', 'Close'),
|
('en', 'form.close', 'Close'),
|
||||||
|
('en', 'form.create', 'Create'),
|
||||||
('en', 'form.loading', 'Loading...'),
|
('en', 'form.loading', 'Loading...'),
|
||||||
('en', 'form.processing', 'Processing...'),
|
('en', 'form.processing', 'Processing...'),
|
||||||
('en', 'form.save', 'Save'),
|
('en', 'form.save', 'Save'),
|
||||||
@@ -221,7 +264,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'settings.api_keys', 'API Keys'),
|
('en', 'settings.api_keys', 'API Keys'),
|
||||||
('en', 'settings.api_keys_desc', 'Configure API keys for external services'),
|
('en', 'settings.api_keys_desc', 'Configure API keys for external services'),
|
||||||
('en', 'settings.auto_translate', 'Auto-translate'),
|
('en', 'settings.auto_translate', 'Auto-translate'),
|
||||||
|
('en', 'settings.change_password', 'Change Password'),
|
||||||
|
('en', 'settings.confirm_password', 'Confirm Password'),
|
||||||
('en', 'settings.confirm_translate', 'Start automatic translation? This may take a few minutes.'),
|
('en', 'settings.confirm_translate', 'Start automatic translation? This may take a few minutes.'),
|
||||||
|
('en', 'settings.current_password', 'Current Password'),
|
||||||
('en', 'settings.description', 'Manage panel configuration and API integrations'),
|
('en', 'settings.description', 'Manage panel configuration and API integrations'),
|
||||||
('en', 'settings.error_empty_key', 'API key cannot be empty'),
|
('en', 'settings.error_empty_key', 'API key cannot be empty'),
|
||||||
('en', 'settings.error_invalid_key', 'Invalid API key format'),
|
('en', 'settings.error_invalid_key', 'Invalid API key format'),
|
||||||
@@ -231,12 +277,28 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'settings.key_saved', 'API key saved successfully'),
|
('en', 'settings.key_saved', 'API key saved successfully'),
|
||||||
('en', 'settings.keys', 'keys'),
|
('en', 'settings.keys', 'keys'),
|
||||||
('en', 'settings.language', 'Language'),
|
('en', 'settings.language', 'Language'),
|
||||||
|
('en', 'settings.min_6_chars', 'Minimum 6 characters'),
|
||||||
|
('en', 'settings.new_password', 'New Password'),
|
||||||
|
('en', 'settings.profile', 'Profile'),
|
||||||
('en', 'settings.progress', 'Progress'),
|
('en', 'settings.progress', 'Progress'),
|
||||||
|
('en', 'settings.translations', 'Translations'),
|
||||||
('en', 'settings.translation_complete', 'Translation completed'),
|
('en', 'settings.translation_complete', 'Translation completed'),
|
||||||
('en', 'settings.translation_status', 'Translation Status'),
|
('en', 'settings.translation_status', 'Translation Status'),
|
||||||
|
('en', 'settings.users', 'Users'),
|
||||||
('en', 'status.active', 'Active'),
|
('en', 'status.active', 'Active'),
|
||||||
('en', 'status.deploying', 'Deploying'),
|
('en', 'status.deploying', 'Deploying'),
|
||||||
('en', 'status.disabled', 'Disabled'),
|
('en', 'status.disabled', 'Disabled'),
|
||||||
('en', 'status.error', 'Error'),
|
('en', 'status.error', 'Error'),
|
||||||
('en', 'status.inactive', 'Inactive')
|
('en', 'status.inactive', 'Inactive'),
|
||||||
|
('en', 'users.add_user', 'Add User'),
|
||||||
|
('en', 'users.all_users', 'All Users'),
|
||||||
|
('en', 'users.administrator', 'Administrator'),
|
||||||
|
('en', 'users.created', 'Created'),
|
||||||
|
('en', 'users.delete_confirm', 'Delete {0}?'),
|
||||||
|
('en', 'users.role', 'Role'),
|
||||||
|
('en', 'users.role_admin', 'Admin'),
|
||||||
|
('en', 'users.role_user', 'User'),
|
||||||
|
('en', 'settings.api_key_configured', 'API Key Configured'),
|
||||||
|
('en', 'settings.no_api_key', 'No API key configured. Auto-translation will not work.'),
|
||||||
|
('en', 'settings.skip_validation', 'Skip validation (save without testing)')
|
||||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- Spanish translations
|
||||||
|
-- This migration adds Spanish language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('es', 'auth.email', 'Correo electrónico'),
|
||||||
|
('es', 'auth.login', 'Iniciar sesión'),
|
||||||
|
('es', 'auth.name', 'Nombre'),
|
||||||
|
('es', 'auth.password', 'Contraseña'),
|
||||||
|
('es', 'auth.register', 'Registrarse'),
|
||||||
|
('es', 'backups.create', 'Crear copia de seguridad'),
|
||||||
|
('es', 'backups.create_confirm', '¿Crear copia de seguridad de todos los clientes en este servidor?'),
|
||||||
|
('es', 'backups.created_success', 'Copia de seguridad creada exitosamente'),
|
||||||
|
('es', 'backups.delete_confirm', '¿Eliminar esta copia de seguridad permanentemente?'),
|
||||||
|
('es', 'backups.deleted_success', 'Copia de seguridad eliminada exitosamente'),
|
||||||
|
('es', 'backups.login_required', 'Por favor inicie sesión vía API para gestionar copias de seguridad'),
|
||||||
|
('es', 'backups.no_backups', 'Aún no hay copias de seguridad'),
|
||||||
|
('es', 'backups.restore', 'Restaurar'),
|
||||||
|
('es', 'backups.restore_confirm', '¿Restaurar clientes desde esta copia de seguridad? Los clientes existentes no se verán afectados.'),
|
||||||
|
('es', 'backups.restored_success', 'Restaurado'),
|
||||||
|
('es', 'backups.title', 'Copias de seguridad del servidor'),
|
||||||
|
('es', 'clients.actions', 'Acciones'),
|
||||||
|
('es', 'clients.add', 'Agregar cliente'),
|
||||||
|
('es', 'clients.create', 'Crear cliente'),
|
||||||
|
('es', 'clients.delete', 'Eliminar'),
|
||||||
|
('es', 'clients.download_config', 'Descargar configuración'),
|
||||||
|
('es', 'clients.expiration', 'Vencimiento'),
|
||||||
|
('es', 'clients.expired', 'Vencido'),
|
||||||
|
('es', 'clients.ip', 'Dirección IP'),
|
||||||
|
('es', 'clients.last_handshake', 'Último contacto'),
|
||||||
|
('es', 'clients.name', 'Nombre del cliente'),
|
||||||
|
('es', 'clients.never_expires', 'Nunca vence'),
|
||||||
|
('es', 'clients.qr_code', 'Código QR'),
|
||||||
|
('es', 'clients.received', 'Recibido'),
|
||||||
|
('es', 'clients.restore', 'Restaurar'),
|
||||||
|
('es', 'clients.revoke', 'Revocar'),
|
||||||
|
('es', 'clients.sent', 'Enviado'),
|
||||||
|
('es', 'clients.server', 'Servidor'),
|
||||||
|
('es', 'clients.status', 'Estado'),
|
||||||
|
('es', 'clients.sync_stats', 'Sincronizar estadísticas'),
|
||||||
|
('es', 'clients.title', 'Clientes'),
|
||||||
|
('es', 'clients.traffic', 'Tráfico'),
|
||||||
|
('es', 'common.days', 'días'),
|
||||||
|
('es', 'dashboard.active_clients', 'Clientes activos'),
|
||||||
|
('es', 'dashboard.add_first_server', 'Agregar primer servidor'),
|
||||||
|
('es', 'dashboard.get_started', 'Comience agregando su primer servidor VPN'),
|
||||||
|
('es', 'dashboard.no_servers', 'Aún no hay servidores'),
|
||||||
|
('es', 'dashboard.quick_actions', 'Acciones rápidas'),
|
||||||
|
('es', 'dashboard.recent_servers', 'Servidores recientes'),
|
||||||
|
('es', 'dashboard.title', 'Panel de control'),
|
||||||
|
('es', 'dashboard.total_clients', 'Total de clientes'),
|
||||||
|
('es', 'dashboard.total_servers', 'Total de servidores'),
|
||||||
|
('es', 'dashboard.total_traffic', 'Tráfico total'),
|
||||||
|
('es', 'dashboard.welcome', 'Bienvenido al Panel de Gestión de Amnezia VPN'),
|
||||||
|
('es', 'form.cancel', 'Cancelar'),
|
||||||
|
('es', 'form.close', 'Cerrar'),
|
||||||
|
('es', 'form.create', 'Crear'),
|
||||||
|
('es', 'form.loading', 'Cargando...'),
|
||||||
|
('es', 'form.processing', 'Procesando...'),
|
||||||
|
('es', 'form.save', 'Guardar'),
|
||||||
|
('es', 'form.submit', 'Enviar'),
|
||||||
|
('es', 'form.update', 'Actualizar'),
|
||||||
|
('es', 'menu.clients', 'Clientes'),
|
||||||
|
('es', 'menu.dashboard', 'Panel de control'),
|
||||||
|
('es', 'menu.logout', 'Cerrar sesión'),
|
||||||
|
('es', 'menu.servers', 'Servidores'),
|
||||||
|
('es', 'menu.settings', 'Configuración'),
|
||||||
|
('es', 'menu.users', 'Usuarios'),
|
||||||
|
('es', 'message.confirm', '¿Está seguro?'),
|
||||||
|
('es', 'message.deleted', 'Eliminado exitosamente'),
|
||||||
|
('es', 'message.deployed', 'Implementado exitosamente'),
|
||||||
|
('es', 'message.error', 'Ha ocurrido un error'),
|
||||||
|
('es', 'message.saved', 'Guardado exitosamente'),
|
||||||
|
('es', 'message.success', 'Operación completada exitosamente'),
|
||||||
|
('es', 'servers.actions', 'Acciones'),
|
||||||
|
('es', 'servers.add', 'Agregar servidor'),
|
||||||
|
('es', 'servers.clients', 'Clientes'),
|
||||||
|
('es', 'servers.delete', 'Eliminar'),
|
||||||
|
('es', 'servers.deploy', 'Implementar'),
|
||||||
|
('es', 'servers.edit', 'Editar'),
|
||||||
|
('es', 'servers.host', 'Host'),
|
||||||
|
('es', 'servers.name', 'Nombre'),
|
||||||
|
('es', 'servers.port', 'Puerto'),
|
||||||
|
('es', 'servers.status', 'Estado'),
|
||||||
|
('es', 'servers.title', 'Servidores'),
|
||||||
|
('es', 'servers.view', 'Ver'),
|
||||||
|
('es', 'settings.actions', 'Acciones'),
|
||||||
|
('es', 'settings.api_key_configured', 'Clave API configurada'),
|
||||||
|
('es', 'settings.api_keys', 'Claves API'),
|
||||||
|
('es', 'settings.api_keys_desc', 'Configurar claves API para servicios externos'),
|
||||||
|
('es', 'settings.auto_translate', 'Auto-traducir'),
|
||||||
|
('es', 'settings.change_password', 'Cambiar contraseña'),
|
||||||
|
('es', 'settings.confirm_password', 'Confirmar contraseña'),
|
||||||
|
('es', 'settings.confirm_translate', '¿Iniciar traducción automática? Esto puede tomar unos minutos.'),
|
||||||
|
('es', 'settings.current_password', 'Contraseña actual'),
|
||||||
|
('es', 'settings.description', 'Gestionar configuración del panel e integraciones API'),
|
||||||
|
('es', 'settings.error_empty_key', 'La clave API no puede estar vacía'),
|
||||||
|
('es', 'settings.error_invalid_key', 'Formato de clave API inválido'),
|
||||||
|
('es', 'settings.error_key_test', 'Prueba de clave API fallida'),
|
||||||
|
('es', 'settings.for_translation', 'para auto-traducción'),
|
||||||
|
('es', 'settings.get_key_at', 'Obtenga su clave API en'),
|
||||||
|
('es', 'settings.key_saved', 'Clave API guardada exitosamente'),
|
||||||
|
('es', 'settings.keys', 'claves'),
|
||||||
|
('es', 'settings.language', 'Idioma'),
|
||||||
|
('es', 'settings.min_6_chars', 'Mínimo 6 caracteres'),
|
||||||
|
('es', 'settings.new_password', 'Nueva contraseña'),
|
||||||
|
('es', 'settings.no_api_key', 'No hay clave API configurada. La auto-traducción no funcionará.'),
|
||||||
|
('es', 'settings.profile', 'Perfil'),
|
||||||
|
('es', 'settings.progress', 'Progreso'),
|
||||||
|
('es', 'settings.skip_validation', 'Omitir validación (guardar sin probar)'),
|
||||||
|
('es', 'settings.translation_complete', 'Traducción completada'),
|
||||||
|
('es', 'settings.translation_status', 'Estado de traducción'),
|
||||||
|
('es', 'settings.translations', 'Traducciones'),
|
||||||
|
('es', 'settings.users', 'Usuarios'),
|
||||||
|
('es', 'status.active', 'Activo'),
|
||||||
|
('es', 'status.deploying', 'Implementando'),
|
||||||
|
('es', 'status.disabled', 'Deshabilitado'),
|
||||||
|
('es', 'status.error', 'Error'),
|
||||||
|
('es', 'status.inactive', 'Inactivo'),
|
||||||
|
('es', 'users.add_user', 'Agregar usuario'),
|
||||||
|
('es', 'users.administrator', 'Administrador'),
|
||||||
|
('es', 'users.all_users', 'Todos los usuarios'),
|
||||||
|
('es', 'users.created', 'Creado'),
|
||||||
|
('es', 'users.delete_confirm', '¿Eliminar {0}?'),
|
||||||
|
('es', 'users.role', 'Rol'),
|
||||||
|
('es', 'users.role_admin', 'Administrador'),
|
||||||
|
('es', 'users.role_user', 'Usuario')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- Russian translations
|
||||||
|
-- This migration adds Russian language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('ru', 'auth.email', 'Email'),
|
||||||
|
('ru', 'auth.login', 'Вход'),
|
||||||
|
('ru', 'auth.name', 'Имя'),
|
||||||
|
('ru', 'auth.password', 'Пароль'),
|
||||||
|
('ru', 'auth.register', 'Регистрация'),
|
||||||
|
('ru', 'backups.create', 'Создать резервную копию'),
|
||||||
|
('ru', 'backups.create_confirm', 'Создать резервную копию всех клиентов на этом сервере?'),
|
||||||
|
('ru', 'backups.created_success', 'Резервная копия успешно создана'),
|
||||||
|
('ru', 'backups.delete_confirm', 'Удалить эту резервную копию навсегда?'),
|
||||||
|
('ru', 'backups.deleted_success', 'Резервная копия успешно удалена'),
|
||||||
|
('ru', 'backups.login_required', 'Пожалуйста, войдите через API для управления резервными копиями'),
|
||||||
|
('ru', 'backups.no_backups', 'Пока нет резервных копий'),
|
||||||
|
('ru', 'backups.restore', 'Восстановить'),
|
||||||
|
('ru', 'backups.restore_confirm', 'Восстановить клиентов из этой резервной копии? Существующие клиенты не будут затронуты.'),
|
||||||
|
('ru', 'backups.restored_success', 'Восстановлено'),
|
||||||
|
('ru', 'backups.title', 'Резервные копии сервера'),
|
||||||
|
('ru', 'clients.actions', 'Действия'),
|
||||||
|
('ru', 'clients.add', 'Добавить клиента'),
|
||||||
|
('ru', 'clients.create', 'Создать клиента'),
|
||||||
|
('ru', 'clients.delete', 'Удалить'),
|
||||||
|
('ru', 'clients.delete_confirm', 'Удалить этого клиента навсегда?'),
|
||||||
|
('ru', 'clients.download_config', 'Скачать конфигурацию'),
|
||||||
|
('ru', 'clients.expiration', 'Срок действия'),
|
||||||
|
('ru', 'clients.expired', 'Истек'),
|
||||||
|
('ru', 'clients.ip', 'IP-адрес'),
|
||||||
|
('ru', 'clients.last_handshake', 'Последнее соединение'),
|
||||||
|
('ru', 'clients.name', 'Имя клиента'),
|
||||||
|
('ru', 'clients.never', 'Никогда'),
|
||||||
|
('ru', 'clients.never_expires', 'Бессрочно'),
|
||||||
|
('ru', 'clients.no_clients', 'Пока нет клиентов'),
|
||||||
|
('ru', 'clients.qr_code', 'QR-код'),
|
||||||
|
('ru', 'clients.received', 'Получено'),
|
||||||
|
('ru', 'clients.restore', 'Восстановить'),
|
||||||
|
('ru', 'clients.revoke', 'Отозвать'),
|
||||||
|
('ru', 'clients.revoke_confirm', 'Отозвать доступ для этого клиента?'),
|
||||||
|
('ru', 'clients.sent', 'Отправлено'),
|
||||||
|
('ru', 'clients.server', 'Сервер'),
|
||||||
|
('ru', 'clients.status', 'Статус'),
|
||||||
|
('ru', 'clients.sync_stats', 'Синхронизировать статистику'),
|
||||||
|
('ru', 'clients.title', 'Клиенты'),
|
||||||
|
('ru', 'clients.traffic', 'Трафик'),
|
||||||
|
('ru', 'common.days', 'дней'),
|
||||||
|
('ru', 'dashboard.active_clients', 'Активные клиенты'),
|
||||||
|
('ru', 'dashboard.add_first_server', 'Добавить первый сервер'),
|
||||||
|
('ru', 'dashboard.get_started', 'Начните с добавления вашего первого VPN-сервера'),
|
||||||
|
('ru', 'dashboard.no_servers', 'Пока нет серверов'),
|
||||||
|
('ru', 'dashboard.quick_actions', 'Быстрые действия'),
|
||||||
|
('ru', 'dashboard.recent_servers', 'Недавние серверы'),
|
||||||
|
('ru', 'dashboard.title', 'Панель управления'),
|
||||||
|
('ru', 'dashboard.total_clients', 'Всего клиентов'),
|
||||||
|
('ru', 'dashboard.total_servers', 'Всего серверов'),
|
||||||
|
('ru', 'dashboard.total_traffic', 'Общий трафик'),
|
||||||
|
('ru', 'dashboard.welcome', 'Добро пожаловать в панель управления Amnezia VPN'),
|
||||||
|
('ru', 'form.cancel', 'Отмена'),
|
||||||
|
('ru', 'form.close', 'Закрыть'),
|
||||||
|
('ru', 'form.create', 'Создать'),
|
||||||
|
('ru', 'form.loading', 'Загрузка...'),
|
||||||
|
('ru', 'form.processing', 'Обработка...'),
|
||||||
|
('ru', 'form.save', 'Сохранить'),
|
||||||
|
('ru', 'form.submit', 'Отправить'),
|
||||||
|
('ru', 'form.update', 'Обновить'),
|
||||||
|
('ru', 'menu.clients', 'Клиенты'),
|
||||||
|
('ru', 'menu.dashboard', 'Панель управления'),
|
||||||
|
('ru', 'menu.logout', 'Выход'),
|
||||||
|
('ru', 'menu.servers', 'Серверы'),
|
||||||
|
('ru', 'menu.settings', 'Настройки'),
|
||||||
|
('ru', 'menu.users', 'Пользователи'),
|
||||||
|
('ru', 'message.confirm', 'Вы уверены?'),
|
||||||
|
('ru', 'message.deleted', 'Успешно удалено'),
|
||||||
|
('ru', 'message.deployed', 'Успешно развернуто'),
|
||||||
|
('ru', 'message.error', 'Произошла ошибка'),
|
||||||
|
('ru', 'message.saved', 'Успешно сохранено'),
|
||||||
|
('ru', 'message.success', 'Операция успешно завершена'),
|
||||||
|
('ru', 'servers.actions', 'Действия'),
|
||||||
|
('ru', 'servers.add', 'Добавить сервер'),
|
||||||
|
('ru', 'servers.clients', 'Клиенты'),
|
||||||
|
('ru', 'servers.delete', 'Удалить'),
|
||||||
|
('ru', 'servers.deploy', 'Развернуть'),
|
||||||
|
('ru', 'servers.edit', 'Редактировать'),
|
||||||
|
('ru', 'servers.host', 'Хост'),
|
||||||
|
('ru', 'servers.name', 'Имя'),
|
||||||
|
('ru', 'servers.port', 'Порт'),
|
||||||
|
('ru', 'servers.status', 'Статус'),
|
||||||
|
('ru', 'servers.title', 'Серверы'),
|
||||||
|
('ru', 'servers.view', 'Просмотр'),
|
||||||
|
('ru', 'settings.actions', 'Действия'),
|
||||||
|
('ru', 'settings.api_key_configured', 'API-ключ настроен'),
|
||||||
|
('ru', 'settings.api_keys', 'API-ключи'),
|
||||||
|
('ru', 'settings.api_keys_desc', 'Настройка API-ключей для внешних сервисов'),
|
||||||
|
('ru', 'settings.auto_translate', 'Автоперевод'),
|
||||||
|
('ru', 'settings.change_password', 'Изменить пароль'),
|
||||||
|
('ru', 'settings.confirm_password', 'Подтвердите пароль'),
|
||||||
|
('ru', 'settings.confirm_translate', 'Начать автоматический перевод? Это может занять несколько минут.'),
|
||||||
|
('ru', 'settings.current_password', 'Текущий пароль'),
|
||||||
|
('ru', 'settings.description', 'Управление конфигурацией панели и интеграциями API'),
|
||||||
|
('ru', 'settings.error_empty_key', 'API-ключ не может быть пустым'),
|
||||||
|
('ru', 'settings.error_invalid_key', 'Неверный формат API-ключа'),
|
||||||
|
('ru', 'settings.error_key_test', 'Тест API-ключа не удался'),
|
||||||
|
('ru', 'settings.for_translation', 'для автоперевода'),
|
||||||
|
('ru', 'settings.get_key_at', 'Получите ваш API-ключ на'),
|
||||||
|
('ru', 'settings.key_saved', 'API-ключ успешно сохранен'),
|
||||||
|
('ru', 'settings.keys', 'ключи'),
|
||||||
|
('ru', 'settings.language', 'Язык'),
|
||||||
|
('ru', 'settings.min_6_chars', 'Минимум 6 символов'),
|
||||||
|
('ru', 'settings.new_password', 'Новый пароль'),
|
||||||
|
('ru', 'settings.no_api_key', 'API-ключ не настроен. Автоперевод не будет работать.'),
|
||||||
|
('ru', 'settings.profile', 'Профиль'),
|
||||||
|
('ru', 'settings.progress', 'Прогресс'),
|
||||||
|
('ru', 'settings.skip_validation', 'Пропустить проверку (сохранить без тестирования)'),
|
||||||
|
('ru', 'settings.translation_complete', 'Перевод завершен'),
|
||||||
|
('ru', 'settings.translation_status', 'Статус перевода'),
|
||||||
|
('ru', 'settings.translations', 'Переводы'),
|
||||||
|
('ru', 'settings.users', 'Пользователи'),
|
||||||
|
('ru', 'status.active', 'Активен'),
|
||||||
|
('ru', 'status.deploying', 'Развертывание'),
|
||||||
|
('ru', 'status.disabled', 'Отключен'),
|
||||||
|
('ru', 'status.error', 'Ошибка'),
|
||||||
|
('ru', 'status.inactive', 'Неактивен'),
|
||||||
|
('ru', 'users.add_user', 'Добавить пользователя'),
|
||||||
|
('ru', 'users.administrator', 'Администратор'),
|
||||||
|
('ru', 'users.all_users', 'Все пользователи'),
|
||||||
|
('ru', 'users.created', 'Создан'),
|
||||||
|
('ru', 'users.delete_confirm', 'Удалить {0}?'),
|
||||||
|
('ru', 'users.role', 'Роль'),
|
||||||
|
('ru', 'users.role_admin', 'Администратор'),
|
||||||
|
('ru', 'users.role_user', 'Пользователь')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- Spanish translations
|
||||||
|
-- This migration adds Spanish language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('es', 'auth.email', 'Correo electrónico'),
|
||||||
|
('es', 'auth.login', 'Iniciar sesión'),
|
||||||
|
('es', 'auth.name', 'Nombre'),
|
||||||
|
('es', 'auth.password', 'Contraseña'),
|
||||||
|
('es', 'auth.register', 'Registrarse'),
|
||||||
|
('es', 'backups.create', 'Crear copia de seguridad'),
|
||||||
|
('es', 'backups.create_confirm', '¿Crear copia de seguridad de todos los clientes en este servidor?'),
|
||||||
|
('es', 'backups.created_success', 'Copia de seguridad creada exitosamente'),
|
||||||
|
('es', 'backups.delete_confirm', '¿Eliminar esta copia de seguridad permanentemente?'),
|
||||||
|
('es', 'backups.deleted_success', 'Copia de seguridad eliminada exitosamente'),
|
||||||
|
('es', 'backups.login_required', 'Por favor inicie sesión vía API para gestionar copias de seguridad'),
|
||||||
|
('es', 'backups.no_backups', 'Aún no hay copias de seguridad'),
|
||||||
|
('es', 'backups.restore', 'Restaurar'),
|
||||||
|
('es', 'backups.restore_confirm', '¿Restaurar clientes desde esta copia de seguridad? Los clientes existentes no se verán afectados.'),
|
||||||
|
('es', 'backups.restored_success', 'Restaurado'),
|
||||||
|
('es', 'backups.title', 'Copias de seguridad del servidor'),
|
||||||
|
('es', 'clients.actions', 'Acciones'),
|
||||||
|
('es', 'clients.add', 'Agregar cliente'),
|
||||||
|
('es', 'clients.create', 'Crear cliente'),
|
||||||
|
('es', 'clients.delete', 'Eliminar'),
|
||||||
|
('es', 'clients.delete_confirm', '¿Eliminar este cliente permanentemente?'),
|
||||||
|
('es', 'clients.download_config', 'Descargar configuración'),
|
||||||
|
('es', 'clients.expiration', 'Vencimiento'),
|
||||||
|
('es', 'clients.expired', 'Vencido'),
|
||||||
|
('es', 'clients.ip', 'Dirección IP'),
|
||||||
|
('es', 'clients.last_handshake', 'Último contacto'),
|
||||||
|
('es', 'clients.name', 'Nombre del cliente'),
|
||||||
|
('es', 'clients.never', 'Nunca'),
|
||||||
|
('es', 'clients.never_expires', 'Nunca vence'),
|
||||||
|
('es', 'clients.no_clients', 'Aún no hay clientes'),
|
||||||
|
('es', 'clients.qr_code', 'Código QR'),
|
||||||
|
('es', 'clients.received', 'Recibido'),
|
||||||
|
('es', 'clients.restore', 'Restaurar'),
|
||||||
|
('es', 'clients.revoke', 'Revocar'),
|
||||||
|
('es', 'clients.revoke_confirm', '¿Revocar acceso para este cliente?'),
|
||||||
|
('es', 'clients.sent', 'Enviado'),
|
||||||
|
('es', 'clients.server', 'Servidor'),
|
||||||
|
('es', 'clients.status', 'Estado'),
|
||||||
|
('es', 'clients.sync_stats', 'Sincronizar estadísticas'),
|
||||||
|
('es', 'clients.title', 'Clientes'),
|
||||||
|
('es', 'clients.traffic', 'Tráfico'),
|
||||||
|
('es', 'common.days', 'días'),
|
||||||
|
('es', 'dashboard.active_clients', 'Clientes activos'),
|
||||||
|
('es', 'dashboard.add_first_server', 'Agregar primer servidor'),
|
||||||
|
('es', 'dashboard.get_started', 'Comience agregando su primer servidor VPN'),
|
||||||
|
('es', 'dashboard.no_servers', 'Aún no hay servidores'),
|
||||||
|
('es', 'dashboard.quick_actions', 'Acciones rápidas'),
|
||||||
|
('es', 'dashboard.recent_servers', 'Servidores recientes'),
|
||||||
|
('es', 'dashboard.title', 'Panel de control'),
|
||||||
|
('es', 'dashboard.total_clients', 'Total de clientes'),
|
||||||
|
('es', 'dashboard.total_servers', 'Total de servidores'),
|
||||||
|
('es', 'dashboard.total_traffic', 'Tráfico total'),
|
||||||
|
('es', 'dashboard.welcome', 'Bienvenido al Panel de Gestión de Amnezia VPN'),
|
||||||
|
('es', 'form.cancel', 'Cancelar'),
|
||||||
|
('es', 'form.close', 'Cerrar'),
|
||||||
|
('es', 'form.create', 'Crear'),
|
||||||
|
('es', 'form.loading', 'Cargando...'),
|
||||||
|
('es', 'form.processing', 'Procesando...'),
|
||||||
|
('es', 'form.save', 'Guardar'),
|
||||||
|
('es', 'form.submit', 'Enviar'),
|
||||||
|
('es', 'form.update', 'Actualizar'),
|
||||||
|
('es', 'menu.clients', 'Clientes'),
|
||||||
|
('es', 'menu.dashboard', 'Panel de control'),
|
||||||
|
('es', 'menu.logout', 'Cerrar sesión'),
|
||||||
|
('es', 'menu.servers', 'Servidores'),
|
||||||
|
('es', 'menu.settings', 'Configuración'),
|
||||||
|
('es', 'menu.users', 'Usuarios'),
|
||||||
|
('es', 'message.confirm', '¿Está seguro?'),
|
||||||
|
('es', 'message.deleted', 'Eliminado exitosamente'),
|
||||||
|
('es', 'message.deployed', 'Implementado exitosamente'),
|
||||||
|
('es', 'message.error', 'Ha ocurrido un error'),
|
||||||
|
('es', 'message.saved', 'Guardado exitosamente'),
|
||||||
|
('es', 'message.success', 'Operación completada exitosamente'),
|
||||||
|
('es', 'servers.actions', 'Acciones'),
|
||||||
|
('es', 'servers.add', 'Agregar servidor'),
|
||||||
|
('es', 'servers.clients', 'Clientes'),
|
||||||
|
('es', 'servers.delete', 'Eliminar'),
|
||||||
|
('es', 'servers.deploy', 'Implementar'),
|
||||||
|
('es', 'servers.edit', 'Editar'),
|
||||||
|
('es', 'servers.host', 'Host'),
|
||||||
|
('es', 'servers.name', 'Nombre'),
|
||||||
|
('es', 'servers.port', 'Puerto'),
|
||||||
|
('es', 'servers.status', 'Estado'),
|
||||||
|
('es', 'servers.title', 'Servidores'),
|
||||||
|
('es', 'servers.view', 'Ver'),
|
||||||
|
('es', 'settings.actions', 'Acciones'),
|
||||||
|
('es', 'settings.api_key_configured', 'Clave API configurada'),
|
||||||
|
('es', 'settings.api_keys', 'Claves API'),
|
||||||
|
('es', 'settings.api_keys_desc', 'Configurar claves API para servicios externos'),
|
||||||
|
('es', 'settings.auto_translate', 'Auto-traducir'),
|
||||||
|
('es', 'settings.change_password', 'Cambiar contraseña'),
|
||||||
|
('es', 'settings.confirm_password', 'Confirmar contraseña'),
|
||||||
|
('es', 'settings.confirm_translate', '¿Iniciar traducción automática? Esto puede tomar unos minutos.'),
|
||||||
|
('es', 'settings.current_password', 'Contraseña actual'),
|
||||||
|
('es', 'settings.description', 'Gestionar configuración del panel e integraciones API'),
|
||||||
|
('es', 'settings.error_empty_key', 'La clave API no puede estar vacía'),
|
||||||
|
('es', 'settings.error_invalid_key', 'Formato de clave API inválido'),
|
||||||
|
('es', 'settings.error_key_test', 'Prueba de clave API fallida'),
|
||||||
|
('es', 'settings.for_translation', 'para auto-traducción'),
|
||||||
|
('es', 'settings.get_key_at', 'Obtenga su clave API en'),
|
||||||
|
('es', 'settings.key_saved', 'Clave API guardada exitosamente'),
|
||||||
|
('es', 'settings.keys', 'claves'),
|
||||||
|
('es', 'settings.language', 'Idioma'),
|
||||||
|
('es', 'settings.min_6_chars', 'Mínimo 6 caracteres'),
|
||||||
|
('es', 'settings.new_password', 'Nueva contraseña'),
|
||||||
|
('es', 'settings.no_api_key', 'No hay clave API configurada. La auto-traducción no funcionará.'),
|
||||||
|
('es', 'settings.profile', 'Perfil'),
|
||||||
|
('es', 'settings.progress', 'Progreso'),
|
||||||
|
('es', 'settings.skip_validation', 'Omitir validación (guardar sin probar)'),
|
||||||
|
('es', 'settings.translation_complete', 'Traducción completada'),
|
||||||
|
('es', 'settings.translation_status', 'Estado de traducción'),
|
||||||
|
('es', 'settings.translations', 'Traducciones'),
|
||||||
|
('es', 'settings.users', 'Usuarios'),
|
||||||
|
('es', 'status.active', 'Activo'),
|
||||||
|
('es', 'status.deploying', 'Implementando'),
|
||||||
|
('es', 'status.disabled', 'Deshabilitado'),
|
||||||
|
('es', 'status.error', 'Error'),
|
||||||
|
('es', 'status.inactive', 'Inactivo'),
|
||||||
|
('es', 'users.add_user', 'Agregar usuario'),
|
||||||
|
('es', 'users.administrator', 'Administrador'),
|
||||||
|
('es', 'users.all_users', 'Todos los usuarios'),
|
||||||
|
('es', 'users.created', 'Creado'),
|
||||||
|
('es', 'users.delete_confirm', '¿Eliminar {0}?'),
|
||||||
|
('es', 'users.role', 'Rol'),
|
||||||
|
('es', 'users.role_admin', 'Administrador'),
|
||||||
|
('es', 'users.role_user', 'Usuario')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- German translations
|
||||||
|
-- This migration adds German language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('de', 'auth.email', 'E-Mail'),
|
||||||
|
('de', 'auth.login', 'Anmelden'),
|
||||||
|
('de', 'auth.name', 'Name'),
|
||||||
|
('de', 'auth.password', 'Passwort'),
|
||||||
|
('de', 'auth.register', 'Registrieren'),
|
||||||
|
('de', 'backups.create', 'Backup erstellen'),
|
||||||
|
('de', 'backups.create_confirm', 'Backup aller Clients auf diesem Server erstellen?'),
|
||||||
|
('de', 'backups.created_success', 'Backup erfolgreich erstellt'),
|
||||||
|
('de', 'backups.delete_confirm', 'Dieses Backup endgültig löschen?'),
|
||||||
|
('de', 'backups.deleted_success', 'Backup erfolgreich gelöscht'),
|
||||||
|
('de', 'backups.login_required', 'Bitte melden Sie sich über die API an, um Backups zu verwalten'),
|
||||||
|
('de', 'backups.no_backups', 'Noch keine Backups'),
|
||||||
|
('de', 'backups.restore', 'Wiederherstellen'),
|
||||||
|
('de', 'backups.restore_confirm', 'Clients aus diesem Backup wiederherstellen? Bestehende Clients bleiben unberührt.'),
|
||||||
|
('de', 'backups.restored_success', 'Wiederhergestellt'),
|
||||||
|
('de', 'backups.title', 'Server-Backups'),
|
||||||
|
('de', 'clients.actions', 'Aktionen'),
|
||||||
|
('de', 'clients.add', 'Client hinzufügen'),
|
||||||
|
('de', 'clients.create', 'Client erstellen'),
|
||||||
|
('de', 'clients.delete', 'Löschen'),
|
||||||
|
('de', 'clients.download_config', 'Konfiguration herunterladen'),
|
||||||
|
('de', 'clients.expiration', 'Ablaufdatum'),
|
||||||
|
('de', 'clients.expired', 'Abgelaufen'),
|
||||||
|
('de', 'clients.ip', 'IP-Adresse'),
|
||||||
|
('de', 'clients.last_handshake', 'Letzter Handshake'),
|
||||||
|
('de', 'clients.name', 'Client-Name'),
|
||||||
|
('de', 'clients.never_expires', 'Läuft nie ab'),
|
||||||
|
('de', 'clients.qr_code', 'QR-Code'),
|
||||||
|
('de', 'clients.received', 'Empfangen'),
|
||||||
|
('de', 'clients.restore', 'Wiederherstellen'),
|
||||||
|
('de', 'clients.revoke', 'Widerrufen'),
|
||||||
|
('de', 'clients.sent', 'Gesendet'),
|
||||||
|
('de', 'clients.server', 'Server'),
|
||||||
|
('de', 'clients.status', 'Status'),
|
||||||
|
('de', 'clients.sync_stats', 'Statistiken synchronisieren'),
|
||||||
|
('de', 'clients.title', 'Clients'),
|
||||||
|
('de', 'clients.traffic', 'Datenverkehr'),
|
||||||
|
('de', 'common.days', 'Tage'),
|
||||||
|
('de', 'dashboard.active_clients', 'Aktive Clients'),
|
||||||
|
('de', 'dashboard.add_first_server', 'Ersten Server hinzufügen'),
|
||||||
|
('de', 'dashboard.get_started', 'Beginnen Sie mit dem Hinzufügen Ihres ersten VPN-Servers'),
|
||||||
|
('de', 'dashboard.no_servers', 'Noch keine Server'),
|
||||||
|
('de', 'dashboard.quick_actions', 'Schnellaktionen'),
|
||||||
|
('de', 'dashboard.recent_servers', 'Aktuelle Server'),
|
||||||
|
('de', 'dashboard.title', 'Dashboard'),
|
||||||
|
('de', 'dashboard.total_clients', 'Gesamtzahl Clients'),
|
||||||
|
('de', 'dashboard.total_servers', 'Gesamtzahl Server'),
|
||||||
|
('de', 'dashboard.total_traffic', 'Gesamter Datenverkehr'),
|
||||||
|
('de', 'dashboard.welcome', 'Willkommen im Amnezia VPN Verwaltungspanel'),
|
||||||
|
('de', 'form.cancel', 'Abbrechen'),
|
||||||
|
('de', 'form.close', 'Schließen'),
|
||||||
|
('de', 'form.create', 'Erstellen'),
|
||||||
|
('de', 'form.loading', 'Lädt...'),
|
||||||
|
('de', 'form.processing', 'Verarbeitung...'),
|
||||||
|
('de', 'form.save', 'Speichern'),
|
||||||
|
('de', 'form.submit', 'Absenden'),
|
||||||
|
('de', 'form.update', 'Aktualisieren'),
|
||||||
|
('de', 'menu.clients', 'Clients'),
|
||||||
|
('de', 'menu.dashboard', 'Dashboard'),
|
||||||
|
('de', 'menu.logout', 'Abmelden'),
|
||||||
|
('de', 'menu.servers', 'Server'),
|
||||||
|
('de', 'menu.settings', 'Einstellungen'),
|
||||||
|
('de', 'menu.users', 'Benutzer'),
|
||||||
|
('de', 'message.confirm', 'Sind Sie sicher?'),
|
||||||
|
('de', 'message.deleted', 'Erfolgreich gelöscht'),
|
||||||
|
('de', 'message.deployed', 'Erfolgreich bereitgestellt'),
|
||||||
|
('de', 'message.error', 'Ein Fehler ist aufgetreten'),
|
||||||
|
('de', 'message.saved', 'Erfolgreich gespeichert'),
|
||||||
|
('de', 'message.success', 'Vorgang erfolgreich abgeschlossen'),
|
||||||
|
('de', 'servers.actions', 'Aktionen'),
|
||||||
|
('de', 'servers.add', 'Server hinzufügen'),
|
||||||
|
('de', 'servers.clients', 'Clients'),
|
||||||
|
('de', 'servers.delete', 'Löschen'),
|
||||||
|
('de', 'servers.deploy', 'Bereitstellen'),
|
||||||
|
('de', 'servers.edit', 'Bearbeiten'),
|
||||||
|
('de', 'servers.host', 'Host'),
|
||||||
|
('de', 'servers.name', 'Name'),
|
||||||
|
('de', 'servers.port', 'Port'),
|
||||||
|
('de', 'servers.status', 'Status'),
|
||||||
|
('de', 'servers.title', 'Server'),
|
||||||
|
('de', 'servers.view', 'Ansehen'),
|
||||||
|
('de', 'settings.actions', 'Aktionen'),
|
||||||
|
('de', 'settings.api_key_configured', 'API-Schlüssel konfiguriert'),
|
||||||
|
('de', 'settings.api_keys', 'API-Schlüssel'),
|
||||||
|
('de', 'settings.api_keys_desc', 'API-Schlüssel für externe Dienste konfigurieren'),
|
||||||
|
('de', 'settings.auto_translate', 'Automatische Übersetzung'),
|
||||||
|
('de', 'settings.change_password', 'Passwort ändern'),
|
||||||
|
('de', 'settings.confirm_password', 'Passwort bestätigen'),
|
||||||
|
('de', 'settings.confirm_translate', 'Automatische Übersetzung starten? Dies kann einige Minuten dauern.'),
|
||||||
|
('de', 'settings.current_password', 'Aktuelles Passwort'),
|
||||||
|
('de', 'settings.description', 'Panel-Konfiguration und API-Integrationen verwalten'),
|
||||||
|
('de', 'settings.error_empty_key', 'API-Schlüssel darf nicht leer sein'),
|
||||||
|
('de', 'settings.error_invalid_key', 'Ungültiges API-Schlüssel-Format'),
|
||||||
|
('de', 'settings.error_key_test', 'API-Schlüssel-Test fehlgeschlagen'),
|
||||||
|
('de', 'settings.for_translation', 'für automatische Übersetzung'),
|
||||||
|
('de', 'settings.get_key_at', 'Holen Sie sich Ihren API-Schlüssel bei'),
|
||||||
|
('de', 'settings.key_saved', 'API-Schlüssel erfolgreich gespeichert'),
|
||||||
|
('de', 'settings.keys', 'Schlüssel'),
|
||||||
|
('de', 'settings.language', 'Sprache'),
|
||||||
|
('de', 'settings.min_6_chars', 'Mindestens 6 Zeichen'),
|
||||||
|
('de', 'settings.new_password', 'Neues Passwort'),
|
||||||
|
('de', 'settings.no_api_key', 'Kein API-Schlüssel konfiguriert. Automatische Übersetzung wird nicht funktionieren.'),
|
||||||
|
('de', 'settings.profile', 'Profil'),
|
||||||
|
('de', 'settings.progress', 'Fortschritt'),
|
||||||
|
('de', 'settings.skip_validation', 'Validierung überspringen (ohne Test speichern)'),
|
||||||
|
('de', 'settings.translation_complete', 'Übersetzung abgeschlossen'),
|
||||||
|
('de', 'settings.translation_status', 'Übersetzungsstatus'),
|
||||||
|
('de', 'settings.translations', 'Übersetzungen'),
|
||||||
|
('de', 'settings.users', 'Benutzer'),
|
||||||
|
('de', 'status.active', 'Aktiv'),
|
||||||
|
('de', 'status.deploying', 'Wird bereitgestellt'),
|
||||||
|
('de', 'status.disabled', 'Deaktiviert'),
|
||||||
|
('de', 'status.error', 'Fehler'),
|
||||||
|
('de', 'status.inactive', 'Inaktiv'),
|
||||||
|
('de', 'users.add_user', 'Benutzer hinzufügen'),
|
||||||
|
('de', 'users.administrator', 'Administrator'),
|
||||||
|
('de', 'users.all_users', 'Alle Benutzer'),
|
||||||
|
('de', 'users.created', 'Erstellt'),
|
||||||
|
('de', 'users.delete_confirm', '{0} löschen?'),
|
||||||
|
('de', 'users.role', 'Rolle'),
|
||||||
|
('de', 'users.role_admin', 'Admin'),
|
||||||
|
('de', 'users.role_user', 'Benutzer')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- German translations
|
||||||
|
-- This migration adds German language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('de', 'auth.email', 'E-Mail'),
|
||||||
|
('de', 'auth.login', 'Anmelden'),
|
||||||
|
('de', 'auth.name', 'Name'),
|
||||||
|
('de', 'auth.password', 'Passwort'),
|
||||||
|
('de', 'auth.register', 'Registrieren'),
|
||||||
|
('de', 'backups.create', 'Backup erstellen'),
|
||||||
|
('de', 'backups.create_confirm', 'Backup aller Clients auf diesem Server erstellen?'),
|
||||||
|
('de', 'backups.created_success', 'Backup erfolgreich erstellt'),
|
||||||
|
('de', 'backups.delete_confirm', 'Dieses Backup endgültig löschen?'),
|
||||||
|
('de', 'backups.deleted_success', 'Backup erfolgreich gelöscht'),
|
||||||
|
('de', 'backups.login_required', 'Bitte melden Sie sich über die API an, um Backups zu verwalten'),
|
||||||
|
('de', 'backups.no_backups', 'Noch keine Backups'),
|
||||||
|
('de', 'backups.restore', 'Wiederherstellen'),
|
||||||
|
('de', 'backups.restore_confirm', 'Clients aus diesem Backup wiederherstellen? Bestehende Clients bleiben unberührt.'),
|
||||||
|
('de', 'backups.restored_success', 'Wiederhergestellt'),
|
||||||
|
('de', 'backups.title', 'Server-Backups'),
|
||||||
|
('de', 'clients.actions', 'Aktionen'),
|
||||||
|
('de', 'clients.add', 'Client hinzufügen'),
|
||||||
|
('de', 'clients.create', 'Client erstellen'),
|
||||||
|
('de', 'clients.delete', 'Löschen'),
|
||||||
|
('de', 'clients.delete_confirm', 'Diesen Client dauerhaft löschen?'),
|
||||||
|
('de', 'clients.download_config', 'Konfiguration herunterladen'),
|
||||||
|
('de', 'clients.expiration', 'Ablaufdatum'),
|
||||||
|
('de', 'clients.expired', 'Abgelaufen'),
|
||||||
|
('de', 'clients.ip', 'IP-Adresse'),
|
||||||
|
('de', 'clients.last_handshake', 'Letzter Handshake'),
|
||||||
|
('de', 'clients.name', 'Client-Name'),
|
||||||
|
('de', 'clients.never', 'Niemals'),
|
||||||
|
('de', 'clients.never_expires', 'Läuft nie ab'),
|
||||||
|
('de', 'clients.no_clients', 'Noch keine Kunden'),
|
||||||
|
('de', 'clients.qr_code', 'QR-Code'),
|
||||||
|
('de', 'clients.received', 'Empfangen'),
|
||||||
|
('de', 'clients.restore', 'Wiederherstellen'),
|
||||||
|
('de', 'clients.revoke', 'Widerrufen'),
|
||||||
|
('de', 'clients.revoke_confirm', 'Zugriff für diesen Client widerrufen?'),
|
||||||
|
('de', 'clients.sent', 'Gesendet'),
|
||||||
|
('de', 'clients.server', 'Server'),
|
||||||
|
('de', 'clients.status', 'Status'),
|
||||||
|
('de', 'clients.sync_stats', 'Statistiken synchronisieren'),
|
||||||
|
('de', 'clients.title', 'Clients'),
|
||||||
|
('de', 'clients.traffic', 'Datenverkehr'),
|
||||||
|
('de', 'common.days', 'Tage'),
|
||||||
|
('de', 'dashboard.active_clients', 'Aktive Clients'),
|
||||||
|
('de', 'dashboard.add_first_server', 'Ersten Server hinzufügen'),
|
||||||
|
('de', 'dashboard.get_started', 'Beginnen Sie mit dem Hinzufügen Ihres ersten VPN-Servers'),
|
||||||
|
('de', 'dashboard.no_servers', 'Noch keine Server'),
|
||||||
|
('de', 'dashboard.quick_actions', 'Schnellaktionen'),
|
||||||
|
('de', 'dashboard.recent_servers', 'Aktuelle Server'),
|
||||||
|
('de', 'dashboard.title', 'Dashboard'),
|
||||||
|
('de', 'dashboard.total_clients', 'Gesamtzahl Clients'),
|
||||||
|
('de', 'dashboard.total_servers', 'Gesamtzahl Server'),
|
||||||
|
('de', 'dashboard.total_traffic', 'Gesamter Datenverkehr'),
|
||||||
|
('de', 'dashboard.welcome', 'Willkommen im Amnezia VPN Verwaltungspanel'),
|
||||||
|
('de', 'form.cancel', 'Abbrechen'),
|
||||||
|
('de', 'form.close', 'Schließen'),
|
||||||
|
('de', 'form.create', 'Erstellen'),
|
||||||
|
('de', 'form.loading', 'Lädt...'),
|
||||||
|
('de', 'form.processing', 'Verarbeitung...'),
|
||||||
|
('de', 'form.save', 'Speichern'),
|
||||||
|
('de', 'form.submit', 'Absenden'),
|
||||||
|
('de', 'form.update', 'Aktualisieren'),
|
||||||
|
('de', 'menu.clients', 'Clients'),
|
||||||
|
('de', 'menu.dashboard', 'Dashboard'),
|
||||||
|
('de', 'menu.logout', 'Abmelden'),
|
||||||
|
('de', 'menu.servers', 'Server'),
|
||||||
|
('de', 'menu.settings', 'Einstellungen'),
|
||||||
|
('de', 'menu.users', 'Benutzer'),
|
||||||
|
('de', 'message.confirm', 'Sind Sie sicher?'),
|
||||||
|
('de', 'message.deleted', 'Erfolgreich gelöscht'),
|
||||||
|
('de', 'message.deployed', 'Erfolgreich bereitgestellt'),
|
||||||
|
('de', 'message.error', 'Ein Fehler ist aufgetreten'),
|
||||||
|
('de', 'message.saved', 'Erfolgreich gespeichert'),
|
||||||
|
('de', 'message.success', 'Vorgang erfolgreich abgeschlossen'),
|
||||||
|
('de', 'servers.actions', 'Aktionen'),
|
||||||
|
('de', 'servers.add', 'Server hinzufügen'),
|
||||||
|
('de', 'servers.clients', 'Clients'),
|
||||||
|
('de', 'servers.delete', 'Löschen'),
|
||||||
|
('de', 'servers.deploy', 'Bereitstellen'),
|
||||||
|
('de', 'servers.edit', 'Bearbeiten'),
|
||||||
|
('de', 'servers.host', 'Host'),
|
||||||
|
('de', 'servers.name', 'Name'),
|
||||||
|
('de', 'servers.port', 'Port'),
|
||||||
|
('de', 'servers.status', 'Status'),
|
||||||
|
('de', 'servers.title', 'Server'),
|
||||||
|
('de', 'servers.view', 'Ansehen'),
|
||||||
|
('de', 'settings.actions', 'Aktionen'),
|
||||||
|
('de', 'settings.api_key_configured', 'API-Schlüssel konfiguriert'),
|
||||||
|
('de', 'settings.api_keys', 'API-Schlüssel'),
|
||||||
|
('de', 'settings.api_keys_desc', 'API-Schlüssel für externe Dienste konfigurieren'),
|
||||||
|
('de', 'settings.auto_translate', 'Automatische Übersetzung'),
|
||||||
|
('de', 'settings.change_password', 'Passwort ändern'),
|
||||||
|
('de', 'settings.confirm_password', 'Passwort bestätigen'),
|
||||||
|
('de', 'settings.confirm_translate', 'Automatische Übersetzung starten? Dies kann einige Minuten dauern.'),
|
||||||
|
('de', 'settings.current_password', 'Aktuelles Passwort'),
|
||||||
|
('de', 'settings.description', 'Panel-Konfiguration und API-Integrationen verwalten'),
|
||||||
|
('de', 'settings.error_empty_key', 'API-Schlüssel darf nicht leer sein'),
|
||||||
|
('de', 'settings.error_invalid_key', 'Ungültiges API-Schlüssel-Format'),
|
||||||
|
('de', 'settings.error_key_test', 'API-Schlüssel-Test fehlgeschlagen'),
|
||||||
|
('de', 'settings.for_translation', 'für automatische Übersetzung'),
|
||||||
|
('de', 'settings.get_key_at', 'Holen Sie sich Ihren API-Schlüssel bei'),
|
||||||
|
('de', 'settings.key_saved', 'API-Schlüssel erfolgreich gespeichert'),
|
||||||
|
('de', 'settings.keys', 'Schlüssel'),
|
||||||
|
('de', 'settings.language', 'Sprache'),
|
||||||
|
('de', 'settings.min_6_chars', 'Mindestens 6 Zeichen'),
|
||||||
|
('de', 'settings.new_password', 'Neues Passwort'),
|
||||||
|
('de', 'settings.no_api_key', 'Kein API-Schlüssel konfiguriert. Automatische Übersetzung wird nicht funktionieren.'),
|
||||||
|
('de', 'settings.profile', 'Profil'),
|
||||||
|
('de', 'settings.progress', 'Fortschritt'),
|
||||||
|
('de', 'settings.skip_validation', 'Validierung überspringen (ohne Test speichern)'),
|
||||||
|
('de', 'settings.translation_complete', 'Übersetzung abgeschlossen'),
|
||||||
|
('de', 'settings.translation_status', 'Übersetzungsstatus'),
|
||||||
|
('de', 'settings.translations', 'Übersetzungen'),
|
||||||
|
('de', 'settings.users', 'Benutzer'),
|
||||||
|
('de', 'status.active', 'Aktiv'),
|
||||||
|
('de', 'status.deploying', 'Wird bereitgestellt'),
|
||||||
|
('de', 'status.disabled', 'Deaktiviert'),
|
||||||
|
('de', 'status.error', 'Fehler'),
|
||||||
|
('de', 'status.inactive', 'Inaktiv'),
|
||||||
|
('de', 'users.add_user', 'Benutzer hinzufügen'),
|
||||||
|
('de', 'users.administrator', 'Administrator'),
|
||||||
|
('de', 'users.all_users', 'Alle Benutzer'),
|
||||||
|
('de', 'users.created', 'Erstellt'),
|
||||||
|
('de', 'users.delete_confirm', '{0} löschen?'),
|
||||||
|
('de', 'users.role', 'Rolle'),
|
||||||
|
('de', 'users.role_admin', 'Admin'),
|
||||||
|
('de', 'users.role_user', 'Benutzer')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- Russian translations
|
||||||
|
-- This migration adds Russian language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('ru', 'auth.email', 'Email'),
|
||||||
|
('ru', 'auth.login', 'Вход'),
|
||||||
|
('ru', 'auth.name', 'Имя'),
|
||||||
|
('ru', 'auth.password', 'Пароль'),
|
||||||
|
('ru', 'auth.register', 'Регистрация'),
|
||||||
|
('ru', 'backups.create', 'Создать резервную копию'),
|
||||||
|
('ru', 'backups.create_confirm', 'Создать резервную копию всех клиентов на этом сервере?'),
|
||||||
|
('ru', 'backups.created_success', 'Резервная копия успешно создана'),
|
||||||
|
('ru', 'backups.delete_confirm', 'Удалить эту резервную копию навсегда?'),
|
||||||
|
('ru', 'backups.deleted_success', 'Резервная копия успешно удалена'),
|
||||||
|
('ru', 'backups.login_required', 'Пожалуйста, войдите через API для управления резервными копиями'),
|
||||||
|
('ru', 'backups.no_backups', 'Пока нет резервных копий'),
|
||||||
|
('ru', 'backups.restore', 'Восстановить'),
|
||||||
|
('ru', 'backups.restore_confirm', 'Восстановить клиентов из этой резервной копии? Существующие клиенты не будут затронуты.'),
|
||||||
|
('ru', 'backups.restored_success', 'Восстановлено'),
|
||||||
|
('ru', 'backups.title', 'Резервные копии сервера'),
|
||||||
|
('ru', 'clients.actions', 'Действия'),
|
||||||
|
('ru', 'clients.add', 'Добавить клиента'),
|
||||||
|
('ru', 'clients.create', 'Создать клиента'),
|
||||||
|
('ru', 'clients.delete', 'Удалить'),
|
||||||
|
('ru', 'clients.download_config', 'Скачать конфигурацию'),
|
||||||
|
('ru', 'clients.expiration', 'Срок действия'),
|
||||||
|
('ru', 'clients.expired', 'Истек'),
|
||||||
|
('ru', 'clients.ip', 'IP-адрес'),
|
||||||
|
('ru', 'clients.last_handshake', 'Последнее соединение'),
|
||||||
|
('ru', 'clients.name', 'Имя клиента'),
|
||||||
|
('ru', 'clients.never_expires', 'Бессрочно'),
|
||||||
|
('ru', 'clients.qr_code', 'QR-код'),
|
||||||
|
('ru', 'clients.received', 'Получено'),
|
||||||
|
('ru', 'clients.restore', 'Восстановить'),
|
||||||
|
('ru', 'clients.revoke', 'Отозвать'),
|
||||||
|
('ru', 'clients.sent', 'Отправлено'),
|
||||||
|
('ru', 'clients.server', 'Сервер'),
|
||||||
|
('ru', 'clients.status', 'Статус'),
|
||||||
|
('ru', 'clients.sync_stats', 'Синхронизировать статистику'),
|
||||||
|
('ru', 'clients.title', 'Клиенты'),
|
||||||
|
('ru', 'clients.traffic', 'Трафик'),
|
||||||
|
('ru', 'common.days', 'дней'),
|
||||||
|
('ru', 'dashboard.active_clients', 'Активные клиенты'),
|
||||||
|
('ru', 'dashboard.add_first_server', 'Добавить первый сервер'),
|
||||||
|
('ru', 'dashboard.get_started', 'Начните с добавления вашего первого VPN-сервера'),
|
||||||
|
('ru', 'dashboard.no_servers', 'Пока нет серверов'),
|
||||||
|
('ru', 'dashboard.quick_actions', 'Быстрые действия'),
|
||||||
|
('ru', 'dashboard.recent_servers', 'Недавние серверы'),
|
||||||
|
('ru', 'dashboard.title', 'Панель управления'),
|
||||||
|
('ru', 'dashboard.total_clients', 'Всего клиентов'),
|
||||||
|
('ru', 'dashboard.total_servers', 'Всего серверов'),
|
||||||
|
('ru', 'dashboard.total_traffic', 'Общий трафик'),
|
||||||
|
('ru', 'dashboard.welcome', 'Добро пожаловать в панель управления Amnezia VPN'),
|
||||||
|
('ru', 'form.cancel', 'Отмена'),
|
||||||
|
('ru', 'form.close', 'Закрыть'),
|
||||||
|
('ru', 'form.create', 'Создать'),
|
||||||
|
('ru', 'form.loading', 'Загрузка...'),
|
||||||
|
('ru', 'form.processing', 'Обработка...'),
|
||||||
|
('ru', 'form.save', 'Сохранить'),
|
||||||
|
('ru', 'form.submit', 'Отправить'),
|
||||||
|
('ru', 'form.update', 'Обновить'),
|
||||||
|
('ru', 'menu.clients', 'Клиенты'),
|
||||||
|
('ru', 'menu.dashboard', 'Панель управления'),
|
||||||
|
('ru', 'menu.logout', 'Выход'),
|
||||||
|
('ru', 'menu.servers', 'Серверы'),
|
||||||
|
('ru', 'menu.settings', 'Настройки'),
|
||||||
|
('ru', 'menu.users', 'Пользователи'),
|
||||||
|
('ru', 'message.confirm', 'Вы уверены?'),
|
||||||
|
('ru', 'message.deleted', 'Успешно удалено'),
|
||||||
|
('ru', 'message.deployed', 'Успешно развернуто'),
|
||||||
|
('ru', 'message.error', 'Произошла ошибка'),
|
||||||
|
('ru', 'message.saved', 'Успешно сохранено'),
|
||||||
|
('ru', 'message.success', 'Операция успешно завершена'),
|
||||||
|
('ru', 'servers.actions', 'Действия'),
|
||||||
|
('ru', 'servers.add', 'Добавить сервер'),
|
||||||
|
('ru', 'servers.clients', 'Клиенты'),
|
||||||
|
('ru', 'servers.delete', 'Удалить'),
|
||||||
|
('ru', 'servers.deploy', 'Развернуть'),
|
||||||
|
('ru', 'servers.edit', 'Редактировать'),
|
||||||
|
('ru', 'servers.host', 'Хост'),
|
||||||
|
('ru', 'servers.name', 'Имя'),
|
||||||
|
('ru', 'servers.port', 'Порт'),
|
||||||
|
('ru', 'servers.status', 'Статус'),
|
||||||
|
('ru', 'servers.title', 'Серверы'),
|
||||||
|
('ru', 'servers.view', 'Просмотр'),
|
||||||
|
('ru', 'settings.actions', 'Действия'),
|
||||||
|
('ru', 'settings.api_key_configured', 'API-ключ настроен'),
|
||||||
|
('ru', 'settings.api_keys', 'API-ключи'),
|
||||||
|
('ru', 'settings.api_keys_desc', 'Настройка API-ключей для внешних сервисов'),
|
||||||
|
('ru', 'settings.auto_translate', 'Автоперевод'),
|
||||||
|
('ru', 'settings.change_password', 'Изменить пароль'),
|
||||||
|
('ru', 'settings.confirm_password', 'Подтвердите пароль'),
|
||||||
|
('ru', 'settings.confirm_translate', 'Начать автоматический перевод? Это может занять несколько минут.'),
|
||||||
|
('ru', 'settings.current_password', 'Текущий пароль'),
|
||||||
|
('ru', 'settings.description', 'Управление конфигурацией панели и интеграциями API'),
|
||||||
|
('ru', 'settings.error_empty_key', 'API-ключ не может быть пустым'),
|
||||||
|
('ru', 'settings.error_invalid_key', 'Неверный формат API-ключа'),
|
||||||
|
('ru', 'settings.error_key_test', 'Тест API-ключа не удался'),
|
||||||
|
('ru', 'settings.for_translation', 'для автоперевода'),
|
||||||
|
('ru', 'settings.get_key_at', 'Получите ваш API-ключ на'),
|
||||||
|
('ru', 'settings.key_saved', 'API-ключ успешно сохранен'),
|
||||||
|
('ru', 'settings.keys', 'ключи'),
|
||||||
|
('ru', 'settings.language', 'Язык'),
|
||||||
|
('ru', 'settings.min_6_chars', 'Минимум 6 символов'),
|
||||||
|
('ru', 'settings.new_password', 'Новый пароль'),
|
||||||
|
('ru', 'settings.no_api_key', 'API-ключ не настроен. Автоперевод не будет работать.'),
|
||||||
|
('ru', 'settings.profile', 'Профиль'),
|
||||||
|
('ru', 'settings.progress', 'Прогресс'),
|
||||||
|
('ru', 'settings.skip_validation', 'Пропустить проверку (сохранить без тестирования)'),
|
||||||
|
('ru', 'settings.translation_complete', 'Перевод завершен'),
|
||||||
|
('ru', 'settings.translation_status', 'Статус перевода'),
|
||||||
|
('ru', 'settings.translations', 'Переводы'),
|
||||||
|
('ru', 'settings.users', 'Пользователи'),
|
||||||
|
('ru', 'status.active', 'Активен'),
|
||||||
|
('ru', 'status.deploying', 'Развертывание'),
|
||||||
|
('ru', 'status.disabled', 'Отключен'),
|
||||||
|
('ru', 'status.error', 'Ошибка'),
|
||||||
|
('ru', 'status.inactive', 'Неактивен'),
|
||||||
|
('ru', 'users.add_user', 'Добавить пользователя'),
|
||||||
|
('ru', 'users.administrator', 'Администратор'),
|
||||||
|
('ru', 'users.all_users', 'Все пользователи'),
|
||||||
|
('ru', 'users.created', 'Создан'),
|
||||||
|
('ru', 'users.delete_confirm', 'Удалить {0}?'),
|
||||||
|
('ru', 'users.role', 'Роль'),
|
||||||
|
('ru', 'users.role_admin', 'Администратор'),
|
||||||
|
('ru', 'users.role_user', 'Пользователь')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- French translations
|
||||||
|
-- This migration adds French language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('fr', 'auth.email', 'Email'),
|
||||||
|
('fr', 'auth.login', 'Connexion'),
|
||||||
|
('fr', 'auth.name', 'Nom'),
|
||||||
|
('fr', 'auth.password', 'Mot de passe'),
|
||||||
|
('fr', 'auth.register', 'S''inscrire'),
|
||||||
|
('fr', 'backups.create', 'Créer une sauvegarde'),
|
||||||
|
('fr', 'backups.create_confirm', 'Créer une sauvegarde de tous les clients sur ce serveur ?'),
|
||||||
|
('fr', 'backups.created_success', 'Sauvegarde créée avec succès'),
|
||||||
|
('fr', 'backups.delete_confirm', 'Supprimer définitivement cette sauvegarde ?'),
|
||||||
|
('fr', 'backups.deleted_success', 'Sauvegarde supprimée avec succès'),
|
||||||
|
('fr', 'backups.login_required', 'Veuillez vous connecter via l''API pour gérer les sauvegardes'),
|
||||||
|
('fr', 'backups.no_backups', 'Aucune sauvegarde pour le moment'),
|
||||||
|
('fr', 'backups.restore', 'Restaurer'),
|
||||||
|
('fr', 'backups.restore_confirm', 'Restaurer les clients depuis cette sauvegarde ? Les clients existants ne seront pas affectés.'),
|
||||||
|
('fr', 'backups.restored_success', 'Restauré'),
|
||||||
|
('fr', 'backups.title', 'Sauvegardes du serveur'),
|
||||||
|
('fr', 'clients.actions', 'Actions'),
|
||||||
|
('fr', 'clients.add', 'Ajouter un client'),
|
||||||
|
('fr', 'clients.create', 'Créer un client'),
|
||||||
|
('fr', 'clients.delete', 'Supprimer'),
|
||||||
|
('fr', 'clients.download_config', 'Télécharger la configuration'),
|
||||||
|
('fr', 'clients.expiration', 'Expiration'),
|
||||||
|
('fr', 'clients.expired', 'Expiré'),
|
||||||
|
('fr', 'clients.ip', 'Adresse IP'),
|
||||||
|
('fr', 'clients.last_handshake', 'Dernière connexion'),
|
||||||
|
('fr', 'clients.name', 'Nom du client'),
|
||||||
|
('fr', 'clients.never_expires', 'N''expire jamais'),
|
||||||
|
('fr', 'clients.qr_code', 'Code QR'),
|
||||||
|
('fr', 'clients.received', 'Reçu'),
|
||||||
|
('fr', 'clients.restore', 'Restaurer'),
|
||||||
|
('fr', 'clients.revoke', 'Révoquer'),
|
||||||
|
('fr', 'clients.sent', 'Envoyé'),
|
||||||
|
('fr', 'clients.server', 'Serveur'),
|
||||||
|
('fr', 'clients.status', 'Statut'),
|
||||||
|
('fr', 'clients.sync_stats', 'Synchroniser les statistiques'),
|
||||||
|
('fr', 'clients.title', 'Clients'),
|
||||||
|
('fr', 'clients.traffic', 'Trafic'),
|
||||||
|
('fr', 'common.days', 'jours'),
|
||||||
|
('fr', 'dashboard.active_clients', 'Clients actifs'),
|
||||||
|
('fr', 'dashboard.add_first_server', 'Ajouter le premier serveur'),
|
||||||
|
('fr', 'dashboard.get_started', 'Commencez par ajouter votre premier serveur VPN'),
|
||||||
|
('fr', 'dashboard.no_servers', 'Aucun serveur pour le moment'),
|
||||||
|
('fr', 'dashboard.quick_actions', 'Actions rapides'),
|
||||||
|
('fr', 'dashboard.recent_servers', 'Serveurs récents'),
|
||||||
|
('fr', 'dashboard.title', 'Tableau de bord'),
|
||||||
|
('fr', 'dashboard.total_clients', 'Total des clients'),
|
||||||
|
('fr', 'dashboard.total_servers', 'Total des serveurs'),
|
||||||
|
('fr', 'dashboard.total_traffic', 'Trafic total'),
|
||||||
|
('fr', 'dashboard.welcome', 'Bienvenue sur le panneau de gestion Amnezia VPN'),
|
||||||
|
('fr', 'form.cancel', 'Annuler'),
|
||||||
|
('fr', 'form.close', 'Fermer'),
|
||||||
|
('fr', 'form.create', 'Créer'),
|
||||||
|
('fr', 'form.loading', 'Chargement...'),
|
||||||
|
('fr', 'form.processing', 'Traitement...'),
|
||||||
|
('fr', 'form.save', 'Enregistrer'),
|
||||||
|
('fr', 'form.submit', 'Soumettre'),
|
||||||
|
('fr', 'form.update', 'Mettre à jour'),
|
||||||
|
('fr', 'menu.clients', 'Clients'),
|
||||||
|
('fr', 'menu.dashboard', 'Tableau de bord'),
|
||||||
|
('fr', 'menu.logout', 'Déconnexion'),
|
||||||
|
('fr', 'menu.servers', 'Serveurs'),
|
||||||
|
('fr', 'menu.settings', 'Paramètres'),
|
||||||
|
('fr', 'menu.users', 'Utilisateurs'),
|
||||||
|
('fr', 'message.confirm', 'Êtes-vous sûr ?'),
|
||||||
|
('fr', 'message.deleted', 'Supprimé avec succès'),
|
||||||
|
('fr', 'message.deployed', 'Déployé avec succès'),
|
||||||
|
('fr', 'message.error', 'Une erreur est survenue'),
|
||||||
|
('fr', 'message.saved', 'Enregistré avec succès'),
|
||||||
|
('fr', 'message.success', 'Opération terminée avec succès'),
|
||||||
|
('fr', 'servers.actions', 'Actions'),
|
||||||
|
('fr', 'servers.add', 'Ajouter un serveur'),
|
||||||
|
('fr', 'servers.clients', 'Clients'),
|
||||||
|
('fr', 'servers.delete', 'Supprimer'),
|
||||||
|
('fr', 'servers.deploy', 'Déployer'),
|
||||||
|
('fr', 'servers.edit', 'Modifier'),
|
||||||
|
('fr', 'servers.host', 'Hôte'),
|
||||||
|
('fr', 'servers.name', 'Nom'),
|
||||||
|
('fr', 'servers.port', 'Port'),
|
||||||
|
('fr', 'servers.status', 'Statut'),
|
||||||
|
('fr', 'servers.title', 'Serveurs'),
|
||||||
|
('fr', 'servers.view', 'Voir'),
|
||||||
|
('fr', 'settings.actions', 'Actions'),
|
||||||
|
('fr', 'settings.api_key_configured', 'Clé API configurée'),
|
||||||
|
('fr', 'settings.api_keys', 'Clés API'),
|
||||||
|
('fr', 'settings.api_keys_desc', 'Configurer les clés API pour les services externes'),
|
||||||
|
('fr', 'settings.auto_translate', 'Traduction automatique'),
|
||||||
|
('fr', 'settings.change_password', 'Changer le mot de passe'),
|
||||||
|
('fr', 'settings.confirm_password', 'Confirmer le mot de passe'),
|
||||||
|
('fr', 'settings.confirm_translate', 'Démarrer la traduction automatique ? Cela peut prendre quelques minutes.'),
|
||||||
|
('fr', 'settings.current_password', 'Mot de passe actuel'),
|
||||||
|
('fr', 'settings.description', 'Gérer la configuration du panneau et les intégrations API'),
|
||||||
|
('fr', 'settings.error_empty_key', 'La clé API ne peut pas être vide'),
|
||||||
|
('fr', 'settings.error_invalid_key', 'Format de clé API invalide'),
|
||||||
|
('fr', 'settings.error_key_test', 'Test de la clé API échoué'),
|
||||||
|
('fr', 'settings.for_translation', 'pour la traduction automatique'),
|
||||||
|
('fr', 'settings.get_key_at', 'Obtenez votre clé API sur'),
|
||||||
|
('fr', 'settings.key_saved', 'Clé API enregistrée avec succès'),
|
||||||
|
('fr', 'settings.keys', 'clés'),
|
||||||
|
('fr', 'settings.language', 'Langue'),
|
||||||
|
('fr', 'settings.min_6_chars', 'Minimum 6 caractères'),
|
||||||
|
('fr', 'settings.new_password', 'Nouveau mot de passe'),
|
||||||
|
('fr', 'settings.no_api_key', 'Aucune clé API configurée. La traduction automatique ne fonctionnera pas.'),
|
||||||
|
('fr', 'settings.profile', 'Profil'),
|
||||||
|
('fr', 'settings.progress', 'Progression'),
|
||||||
|
('fr', 'settings.skip_validation', 'Ignorer la validation (enregistrer sans tester)'),
|
||||||
|
('fr', 'settings.translation_complete', 'Traduction terminée'),
|
||||||
|
('fr', 'settings.translation_status', 'État de la traduction'),
|
||||||
|
('fr', 'settings.translations', 'Traductions'),
|
||||||
|
('fr', 'settings.users', 'Utilisateurs'),
|
||||||
|
('fr', 'status.active', 'Actif'),
|
||||||
|
('fr', 'status.deploying', 'Déploiement'),
|
||||||
|
('fr', 'status.disabled', 'Désactivé'),
|
||||||
|
('fr', 'status.error', 'Erreur'),
|
||||||
|
('fr', 'status.inactive', 'Inactif'),
|
||||||
|
('fr', 'users.add_user', 'Ajouter un utilisateur'),
|
||||||
|
('fr', 'users.administrator', 'Administrateur'),
|
||||||
|
('fr', 'users.all_users', 'Tous les utilisateurs'),
|
||||||
|
('fr', 'users.created', 'Créé'),
|
||||||
|
('fr', 'users.delete_confirm', 'Supprimer {0} ?'),
|
||||||
|
('fr', 'users.role', 'Rôle'),
|
||||||
|
('fr', 'users.role_admin', 'Administrateur'),
|
||||||
|
('fr', 'users.role_user', 'Utilisateur')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- French translations
|
||||||
|
-- This migration adds French language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('fr', 'auth.email', 'Email'),
|
||||||
|
('fr', 'auth.login', 'Connexion'),
|
||||||
|
('fr', 'auth.name', 'Nom'),
|
||||||
|
('fr', 'auth.password', 'Mot de passe'),
|
||||||
|
('fr', 'auth.register', 'S''inscrire'),
|
||||||
|
('fr', 'backups.create', 'Créer une sauvegarde'),
|
||||||
|
('fr', 'backups.create_confirm', 'Créer une sauvegarde de tous les clients sur ce serveur ?'),
|
||||||
|
('fr', 'backups.created_success', 'Sauvegarde créée avec succès'),
|
||||||
|
('fr', 'backups.delete_confirm', 'Supprimer définitivement cette sauvegarde ?'),
|
||||||
|
('fr', 'backups.deleted_success', 'Sauvegarde supprimée avec succès'),
|
||||||
|
('fr', 'backups.login_required', 'Veuillez vous connecter via l''API pour gérer les sauvegardes'),
|
||||||
|
('fr', 'backups.no_backups', 'Aucune sauvegarde pour le moment'),
|
||||||
|
('fr', 'backups.restore', 'Restaurer'),
|
||||||
|
('fr', 'backups.restore_confirm', 'Restaurer les clients depuis cette sauvegarde ? Les clients existants ne seront pas affectés.'),
|
||||||
|
('fr', 'backups.restored_success', 'Restauré'),
|
||||||
|
('fr', 'backups.title', 'Sauvegardes du serveur'),
|
||||||
|
('fr', 'clients.actions', 'Actions'),
|
||||||
|
('fr', 'clients.add', 'Ajouter un client'),
|
||||||
|
('fr', 'clients.create', 'Créer un client'),
|
||||||
|
('fr', 'clients.delete', 'Supprimer'),
|
||||||
|
('fr', 'clients.delete_confirm', 'Supprimer ce client définitivement?'),
|
||||||
|
('fr', 'clients.download_config', 'Télécharger la configuration'),
|
||||||
|
('fr', 'clients.expiration', 'Expiration'),
|
||||||
|
('fr', 'clients.expired', 'Expiré'),
|
||||||
|
('fr', 'clients.ip', 'Adresse IP'),
|
||||||
|
('fr', 'clients.last_handshake', 'Dernière connexion'),
|
||||||
|
('fr', 'clients.name', 'Nom du client'),
|
||||||
|
('fr', 'clients.never', 'Jamais'),
|
||||||
|
('fr', 'clients.never_expires', 'N''expire jamais'),
|
||||||
|
('fr', 'clients.no_clients', 'Pas encore de clients'),
|
||||||
|
('fr', 'clients.qr_code', 'Code QR'),
|
||||||
|
('fr', 'clients.received', 'Reçu'),
|
||||||
|
('fr', 'clients.restore', 'Restaurer'),
|
||||||
|
('fr', 'clients.revoke', 'Révoquer'),
|
||||||
|
('fr', 'clients.revoke_confirm', 'Révoquer l''accès pour ce client?'),
|
||||||
|
('fr', 'clients.sent', 'Envoyé'),
|
||||||
|
('fr', 'clients.server', 'Serveur'),
|
||||||
|
('fr', 'clients.status', 'Statut'),
|
||||||
|
('fr', 'clients.sync_stats', 'Synchroniser les statistiques'),
|
||||||
|
('fr', 'clients.title', 'Clients'),
|
||||||
|
('fr', 'clients.traffic', 'Trafic'),
|
||||||
|
('fr', 'common.days', 'jours'),
|
||||||
|
('fr', 'dashboard.active_clients', 'Clients actifs'),
|
||||||
|
('fr', 'dashboard.add_first_server', 'Ajouter le premier serveur'),
|
||||||
|
('fr', 'dashboard.get_started', 'Commencez par ajouter votre premier serveur VPN'),
|
||||||
|
('fr', 'dashboard.no_servers', 'Aucun serveur pour le moment'),
|
||||||
|
('fr', 'dashboard.quick_actions', 'Actions rapides'),
|
||||||
|
('fr', 'dashboard.recent_servers', 'Serveurs récents'),
|
||||||
|
('fr', 'dashboard.title', 'Tableau de bord'),
|
||||||
|
('fr', 'dashboard.total_clients', 'Total des clients'),
|
||||||
|
('fr', 'dashboard.total_servers', 'Total des serveurs'),
|
||||||
|
('fr', 'dashboard.total_traffic', 'Trafic total'),
|
||||||
|
('fr', 'dashboard.welcome', 'Bienvenue sur le panneau de gestion Amnezia VPN'),
|
||||||
|
('fr', 'form.cancel', 'Annuler'),
|
||||||
|
('fr', 'form.close', 'Fermer'),
|
||||||
|
('fr', 'form.create', 'Créer'),
|
||||||
|
('fr', 'form.loading', 'Chargement...'),
|
||||||
|
('fr', 'form.processing', 'Traitement...'),
|
||||||
|
('fr', 'form.save', 'Enregistrer'),
|
||||||
|
('fr', 'form.submit', 'Soumettre'),
|
||||||
|
('fr', 'form.update', 'Mettre à jour'),
|
||||||
|
('fr', 'menu.clients', 'Clients'),
|
||||||
|
('fr', 'menu.dashboard', 'Tableau de bord'),
|
||||||
|
('fr', 'menu.logout', 'Déconnexion'),
|
||||||
|
('fr', 'menu.servers', 'Serveurs'),
|
||||||
|
('fr', 'menu.settings', 'Paramètres'),
|
||||||
|
('fr', 'menu.users', 'Utilisateurs'),
|
||||||
|
('fr', 'message.confirm', 'Êtes-vous sûr ?'),
|
||||||
|
('fr', 'message.deleted', 'Supprimé avec succès'),
|
||||||
|
('fr', 'message.deployed', 'Déployé avec succès'),
|
||||||
|
('fr', 'message.error', 'Une erreur est survenue'),
|
||||||
|
('fr', 'message.saved', 'Enregistré avec succès'),
|
||||||
|
('fr', 'message.success', 'Opération terminée avec succès'),
|
||||||
|
('fr', 'servers.actions', 'Actions'),
|
||||||
|
('fr', 'servers.add', 'Ajouter un serveur'),
|
||||||
|
('fr', 'servers.clients', 'Clients'),
|
||||||
|
('fr', 'servers.delete', 'Supprimer'),
|
||||||
|
('fr', 'servers.deploy', 'Déployer'),
|
||||||
|
('fr', 'servers.edit', 'Modifier'),
|
||||||
|
('fr', 'servers.host', 'Hôte'),
|
||||||
|
('fr', 'servers.name', 'Nom'),
|
||||||
|
('fr', 'servers.port', 'Port'),
|
||||||
|
('fr', 'servers.status', 'Statut'),
|
||||||
|
('fr', 'servers.title', 'Serveurs'),
|
||||||
|
('fr', 'servers.view', 'Voir'),
|
||||||
|
('fr', 'settings.actions', 'Actions'),
|
||||||
|
('fr', 'settings.api_key_configured', 'Clé API configurée'),
|
||||||
|
('fr', 'settings.api_keys', 'Clés API'),
|
||||||
|
('fr', 'settings.api_keys_desc', 'Configurer les clés API pour les services externes'),
|
||||||
|
('fr', 'settings.auto_translate', 'Traduction automatique'),
|
||||||
|
('fr', 'settings.change_password', 'Changer le mot de passe'),
|
||||||
|
('fr', 'settings.confirm_password', 'Confirmer le mot de passe'),
|
||||||
|
('fr', 'settings.confirm_translate', 'Démarrer la traduction automatique ? Cela peut prendre quelques minutes.'),
|
||||||
|
('fr', 'settings.current_password', 'Mot de passe actuel'),
|
||||||
|
('fr', 'settings.description', 'Gérer la configuration du panneau et les intégrations API'),
|
||||||
|
('fr', 'settings.error_empty_key', 'La clé API ne peut pas être vide'),
|
||||||
|
('fr', 'settings.error_invalid_key', 'Format de clé API invalide'),
|
||||||
|
('fr', 'settings.error_key_test', 'Test de la clé API échoué'),
|
||||||
|
('fr', 'settings.for_translation', 'pour la traduction automatique'),
|
||||||
|
('fr', 'settings.get_key_at', 'Obtenez votre clé API sur'),
|
||||||
|
('fr', 'settings.key_saved', 'Clé API enregistrée avec succès'),
|
||||||
|
('fr', 'settings.keys', 'clés'),
|
||||||
|
('fr', 'settings.language', 'Langue'),
|
||||||
|
('fr', 'settings.min_6_chars', 'Minimum 6 caractères'),
|
||||||
|
('fr', 'settings.new_password', 'Nouveau mot de passe'),
|
||||||
|
('fr', 'settings.no_api_key', 'Aucune clé API configurée. La traduction automatique ne fonctionnera pas.'),
|
||||||
|
('fr', 'settings.profile', 'Profil'),
|
||||||
|
('fr', 'settings.progress', 'Progression'),
|
||||||
|
('fr', 'settings.skip_validation', 'Ignorer la validation (enregistrer sans tester)'),
|
||||||
|
('fr', 'settings.translation_complete', 'Traduction terminée'),
|
||||||
|
('fr', 'settings.translation_status', 'État de la traduction'),
|
||||||
|
('fr', 'settings.translations', 'Traductions'),
|
||||||
|
('fr', 'settings.users', 'Utilisateurs'),
|
||||||
|
('fr', 'status.active', 'Actif'),
|
||||||
|
('fr', 'status.deploying', 'Déploiement'),
|
||||||
|
('fr', 'status.disabled', 'Désactivé'),
|
||||||
|
('fr', 'status.error', 'Erreur'),
|
||||||
|
('fr', 'status.inactive', 'Inactif'),
|
||||||
|
('fr', 'users.add_user', 'Ajouter un utilisateur'),
|
||||||
|
('fr', 'users.administrator', 'Administrateur'),
|
||||||
|
('fr', 'users.all_users', 'Tous les utilisateurs'),
|
||||||
|
('fr', 'users.created', 'Créé'),
|
||||||
|
('fr', 'users.delete_confirm', 'Supprimer {0} ?'),
|
||||||
|
('fr', 'users.role', 'Rôle'),
|
||||||
|
('fr', 'users.role_admin', 'Administrateur'),
|
||||||
|
('fr', 'users.role_user', 'Utilisateur')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- Chinese translations
|
||||||
|
-- This migration adds Chinese language translations
|
||||||
|
|
||||||
|
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
||||||
|
('zh', 'auth.email', '邮箱'),
|
||||||
|
('zh', 'auth.login', '登录'),
|
||||||
|
('zh', 'auth.name', '姓名'),
|
||||||
|
('zh', 'auth.password', '密码'),
|
||||||
|
('zh', 'auth.register', '注册'),
|
||||||
|
('zh', 'backups.create', '创建备份'),
|
||||||
|
('zh', 'backups.create_confirm', '创建此服务器上所有客户端的备份?'),
|
||||||
|
('zh', 'backups.created_success', '备份创建成功'),
|
||||||
|
('zh', 'backups.delete_confirm', '永久删除此备份?'),
|
||||||
|
('zh', 'backups.deleted_success', '备份删除成功'),
|
||||||
|
('zh', 'backups.login_required', '请通过API登录以管理备份'),
|
||||||
|
('zh', 'backups.no_backups', '暂无备份'),
|
||||||
|
('zh', 'backups.restore', '恢复'),
|
||||||
|
('zh', 'backups.restore_confirm', '从此备份恢复客户端?现有客户端不会受到影响。'),
|
||||||
|
('zh', 'backups.restored_success', '已恢复'),
|
||||||
|
('zh', 'backups.title', '服务器备份'),
|
||||||
|
('zh', 'clients.actions', '操作'),
|
||||||
|
('zh', 'clients.add', '添加客户端'),
|
||||||
|
('zh', 'clients.create', '创建客户端'),
|
||||||
|
('zh', 'clients.delete', '删除'),
|
||||||
|
('zh', 'clients.delete_confirm', '永久删除此客户端?'),
|
||||||
|
('zh', 'clients.download_config', '下载配置'),
|
||||||
|
('zh', 'clients.expiration', '过期时间'),
|
||||||
|
('zh', 'clients.expired', '已过期'),
|
||||||
|
('zh', 'clients.ip', 'IP地址'),
|
||||||
|
('zh', 'clients.last_handshake', '最后握手'),
|
||||||
|
('zh', 'clients.name', '客户端名称'),
|
||||||
|
('zh', 'clients.never', '从不'),
|
||||||
|
('zh', 'clients.never_expires', '永不过期'),
|
||||||
|
('zh', 'clients.no_clients', '还没有客户'),
|
||||||
|
('zh', 'clients.qr_code', '二维码'),
|
||||||
|
('zh', 'clients.received', '已接收'),
|
||||||
|
('zh', 'clients.restore', '恢复'),
|
||||||
|
('zh', 'clients.revoke', '撤销'),
|
||||||
|
('zh', 'clients.revoke_confirm', '撤销此客户端的访问权限?'),
|
||||||
|
('zh', 'clients.sent', '已发送'),
|
||||||
|
('zh', 'clients.server', '服务器'),
|
||||||
|
('zh', 'clients.status', '状态'),
|
||||||
|
('zh', 'clients.sync_stats', '同步统计'),
|
||||||
|
('zh', 'clients.title', '客户端'),
|
||||||
|
('zh', 'clients.traffic', '流量'),
|
||||||
|
('zh', 'common.days', '天'),
|
||||||
|
('zh', 'dashboard.active_clients', '活跃客户端'),
|
||||||
|
('zh', 'dashboard.add_first_server', '添加第一个服务器'),
|
||||||
|
('zh', 'dashboard.get_started', '从添加第一个VPN服务器开始'),
|
||||||
|
('zh', 'dashboard.no_servers', '暂无服务器'),
|
||||||
|
('zh', 'dashboard.quick_actions', '快捷操作'),
|
||||||
|
('zh', 'dashboard.recent_servers', '最近的服务器'),
|
||||||
|
('zh', 'dashboard.title', '仪表板'),
|
||||||
|
('zh', 'dashboard.total_clients', '客户端总数'),
|
||||||
|
('zh', 'dashboard.total_servers', '服务器总数'),
|
||||||
|
('zh', 'dashboard.total_traffic', '总流量'),
|
||||||
|
('zh', 'dashboard.welcome', '欢迎使用Amnezia VPN管理面板'),
|
||||||
|
('zh', 'form.cancel', '取消'),
|
||||||
|
('zh', 'form.close', '关闭'),
|
||||||
|
('zh', 'form.create', '创建'),
|
||||||
|
('zh', 'form.loading', '加载中...'),
|
||||||
|
('zh', 'form.processing', '处理中...'),
|
||||||
|
('zh', 'form.save', '保存'),
|
||||||
|
('zh', 'form.submit', '提交'),
|
||||||
|
('zh', 'form.update', '更新'),
|
||||||
|
('zh', 'menu.clients', '客户端'),
|
||||||
|
('zh', 'menu.dashboard', '仪表板'),
|
||||||
|
('zh', 'menu.logout', '退出'),
|
||||||
|
('zh', 'menu.servers', '服务器'),
|
||||||
|
('zh', 'menu.settings', '设置'),
|
||||||
|
('zh', 'menu.users', '用户'),
|
||||||
|
('zh', 'message.confirm', '确定吗?'),
|
||||||
|
('zh', 'message.deleted', '删除成功'),
|
||||||
|
('zh', 'message.deployed', '部署成功'),
|
||||||
|
('zh', 'message.error', '发生错误'),
|
||||||
|
('zh', 'message.saved', '保存成功'),
|
||||||
|
('zh', 'message.success', '操作完成'),
|
||||||
|
('zh', 'servers.actions', '操作'),
|
||||||
|
('zh', 'servers.add', '添加服务器'),
|
||||||
|
('zh', 'servers.clients', '客户端'),
|
||||||
|
('zh', 'servers.delete', '删除'),
|
||||||
|
('zh', 'servers.deploy', '部署'),
|
||||||
|
('zh', 'servers.edit', '编辑'),
|
||||||
|
('zh', 'servers.host', '主机'),
|
||||||
|
('zh', 'servers.name', '名称'),
|
||||||
|
('zh', 'servers.port', '端口'),
|
||||||
|
('zh', 'servers.status', '状态'),
|
||||||
|
('zh', 'servers.title', '服务器'),
|
||||||
|
('zh', 'servers.view', '查看'),
|
||||||
|
('zh', 'settings.actions', '操作'),
|
||||||
|
('zh', 'settings.api_key_configured', 'API密钥已配置'),
|
||||||
|
('zh', 'settings.api_keys', 'API密钥'),
|
||||||
|
('zh', 'settings.api_keys_desc', '配置外部服务的API密钥'),
|
||||||
|
('zh', 'settings.auto_translate', '自动翻译'),
|
||||||
|
('zh', 'settings.change_password', '修改密码'),
|
||||||
|
('zh', 'settings.confirm_password', '确认密码'),
|
||||||
|
('zh', 'settings.confirm_translate', '开始自动翻译?这可能需要几分钟。'),
|
||||||
|
('zh', 'settings.current_password', '当前密码'),
|
||||||
|
('zh', 'settings.description', '管理面板配置和API集成'),
|
||||||
|
('zh', 'settings.error_empty_key', 'API密钥不能为空'),
|
||||||
|
('zh', 'settings.error_invalid_key', '无效的API密钥格式'),
|
||||||
|
('zh', 'settings.error_key_test', 'API密钥测试失败'),
|
||||||
|
('zh', 'settings.for_translation', '用于自动翻译'),
|
||||||
|
('zh', 'settings.get_key_at', '在此获取API密钥'),
|
||||||
|
('zh', 'settings.key_saved', 'API密钥保存成功'),
|
||||||
|
('zh', 'settings.keys', '密钥'),
|
||||||
|
('zh', 'settings.language', '语言'),
|
||||||
|
('zh', 'settings.min_6_chars', '最少6个字符'),
|
||||||
|
('zh', 'settings.new_password', '新密码'),
|
||||||
|
('zh', 'settings.no_api_key', '未配置API密钥。自动翻译将无法工作。'),
|
||||||
|
('zh', 'settings.profile', '个人资料'),
|
||||||
|
('zh', 'settings.progress', '进度'),
|
||||||
|
('zh', 'settings.skip_validation', '跳过验证(不测试直接保存)'),
|
||||||
|
('zh', 'settings.translation_complete', '翻译完成'),
|
||||||
|
('zh', 'settings.translation_status', '翻译状态'),
|
||||||
|
('zh', 'settings.translations', '翻译'),
|
||||||
|
('zh', 'settings.users', '用户'),
|
||||||
|
('zh', 'status.active', '活跃'),
|
||||||
|
('zh', 'status.deploying', '部署中'),
|
||||||
|
('zh', 'status.disabled', '已禁用'),
|
||||||
|
('zh', 'status.error', '错误'),
|
||||||
|
('zh', 'status.inactive', '不活跃'),
|
||||||
|
('zh', 'users.add_user', '添加用户'),
|
||||||
|
('zh', 'users.administrator', '管理员'),
|
||||||
|
('zh', 'users.all_users', '所有用户'),
|
||||||
|
('zh', 'users.created', '已创建'),
|
||||||
|
('zh', 'users.delete_confirm', '删除 {0}?'),
|
||||||
|
('zh', 'users.role', '角色'),
|
||||||
|
('zh', 'users.role_admin', '管理员'),
|
||||||
|
('zh', 'users.role_user', '用户')
|
||||||
|
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Database Migrations
|
||||||
|
|
||||||
|
This directory contains SQL migration files that are automatically executed when the database container is first initialized.
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
Migration files are executed in **alphabetical order** by MySQL's Docker entrypoint. Files are numbered to ensure correct execution sequence:
|
||||||
|
|
||||||
|
1. `001_init.sql` - Main database schema and tables
|
||||||
|
2. `002_translations_ru.sql` - Russian translations
|
||||||
|
3. `003_translations_es.sql` - Spanish translations
|
||||||
|
4. `004_translations_de.sql` - German translations
|
||||||
|
5. `005_translations_fr.sql` - French translations
|
||||||
|
6. `006_translations_zh.sql` - Chinese translations
|
||||||
|
|
||||||
|
## Adding New Migrations
|
||||||
|
|
||||||
|
When creating new migration files:
|
||||||
|
|
||||||
|
1. Use numerical prefix (e.g., `007_add_feature.sql`)
|
||||||
|
2. Ensure the number is higher than existing migrations
|
||||||
|
3. Use descriptive names
|
||||||
|
4. Always use `ON DUPLICATE KEY UPDATE` for INSERT statements to make migrations idempotent
|
||||||
|
|
||||||
|
## Manual Execution
|
||||||
|
|
||||||
|
To manually run migrations in an existing database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Single migration
|
||||||
|
docker compose exec db mysql -uroot -prootpassword amnezia_panel < migrations/001_init.sql
|
||||||
|
|
||||||
|
# All migrations in order
|
||||||
|
for file in migrations/*.sql; do
|
||||||
|
echo "Executing $file..."
|
||||||
|
docker compose exec -T db mysql -uroot -prootpassword amnezia_panel < "$file"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regenerating Translation Migrations
|
||||||
|
|
||||||
|
To regenerate translation migrations from the current database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export translations for a specific language
|
||||||
|
docker compose exec -T db mysql -uroot -prootpassword amnezia_panel \
|
||||||
|
--default-character-set=utf8mb4 \
|
||||||
|
-e "SELECT CONCAT('(''', language_code, ''', ''', translation_key, ''', ''',
|
||||||
|
REPLACE(translation_value, '''', ''''''), '''),')
|
||||||
|
FROM translations WHERE language_code = 'ru' ORDER BY translation_key;" \
|
||||||
|
| grep -v "CONCAT" > /tmp/translations_ru.sql
|
||||||
|
|
||||||
|
# Then wrap with INSERT statement and ON DUPLICATE KEY UPDATE
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[client]
|
||||||
|
default-character-set = utf8mb4
|
||||||
|
|
||||||
|
[mysql]
|
||||||
|
default-character-set = utf8mb4
|
||||||
|
|
||||||
|
[mysqld]
|
||||||
|
character-set-server = utf8mb4
|
||||||
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
init_connect = 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
|
||||||
|
init_connect = 'SET collation_connection = utf8mb4_unicode_ci'
|
||||||
|
skip-character-set-client-handshake
|
||||||
+304
-3
@@ -81,6 +81,39 @@ function requireAdmin(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get authenticated user (JWT or session)
|
||||||
|
function getAuthUser(): ?array {
|
||||||
|
// Try JWT first
|
||||||
|
$token = JWT::getTokenFromHeader();
|
||||||
|
if ($token !== null) {
|
||||||
|
$user = JWT::verify($token);
|
||||||
|
if ($user !== null) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to session
|
||||||
|
if (Auth::check()) {
|
||||||
|
return Auth::user();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to require authentication (JWT or session) for API
|
||||||
|
function requireApiAuth(): ?array {
|
||||||
|
$user = getAuthUser();
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
http_response_code(401);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => 'Authentication required']);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUBLIC ROUTES
|
* PUBLIC ROUTES
|
||||||
*/
|
*/
|
||||||
@@ -327,8 +360,9 @@ Router::get('/servers/{id}', function ($params) {
|
|||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
]);
|
]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo 'Server not found';
|
echo 'Server not found: ' . htmlspecialchars($e->getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -361,6 +395,7 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
|||||||
requireAuth();
|
requireAuth();
|
||||||
$serverId = (int)$params['id'];
|
$serverId = (int)$params['id'];
|
||||||
$clientName = trim($_POST['name'] ?? '');
|
$clientName = trim($_POST['name'] ?? '');
|
||||||
|
$expiresInDays = !empty($_POST['expires_in_days']) ? (int)$_POST['expires_in_days'] : null;
|
||||||
|
|
||||||
if (empty($clientName)) {
|
if (empty($clientName)) {
|
||||||
redirect('/servers/' . $serverId . '?error=Client+name+is+required');
|
redirect('/servers/' . $serverId . '?error=Client+name+is+required');
|
||||||
@@ -379,7 +414,7 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientId = VpnClient::create($serverId, $user['id'], $clientName);
|
$clientId = VpnClient::create($serverId, $user['id'], $clientName, $expiresInDays);
|
||||||
redirect('/clients/' . $clientId);
|
redirect('/clients/' . $clientId);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
redirect('/servers/' . $serverId . '?error=' . urlencode($e->getMessage()));
|
redirect('/servers/' . $serverId . '?error=' . urlencode($e->getMessage()));
|
||||||
@@ -768,6 +803,157 @@ Router::delete('/api/servers/{id}/delete', function ($params) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Create backup
|
||||||
|
Router::post('/api/servers/{id}/backup', function ($params) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = requireApiAuth();
|
||||||
|
if (!$user) return;
|
||||||
|
|
||||||
|
$serverId = (int)$params['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$server = new VpnServer($serverId);
|
||||||
|
$serverData = $server->getData();
|
||||||
|
|
||||||
|
// Check ownership or admin
|
||||||
|
if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupId = $server->createBackup($user['id'], 'manual');
|
||||||
|
$backup = VpnServer::getBackup($backupId);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'backup' => $backup
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: List backups
|
||||||
|
Router::get('/api/servers/{id}/backups', function ($params) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = requireApiAuth();
|
||||||
|
if (!$user) return;
|
||||||
|
|
||||||
|
$serverId = (int)$params['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$server = new VpnServer($serverId);
|
||||||
|
$serverData = $server->getData();
|
||||||
|
|
||||||
|
// Check ownership or admin
|
||||||
|
if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$backups = $server->listBackups();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'backups' => $backups,
|
||||||
|
'count' => count($backups)
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: Restore backup
|
||||||
|
Router::post('/api/servers/{id}/restore', function ($params) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = requireApiAuth();
|
||||||
|
if (!$user) return;
|
||||||
|
|
||||||
|
$serverId = (int)$params['id'];
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
|
||||||
|
$backupId = (int)($data['backup_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($backupId <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'backup_id is required']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$server = new VpnServer($serverId);
|
||||||
|
$serverData = $server->getData();
|
||||||
|
|
||||||
|
// Check ownership or admin
|
||||||
|
if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $server->restoreBackup($backupId);
|
||||||
|
|
||||||
|
// Log the result for debugging
|
||||||
|
error_log('Restore backup result: ' . json_encode($result));
|
||||||
|
|
||||||
|
// Always return the result, even if success is false
|
||||||
|
echo json_encode($result);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Restore backup exception: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage(), 'success' => false]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: Delete backup
|
||||||
|
Router::delete('/api/backups/{id}', function ($params) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = requireApiAuth();
|
||||||
|
if (!$user) return;
|
||||||
|
|
||||||
|
$backupId = (int)$params['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$backup = VpnServer::getBackup($backupId);
|
||||||
|
|
||||||
|
if (!$backup) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Backup not found']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get server to check ownership
|
||||||
|
$server = new VpnServer($backup['server_id']);
|
||||||
|
$serverData = $server->getData();
|
||||||
|
|
||||||
|
// Check ownership or admin
|
||||||
|
if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VpnServer::deleteBackup($backupId);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Backup deleted successfully'
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// API: List clients
|
// API: List clients
|
||||||
Router::get('/api/clients', function () {
|
Router::get('/api/clients', function () {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -988,6 +1174,7 @@ Router::post('/api/clients/create', function () {
|
|||||||
|
|
||||||
$serverId = (int)($data['server_id'] ?? 0);
|
$serverId = (int)($data['server_id'] ?? 0);
|
||||||
$name = trim($data['name'] ?? '');
|
$name = trim($data['name'] ?? '');
|
||||||
|
$expiresInDays = isset($data['expires_in_days']) ? (int)$data['expires_in_days'] : null;
|
||||||
|
|
||||||
if ($serverId <= 0 || empty($name)) {
|
if ($serverId <= 0 || empty($name)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
@@ -996,7 +1183,7 @@ Router::post('/api/clients/create', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$clientId = VpnClient::create($serverId, $user['id'], $name);
|
$clientId = VpnClient::create($serverId, $user['id'], $name, $expiresInDays);
|
||||||
|
|
||||||
$client = new VpnClient($clientId);
|
$client = new VpnClient($clientId);
|
||||||
$clientData = $client->getData();
|
$clientData = $client->getData();
|
||||||
@@ -1010,6 +1197,7 @@ Router::post('/api/clients/create', function () {
|
|||||||
'server_id' => $clientData['server_id'],
|
'server_id' => $clientData['server_id'],
|
||||||
'client_ip' => $clientData['client_ip'],
|
'client_ip' => $clientData['client_ip'],
|
||||||
'status' => $clientData['status'],
|
'status' => $clientData['status'],
|
||||||
|
'expires_at' => $clientData['expires_at'],
|
||||||
'created_at' => $clientData['created_at'],
|
'created_at' => $clientData['created_at'],
|
||||||
'config' => $clientData['config'],
|
'config' => $clientData['config'],
|
||||||
'qr_code' => $clientData['qr_code'],
|
'qr_code' => $clientData['qr_code'],
|
||||||
@@ -1021,6 +1209,119 @@ Router::post('/api/clients/create', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set client expiration
|
||||||
|
Router::post('/api/clients/{id}/set-expiration', 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);
|
||||||
|
|
||||||
|
$expiresAt = $data['expires_at'] ?? null; // Y-m-d H:i:s format or null
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
VpnClient::setExpiration($clientId, $expiresAt);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'expires_at' => $expiresAt
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extend client expiration
|
||||||
|
Router::post('/api/clients/{id}/extend', 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);
|
||||||
|
|
||||||
|
$days = (int)($data['days'] ?? 30);
|
||||||
|
|
||||||
|
if ($days <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'days must be positive']);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
VpnClient::extendExpiration($clientId, $days);
|
||||||
|
|
||||||
|
// Get updated expiration
|
||||||
|
$client = new VpnClient($clientId);
|
||||||
|
$updated = $client->getData();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'expires_at' => $updated['expires_at'],
|
||||||
|
'extended_days' => $days
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get expiring clients
|
||||||
|
Router::get('/api/clients/expiring', function () {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$user = JWT::requireAuth();
|
||||||
|
if (!$user) return;
|
||||||
|
|
||||||
|
$days = (int)($_GET['days'] ?? 7);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clients = VpnClient::getExpiringClients($days);
|
||||||
|
|
||||||
|
// 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
|
* SETTINGS ROUTES
|
||||||
*/
|
*/
|
||||||
|
|||||||
+227
-21
@@ -13,33 +13,61 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded shadow p-6">
|
<div class="bg-white rounded shadow p-6">
|
||||||
<h3 class="font-bold mb-4">Create Client</h3>
|
<h3 class="font-bold mb-4">{{ t('clients.create') }}</h3>
|
||||||
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="flex gap-2" id="createClientForm">
|
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="space-y-3" id="createClientForm">
|
||||||
<input name="name" placeholder="Client name" required class="flex-1 px-3 py-2 border rounded" id="clientName">
|
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
|
||||||
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded" id="createClientBtn">
|
<div>
|
||||||
<span id="createClientText">Create</span>
|
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
||||||
|
<select name="expires_in_days" class="w-full px-3 py-2 border rounded">
|
||||||
|
<option value="" selected>{{ t('clients.never_expires') }}</option>
|
||||||
|
<option value="7">7 {{ t('common.days') }}</option>
|
||||||
|
<option value="30">30 {{ t('common.days') }}</option>
|
||||||
|
<option value="60">60 {{ t('common.days') }}</option>
|
||||||
|
<option value="90">90 {{ t('common.days') }}</option>
|
||||||
|
<option value="180">180 {{ t('common.days') }}</option>
|
||||||
|
<option value="365">365 {{ t('common.days') }}</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>
|
<i class="fas fa-spinner fa-spin" id="createClientSpinner" style="display:none;"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Backup Section -->
|
||||||
|
<div class="bg-white rounded shadow mb-8">
|
||||||
|
<div class="px-6 py-4 border-b flex justify-between items-center">
|
||||||
|
<h3 class="font-bold">{{ t('backups.title') }}</h3>
|
||||||
|
<button onclick="createBackup({{ server.id }})" class="gradient-bg text-white px-4 py-2 rounded text-sm">
|
||||||
|
<i class="fas fa-save"></i> {{ t('backups.create') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="backupsList" class="p-4">
|
||||||
|
<div class="text-center text-gray-500 py-4">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> {{ t('form.loading') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="bg-white rounded shadow">
|
<div class="bg-white rounded shadow">
|
||||||
<div class="px-6 py-4 border-b flex justify-between items-center">
|
<div class="px-6 py-4 border-b flex justify-between items-center">
|
||||||
<h3 class="font-bold">Clients ({{ clients|length }})</h3>
|
<h3 class="font-bold">{{ t('clients.title') }} ({{ clients|length }})</h3>
|
||||||
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
||||||
<i class="fas fa-sync-alt"></i> Sync Stats
|
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if clients|length > 0 %}
|
{% if clients|length > 0 %}
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.name') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.ip') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
<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">Traffic</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">Last Seen</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">Actions</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -49,9 +77,30 @@
|
|||||||
<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.status == 'active' %}
|
||||||
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">Active</span>
|
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">Disabled</span>
|
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm">
|
||||||
|
{% if client.expires_at %}
|
||||||
|
{% set expires_ts = client.expires_at|date('U') %}
|
||||||
|
{% set now_ts = "now"|date('U') %}
|
||||||
|
{% set diff_days = ((expires_ts - now_ts) / 86400)|round %}
|
||||||
|
|
||||||
|
{% if diff_days < 0 %}
|
||||||
|
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> {{ t('clients.expired') }}
|
||||||
|
</span>
|
||||||
|
{% elseif diff_days <= 7 %}
|
||||||
|
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
|
||||||
|
<i class="fas fa-clock"></i> {{ diff_days }} {{ t('common.days') }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-600">{{ client.expires_at|date('Y-m-d') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-400">{{ t('clients.never_expires') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm">
|
<td class="px-6 py-4 text-sm">
|
||||||
@@ -66,22 +115,22 @@
|
|||||||
{% if client.last_handshake %}
|
{% if client.last_handshake %}
|
||||||
<span class="text-gray-600">{{ client.last_handshake }}</span>
|
<span class="text-gray-600">{{ client.last_handshake }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-gray-400">Never</span>
|
<span class="text-gray-400">{{ t('clients.never') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">View</a>
|
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">{{ t('servers.view') }}</a>
|
||||||
{% if client.status == 'active' %}
|
{% if client.status == 'active' %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
|
||||||
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('Revoke access for this client?')">Revoke</button>
|
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
|
||||||
<button type="submit" class="text-green-600 hover:text-green-800 mr-2">Restore</button>
|
<button type="submit" class="text-green-600 hover:text-green-800 mr-2">{{ t('clients.restore') }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
|
||||||
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('Delete this client permanently?')">Delete</button>
|
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -89,7 +138,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="p-12 text-center text-gray-500">No clients yet</div>
|
<div class="p-12 text-center text-gray-500">{{ t('clients.no_clients') }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,5 +179,162 @@ async function syncAllStats(serverId) {
|
|||||||
alert('Error: ' + error.message);
|
alert('Error: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load backups on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadBackups({{ server.id }});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBackups(serverId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/servers/${serverId}/backups`, {
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
document.getElementById('backupsList').innerHTML = '<div class="text-center text-red-500 py-4">{{ t("message.error") }}: {{ t("backups.login_required") }}</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.backups.length > 0) {
|
||||||
|
let html = '<div class="space-y-2">';
|
||||||
|
data.backups.forEach(backup => {
|
||||||
|
const size = (backup.backup_size / 1024 / 1024).toFixed(2);
|
||||||
|
const statusClass = backup.status === 'completed' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="flex items-center justify-between p-3 border rounded">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">${backup.backup_name}</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
${backup.clients_count} {{ t('clients.title') }} • ${size} MB • ${backup.created_at}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="px-2 py-1 ${statusClass} rounded text-xs">${backup.status}</span>
|
||||||
|
${backup.status === 'completed' ? `
|
||||||
|
<button onclick="restoreBackup(${serverId}, ${backup.id})" class="text-blue-600 hover:text-blue-800 px-3 py-1 border rounded text-sm">
|
||||||
|
<i class="fas fa-undo"></i> {{ t('backups.restore') }}
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
<button onclick="deleteBackup(${backup.id})" class="text-red-600 hover:text-red-800 px-3 py-1 border rounded text-sm">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById('backupsList').innerHTML = html;
|
||||||
|
} else {
|
||||||
|
document.getElementById('backupsList').innerHTML = '<div class="text-center text-gray-500 py-4">{{ t("backups.no_backups") }}</div>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('backupsList').innerHTML = `<div class="text-center text-red-500 py-4">Error: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBackup(serverId) {
|
||||||
|
if (!confirm('{{ t("backups.create_confirm") }}')) return;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById('backupsList').innerHTML = '<div class="text-center text-gray-500 py-4"><i class="fas fa-spinner fa-spin"></i> {{ t("form.processing") }}</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/servers/${serverId}/backup`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('{{ t("backups.created_success") }}');
|
||||||
|
loadBackups(serverId);
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (data.error || 'Unknown error'));
|
||||||
|
loadBackups(serverId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
loadBackups(serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreBackup(serverId, backupId) {
|
||||||
|
if (!confirm('{{ t("backups.restore_confirm") }}')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/servers/${serverId}/restore`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ backup_id: backupId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Restore response:', data);
|
||||||
|
|
||||||
|
if (data.success !== false) {
|
||||||
|
let message = `{{ t("backups.restored_success") }}: ${data.restored} {{ t("clients.title") }}`;
|
||||||
|
|
||||||
|
if (data.failed > 0) {
|
||||||
|
message += `\n\nПропущено: ${data.failed}`;
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
message += '\n\nПричины:\n' + data.errors.slice(0, 5).join('\n');
|
||||||
|
if (data.errors.length > 5) {
|
||||||
|
message += `\n... и ещё ${data.errors.length - 5}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message);
|
||||||
|
if (data.restored > 0) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
loadBackups(serverId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorMsg = 'Error: ';
|
||||||
|
if (data.error) {
|
||||||
|
errorMsg += data.error;
|
||||||
|
} else if (data.errors && data.errors.length > 0) {
|
||||||
|
errorMsg += data.errors.join(', ');
|
||||||
|
} else {
|
||||||
|
errorMsg += 'Unknown error';
|
||||||
|
}
|
||||||
|
alert(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Restore error:', error);
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBackup(backupId) {
|
||||||
|
if (!confirm('{{ t("backups.delete_confirm") }}')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/backups/${backupId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('{{ t("backups.deleted_success") }}');
|
||||||
|
loadBackups({{ server.id }});
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+35
-29
@@ -32,7 +32,7 @@
|
|||||||
<nav class="-mb-px flex space-x-8">
|
<nav class="-mb-px flex space-x-8">
|
||||||
<a href="#" onclick="showTab('profile'); return false;" id="tab-profile"
|
<a href="#" onclick="showTab('profile'); return false;" id="tab-profile"
|
||||||
class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
|
class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
<i class="fas fa-user mr-2"></i>Profile
|
<i class="fas fa-user mr-2"></i>{{ t('settings.profile') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="#" onclick="showTab('api'); return false;" id="tab-api"
|
<a href="#" onclick="showTab('api'); return false;" id="tab-api"
|
||||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
@@ -40,12 +40,12 @@
|
|||||||
</a>
|
</a>
|
||||||
<a href="#" onclick="showTab('translations'); return false;" id="tab-translations"
|
<a href="#" onclick="showTab('translations'); return false;" id="tab-translations"
|
||||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
<i class="fas fa-language mr-2"></i>Translations
|
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
|
||||||
</a>
|
</a>
|
||||||
{% if user.role == 'admin' %}
|
{% if user.role == 'admin' %}
|
||||||
<a href="#" onclick="showTab('users'); return false;" id="tab-users"
|
<a href="#" onclick="showTab('users'); return false;" id="tab-users"
|
||||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||||
<i class="fas fa-users mr-2"></i>Users
|
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -56,30 +56,30 @@
|
|||||||
<div class="bg-white shadow rounded-lg">
|
<div class="bg-white shadow rounded-lg">
|
||||||
<div class="px-6 py-5 border-b border-gray-200">
|
<div class="px-6 py-5 border-b border-gray-200">
|
||||||
<h2 class="text-lg font-medium text-gray-900">
|
<h2 class="text-lg font-medium text-gray-900">
|
||||||
<i class="fas fa-lock mr-2 text-purple-600"></i>Change Password
|
<i class="fas fa-lock mr-2 text-purple-600"></i>{{ t('settings.change_password') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-5">
|
<div class="px-6 py-5">
|
||||||
<form method="POST" action="/settings/change-password">
|
<form method="POST" action="/settings/change-password">
|
||||||
<div class="space-y-4 max-w-md">
|
<div class="space-y-4 max-w-md">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Current Password</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('settings.current_password') }}</label>
|
||||||
<input type="password" name="current_password" required
|
<input type="password" name="current_password" required
|
||||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('settings.new_password') }}</label>
|
||||||
<input type="password" name="new_password" required minlength="6"
|
<input type="password" name="new_password" required minlength="6"
|
||||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
<p class="mt-1 text-xs text-gray-500">Minimum 6 characters</p>
|
<p class="mt-1 text-xs text-gray-500">{{ t('settings.min_6_chars') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('settings.confirm_password') }}</label>
|
||||||
<input type="password" name="confirm_password" required minlength="6"
|
<input type="password" name="confirm_password" required minlength="6"
|
||||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||||
<i class="fas fa-save mr-2"></i>Change Password
|
<i class="fas fa-save mr-2"></i>{{ t('settings.change_password') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||||
<span class="text-sm font-medium text-green-900">API Key Configured</span>
|
<span class="text-sm font-medium text-green-900">{{ t('settings.api_key_configured') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<code class="text-xs text-green-700 bg-green-100 px-2 py-1 rounded">
|
<code class="text-xs text-green-700 bg-green-100 px-2 py-1 rounded">
|
||||||
{{ openrouter_key[:15] }}...{{ openrouter_key[-4:] }}
|
{{ openrouter_key[:15] }}...{{ openrouter_key[-4:] }}
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
<div class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||||
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2"></i>
|
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2"></i>
|
||||||
<span class="text-sm text-yellow-800">No API key configured. Auto-translation will not work.</span>
|
<span class="text-sm text-yellow-800">{{ t('settings.no_api_key') }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -126,6 +126,12 @@
|
|||||||
<i class="fas fa-info-circle"></i> {{ t('settings.get_key_at') }}
|
<i class="fas fa-info-circle"></i> {{ t('settings.get_key_at') }}
|
||||||
<a href="https://openrouter.ai/keys" target="_blank" class="text-purple-600">openrouter.ai/keys</a>
|
<a href="https://openrouter.ai/keys" target="_blank" class="text-purple-600">openrouter.ai/keys</a>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="checkbox" name="skip_test" value="1" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||||
|
<span class="ml-2 text-sm text-gray-600">{{ t('settings.skip_validation') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<input type="hidden" name="service" value="openrouter">
|
<input type="hidden" name="service" value="openrouter">
|
||||||
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||||
<i class="fas fa-save mr-2"></i>{{ openrouter_key ? t('form.update') : t('form.save') }}
|
<i class="fas fa-save mr-2"></i>{{ openrouter_key ? t('form.update') : t('form.save') }}
|
||||||
@@ -194,34 +200,34 @@
|
|||||||
<div class="bg-white shadow rounded-lg mb-6">
|
<div class="bg-white shadow rounded-lg mb-6">
|
||||||
<div class="px-6 py-5 border-b border-gray-200">
|
<div class="px-6 py-5 border-b border-gray-200">
|
||||||
<h2 class="text-lg font-medium text-gray-900">
|
<h2 class="text-lg font-medium text-gray-900">
|
||||||
<i class="fas fa-user-plus mr-2 text-purple-600"></i>Add User
|
<i class="fas fa-user-plus mr-2 text-purple-600"></i>{{ t('users.add_user') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-5">
|
<div class="px-6 py-5">
|
||||||
<form method="POST" action="/settings/add-user">
|
<form method="POST" action="/settings/add-user">
|
||||||
<div class="grid grid-cols-2 gap-4 max-w-2xl">
|
<div class="grid grid-cols-2 gap-4 max-w-2xl">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('auth.name') }}</label>
|
||||||
<input type="text" name="name" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
<input type="text" name="name" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('auth.email') }}</label>
|
||||||
<input type="email" name="email" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
<input type="email" name="email" required class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('auth.password') }}</label>
|
||||||
<input type="password" name="password" required minlength="6" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
<input type="password" name="password" required minlength="6" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('users.role') }}</label>
|
||||||
<select name="role" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
<select name="role" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||||
<option value="user">User</option>
|
<option value="user">{{ t('users.role_user') }}</option>
|
||||||
<option value="admin">Administrator</option>
|
<option value="admin">{{ t('users.role_admin') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
<button type="submit" class="mt-4 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||||
<i class="fas fa-user-plus mr-2"></i>Add User
|
<i class="fas fa-user-plus mr-2"></i>{{ t('users.add_user') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,17 +236,17 @@
|
|||||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||||
<div class="px-6 py-5 border-b border-gray-200">
|
<div class="px-6 py-5 border-b border-gray-200">
|
||||||
<h2 class="text-lg font-medium text-gray-900">
|
<h2 class="text-lg font-medium text-gray-900">
|
||||||
<i class="fas fa-users mr-2 text-purple-600"></i>All Users
|
<i class="fas fa-users mr-2 text-purple-600"></i>{{ t('users.all_users') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('auth.name') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('auth.email') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Role</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('users.role') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('users.created') }}</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('settings.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
@@ -255,17 +261,17 @@
|
|||||||
<td class="px-6 py-4 text-sm text-gray-500">{{ u.email }}</td>
|
<td class="px-6 py-4 text-sm text-gray-500">{{ u.email }}</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{% if u.role == 'admin' %}
|
{% if u.role == 'admin' %}
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">Admin</span>
|
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">{{ t('users.role_admin') }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">User</span>
|
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">{{ t('users.role_user') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">{{ u.created_at|date('Y-m-d') }}</td>
|
<td class="px-6 py-4 text-sm text-gray-500">{{ u.created_at|date('Y-m-d') }}</td>
|
||||||
<td class="px-6 py-4 text-sm">
|
<td class="px-6 py-4 text-sm">
|
||||||
{% if u.id != user.id %}
|
{% if u.id != user.id %}
|
||||||
<form method="POST" action="/settings/delete-user/{{ u.id }}" class="inline" onsubmit="return confirm('Delete {{ u.name }}?')">
|
<form method="POST" action="/settings/delete-user/{{ u.id }}" class="inline" onsubmit="return confirm('{{ t('users.delete_confirm', [u.name]) }}')">
|
||||||
<button type="submit" class="text-red-600 hover:text-red-900">
|
<button type="submit" class="text-red-600 hover:text-red-900">
|
||||||
<i class="fas fa-trash"></i> Delete
|
<i class="fas fa-trash"></i> {{ t('clients.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user