feat: ssh auth, protocol management, and cleanup
This commit is contained in:
@@ -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 %}
|
||||
Reference in New Issue
Block a user