feat: ssh auth, protocol management, and cleanup

This commit is contained in:
infosave2007
2026-01-23 17:55:40 +03:00
parent 4995147bad
commit ea82b78a7d
70 changed files with 16225 additions and 986 deletions
+203 -10
View File
@@ -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;
}