feat: добавлена возможность ручного ввода времени и трафика
- Добавлены custom input поля для expiration (секунды) и traffic (МБ) - Добавлена функциональность редактирования клиента - Исправлена migration 007 (AFTER expires_at) - Удалены дублирующиеся миграции (0025, 0044, 0053, 0057) - Удалён старый init.sql (заменён на 001_init.sql) - Добавлены переводы для custom полей на 6 языках
This commit is contained in:
@@ -208,6 +208,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('en', 'clients.traffic_limit', 'Traffic Limit'),
|
('en', 'clients.traffic_limit', 'Traffic Limit'),
|
||||||
('en', 'clients.unlimited', 'Unlimited'),
|
('en', 'clients.unlimited', 'Unlimited'),
|
||||||
('en', 'clients.overlimit', 'Over Limit'),
|
('en', 'clients.overlimit', 'Over Limit'),
|
||||||
|
('en', 'clients.custom_seconds', 'Custom (seconds)'),
|
||||||
|
('en', 'clients.custom_mb', 'Custom (MB)'),
|
||||||
|
('en', 'clients.enter_seconds', 'Enter seconds'),
|
||||||
|
('en', 'clients.enter_megabytes', 'Enter megabytes'),
|
||||||
('en', 'backups.title', 'Server Backups'),
|
('en', 'backups.title', 'Server Backups'),
|
||||||
('en', 'backups.create', 'Create Backup'),
|
('en', 'backups.create', 'Create Backup'),
|
||||||
('en', 'backups.restore', 'Restore'),
|
('en', 'backups.restore', 'Restore'),
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@@ -46,6 +46,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('ru', 'clients.traffic', 'Трафик'),
|
('ru', 'clients.traffic', 'Трафик'),
|
||||||
('ru', 'clients.traffic_limit', 'Лимит трафика'),
|
('ru', 'clients.traffic_limit', 'Лимит трафика'),
|
||||||
('ru', 'clients.unlimited', 'Безлимитно'),
|
('ru', 'clients.unlimited', 'Безлимитно'),
|
||||||
|
('ru', 'clients.custom_seconds', 'Своё значение (секунды)'),
|
||||||
|
('ru', 'clients.custom_mb', 'Своё значение (МБ)'),
|
||||||
|
('ru', 'clients.enter_seconds', 'Введите секунды'),
|
||||||
|
('ru', 'clients.enter_megabytes', 'Введите мегабайты'),
|
||||||
('ru', 'common.days', 'дней'),
|
('ru', 'common.days', 'дней'),
|
||||||
('ru', 'dashboard.active_clients', 'Активные клиенты'),
|
('ru', 'dashboard.active_clients', 'Активные клиенты'),
|
||||||
('ru', 'dashboard.add_first_server', 'Добавить первый сервер'),
|
('ru', 'dashboard.add_first_server', 'Добавить первый сервер'),
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('es', 'clients.traffic', 'Tráfico'),
|
('es', 'clients.traffic', 'Tráfico'),
|
||||||
('es', 'clients.traffic_limit', 'Límite de tráfico'),
|
('es', 'clients.traffic_limit', 'Límite de tráfico'),
|
||||||
('es', 'clients.unlimited', 'Ilimitado'),
|
('es', 'clients.unlimited', 'Ilimitado'),
|
||||||
|
('es', 'clients.custom_seconds', 'Personalizado (segundos)'),
|
||||||
|
('es', 'clients.custom_mb', 'Personalizado (MB)'),
|
||||||
|
('es', 'clients.enter_seconds', 'Ingrese segundos'),
|
||||||
|
('es', 'clients.enter_megabytes', 'Ingrese megabytes'),
|
||||||
('es', 'common.days', 'días'),
|
('es', 'common.days', 'días'),
|
||||||
('es', 'dashboard.active_clients', 'Clientes activos'),
|
('es', 'dashboard.active_clients', 'Clientes activos'),
|
||||||
('es', 'dashboard.add_first_server', 'Agregar primer servidor'),
|
('es', 'dashboard.add_first_server', 'Agregar primer servidor'),
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@@ -46,6 +46,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('de', 'clients.traffic', 'Datenverkehr'),
|
('de', 'clients.traffic', 'Datenverkehr'),
|
||||||
('de', 'clients.traffic_limit', 'Traffic-Limit'),
|
('de', 'clients.traffic_limit', 'Traffic-Limit'),
|
||||||
('de', 'clients.unlimited', 'Unbegrenzt'),
|
('de', 'clients.unlimited', 'Unbegrenzt'),
|
||||||
|
('de', 'clients.custom_seconds', 'Benutzerdefiniert (Sekunden)'),
|
||||||
|
('de', 'clients.custom_mb', 'Benutzerdefiniert (MB)'),
|
||||||
|
('de', 'clients.enter_seconds', 'Sekunden eingeben'),
|
||||||
|
('de', 'clients.enter_megabytes', 'Megabytes eingeben'),
|
||||||
('de', 'common.days', 'Tage'),
|
('de', 'common.days', 'Tage'),
|
||||||
('de', 'dashboard.active_clients', 'Aktive Clients'),
|
('de', 'dashboard.active_clients', 'Aktive Clients'),
|
||||||
('de', 'dashboard.add_first_server', 'Ersten Server hinzufügen'),
|
('de', 'dashboard.add_first_server', 'Ersten Server hinzufügen'),
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@@ -46,6 +46,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('fr', 'clients.traffic', 'Trafic'),
|
('fr', 'clients.traffic', 'Trafic'),
|
||||||
('fr', 'clients.traffic_limit', 'Limite de trafic'),
|
('fr', 'clients.traffic_limit', 'Limite de trafic'),
|
||||||
('fr', 'clients.unlimited', 'Illimité'),
|
('fr', 'clients.unlimited', 'Illimité'),
|
||||||
|
('fr', 'clients.custom_seconds', 'Personnalisé (secondes)'),
|
||||||
|
('fr', 'clients.custom_mb', 'Personnalisé (MB)'),
|
||||||
|
('fr', 'clients.enter_seconds', 'Saisissez les secondes'),
|
||||||
|
('fr', 'clients.enter_megabytes', 'Saisissez les mégaoctets'),
|
||||||
('fr', 'common.days', 'jours'),
|
('fr', 'common.days', 'jours'),
|
||||||
('fr', 'dashboard.active_clients', 'Clients actifs'),
|
('fr', 'dashboard.active_clients', 'Clients actifs'),
|
||||||
('fr', 'dashboard.add_first_server', 'Ajouter le premier serveur'),
|
('fr', 'dashboard.add_first_server', 'Ajouter le premier serveur'),
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
|||||||
('zh', 'clients.traffic', '流量'),
|
('zh', 'clients.traffic', '流量'),
|
||||||
('zh', 'clients.traffic_limit', '流量限制'),
|
('zh', 'clients.traffic_limit', '流量限制'),
|
||||||
('zh', 'clients.unlimited', '无限制'),
|
('zh', 'clients.unlimited', '无限制'),
|
||||||
|
('zh', 'clients.custom_seconds', '自定义(秒)'),
|
||||||
|
('zh', 'clients.custom_mb', '自定义(MB)'),
|
||||||
|
('zh', 'clients.enter_seconds', '输入秒数'),
|
||||||
|
('zh', 'clients.enter_megabytes', '输入兆字节'),
|
||||||
('zh', 'common.days', '天'),
|
('zh', 'common.days', '天'),
|
||||||
('zh', 'dashboard.active_clients', '活跃客户端'),
|
('zh', 'dashboard.active_clients', '活跃客户端'),
|
||||||
('zh', 'dashboard.add_first_server', '添加第一个服务器'),
|
('zh', 'dashboard.add_first_server', '添加第一个服务器'),
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
-- This migration adds traffic limit functionality to clients
|
-- This migration adds traffic limit functionality to clients
|
||||||
|
|
||||||
ALTER TABLE vpn_clients
|
ALTER TABLE vpn_clients
|
||||||
ADD COLUMN traffic_limit BIGINT UNSIGNED NULL COMMENT 'Traffic limit in bytes (NULL = unlimited)' AFTER traffic_received,
|
ADD COLUMN traffic_limit BIGINT UNSIGNED NULL COMMENT 'Traffic limit in bytes (NULL = unlimited)' AFTER expires_at,
|
||||||
ADD INDEX idx_traffic_limit (traffic_limit);
|
ADD INDEX idx_traffic_limit (traffic_limit);
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
-- Amnezia VPN Panel - Complete Database Schema
|
|
||||||
-- Single migration file containing all tables and initial data
|
|
||||||
|
|
||||||
-- Users table
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
role ENUM('admin', 'user') DEFAULT 'user',
|
|
||||||
preferred_language VARCHAR(10) DEFAULT 'en',
|
|
||||||
status ENUM('active', 'disabled') DEFAULT 'active',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_login_at TIMESTAMP NULL,
|
|
||||||
INDEX idx_email (email),
|
|
||||||
INDEX idx_role (role),
|
|
||||||
INDEX idx_language (preferred_language)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- VPN Servers table
|
|
||||||
CREATE TABLE IF NOT EXISTS vpn_servers (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
host VARCHAR(255) NOT NULL,
|
|
||||||
port INT UNSIGNED NOT NULL,
|
|
||||||
username VARCHAR(255) NOT NULL,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
container_name VARCHAR(255) DEFAULT 'amnezia-awg',
|
|
||||||
vpn_port INT UNSIGNED NULL,
|
|
||||||
vpn_subnet VARCHAR(50) DEFAULT '10.8.1.0/24',
|
|
||||||
server_public_key TEXT NULL,
|
|
||||||
preshared_key TEXT NULL,
|
|
||||||
awg_params JSON NULL COMMENT 'Jc, Jmin, Jmax, S1, S2, H1-H4',
|
|
||||||
status ENUM('deploying', 'active', 'stopped', 'error') DEFAULT 'deploying',
|
|
||||||
deployed_at TIMESTAMP NULL,
|
|
||||||
last_check_at TIMESTAMP NULL,
|
|
||||||
error_message TEXT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_user_id (user_id),
|
|
||||||
INDEX idx_status (status),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- VPN Clients table
|
|
||||||
CREATE TABLE IF NOT EXISTS vpn_clients (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
server_id INT UNSIGNED NOT NULL,
|
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
client_ip VARCHAR(50) NOT NULL,
|
|
||||||
public_key TEXT NOT NULL,
|
|
||||||
private_key TEXT NOT NULL,
|
|
||||||
preshared_key TEXT NULL,
|
|
||||||
config TEXT NULL COMMENT 'Full WireGuard config file',
|
|
||||||
qr_code LONGTEXT NULL COMMENT 'Base64 encoded QR code image',
|
|
||||||
bytes_sent BIGINT UNSIGNED DEFAULT 0 COMMENT 'Total bytes sent by client',
|
|
||||||
bytes_received BIGINT UNSIGNED DEFAULT 0 COMMENT 'Total bytes received by client',
|
|
||||||
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,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- API Tokens table
|
|
||||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
token VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
last_used_at TIMESTAMP NULL,
|
|
||||||
expires_at TIMESTAMP NULL,
|
|
||||||
revoked_at TIMESTAMP NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_token (token),
|
|
||||||
INDEX idx_user_id (user_id),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Settings table
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT UNSIGNED NULL COMMENT 'NULL for global settings',
|
|
||||||
namespace VARCHAR(100) NOT NULL,
|
|
||||||
`key` VARCHAR(100) NOT NULL,
|
|
||||||
value JSON NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY unique_setting (user_id, namespace, `key`),
|
|
||||||
INDEX idx_namespace (namespace)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Languages table
|
|
||||||
CREATE TABLE IF NOT EXISTS languages (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
code VARCHAR(10) NOT NULL UNIQUE COMMENT 'Language code (en, ru, es, de, fr, zh)',
|
|
||||||
name VARCHAR(50) NOT NULL COMMENT 'Language name in English',
|
|
||||||
native_name VARCHAR(50) NOT NULL COMMENT 'Language name in native language',
|
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_code (code),
|
|
||||||
INDEX idx_active (is_active)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Translations table
|
|
||||||
CREATE TABLE IF NOT EXISTS translations (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
language_code VARCHAR(10) NOT NULL,
|
|
||||||
translation_key VARCHAR(255) NOT NULL COMMENT 'Translation key (e.g., menu.dashboard)',
|
|
||||||
translation_value TEXT NOT NULL COMMENT 'Translated text',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY unique_translation (language_code, translation_key),
|
|
||||||
FOREIGN KEY (language_code) REFERENCES languages(code) ON DELETE CASCADE,
|
|
||||||
INDEX idx_key (translation_key),
|
|
||||||
INDEX idx_language (language_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- API Keys table
|
|
||||||
CREATE TABLE IF NOT EXISTS api_keys (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
service_name VARCHAR(50) NOT NULL UNIQUE COMMENT 'Service name (e.g., openrouter)',
|
|
||||||
api_key TEXT NOT NULL COMMENT 'API key value',
|
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_service (service_name),
|
|
||||||
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');
|
|
||||||
|
|
||||||
-- Insert supported languages
|
|
||||||
INSERT INTO languages (code, name, native_name) VALUES
|
|
||||||
('en', 'English', 'English'),
|
|
||||||
('ru', 'Russian', 'Русский'),
|
|
||||||
('es', 'Spanish', 'Español'),
|
|
||||||
('de', 'German', 'Deutsch'),
|
|
||||||
('fr', 'French', 'Français'),
|
|
||||||
('zh', 'Chinese', '中文')
|
|
||||||
ON DUPLICATE KEY UPDATE name=VALUES(name);
|
|
||||||
|
|
||||||
-- Insert English translations
|
|
||||||
INSERT INTO translations (language_code, translation_key, translation_value) VALUES
|
|
||||||
('en', 'auth.email', 'Email'),
|
|
||||||
('en', 'auth.login', 'Login'),
|
|
||||||
('en', 'auth.name', 'Name'),
|
|
||||||
('en', 'auth.password', 'Password'),
|
|
||||||
('en', 'auth.register', 'Register'),
|
|
||||||
('en', 'clients.actions', 'Actions'),
|
|
||||||
('en', 'clients.add', 'Add Client'),
|
|
||||||
('en', 'clients.create', 'Create Client'),
|
|
||||||
('en', 'clients.delete', 'Delete'),
|
|
||||||
('en', 'clients.download_config', 'Download Config'),
|
|
||||||
('en', 'clients.expiration', 'Expiration'),
|
|
||||||
('en', 'clients.expired', 'Expired'),
|
|
||||||
('en', 'clients.never_expires', 'Never expires'),
|
|
||||||
('en', 'clients.ip', 'IP Address'),
|
|
||||||
('en', 'clients.last_handshake', 'Last Handshake'),
|
|
||||||
('en', 'clients.name', 'Client Name'),
|
|
||||||
('en', 'clients.qr_code', 'QR Code'),
|
|
||||||
('en', 'clients.received', 'Received'),
|
|
||||||
('en', 'clients.restore', 'Restore'),
|
|
||||||
('en', 'clients.revoke', 'Revoke'),
|
|
||||||
('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'),
|
|
||||||
('en', 'dashboard.no_servers', 'No servers yet'),
|
|
||||||
('en', 'dashboard.quick_actions', 'Quick Actions'),
|
|
||||||
('en', 'dashboard.recent_servers', 'Recent Servers'),
|
|
||||||
('en', 'dashboard.title', 'Dashboard'),
|
|
||||||
('en', 'dashboard.total_clients', 'Total Clients'),
|
|
||||||
('en', 'dashboard.total_servers', 'Total Servers'),
|
|
||||||
('en', 'dashboard.total_traffic', 'Total Traffic'),
|
|
||||||
('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'),
|
|
||||||
('en', 'form.submit', 'Submit'),
|
|
||||||
('en', 'form.update', 'Update'),
|
|
||||||
('en', 'menu.clients', 'Clients'),
|
|
||||||
('en', 'menu.dashboard', 'Dashboard'),
|
|
||||||
('en', 'menu.logout', 'Logout'),
|
|
||||||
('en', 'menu.servers', 'Servers'),
|
|
||||||
('en', 'menu.settings', 'Settings'),
|
|
||||||
('en', 'menu.users', 'Users'),
|
|
||||||
('en', 'message.confirm', 'Are you sure?'),
|
|
||||||
('en', 'message.deleted', 'Deleted successfully'),
|
|
||||||
('en', 'message.deployed', 'Deployed successfully'),
|
|
||||||
('en', 'message.error', 'An error occurred'),
|
|
||||||
('en', 'message.saved', 'Saved successfully'),
|
|
||||||
('en', 'message.success', 'Operation completed successfully'),
|
|
||||||
('en', 'servers.actions', 'Actions'),
|
|
||||||
('en', 'servers.add', 'Add Server'),
|
|
||||||
('en', 'servers.clients', 'Clients'),
|
|
||||||
('en', 'servers.delete', 'Delete'),
|
|
||||||
('en', 'servers.deploy', 'Deploy'),
|
|
||||||
('en', 'servers.edit', 'Edit'),
|
|
||||||
('en', 'servers.host', 'Host'),
|
|
||||||
('en', 'servers.name', 'Name'),
|
|
||||||
('en', 'servers.port', 'Port'),
|
|
||||||
('en', 'servers.status', 'Status'),
|
|
||||||
('en', 'servers.title', 'Servers'),
|
|
||||||
('en', 'servers.view', 'View'),
|
|
||||||
('en', 'settings.actions', 'Actions'),
|
|
||||||
('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'),
|
|
||||||
('en', 'settings.error_key_test', 'API key test failed'),
|
|
||||||
('en', 'settings.for_translation', 'for auto-translation'),
|
|
||||||
('en', 'settings.get_key_at', 'Get your API key at'),
|
|
||||||
('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', '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);
|
|
||||||
+21
-5
@@ -395,8 +395,25 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
|||||||
requireAuth();
|
requireAuth();
|
||||||
$serverId = (int)$params['id'];
|
$serverId = (int)$params['id'];
|
||||||
$clientName = trim($_POST['name'] ?? '');
|
$clientName = trim($_POST['name'] ?? '');
|
||||||
$expiresInDays = !empty($_POST['expires_in_days']) ? (int)$_POST['expires_in_days'] : null;
|
|
||||||
$trafficLimitGb = !empty($_POST['traffic_limit_gb']) ? (float)$_POST['traffic_limit_gb'] : null;
|
// Handle expiration: either from dropdown (days) or custom input (seconds)
|
||||||
|
$expiresInDays = null;
|
||||||
|
if (!empty($_POST['expires_in_seconds'])) {
|
||||||
|
// Convert seconds to days (round up)
|
||||||
|
$expiresInDays = (int)ceil((int)$_POST['expires_in_seconds'] / 86400);
|
||||||
|
} elseif (!empty($_POST['expires_in_days']) && $_POST['expires_in_days'] !== 'custom') {
|
||||||
|
$expiresInDays = (int)$_POST['expires_in_days'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle traffic limit: either from dropdown (GB) or custom input (MB)
|
||||||
|
$trafficLimitBytes = null;
|
||||||
|
if (!empty($_POST['traffic_limit_mb'])) {
|
||||||
|
// Convert MB to bytes
|
||||||
|
$trafficLimitBytes = (int)((float)$_POST['traffic_limit_mb'] * 1048576);
|
||||||
|
} elseif (!empty($_POST['traffic_limit_gb']) && $_POST['traffic_limit_gb'] !== 'custom') {
|
||||||
|
// Convert GB to bytes
|
||||||
|
$trafficLimitBytes = (int)((float)$_POST['traffic_limit_gb'] * 1073741824);
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($clientName)) {
|
if (empty($clientName)) {
|
||||||
redirect('/servers/' . $serverId . '?error=Client+name+is+required');
|
redirect('/servers/' . $serverId . '?error=Client+name+is+required');
|
||||||
@@ -418,10 +435,9 @@ Router::post('/servers/{id}/clients/create', function ($params) {
|
|||||||
$clientId = VpnClient::create($serverId, $user['id'], $clientName, $expiresInDays);
|
$clientId = VpnClient::create($serverId, $user['id'], $clientName, $expiresInDays);
|
||||||
|
|
||||||
// Set traffic limit if specified
|
// Set traffic limit if specified
|
||||||
if ($trafficLimitGb !== null) {
|
if ($trafficLimitBytes !== null && $trafficLimitBytes > 0) {
|
||||||
$client = new VpnClient($clientId);
|
$client = new VpnClient($clientId);
|
||||||
$limitBytes = (int)($trafficLimitGb * 1073741824); // Convert GB to bytes
|
$client->setTrafficLimit($trafficLimitBytes);
|
||||||
$client->setTrafficLimit($limitBytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect('/clients/' . $clientId);
|
redirect('/clients/' . $clientId);
|
||||||
|
|||||||
@@ -64,6 +64,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="bg-white rounded shadow p-6">
|
||||||
|
<h3 class="font-bold mb-4">{{ t('clients.expiration') }}</h3>
|
||||||
|
<form onsubmit="updateExpiration(event, {{ client.id }})" class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
||||||
|
<select id="expirationSelect" class="w-full px-3 py-2 border rounded mb-2" onchange="toggleExpirationEdit()">
|
||||||
|
<option value="" selected disabled>-- Select from list --</option>
|
||||||
|
<option value="never">{{ t('clients.never_expires') }}</option>
|
||||||
|
<option value="7">7 {{ t('common.days') }}</option>
|
||||||
|
<option value="30">30 {{ t('common.days') }}</option>
|
||||||
|
<option value="60">60 {{ t('common.days') }}</option>
|
||||||
|
<option value="90">90 {{ t('common.days') }}</option>
|
||||||
|
<option value="180">180 {{ t('common.days') }}</option>
|
||||||
|
<option value="365">365 {{ t('common.days') }}</option>
|
||||||
|
<option value="custom">{{ t('clients.custom_seconds') }}</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="expirationSeconds" class="w-full px-3 py-2 border rounded" placeholder="{{ t('clients.enter_seconds') }}" style="display:none;" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-blue-50 border border-blue-200 rounded">
|
||||||
|
<div class="text-xs text-blue-600 font-semibold mb-1">CURRENT VALUE:</div>
|
||||||
|
<div class="text-sm text-blue-900 font-medium" id="currentExpiration">
|
||||||
|
{% 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 %}
|
||||||
|
{{ client.expires_at|date('Y-m-d H:i:s') }} ({{ diff_days }} {{ t('common.days') }})
|
||||||
|
{% else %}
|
||||||
|
{{ t('clients.never_expires') }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded w-full">
|
||||||
|
<i class="fas fa-save"></i> {{ t('form.update') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded shadow p-6">
|
||||||
|
<h3 class="font-bold mb-4">{{ t('clients.traffic_limit') }}</h3>
|
||||||
|
<form onsubmit="updateTrafficLimit(event, {{ client.id }})" class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.traffic_limit') }}</label>
|
||||||
|
<select id="trafficSelect" class="w-full px-3 py-2 border rounded mb-2" onchange="toggleTrafficEdit()">
|
||||||
|
<option value="" selected disabled>-- Select from list --</option>
|
||||||
|
<option value="unlimited">{{ t('clients.unlimited') }}</option>
|
||||||
|
<option value="1">1 GB</option>
|
||||||
|
<option value="5">5 GB</option>
|
||||||
|
<option value="10">10 GB</option>
|
||||||
|
<option value="25">25 GB</option>
|
||||||
|
<option value="50">50 GB</option>
|
||||||
|
<option value="100">100 GB</option>
|
||||||
|
<option value="250">250 GB</option>
|
||||||
|
<option value="500">500 GB</option>
|
||||||
|
<option value="1000">1000 GB (1 TB)</option>
|
||||||
|
<option value="custom">{{ t('clients.custom_mb') }}</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="trafficMegabytes" class="w-full px-3 py-2 border rounded" placeholder="{{ t('clients.enter_megabytes') }}" style="display:none;" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-blue-50 border border-blue-200 rounded">
|
||||||
|
<div class="text-xs text-blue-600 font-semibold mb-1">CURRENT VALUE:</div>
|
||||||
|
<div class="text-sm text-blue-900 font-medium" id="currentTrafficLimit">
|
||||||
|
{% if client.traffic_limit %}
|
||||||
|
{% set limit_gb = (client.traffic_limit / 1073741824)|number_format(2) %}
|
||||||
|
{% set total_traffic = (client.bytes_sent|default(0) + client.bytes_received|default(0)) %}
|
||||||
|
{% set used_gb = (total_traffic / 1073741824)|number_format(2) %}
|
||||||
|
{{ limit_gb }} GB (used: {{ used_gb }} GB)
|
||||||
|
{% else %}
|
||||||
|
{{ t('clients.unlimited') }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded w-full">
|
||||||
|
<i class="fas fa-save"></i> {{ t('form.update') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if client.qr_code %}
|
{% if client.qr_code %}
|
||||||
<div class="bg-white rounded shadow p-6 text-center">
|
<div class="bg-white rounded shadow p-6 text-center">
|
||||||
<h3 class="font-bold mb-4">QR Code</h3>
|
<h3 class="font-bold mb-4">QR Code</h3>
|
||||||
@@ -74,6 +153,152 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function toggleExpirationEdit() {
|
||||||
|
const select = document.getElementById('expirationSelect');
|
||||||
|
const input = document.getElementById('expirationSeconds');
|
||||||
|
if (select.value === 'custom') {
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.required = true;
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
input.style.display = 'none';
|
||||||
|
input.required = false;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTrafficEdit() {
|
||||||
|
const select = document.getElementById('trafficSelect');
|
||||||
|
const input = document.getElementById('trafficMegabytes');
|
||||||
|
if (select.value === 'custom') {
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.required = true;
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
input.style.display = 'none';
|
||||||
|
input.required = false;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateExpiration(event, clientId) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const select = document.getElementById('expirationSelect');
|
||||||
|
const customInput = document.getElementById('expirationSeconds');
|
||||||
|
|
||||||
|
let expiresAt = null;
|
||||||
|
let displayText = '{{ t("clients.never_expires") }}';
|
||||||
|
|
||||||
|
if (select.value === 'custom' && customInput.value) {
|
||||||
|
// Convert seconds to timestamp
|
||||||
|
const seconds = parseInt(customInput.value);
|
||||||
|
const date = new Date(Date.now() + seconds * 1000);
|
||||||
|
expiresAt = date.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
|
const days = Math.round(seconds / 86400);
|
||||||
|
displayText = `${expiresAt} (${days} {{ t("common.days") }})`;
|
||||||
|
} else if (select.value === 'never') {
|
||||||
|
// Set to unlimited
|
||||||
|
expiresAt = null;
|
||||||
|
displayText = '{{ t("clients.never_expires") }}';
|
||||||
|
} else if (select.value && select.value !== 'custom' && select.value !== '') {
|
||||||
|
// Convert days to timestamp
|
||||||
|
const days = parseInt(select.value);
|
||||||
|
const date = new Date(Date.now() + days * 86400 * 1000);
|
||||||
|
expiresAt = date.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
|
displayText = `${expiresAt} (${days} {{ t("common.days") }})`;
|
||||||
|
} else {
|
||||||
|
alert('Please select an option');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/clients/${clientId}/set-expiration`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ expires_at: expiresAt })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success !== false) {
|
||||||
|
alert('Expiration updated successfully');
|
||||||
|
document.getElementById('currentExpiration').textContent = displayText;
|
||||||
|
// Reset form
|
||||||
|
select.value = '';
|
||||||
|
customInput.value = '';
|
||||||
|
customInput.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTrafficLimit(event, clientId) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const select = document.getElementById('trafficSelect');
|
||||||
|
const customInput = document.getElementById('trafficMegabytes');
|
||||||
|
|
||||||
|
let limitBytes = null;
|
||||||
|
let displayText = '{{ t("clients.unlimited") }}';
|
||||||
|
|
||||||
|
if (select.value === 'custom' && customInput.value) {
|
||||||
|
// Convert MB to bytes
|
||||||
|
limitBytes = parseInt(customInput.value) * 1048576;
|
||||||
|
const limitGb = (limitBytes / 1073741824).toFixed(2);
|
||||||
|
displayText = `${limitGb} GB`;
|
||||||
|
} else if (select.value === 'unlimited') {
|
||||||
|
// Set to unlimited
|
||||||
|
limitBytes = null;
|
||||||
|
displayText = '{{ t("clients.unlimited") }}';
|
||||||
|
} else if (select.value && select.value !== 'custom' && select.value !== '') {
|
||||||
|
// Convert GB to bytes
|
||||||
|
limitBytes = parseFloat(select.value) * 1073741824;
|
||||||
|
displayText = `${parseFloat(select.value).toFixed(2)} GB`;
|
||||||
|
} else {
|
||||||
|
alert('Please select an option');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/clients/${clientId}/set-traffic-limit`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ limit_bytes: limitBytes })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success !== false) {
|
||||||
|
alert('Traffic limit updated successfully');
|
||||||
|
// Update current value display (keep usage info if available)
|
||||||
|
const currentDiv = document.getElementById('currentTrafficLimit');
|
||||||
|
const usageMatch = currentDiv.textContent.match(/used: ([\d.]+) GB/);
|
||||||
|
if (usageMatch && limitBytes !== null) {
|
||||||
|
displayText += ` (used: ${usageMatch[1]} GB)`;
|
||||||
|
}
|
||||||
|
currentDiv.textContent = displayText;
|
||||||
|
// Reset form
|
||||||
|
select.value = '';
|
||||||
|
customInput.value = '';
|
||||||
|
customInput.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function syncStats(clientId) {
|
async function syncStats(clientId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/clients/${clientId}/sync-stats`, {
|
const response = await fetch(`/clients/${clientId}/sync-stats`, {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
|
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
||||||
<select name="expires_in_days" class="w-full px-3 py-2 border rounded">
|
<select name="expires_in_days" class="w-full px-3 py-2 border rounded mb-2" id="expirationSelect" onchange="toggleExpirationInput()">
|
||||||
<option value="" selected>{{ t('clients.never_expires') }}</option>
|
<option value="" selected>{{ t('clients.never_expires') }}</option>
|
||||||
<option value="7">7 {{ t('common.days') }}</option>
|
<option value="7">7 {{ t('common.days') }}</option>
|
||||||
<option value="30">30 {{ t('common.days') }}</option>
|
<option value="30">30 {{ t('common.days') }}</option>
|
||||||
@@ -26,11 +26,13 @@
|
|||||||
<option value="90">90 {{ t('common.days') }}</option>
|
<option value="90">90 {{ t('common.days') }}</option>
|
||||||
<option value="180">180 {{ t('common.days') }}</option>
|
<option value="180">180 {{ t('common.days') }}</option>
|
||||||
<option value="365">365 {{ t('common.days') }}</option>
|
<option value="365">365 {{ t('common.days') }}</option>
|
||||||
|
<option value="custom">{{ t('clients.custom_seconds') }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="number" name="expires_in_seconds" id="expirationSeconds" class="w-full px-3 py-2 border rounded" placeholder="{{ t('clients.enter_seconds') }}" style="display:none;" min="1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.traffic_limit') }}</label>
|
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.traffic_limit') }}</label>
|
||||||
<select name="traffic_limit_gb" class="w-full px-3 py-2 border rounded">
|
<select name="traffic_limit_gb" class="w-full px-3 py-2 border rounded mb-2" id="trafficSelect" onchange="toggleTrafficInput()">
|
||||||
<option value="" selected>{{ t('clients.unlimited') }}</option>
|
<option value="" selected>{{ t('clients.unlimited') }}</option>
|
||||||
<option value="1">1 GB</option>
|
<option value="1">1 GB</option>
|
||||||
<option value="5">5 GB</option>
|
<option value="5">5 GB</option>
|
||||||
@@ -41,7 +43,9 @@
|
|||||||
<option value="250">250 GB</option>
|
<option value="250">250 GB</option>
|
||||||
<option value="500">500 GB</option>
|
<option value="500">500 GB</option>
|
||||||
<option value="1000">1000 GB (1 TB)</option>
|
<option value="1000">1000 GB (1 TB)</option>
|
||||||
|
<option value="custom">{{ t('clients.custom_mb') }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="number" name="traffic_limit_mb" id="trafficMegabytes" class="w-full px-3 py-2 border rounded" placeholder="{{ t('clients.enter_megabytes') }}" style="display:none;" min="1">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded w-full" id="createClientBtn">
|
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded w-full" id="createClientBtn">
|
||||||
<span id="createClientText">{{ t('form.create') }}</span>
|
<span id="createClientText">{{ t('form.create') }}</span>
|
||||||
@@ -182,6 +186,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function toggleExpirationInput() {
|
||||||
|
const select = document.getElementById('expirationSelect');
|
||||||
|
const input = document.getElementById('expirationSeconds');
|
||||||
|
if (select.value === 'custom') {
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.required = true;
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
input.style.display = 'none';
|
||||||
|
input.required = false;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTrafficInput() {
|
||||||
|
const select = document.getElementById('trafficSelect');
|
||||||
|
const input = document.getElementById('trafficMegabytes');
|
||||||
|
if (select.value === 'custom') {
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.required = true;
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
input.style.display = 'none';
|
||||||
|
input.required = false;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.getElementById('createClientForm');
|
const form = document.getElementById('createClientForm');
|
||||||
if (form) {
|
if (form) {
|
||||||
|
|||||||
Reference in New Issue
Block a user