diff --git a/DEVELOPER.md b/DEVELOPER.md index c208d13..7514c53 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -48,6 +48,11 @@ FLUSH PRIVILEGES; USE amnezia_panel; 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** diff --git a/README.md b/README.md index c3870d4..8debc87 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,14 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers. ## Features - VPN server deployment via SSH -- Client configuration management +- Client configuration management with **expiration dates** +- **Server backup and restore** functionality - Traffic statistics monitoring - QR code generation for mobile apps - Multi-language interface (English, Russian, Spanish, German, French, Chinese) - REST API with JWT authentication - User authentication and access control +- **Automatic client expiration check** via cron ## Requirements @@ -61,8 +63,54 @@ JWT_SECRET=your-secret-key-change-this 1. Open server details 2. Enter client name -3. Click Create Client -4. Download config or scan QR code +3. **Select expiration period** (optional, default: never expires) +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 " \ + -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 " \ + -d '{"days": 30}' + +# Get expiring clients (within 7 days) +curl http://localhost:8082/api/clients/expiring?days=7 \ + -H "Authorization: Bearer " +``` + +### 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 " + +# List backups +curl http://localhost:8082/api/servers/1/backups \ + -H "Authorization: Bearer " + +# Restore from backup +curl -X POST http://localhost:8082/api/servers/1/restore \ + -H "Authorization: Bearer " \ + -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 @@ -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}/qr - Get client 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}/restore - Restore client access 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 @@ -133,7 +196,7 @@ inc/ - Core classes JWT.php - Token auth QrUtil.php - QR code generation templates/ - Twig templates -migrations/ - SQL migrations +migrations/ - SQL migrations (executed in alphabetical order) ``` ## Tech Stack diff --git a/backups/backup_1_2025-11-08_060624.json b/backups/backup_1_2025-11-08_060624.json new file mode 100644 index 0000000..34a8f3f --- /dev/null +++ b/backups/backup_1_2025-11-08_060624.json @@ -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" +} \ No newline at end of file diff --git a/bin/check_expired_clients.php b/bin/check_expired_clients.php new file mode 100755 index 0000000..61215e4 --- /dev/null +++ b/bin/check_expired_clients.php @@ -0,0 +1,47 @@ +#!/usr/bin/env php + 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); +} diff --git a/controllers/SettingsController.php b/controllers/SettingsController.php index 22a3d64..05790fc 100644 --- a/controllers/SettingsController.php +++ b/controllers/SettingsController.php @@ -176,6 +176,7 @@ class SettingsController { $service = $_POST['service'] ?? ''; $apiKey = trim($_POST['api_key'] ?? ''); + $skipTest = isset($_POST['skip_test']); // Allow saving without testing if (empty($service) || empty($apiKey)) { View::render('settings.twig', [ @@ -185,22 +186,20 @@ class SettingsController { return; } - // Validate OpenRouter key format - if ($service === 'openrouter' && !preg_match('/^sk-or-v1-[a-zA-Z0-9]{64,}$/', $apiKey)) { - View::render('settings.twig', [ - 'error' => $this->translator->translate('settings.error_invalid_key'), - 'translation_stats' => $this->getTranslationStats() - ]); - return; - } - - // Test the API key - if ($service === 'openrouter') { + // Test the API key (unless skip_test is set) + if ($service === 'openrouter' && !$skipTest) { $testResult = $this->testOpenRouterKey($apiKey); 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', [ - 'error' => $this->translator->translate('settings.error_key_test') . ': ' . $testResult['error'], - 'translation_stats' => $this->getTranslationStats() + 'error' => $errorMsg, + 'translation_stats' => $this->getTranslationStats(), + 'openrouter_key' => '' ]); return; } @@ -210,35 +209,32 @@ class SettingsController { $saved = $this->translator->saveApiKey($service, $apiKey); if ($saved) { - View::render('settings.twig', [ - 'success' => $this->translator->translate('settings.key_saved'), - 'translation_stats' => $this->getTranslationStats(), - 'openrouter_key' => '' // Don't show the saved key - ]); + $_SESSION['settings_success'] = $this->translator->translate('settings.key_saved'); + header('Location: /settings#api'); + exit; } else { - View::render('settings.twig', [ - 'error' => $this->translator->translate('message.error'), - 'translation_stats' => $this->getTranslationStats() - ]); + $_SESSION['settings_error'] = $this->translator->translate('message.error'); + header('Location: /settings#api'); + exit; } } 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'; $data = [ - 'model' => 'google/gemini-2.0-flash-exp:free', + 'model' => 'openai/gpt-4o-mini', 'messages' => [ - ['role' => 'user', 'content' => 'Translate "test" to Spanish. Reply only with the translation.'] + ['role' => 'user', 'content' => 'Reply with: OK'] ], - 'temperature' => 0.3, - 'max_tokens' => 10 + 'max_tokens' => 5 ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, @@ -248,19 +244,55 @@ class SettingsController { $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); - if ($httpCode === 200) { - $result = json_decode($response, true); - if (isset($result['choices'][0]['message']['content'])) { - return ['success' => true]; + // Handle cURL errors + if ($curlError) { + return [ + 'success' => false, + 'error' => 'Network error: ' . $curlError + ]; + } + + // Parse response + $result = json_decode($response, true); + + // Success - got a valid response + if ($httpCode === 200 && isset($result['choices'][0]['message'])) { + 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']; } } - $error = json_decode($response, true); + // 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'; + } + return [ 'success' => false, - 'error' => $error['error']['message'] ?? 'Unknown error (HTTP ' . $httpCode . ')' + 'error' => $errorMsg ]; } diff --git a/docker-compose.yml b/docker-compose.yml index 98f5d35..b6aea98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: image: mysql:8.0 container_name: amnezia-panel-db restart: unless-stopped + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-authentication-plugin=mysql_native_password env_file: - .env environment: @@ -15,6 +16,7 @@ services: volumes: - db_data:/var/lib/mysql - ./migrations:/docker-entrypoint-initdb.d + - ./my.cnf:/etc/mysql/conf.d/my.cnf:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s diff --git a/inc/DB.php b/inc/DB.php index 28c2904..5d2ce6b 100644 --- a/inc/DB.php +++ b/inc/DB.php @@ -16,6 +16,10 @@ class DB { PDO::ATTR_EMULATE_PREPARES => false, ]; 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; } } \ No newline at end of file diff --git a/inc/Translator.php b/inc/Translator.php index b150b87..09490c8 100644 --- a/inc/Translator.php +++ b/inc/Translator.php @@ -196,11 +196,11 @@ class Translator { * Translate text using AI with model fallback */ private static function translateWithAI(string $text, string $targetLanguage): ?string { - // Try multiple free models for reliability + // Use reliable paid models with fallback $models = [ - 'google/gemini-2.0-flash-exp:free', - 'meta-llama/llama-3.2-3b-instruct:free', - 'qwen/qwen-2-7b-instruct:free' + 'anthropic/claude-3.5-sonnet', + 'openai/gpt-4o-mini', + 'google/gemini-pro-1.5' ]; foreach ($models as $model) { @@ -350,9 +350,10 @@ class Translator { foreach ($missingKeys as $key => $value) { if (self::autoTranslate($targetLang, $key, $value)) { $stats['translated']++; - usleep(500000); // 500ms delay between requests + sleep(3); // 3 second delay between requests to avoid rate limits } else { $stats['failed']++; + sleep(2); // Also delay on failure } } @@ -390,8 +391,9 @@ class Translator { $jsonTexts = json_encode($textsForJson, JSON_UNESCAPED_UNICODE); $models = [ - 'google/gemini-2.0-flash-exp:free', - 'meta-llama/llama-3.2-3b-instruct:free' + 'anthropic/claude-3.5-sonnet', + 'openai/gpt-4o-mini', + 'google/gemini-pro-1.5' ]; foreach ($models as $model) { diff --git a/inc/VpnClient.php b/inc/VpnClient.php index dc3b8a6..cea7c35 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -30,8 +30,14 @@ class VpnClient { /** * 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(); // Get server data @@ -69,11 +75,14 @@ class VpnClient { // Generate QR code $qrCode = self::generateQRCode($config); + // Calculate expiration date + $expiresAt = $expiresInDays ? date('Y-m-d H:i:s', strtotime("+{$expiresInDays} days")) : null; + // Insert into database $stmt = $pdo->prepare(' INSERT INTO vpn_clients - (server_id, user_id, name, client_ip, public_key, private_key, preshared_key, config, qr_code, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (server_id, user_id, name, client_ip, public_key, private_key, preshared_key, config, qr_code, status, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ @@ -86,7 +95,8 @@ class VpnClient { $serverData['preshared_key'], $config, $qrCode, - 'active' + 'active', + $expiresAt ]); return (int)$pdo->lastInsertId(); @@ -685,4 +695,135 @@ class VpnClient { 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); + } } + diff --git a/inc/VpnServer.php b/inc/VpnServer.php index 88ebed8..78e07f9 100644 --- a/inc/VpnServer.php +++ b/inc/VpnServer.php @@ -446,4 +446,266 @@ BASH; public function getData(): ?array { 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; + } } diff --git a/migrations/init.sql b/migrations/001_init.sql similarity index 75% rename from migrations/init.sql rename to migrations/001_init.sql index cd8e06b..d5cb523 100644 --- a/migrations/init.sql +++ b/migrations/001_init.sql @@ -60,11 +60,13 @@ CREATE TABLE IF NOT EXISTS vpn_clients ( last_handshake TIMESTAMP NULL COMMENT 'Last successful WireGuard handshake', last_sync_at TIMESTAMP NULL COMMENT 'Last time stats were synced from server', status ENUM('active', 'disabled') DEFAULT 'active', + expires_at TIMESTAMP NULL COMMENT 'Client expiration date (NULL = never expires)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_server_id (server_id), INDEX idx_user_id (user_id), INDEX idx_status (status), + INDEX idx_expires_at (expires_at), INDEX idx_last_handshake (last_handshake), UNIQUE KEY unique_server_client_ip (server_id, client_ip), 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) ) 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 IGNORE INTO users (email, password_hash, name, role, status) 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', 'clients.actions', 'Actions'), ('en', 'clients.add', 'Add Client'), +('en', 'clients.create', 'Create Client'), ('en', 'clients.delete', 'Delete'), +('en', 'clients.delete_confirm', 'Delete this client permanently?'), ('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.last_handshake', 'Last Handshake'), ('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.restore', 'Restore'), ('en', 'clients.revoke', 'Revoke'), +('en', 'clients.revoke_confirm', 'Revoke access for this client?'), ('en', 'clients.sent', 'Sent'), ('en', 'clients.server', 'Server'), ('en', 'clients.status', 'Status'), ('en', 'clients.sync_stats', 'Sync Stats'), ('en', 'clients.title', 'Clients'), ('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.add_first_server', 'Add First 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', 'form.cancel', 'Cancel'), ('en', 'form.close', 'Close'), +('en', 'form.create', 'Create'), ('en', 'form.loading', 'Loading...'), ('en', 'form.processing', 'Processing...'), ('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_desc', 'Configure API keys for external services'), ('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.current_password', 'Current Password'), ('en', 'settings.description', 'Manage panel configuration and API integrations'), ('en', 'settings.error_empty_key', 'API key cannot be empty'), ('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.keys', 'keys'), ('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.translations', 'Translations'), ('en', 'settings.translation_complete', 'Translation completed'), ('en', 'settings.translation_status', 'Translation Status'), +('en', 'settings.users', 'Users'), ('en', 'status.active', 'Active'), ('en', 'status.deploying', 'Deploying'), ('en', 'status.disabled', 'Disabled'), ('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); diff --git a/migrations/0025_translations_es.sql b/migrations/0025_translations_es.sql new file mode 100644 index 0000000..5528bf6 --- /dev/null +++ b/migrations/0025_translations_es.sql @@ -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); diff --git a/migrations/002_translations_ru.sql b/migrations/002_translations_ru.sql new file mode 100644 index 0000000..e101c54 --- /dev/null +++ b/migrations/002_translations_ru.sql @@ -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); diff --git a/migrations/003_translations_es.sql b/migrations/003_translations_es.sql new file mode 100644 index 0000000..c491bf9 --- /dev/null +++ b/migrations/003_translations_es.sql @@ -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); diff --git a/migrations/0044_translations_de.sql b/migrations/0044_translations_de.sql new file mode 100644 index 0000000..2b54a85 --- /dev/null +++ b/migrations/0044_translations_de.sql @@ -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); diff --git a/migrations/004_translations_de.sql b/migrations/004_translations_de.sql new file mode 100644 index 0000000..150df1c --- /dev/null +++ b/migrations/004_translations_de.sql @@ -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); diff --git a/migrations/0053_translations_ru.sql b/migrations/0053_translations_ru.sql new file mode 100644 index 0000000..e61b5ba --- /dev/null +++ b/migrations/0053_translations_ru.sql @@ -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); diff --git a/migrations/0057_translations_fr.sql b/migrations/0057_translations_fr.sql new file mode 100644 index 0000000..655185d --- /dev/null +++ b/migrations/0057_translations_fr.sql @@ -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); diff --git a/migrations/005_translations_fr.sql b/migrations/005_translations_fr.sql new file mode 100644 index 0000000..7edefb9 --- /dev/null +++ b/migrations/005_translations_fr.sql @@ -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); diff --git a/migrations/006_translations_zh.sql b/migrations/006_translations_zh.sql new file mode 100644 index 0000000..33343ae --- /dev/null +++ b/migrations/006_translations_zh.sql @@ -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); diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..efa9ced --- /dev/null +++ b/migrations/README.md @@ -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 +``` diff --git a/my.cnf b/my.cnf new file mode 100644 index 0000000..92a0323 --- /dev/null +++ b/my.cnf @@ -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 diff --git a/public/index.php b/public/index.php index 2f3fbab..13b65ee 100644 --- a/public/index.php +++ b/public/index.php @@ -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 */ @@ -327,8 +360,9 @@ Router::get('/servers/{id}', function ($params) { 'clients' => $clients, ]); } catch (Exception $e) { + error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine()); 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(); $serverId = (int)$params['id']; $clientName = trim($_POST['name'] ?? ''); + $expiresInDays = !empty($_POST['expires_in_days']) ? (int)$_POST['expires_in_days'] : null; if (empty($clientName)) { redirect('/servers/' . $serverId . '?error=Client+name+is+required'); @@ -379,7 +414,7 @@ Router::post('/servers/{id}/clients/create', function ($params) { return; } - $clientId = VpnClient::create($serverId, $user['id'], $clientName); + $clientId = VpnClient::create($serverId, $user['id'], $clientName, $expiresInDays); redirect('/clients/' . $clientId); } catch (Exception $e) { 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 Router::get('/api/clients', function () { header('Content-Type: application/json'); @@ -988,6 +1174,7 @@ Router::post('/api/clients/create', function () { $serverId = (int)($data['server_id'] ?? 0); $name = trim($data['name'] ?? ''); + $expiresInDays = isset($data['expires_in_days']) ? (int)$data['expires_in_days'] : null; if ($serverId <= 0 || empty($name)) { http_response_code(400); @@ -996,7 +1183,7 @@ Router::post('/api/clients/create', function () { } try { - $clientId = VpnClient::create($serverId, $user['id'], $name); + $clientId = VpnClient::create($serverId, $user['id'], $name, $expiresInDays); $client = new VpnClient($clientId); $clientData = $client->getData(); @@ -1010,6 +1197,7 @@ Router::post('/api/clients/create', function () { 'server_id' => $clientData['server_id'], 'client_ip' => $clientData['client_ip'], 'status' => $clientData['status'], + 'expires_at' => $clientData['expires_at'], 'created_at' => $clientData['created_at'], 'config' => $clientData['config'], '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 */ diff --git a/templates/servers/view.twig b/templates/servers/view.twig index 9df5415..beb376c 100644 --- a/templates/servers/view.twig +++ b/templates/servers/view.twig @@ -13,33 +13,61 @@
-

Create Client

-
- -
+ + +
+
+

{{ t('backups.title') }}

+ +
+
+
+ {{ t('form.loading') }} +
+
+
-

Clients ({{ clients|length }})

+

{{ t('clients.title') }} ({{ clients|length }})

{% if clients|length > 0 %} - - - - - - + + + + + + + @@ -49,9 +77,30 @@ + @@ -89,7 +138,7 @@
NameIPStatusTrafficLast SeenActions{{ t('clients.name') }}{{ t('clients.ip') }}{{ t('clients.status') }}{{ t('clients.expiration') }}{{ t('clients.traffic') }}{{ t('clients.last_handshake') }}{{ t('clients.actions') }}
{{ client.client_ip }} {% if client.status == 'active' %} - Active + {{ t('status.active') }} {% else %} - Disabled + {{ t('status.disabled') }} + {% endif %} + + {% 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 %} + + {{ t('clients.expired') }} + + {% elseif diff_days <= 7 %} + + {{ diff_days }} {{ t('common.days') }} + + {% else %} + {{ client.expires_at|date('Y-m-d') }} + {% endif %} + {% else %} + {{ t('clients.never_expires') }} {% endif %} @@ -66,22 +115,22 @@ {% if client.last_handshake %} {{ client.last_handshake }} {% else %} - Never + {{ t('clients.never') }} {% endif %} - View + {{ t('servers.view') }} {% if client.status == 'active' %}
- +
{% else %}
- +
{% endif %}
- +
{% else %} -
No clients yet
+
{{ t('clients.no_clients') }}
{% endif %}
@@ -130,5 +179,162 @@ async function syncAllStats(serverId) { 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 = '
{{ t("message.error") }}: {{ t("backups.login_required") }}
'; + return; + } + + const data = await response.json(); + + if (data.success && data.backups.length > 0) { + let html = '
'; + 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 += ` +
+
+
${backup.backup_name}
+
+ ${backup.clients_count} {{ t('clients.title') }} • ${size} MB • ${backup.created_at} +
+
+
+ ${backup.status} + ${backup.status === 'completed' ? ` + + ` : ''} + +
+
+ `; + }); + html += '
'; + document.getElementById('backupsList').innerHTML = html; + } else { + document.getElementById('backupsList').innerHTML = '
{{ t("backups.no_backups") }}
'; + } + } catch (error) { + document.getElementById('backupsList').innerHTML = `
Error: ${error.message}
`; + } +} + +async function createBackup(serverId) { + if (!confirm('{{ t("backups.create_confirm") }}')) return; + + // Show loading state + document.getElementById('backupsList').innerHTML = '
{{ t("form.processing") }}
'; + + 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); + } +} {% endblock %} diff --git a/templates/settings.twig b/templates/settings.twig index cabe711..0c1a803 100644 --- a/templates/settings.twig +++ b/templates/settings.twig @@ -32,7 +32,7 @@ @@ -56,30 +56,30 @@

- Change Password + {{ t('settings.change_password') }}

- +
- + -

Minimum 6 characters

+

{{ t('settings.min_6_chars') }}

- +
@@ -101,7 +101,7 @@
- API Key Configured + {{ t('settings.api_key_configured') }}
{{ openrouter_key[:15] }}...{{ openrouter_key[-4:] }} @@ -111,7 +111,7 @@ {% else %}
- No API key configured. Auto-translation will not work. + {{ t('settings.no_api_key') }}
{% endif %} @@ -126,6 +126,12 @@ {{ t('settings.get_key_at') }} openrouter.ai/keys

+
+ +
@@ -230,17 +236,17 @@

- All Users + {{ t('users.all_users') }}

- - - - - + + + + + @@ -255,17 +261,17 @@
NameEmailRoleCreatedActions{{ t('auth.name') }}{{ t('auth.email') }}{{ t('users.role') }}{{ t('users.created') }}{{ t('settings.actions') }}
{{ u.email }} {% if u.role == 'admin' %} - Admin + {{ t('users.role_admin') }} {% else %} - User + {{ t('users.role_user') }} {% endif %} {{ u.created_at|date('Y-m-d') }} {% if u.id != user.id %} -
+
{% endif %}