feat: добавлена возможность импорта из wg-easy и 3x-ui панелей
Основные изменения:
- Создан класс PanelImporter для парсинга и импорта клиентов
- Добавлена поддержка wg-easy (db.json)
- Добавлена поддержка 3x-ui (export JSON)
- Создана таблица panel_imports для отслеживания истории
- Добавлен UI для загрузки backup файлов при создании сервера
- Добавлены API endpoints: POST /api/servers/{id}/import и GET /api/servers/{id}/imports
- Автоматический импорт после деплоя сервера
- Переводы на всех 6 языках (EN, RU, ES, DE, FR, ZH)
- Обновлена документация в README
Функционал:
- Импорт клиентов с сохранением ключей и IP (wg-easy)
- Импорт клиентов с автогенерацией ключей (3x-ui)
- Поддержка экспирации и лимитов трафика из исходных панелей
- История импортов с информацией о количестве клиентов
- Обработка ошибок с детальным логированием
This commit is contained in:
@@ -5,6 +5,7 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
|
||||
## Features
|
||||
|
||||
- VPN server deployment via SSH
|
||||
- **Import from existing VPN panels** (wg-easy, 3x-ui)
|
||||
- Client configuration management with **expiration dates**
|
||||
- **Traffic limits** for clients with automatic enforcement
|
||||
- **Server backup and restore** functionality
|
||||
@@ -57,8 +58,13 @@ JWT_SECRET=your-secret-key-change-this
|
||||
|
||||
1. Servers → Add Server
|
||||
2. Enter: name, host IP, SSH port, username, password
|
||||
3. Click Deploy Server
|
||||
4. Wait for deployment
|
||||
3. **(Optional) Enable import from existing panel:**
|
||||
- Check "Import from existing panel"
|
||||
- Select panel type (wg-easy or 3x-ui)
|
||||
- Upload backup file (JSON)
|
||||
4. Click "Create Server"
|
||||
5. Wait for deployment
|
||||
6. Clients will be imported automatically if import was enabled
|
||||
|
||||
### Create Client
|
||||
|
||||
@@ -221,6 +227,13 @@ POST /api/servers/{id}/restore - Restore from backup
|
||||
DELETE /api/backups/{id} - Delete backup
|
||||
```
|
||||
|
||||
### Panel Import
|
||||
```
|
||||
POST /api/servers/{id}/import - Import clients from existing panel
|
||||
Parameters: panel_type (wg-easy|3x-ui), backup_file (multipart/form-data)
|
||||
GET /api/servers/{id}/imports - Get import history for server
|
||||
```
|
||||
|
||||
## Translation
|
||||
|
||||
Add OpenRouter API key in Settings, then run:
|
||||
@@ -244,6 +257,7 @@ inc/ - Core classes
|
||||
Translator.php - Multi-language
|
||||
JWT.php - Token auth
|
||||
QrUtil.php - QR code generation
|
||||
PanelImporter.php - Import from wg-easy/3x-ui
|
||||
templates/ - Twig templates
|
||||
migrations/ - SQL migrations (executed in alphabetical order)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PanelImporter - Import clients from other VPN panels
|
||||
*
|
||||
* Supports:
|
||||
* - wg-easy: Import from db.json backup
|
||||
* - 3x-ui: Import from panel export
|
||||
*/
|
||||
class PanelImporter
|
||||
{
|
||||
private int $serverId;
|
||||
private int $userId;
|
||||
private string $panelType;
|
||||
private array $importData;
|
||||
|
||||
public function __construct(int $serverId, int $userId, string $panelType)
|
||||
{
|
||||
$this->serverId = $serverId;
|
||||
$this->userId = $userId;
|
||||
$this->panelType = $panelType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate backup file
|
||||
*/
|
||||
public function parseBackupFile(string $jsonContent): bool
|
||||
{
|
||||
$data = json_decode($jsonContent, true);
|
||||
|
||||
if (!$data) {
|
||||
throw new Exception('Invalid JSON format');
|
||||
}
|
||||
|
||||
$this->importData = $data;
|
||||
|
||||
// Validate based on panel type
|
||||
switch ($this->panelType) {
|
||||
case 'wg-easy':
|
||||
return $this->validateWgEasy($data);
|
||||
case '3x-ui':
|
||||
return $this->validate3xUi($data);
|
||||
default:
|
||||
throw new Exception('Unsupported panel type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate wg-easy backup format
|
||||
*/
|
||||
private function validateWgEasy(array $data): bool
|
||||
{
|
||||
if (!isset($data['clients']) || !is_array($data['clients'])) {
|
||||
throw new Exception('Invalid wg-easy format: missing clients array');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate 3x-ui export format
|
||||
*/
|
||||
private function validate3xUi(array $data): bool
|
||||
{
|
||||
if (!isset($data['clients']) || !is_array($data['clients'])) {
|
||||
throw new Exception('Invalid 3x-ui format: missing clients array');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import clients from parsed data
|
||||
*/
|
||||
public function import(): array
|
||||
{
|
||||
$importId = $this->createImportRecord();
|
||||
|
||||
try {
|
||||
$this->updateImportStatus($importId, 'processing');
|
||||
|
||||
$clients = $this->extractClients();
|
||||
$imported = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($clients as $clientData) {
|
||||
try {
|
||||
$clientId = $this->createClient($clientData);
|
||||
$imported[] = [
|
||||
'id' => $clientId,
|
||||
'name' => $clientData['name']
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$errors[] = [
|
||||
'name' => $clientData['name'],
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateImportRecord($importId, count($imported));
|
||||
$this->updateImportStatus($importId, 'completed');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'import_id' => $importId,
|
||||
'imported' => $imported,
|
||||
'errors' => $errors,
|
||||
'total' => count($clients),
|
||||
'imported_count' => count($imported),
|
||||
'error_count' => count($errors)
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->updateImportStatus($importId, 'failed', $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract client data based on panel type
|
||||
*/
|
||||
private function extractClients(): array
|
||||
{
|
||||
switch ($this->panelType) {
|
||||
case 'wg-easy':
|
||||
return $this->extractWgEasyClients();
|
||||
case '3x-ui':
|
||||
return $this->extract3xUiClients();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract clients from wg-easy backup
|
||||
*/
|
||||
private function extractWgEasyClients(): array
|
||||
{
|
||||
$clients = [];
|
||||
|
||||
foreach ($this->importData['clients'] as $client) {
|
||||
$clients[] = [
|
||||
'name' => $client['name'] ?? 'imported_client_' . uniqid(),
|
||||
'public_key' => $client['publicKey'] ?? null,
|
||||
'private_key' => $client['privateKey'] ?? null,
|
||||
'preshared_key' => $client['preSharedKey'] ?? null,
|
||||
'address' => $client['address'] ?? null,
|
||||
'enabled' => $client['enabled'] ?? true,
|
||||
'created_at' => $client['createdAt'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
return $clients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract clients from 3x-ui export
|
||||
*/
|
||||
private function extract3xUiClients(): array
|
||||
{
|
||||
$clients = [];
|
||||
|
||||
foreach ($this->importData['clients'] as $client) {
|
||||
// 3x-ui uses email as client name
|
||||
$name = $client['email'] ?? 'imported_client_' . uniqid();
|
||||
|
||||
$clients[] = [
|
||||
'name' => $name,
|
||||
'public_key' => null, // Will be generated
|
||||
'private_key' => null, // Will be generated
|
||||
'preshared_key' => null,
|
||||
'address' => null, // Will be assigned from pool
|
||||
'enabled' => $client['enable'] ?? true,
|
||||
'expiry_time' => $client['expiryTime'] ?? 0,
|
||||
'total_gb' => $client['totalGB'] ?? 0
|
||||
];
|
||||
}
|
||||
|
||||
return $clients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create client from imported data
|
||||
*/
|
||||
private function createClient(array $clientData): int
|
||||
{
|
||||
// If we have keys and address from wg-easy, use them
|
||||
if (!empty($clientData['public_key']) && !empty($clientData['address'])) {
|
||||
return $this->createClientWithKeys($clientData);
|
||||
}
|
||||
|
||||
// Otherwise, create new client with generated keys
|
||||
return $this->createNewClient($clientData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create client with existing keys (wg-easy import)
|
||||
*/
|
||||
private function createClientWithKeys(array $clientData): int
|
||||
{
|
||||
$db = DB::conn();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO vpn_clients (
|
||||
server_id, user_id, name, client_ip,
|
||||
public_key, private_key, preshared_key,
|
||||
status, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$status = ($clientData['enabled'] ?? true) ? 'active' : 'disabled';
|
||||
$privateKey = $clientData['private_key'] ?? '';
|
||||
$presharedKey = $clientData['preshared_key'] ?? '';
|
||||
|
||||
$stmt->execute([
|
||||
$this->serverId,
|
||||
$this->userId,
|
||||
$clientData['name'],
|
||||
$clientData['address'],
|
||||
$clientData['public_key'],
|
||||
$privateKey,
|
||||
$presharedKey,
|
||||
$status
|
||||
]);
|
||||
|
||||
$clientId = (int)$db->lastInsertId();
|
||||
|
||||
// Generate config and QR code
|
||||
$client = new VpnClient($clientId);
|
||||
$client->generateConfig();
|
||||
|
||||
return $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new client with generated keys (3x-ui import)
|
||||
*/
|
||||
private function createNewClient(array $clientData): int
|
||||
{
|
||||
// Calculate expiration if provided
|
||||
$expiresInDays = null;
|
||||
if (!empty($clientData['expiry_time']) && $clientData['expiry_time'] > 0) {
|
||||
$expiryTimestamp = $clientData['expiry_time'] / 1000; // Convert from ms
|
||||
$daysUntilExpiry = ceil(($expiryTimestamp - time()) / 86400);
|
||||
if ($daysUntilExpiry > 0) {
|
||||
$expiresInDays = (int)$daysUntilExpiry;
|
||||
}
|
||||
}
|
||||
|
||||
// Create client using standard method
|
||||
$clientId = VpnClient::create(
|
||||
$this->serverId,
|
||||
$this->userId,
|
||||
$clientData['name'],
|
||||
$expiresInDays
|
||||
);
|
||||
|
||||
// Set traffic limit if provided
|
||||
if (!empty($clientData['total_gb']) && $clientData['total_gb'] > 0) {
|
||||
$client = new VpnClient($clientId);
|
||||
$limitBytes = $clientData['total_gb'] * 1073741824; // GB to bytes
|
||||
$client->setTrafficLimit($limitBytes);
|
||||
}
|
||||
|
||||
// Set status
|
||||
if (isset($clientData['enabled']) && !$clientData['enabled']) {
|
||||
$db = DB::conn();
|
||||
$stmt = $db->prepare("UPDATE vpn_clients SET status = 'disabled' WHERE id = ?");
|
||||
$stmt->execute([$clientId]);
|
||||
}
|
||||
|
||||
return $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create import record in database
|
||||
*/
|
||||
private function createImportRecord(): int
|
||||
{
|
||||
$db = DB::conn();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO panel_imports (
|
||||
server_id, panel_type, import_file_name,
|
||||
import_data, status, created_by
|
||||
) VALUES (?, ?, ?, ?, 'pending', ?)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$this->serverId,
|
||||
$this->panelType,
|
||||
'backup_' . date('Y-m-d_H-i-s') . '.json',
|
||||
json_encode($this->importData),
|
||||
$this->userId
|
||||
]);
|
||||
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import record with results
|
||||
*/
|
||||
private function updateImportRecord(int $importId, int $clientsImported): void
|
||||
{
|
||||
$db = DB::conn();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
UPDATE panel_imports
|
||||
SET clients_imported = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$clientsImported, $importId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import status
|
||||
*/
|
||||
private function updateImportStatus(int $importId, string $status, ?string $error = null): void
|
||||
{
|
||||
$db = DB::conn();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
UPDATE panel_imports
|
||||
SET status = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$status, $error, $importId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get import history for server
|
||||
*/
|
||||
public static function getImportHistory(int $serverId): array
|
||||
{
|
||||
$db = DB::conn();
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT
|
||||
pi.*,
|
||||
u.name as created_by_name
|
||||
FROM panel_imports pi
|
||||
LEFT JOIN users u ON pi.created_by = u.id
|
||||
WHERE pi.server_id = ?
|
||||
ORDER BY pi.created_at DESC
|
||||
");
|
||||
|
||||
$stmt->execute([$serverId]);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -307,5 +307,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('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)')
|
||||
('en', 'settings.skip_validation', 'Skip validation (save without testing)'),
|
||||
('en', 'servers.import_from_panel', 'Import from existing panel'),
|
||||
('en', 'servers.select_panel_type', 'Select panel type'),
|
||||
('en', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('en', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('en', 'servers.upload_backup_file', 'Upload backup file (JSON)'),
|
||||
('en', 'servers.import_in_progress', 'Import in progress...'),
|
||||
('en', 'servers.import_success', 'Successfully imported {0} clients'),
|
||||
('en', 'servers.import_failed', 'Import failed'),
|
||||
('en', 'servers.import_partial', 'Imported {0} of {1} clients'),
|
||||
('en', 'servers.import_history', 'Import History')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -134,5 +134,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('ru', 'users.delete_confirm', 'Удалить {0}?'),
|
||||
('ru', 'users.role', 'Роль'),
|
||||
('ru', 'users.role_admin', 'Администратор'),
|
||||
('ru', 'users.role_user', 'Пользователь')
|
||||
('ru', 'users.role_user', 'Пользователь'),
|
||||
('ru', 'servers.import_from_panel', 'Импорт из другой панели'),
|
||||
('ru', 'servers.select_panel_type', 'Выберите тип панели'),
|
||||
('ru', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('ru', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('ru', 'servers.upload_backup_file', 'Загрузите файл резервной копии (JSON)'),
|
||||
('ru', 'servers.import_in_progress', 'Импорт выполняется...'),
|
||||
('ru', 'servers.import_success', 'Успешно импортировано клиентов: {0}'),
|
||||
('ru', 'servers.import_failed', 'Ошибка импорта'),
|
||||
('ru', 'servers.import_partial', 'Импортировано {0} из {1} клиентов'),
|
||||
('ru', 'servers.import_history', 'История импорта')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -134,5 +134,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('es', 'users.delete_confirm', '¿Eliminar {0}?'),
|
||||
('es', 'users.role', 'Rol'),
|
||||
('es', 'users.role_admin', 'Administrador'),
|
||||
('es', 'users.role_user', 'Usuario')
|
||||
('es', 'users.role_user', 'Usuario'),
|
||||
('es', 'servers.import_from_panel', 'Importar desde panel existente'),
|
||||
('es', 'servers.select_panel_type', 'Seleccione tipo de panel'),
|
||||
('es', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('es', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('es', 'servers.upload_backup_file', 'Subir archivo de respaldo (JSON)'),
|
||||
('es', 'servers.import_in_progress', 'Importación en progreso...'),
|
||||
('es', 'servers.import_success', 'Se importaron {0} clientes correctamente'),
|
||||
('es', 'servers.import_failed', 'Error de importación'),
|
||||
('es', 'servers.import_partial', 'Importados {0} de {1} clientes'),
|
||||
('es', 'servers.import_history', 'Historial de importación')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -134,5 +134,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('de', 'users.delete_confirm', '{0} löschen?'),
|
||||
('de', 'users.role', 'Rolle'),
|
||||
('de', 'users.role_admin', 'Admin'),
|
||||
('de', 'users.role_user', 'Benutzer')
|
||||
('de', 'users.role_user', 'Benutzer'),
|
||||
('de', 'servers.import_from_panel', 'Import aus bestehendem Panel'),
|
||||
('de', 'servers.select_panel_type', 'Panel-Typ auswählen'),
|
||||
('de', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('de', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('de', 'servers.upload_backup_file', 'Backup-Datei hochladen (JSON)'),
|
||||
('de', 'servers.import_in_progress', 'Import läuft...'),
|
||||
('de', 'servers.import_success', '{0} Clients erfolgreich importiert'),
|
||||
('de', 'servers.import_failed', 'Import fehlgeschlagen'),
|
||||
('de', 'servers.import_partial', '{0} von {1} Clients importiert'),
|
||||
('de', 'servers.import_history', 'Import-Historie')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -134,5 +134,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('fr', 'users.delete_confirm', 'Supprimer {0} ?'),
|
||||
('fr', 'users.role', 'Rôle'),
|
||||
('fr', 'users.role_admin', 'Administrateur'),
|
||||
('fr', 'users.role_user', 'Utilisateur')
|
||||
('fr', 'users.role_user', 'Utilisateur'),
|
||||
('fr', 'servers.import_from_panel', 'Importer depuis un panel existant'),
|
||||
('fr', 'servers.select_panel_type', 'Sélectionnez le type de panel'),
|
||||
('fr', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('fr', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('fr', 'servers.upload_backup_file', 'Télécharger le fichier de sauvegarde (JSON)'),
|
||||
('fr', 'servers.import_in_progress', 'Importation en cours...'),
|
||||
('fr', 'servers.import_success', '{0} clients importés avec succès'),
|
||||
('fr', 'servers.import_failed', 'Échec de l''importation'),
|
||||
('fr', 'servers.import_partial', '{0} clients importés sur {1}'),
|
||||
('fr', 'servers.import_history', 'Historique d''importation')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -134,5 +134,15 @@ INSERT INTO translations (language_code, translation_key, translation_value) VAL
|
||||
('zh', 'users.delete_confirm', '删除 {0}?'),
|
||||
('zh', 'users.role', '角色'),
|
||||
('zh', 'users.role_admin', '管理员'),
|
||||
('zh', 'users.role_user', '用户')
|
||||
('zh', 'users.role_user', '用户'),
|
||||
('zh', 'servers.import_from_panel', '从现有面板导入'),
|
||||
('zh', 'servers.select_panel_type', '选择面板类型'),
|
||||
('zh', 'servers.panel_type_wgeasy', 'wg-easy'),
|
||||
('zh', 'servers.panel_type_3xui', '3x-ui'),
|
||||
('zh', 'servers.upload_backup_file', '上传备份文件 (JSON)'),
|
||||
('zh', 'servers.import_in_progress', '导入进行中...'),
|
||||
('zh', 'servers.import_success', '成功导入 {0} 个客户端'),
|
||||
('zh', 'servers.import_failed', '导入失败'),
|
||||
('zh', 'servers.import_partial', '已导入 {0}/{1} 个客户端'),
|
||||
('zh', 'servers.import_history', '导入历史')
|
||||
ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Add panel imports tracking table
|
||||
-- This migration adds functionality to track imports from other VPN panels
|
||||
|
||||
CREATE TABLE IF NOT EXISTS panel_imports (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
server_id INT UNSIGNED NOT NULL,
|
||||
panel_type ENUM('wg-easy', '3x-ui') NOT NULL,
|
||||
import_file_name VARCHAR(255) NOT NULL,
|
||||
clients_imported INT UNSIGNED DEFAULT 0,
|
||||
import_data JSON NULL COMMENT 'Original import data for reference',
|
||||
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
|
||||
error_message TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT UNSIGNED NULL,
|
||||
INDEX idx_server_id (server_id),
|
||||
INDEX idx_panel_type (panel_type),
|
||||
INDEX idx_status (status),
|
||||
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;
|
||||
@@ -18,6 +18,7 @@ require_once __DIR__ . '/../inc/VpnServer.php';
|
||||
require_once __DIR__ . '/../inc/VpnClient.php';
|
||||
require_once __DIR__ . '/../inc/Translator.php';
|
||||
require_once __DIR__ . '/../inc/JWT.php';
|
||||
require_once __DIR__ . '/../inc/PanelImporter.php';
|
||||
|
||||
// Load environment configuration
|
||||
Config::load(__DIR__ . '/../.env');
|
||||
@@ -252,6 +253,21 @@ Router::post('/servers/create', function () {
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
// Handle import if enabled
|
||||
if (!empty($_POST['enable_import']) && !empty($_POST['panel_type']) && isset($_FILES['backup_file'])) {
|
||||
$panelType = $_POST['panel_type'];
|
||||
|
||||
if (in_array($panelType, ['wg-easy', '3x-ui']) && $_FILES['backup_file']['error'] === UPLOAD_ERR_OK) {
|
||||
// Store import info in session for processing after deployment
|
||||
$_SESSION['pending_import'] = [
|
||||
'server_id' => $serverId,
|
||||
'panel_type' => $panelType,
|
||||
'backup_file' => $_FILES['backup_file']['tmp_name'],
|
||||
'backup_name' => $_FILES['backup_file']['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
redirect('/servers/' . $serverId . '/deploy');
|
||||
} catch (Exception $e) {
|
||||
View::render('servers/create.twig', ['error' => $e->getMessage()]);
|
||||
@@ -355,9 +371,48 @@ Router::get('/servers/{id}', function ($params) {
|
||||
// Get clients for this server
|
||||
$clients = VpnClient::listByServer($serverId);
|
||||
|
||||
// Check for pending import
|
||||
$importMessage = null;
|
||||
if (!empty($_SESSION['pending_import']) && $_SESSION['pending_import']['server_id'] == $serverId) {
|
||||
$pendingImport = $_SESSION['pending_import'];
|
||||
|
||||
// Only process import if server is active
|
||||
if ($serverData['status'] === 'active') {
|
||||
try {
|
||||
$backupContent = file_get_contents($pendingImport['backup_file']);
|
||||
|
||||
$importer = new PanelImporter($serverId, $user['id'], $pendingImport['panel_type']);
|
||||
$importer->parseBackupFile($backupContent);
|
||||
$result = $importer->import();
|
||||
|
||||
if ($result['success']) {
|
||||
$importMessage = [
|
||||
'type' => 'success',
|
||||
'text' => "Successfully imported {$result['imported_count']} clients"
|
||||
];
|
||||
}
|
||||
|
||||
// Clean up
|
||||
@unlink($pendingImport['backup_file']);
|
||||
unset($_SESSION['pending_import']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$importMessage = [
|
||||
'type' => 'error',
|
||||
'text' => 'Import failed: ' . $e->getMessage()
|
||||
];
|
||||
unset($_SESSION['pending_import']);
|
||||
}
|
||||
|
||||
// Refresh clients list after import
|
||||
$clients = VpnClient::listByServer($serverId);
|
||||
}
|
||||
}
|
||||
|
||||
View::render('servers/view.twig', [
|
||||
'server' => $serverData,
|
||||
'clients' => $clients,
|
||||
'import_message' => $importMessage,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
|
||||
@@ -802,6 +857,87 @@ Router::delete('/api/servers/{id}/delete', function ($params) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
|
||||
// API: Import from existing panel
|
||||
Router::post('/api/servers/{id}/import', function ($params) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
if (!$user) return;
|
||||
|
||||
$serverId = (int)$params['id'];
|
||||
|
||||
// Validate server ownership
|
||||
$server = VpnServer::getById($serverId);
|
||||
if (!$server || $server['user_id'] != $user['id']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Server not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$panelType = $_POST['panel_type'] ?? '';
|
||||
|
||||
if (!in_array($panelType, ['wg-easy', '3x-ui'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid panel type. Supported: wg-easy, 3x-ui']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
if (!isset($_FILES['backup_file']) || $_FILES['backup_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'No backup file uploaded']);
|
||||
return;
|
||||
}
|
||||
|
||||
$backupContent = file_get_contents($_FILES['backup_file']['tmp_name']);
|
||||
|
||||
try {
|
||||
$importer = new PanelImporter($serverId, $user['id'], $panelType);
|
||||
|
||||
if (!$importer->parseBackupFile($backupContent)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid backup file format']);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $importer->import();
|
||||
|
||||
echo json_encode($result);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Get import history
|
||||
Router::get('/api/servers/{id}/imports', function ($params) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = JWT::requireAuth();
|
||||
if (!$user) return;
|
||||
|
||||
$serverId = (int)$params['id'];
|
||||
|
||||
// Validate server ownership
|
||||
$server = VpnServer::getById($serverId);
|
||||
if (!$server || $server['user_id'] != $user['id']) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Server not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$imports = PanelImporter::getImportHistory($serverId);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'imports' => $imports
|
||||
]);
|
||||
});
|
||||
if (!$user) return;
|
||||
|
||||
$serverId = (int)$params['id'];
|
||||
|
||||
@@ -4,13 +4,54 @@
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold mb-8"><i class="fas fa-plus-circle text-purple-600"></i> Add New Server</h1>
|
||||
{% if error %}<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>{% endif %}
|
||||
<form method="POST" class="bg-white shadow rounded-lg p-6 space-y-6">
|
||||
<form method="POST" enctype="multipart/form-data" class="bg-white shadow rounded-lg p-6 space-y-6">
|
||||
<div><label class="block text-sm font-medium text-gray-700">Server Name</label><input name="name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">Host IP/Domain</label><input name="host" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Port</label><input name="port" type="number" value="22" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Username</label><input name="username" value="root" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Password</label><input name="password" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90"><i class="fas fa-save mr-2"></i>Create Server</button>
|
||||
|
||||
<!-- Import from existing panel -->
|
||||
<div class="border-t pt-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="enableImport" name="enable_import" class="h-4 w-4 text-purple-600 rounded" onchange="toggleImportFields()">
|
||||
<label for="enableImport" class="ml-2 text-sm font-medium text-gray-700">
|
||||
{{ t('servers.import_from_panel') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="importFields" style="display: none;" class="space-y-4 pl-6 border-l-2 border-purple-200">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.select_panel_type') }}</label>
|
||||
<select name="panel_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="">-- {{ t('servers.select_panel_type') }} --</option>
|
||||
<option value="wg-easy">{{ t('servers.panel_type_wgeasy') }}</option>
|
||||
<option value="3x-ui">{{ t('servers.panel_type_3xui') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.upload_backup_file') }}</label>
|
||||
<input type="file" name="backup_file" accept=".json" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
wg-easy: db.json | 3x-ui: export.json
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90">
|
||||
<i class="fas fa-save mr-2"></i>Create Server
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleImportFields() {
|
||||
const checkbox = document.getElementById('enableImport');
|
||||
const fields = document.getElementById('importFields');
|
||||
fields.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="mb-6"><h1 class="text-3xl font-bold">{{ server.name }}</h1><p class="text-gray-600">{{ server.host }}</p></div>
|
||||
|
||||
{% if import_message %}
|
||||
<div class="mb-6 {% if import_message.type == 'success' %}bg-green-50 border-green-400 text-green-700{% else %}bg-red-50 border-red-400 text-red-700{% endif %} border px-4 py-3 rounded">
|
||||
<i class="fas {% if import_message.type == 'success' %}fa-check-circle{% else %}fa-exclamation-circle{% endif %} mr-2"></i>
|
||||
{{ import_message.text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-white rounded shadow p-6">
|
||||
<h3 class="font-bold mb-4">Server Info</h3>
|
||||
|
||||
Reference in New Issue
Block a user