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
+250
View File
@@ -0,0 +1,250 @@
{% extends "layout.twig" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
{% if scenario %}
{{ 'Редактирование сценария:' | trans }} {{ scenario.name }}
{% else %}
{{ 'Новый сценарий' | trans }}
{% endif %}
</h5>
</div>
<div class="card-body">
<form id="scenarioForm">
<input type="hidden" name="id" value="{{ scenario.id | default('') }}">
<div class="mb-3">
<label for="slug" class="form-label">{{ 'Уникальный идентификатор' | trans }} *</label>
<input type="text" class="form-control" id="slug" name="slug"
value="{{ scenario.slug | default('') }}" required
pattern="^[a-z0-9\-]+$" title="{{ 'Только строчные буквы, цифры и дефисы' | trans }}">
<small class="form-text text-muted">{{ 'например: xray-vless, openvpn-tls' | trans }}</small>
</div>
<div class="mb-3">
<label for="name" class="form-label">{{ 'Название протокола' | trans }} *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ scenario.name | default('') }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">{{ 'Описание' | trans }}</label>
<textarea class="form-control" id="description" name="description" rows="2">{{ scenario.description | default('') }}</textarea>
</div>
<div class="mb-3">
<label for="definition" class="form-label">{{ 'Определение сценария (JSON)' | trans }} *</label>
<textarea class="form-control font-monospace" id="definition" name="definition"
rows="20" required>{{ templateDefinition }}</textarea>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Структура JSON:' | trans }}</strong><br>
<code>{ "engine": "shell|builtin_awg", "metadata": {...}, "scripts": { "detect": "...", "install": "...", "restore": "..." } }</code>
</small>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Доступные переменные в скриптах:' | trans }}</strong><br>
<code>{{ "{{server.host}}, {{server.username}}, {{server.container_name}}, {{metadata.*}}" | trans }}</code>
</small>
<div id="jsonError" class="alert alert-danger mt-2" style="display: none;"></div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1"
{% if scenario.is_active ?? true %}checked{% endif %}>
<label class="form-check-label" for="is_active">
{{ 'Активный сценарий' | trans }}
</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> {{ 'Сохранить' | trans }}
</button>
<a href="/admin/scenarios" class="btn btn-secondary">
{{ 'Отмена' | trans }}
</a>
{% if scenario %}
<button type="button" class="btn btn-info ms-auto" id="testBtn">
<i class="fas fa-flask"></i> {{ 'Тест на сервере' | trans }}
</button>
{% endif %}
</div>
</form>
</div>
</div>
<!-- JSON Validation Helper -->
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="mb-0">{{ 'Справка по формату' | trans }}</h6>
</div>
<div class="card-body">
<h6>{{ 'Поля сценария:' | trans }}</h6>
<ul>
<li><strong>engine:</strong> Тип движка ("shell" или "builtin_awg")</li>
<li><strong>metadata:</strong> Объект с параметрами протокола (container_name, config_path и т.д.)</li>
<li><strong>scripts:</strong> Объект со скриптами (detect, install, restore)</li>
</ul>
<h6 class="mt-3">{{ 'Поля скриптов:' | trans }}</h6>
<ul>
<li><strong>detect:</strong> Bash скрипт для определения установленной конфигурации. Должен вывести JSON с полями "status" (absent/partial/existing) и "details"</li>
<li><strong>install:</strong> Bash скрипт для установки протокола. Должен вывести JSON с "success": true/false</li>
<li><strong>restore:</strong> Bash скрипт для восстановления конфигурации из detection результата</li>
</ul>
<h6 class="mt-3">{{ 'Переменные окружения в скриптах:' | trans }}</h6>
<ul>
<li><code>SERVER_HOST</code> - IP/домен сервера</li>
<li><code>SERVER_USER</code> - SSH пользователь</li>
<li><code>SERVER_CONTAINER</code> - имя контейнера</li>
<li><code>PROTOCOL_*</code> - все поля из metadata (например, PROTOCOL_CONTAINER_NAME, PROTOCOL_CONFIG_PATH)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Test Modal -->
<div class="modal fade" id="testModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ 'Тест сценария' | trans }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="testServer" class="form-label">{{ 'Выбрать сервер' | trans }}</label>
<select class="form-control" id="testServer">
<option value="">{{ 'Загружаю...' | trans }}</option>
</select>
</div>
<div id="testResult" class="mt-3" style="display: none;">
<strong>{{ 'Результат:' | trans }}</strong>
<pre id="testResultContent" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;"></pre>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Закрыть' | trans }}</button>
<button type="button" class="btn btn-primary" id="runTestBtn">{{ 'Запустить тест' | trans }}</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('scenarioForm');
const definitionTextarea = document.getElementById('definition');
const jsonError = document.getElementById('jsonError');
const testBtn = document.getElementById('testBtn');
// Validate JSON on change
definitionTextarea.addEventListener('change', validateJson);
definitionTextarea.addEventListener('blur', validateJson);
function validateJson() {
jsonError.style.display = 'none';
try {
JSON.parse(definitionTextarea.value);
} catch (e) {
jsonError.textContent = `{{ "Ошибка JSON:" | trans }} ${e.message}`;
jsonError.style.display = 'block';
}
}
// Form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (!validateJson()) return;
const formData = new FormData(this);
try {
const response = await fetch('/admin/scenario', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('{{ "Сценарий успешно сохранен" | trans }}');
window.location.href = data.redirect;
} else {
alert(`{{ "Ошибка:" | trans }} ${data.message}`);
}
} catch (err) {
alert(`{{ "Ошибка отправки:" | trans }} ${err}`);
}
});
// Test button
if (testBtn) {
testBtn.addEventListener('click', function() {
loadServers();
new bootstrap.Modal(document.getElementById('testModal')).show();
});
}
// Load available servers
async function loadServers() {
try {
const response = await fetch('/api/servers?limit=50');
const data = await response.json();
const select = document.getElementById('testServer');
select.innerHTML = '';
if (data.servers && data.servers.length > 0) {
data.servers.forEach(server => {
const option = document.createElement('option');
option.value = server.id;
option.textContent = `${server.name} (${server.host})`;
select.appendChild(option);
});
} else {
select.innerHTML = '<option value="">{{ "Сервера не найдены" | trans }}</option>';
}
} catch (err) {
console.error('Error loading servers:', err);
}
}
// Run test
document.getElementById('runTestBtn').addEventListener('click', async function() {
const serverId = document.getElementById('testServer').value;
if (!serverId) {
alert('{{ "Выберите сервер" | trans }}');
return;
}
const scenarioId = document.querySelector('input[name="id"]').value;
try {
const response = await fetch(`/admin/scenario/${scenarioId}/test`, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `server_id=${serverId}`
});
const data = await response.json();
const resultDiv = document.getElementById('testResult');
const resultContent = document.getElementById('testResultContent');
resultContent.textContent = JSON.stringify(data.result, null, 2);
resultDiv.style.display = 'block';
} catch (err) {
alert(`{{ "Ошибка теста:" | trans }} ${err}`);
}
});
});
</script>
{% endblock %}