feat: ssh auth, protocol management, and cleanup
This commit is contained in:
+203
-10
@@ -60,8 +60,50 @@
|
||||
<div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
|
||||
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
|
||||
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<button type="button" id="uninstallAllBtn" class="px-3 py-1 bg-gray-600 text-white rounded text-sm">Удалить все протоколы</button>
|
||||
<span id="uninstallMsg" class="ml-3 text-sm text-gray-600"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm text-gray-600 mb-1">Добавить протокол</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select id="availableProtocolSelect" class="px-3 py-2 border rounded">
|
||||
{% for p in available_protocols %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="activateProtocolBtn" class="px-3 py-1 bg-green-600 text-white rounded text-sm">Установить</button>
|
||||
<span id="activateMsg" class="ml-3 text-sm text-gray-600"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Установка протоколов выполняется только через Настройки -->
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm text-gray-600 mb-1">Установленные протоколы</label>
|
||||
<div class="space-y-2">
|
||||
{% for sp in server_protocols %}
|
||||
<div class="border rounded px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium">{{ sp.name }} <span class="text-gray-500">({{ sp.slug }})</span></div>
|
||||
<button type="button" class="px-3 py-1 bg-red-600 text-white rounded text-sm btn-uninstall-sp" data-slug="{{ sp.slug }}">Удалить</button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
{% if sp.server_host %}<span>Host: {{ sp.server_host }}</span>{% endif %}
|
||||
{% if sp.server_port %}<span class="ml-2">Port: {{ sp.server_port }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-500">Нет установленных протоколов</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="uninstallSpMsg" class="mt-2 text-sm text-gray-600"></div>
|
||||
</div>
|
||||
|
||||
{% if server.status == 'active' %}
|
||||
<div class="metric-mini" id="serverMetrics">
|
||||
<div class="metric-row">
|
||||
@@ -96,7 +138,17 @@
|
||||
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="space-y-3" id="createClientForm">
|
||||
<div>
|
||||
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
|
||||
<p class="text-xs text-gray-500 mt-1">Spaces will be replaced with underscore. All characters allowed including Cyrillic.</p>
|
||||
</div>
|
||||
<div>
|
||||
<input name="login" placeholder="Логин (уникально на сервере, пусто — из имени)" class="w-full px-3 py-2 border rounded" id="clientLogin">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">{{ t('ai.protocol_type') }}</label>
|
||||
<select name="protocol_id" class="w-full px-3 py-2 border rounded">
|
||||
{% for sp in server_protocols %}
|
||||
<option value="{{ sp.protocol_id }}">{{ sp.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
||||
@@ -136,6 +188,31 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded shadow p-6 mb-8">
|
||||
<h3 class="font-bold mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-file-import text-purple-500"></i>
|
||||
{{ t('servers.config_import_title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">{{ t('servers.config_import_hint') }}</p>
|
||||
<form method="POST" action="/servers/{{ server.id }}/config/import" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_type_label') }}</label>
|
||||
<select name="import_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="panel_backup">{{ t('servers.config_import_type_panel') }}</option>
|
||||
<option value="amnezia_app">{{ t('servers.config_import_type_amnezia') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_file_label') }}</label>
|
||||
<input type="file" name="config_file" accept=".json,.backup" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<p class="text-xs text-gray-500 mt-1">{{ t('servers.config_import_file_hint') }}</p>
|
||||
</div>
|
||||
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded">
|
||||
<i class="fas fa-upload mr-2"></i>{{ t('servers.config_import_submit') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Backup Section -->
|
||||
<div class="bg-white rounded shadow mb-8">
|
||||
@@ -154,15 +231,28 @@
|
||||
<div class="bg-white rounded shadow">
|
||||
<div class="px-6 py-4 border-b flex justify-between items-center">
|
||||
<h3 class="font-bold">{{ t('clients.title') }} ({{ clients|length }})</h3>
|
||||
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
||||
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<form method="GET" action="/servers/{{ server.id }}" class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">{{ t('ai.protocol_type') }}</label>
|
||||
<select name="protocol_id" class="px-3 py-2 border rounded" onchange="this.form.submit()">
|
||||
<option value="">Все</option>
|
||||
{% for sp in server_protocols %}
|
||||
<option value="{{ sp.protocol_id }}" {% if selected_protocol_id == sp.protocol_id %}selected{% endif %}>{{ sp.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
||||
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if clients|length > 0 %}
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.name') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Логин</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ai.protocol_type') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.ip') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.status') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.expiration') }}</th>
|
||||
@@ -177,6 +267,14 @@
|
||||
{% for client in clients %}
|
||||
<tr class="border-t">
|
||||
<td class="px-6 py-4">{{ client.name }}</td>
|
||||
<td class="px-6 py-4">{{ client.login|default('-') }}</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if client.protocol_name %}
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if client.status == 'active' %}
|
||||
@@ -286,6 +384,102 @@ function toggleExpirationInput() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const uninstallAllBtn = document.getElementById('uninstallAllBtn');
|
||||
const msg = document.getElementById('uninstallMsg');
|
||||
|
||||
if (uninstallAllBtn) {
|
||||
uninstallAllBtn.addEventListener('click', async function(e) {
|
||||
console.log('uninstallAllBtn clicked');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!confirm('Удалить все Amnezia-контейнеры на сервере?')) {
|
||||
console.log('User canceled');
|
||||
return;
|
||||
}
|
||||
console.log('Starting uninstall all...');
|
||||
uninstallAllBtn.disabled = true;
|
||||
msg.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление всех контейнеров...</span>';
|
||||
try {
|
||||
const res = await fetch(`/servers/{{ server.id }}/protocols/uninstall-all`, { method: 'POST', credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
console.log('Response:', data);
|
||||
if (data.success) {
|
||||
msg.textContent = data.message || 'Успешно';
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} else {
|
||||
msg.textContent = data.error || 'Ошибка';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
msg.textContent = e.message;
|
||||
}
|
||||
uninstallAllBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
const activateBtn = document.getElementById('activateProtocolBtn');
|
||||
if (activateBtn) {
|
||||
activateBtn.addEventListener('click', async function() {
|
||||
const select = document.getElementById('availableProtocolSelect');
|
||||
const msg2 = document.getElementById('activateMsg');
|
||||
msg2.textContent = '';
|
||||
const pid = select ? select.value : '';
|
||||
if (!pid) { msg2.textContent = 'Нет доступных протоколов'; return; }
|
||||
activateBtn.disabled = true;
|
||||
msg2.innerHTML = '<i class="fas fa-circle-notch fa-spin text-blue-600 mr-2"></i><span class="text-gray-700">Установка протокола...</span>';
|
||||
try {
|
||||
const res = await fetch('/servers/{{ server.id }}/protocols/activate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'protocol_id=' + encodeURIComponent(pid)
|
||||
});
|
||||
let data;
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) { data = await res.json(); } else { data = { error: await res.text() }; }
|
||||
if (res.ok && data && data.success !== false && !data.error) {
|
||||
msg2.textContent = 'Готово';
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
msg2.textContent = (data && data.error) ? data.error : ('Ошибка установки (' + res.status + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
msg2.textContent = e.message || 'Ошибка связи';
|
||||
}
|
||||
activateBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.btn-uninstall-sp').forEach(btn => {
|
||||
btn.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('Удалить протокол и всех его клиентов?')) return;
|
||||
const slug = btn.getAttribute('data-slug');
|
||||
const m = document.getElementById('uninstallSpMsg');
|
||||
m.textContent = '';
|
||||
btn.disabled = true;
|
||||
m.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление протокола...</span>';
|
||||
try {
|
||||
const resp = await fetch('/servers/{{ server.id }}/protocols/' + encodeURIComponent(slug) + '/uninstall', { method: 'POST', credentials: 'same-origin' });
|
||||
let data;
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) { data = await resp.json(); } else { data = { error: await resp.text() }; }
|
||||
if (resp.ok && data && !data.error) {
|
||||
m.textContent = 'Удалено. Клиенты: ' + (data.clients_removed || 0);
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
m.textContent = (data && data.error) ? data.error : ('Ошибка удаления (' + resp.status + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
m.textContent = e.message || 'Ошибка связи';
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
function toggleTrafficInput() {
|
||||
const select = document.getElementById('trafficSelect');
|
||||
const input = document.getElementById('trafficMegabytes');
|
||||
@@ -304,12 +498,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('createClientForm');
|
||||
const clientNameInput = document.getElementById('clientName');
|
||||
|
||||
// Auto-replace spaces with underscores
|
||||
if (clientNameInput) {
|
||||
clientNameInput.addEventListener('input', function(e) {
|
||||
// Replace only spaces with underscore, allow all other characters including Cyrillic
|
||||
const clientLoginInput = document.getElementById('clientLogin');
|
||||
if (clientLoginInput) {
|
||||
clientLoginInput.addEventListener('input', function(e) {
|
||||
let value = e.target.value;
|
||||
let sanitized = value.replace(/ /g, '_');
|
||||
let sanitized = value.replace(/\s+/g, '_');
|
||||
if (value !== sanitized) {
|
||||
e.target.value = sanitized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user