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:
infosave2007
2025-11-08 12:40:43 +03:00
parent bbb0fbeeb9
commit fc39346240
12 changed files with 643 additions and 10 deletions
+16 -2
View File
@@ -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)
```
+354
View File
@@ -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
View File
@@ -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);
+11 -1
View File
@@ -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);
+11 -1
View File
@@ -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);
+11 -1
View File
@@ -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);
+11 -1
View File
@@ -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);
+11 -1
View File
@@ -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);
+20
View File
@@ -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;
+136
View File
@@ -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'];
+43 -2
View File
@@ -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 %}
+8
View File
@@ -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>