diff --git a/README.md b/README.md index ca3a9b3..f1e4aaa 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - установка и управление `xlxd`; - главный пользовательский дашборд `/`; - админка настроек `xlxd` `/admin`; +- графический редактор `/xlxd/xlxd.blacklist`, `xlxd.whitelist`, `xlxd.interlink`, `xlxd.terminal`; - экспорт активных пользователей для будущего access gateway. ## Компоненты diff --git a/docs/ADMIN_DASHBOARD.md b/docs/ADMIN_DASHBOARD.md index b47fb70..e0e1891 100644 --- a/docs/ADMIN_DASHBOARD.md +++ b/docs/ADMIN_DASHBOARD.md @@ -31,3 +31,22 @@ - URL репозитория `xlxd`. При сохранении с флагом `apply` API пытается обновить `/etc/xlx/xlxd.env`. Если PHP работает без прав записи в `/etc/xlx`, настройки все равно сохранятся в БД, а в админке будет виден готовый env-preview. + +## Редактор файлов XLX + +В админке есть отдельный блок для редактирования: + +- `/xlxd/xlxd.blacklist`; +- `/xlxd/xlxd.whitelist`; +- `/xlxd/xlxd.interlink`; +- `/xlxd/xlxd.terminal`. + +API открывает только эти четыре файла. Произвольный путь передать нельзя. + +После изменения файлов при необходимости перезапусти reflector: + +```bash +sudo systemctl restart xlxd +``` + +Установщик выдает группе `www-data` права записи на эти файлы, чтобы сохранение работало из веб-интерфейса. diff --git a/public/assets/admin.js b/public/assets/admin.js index 00067ba..fcb6c57 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -5,6 +5,15 @@ const adminResult = document.querySelector('#adminResult'); const loadButton = document.querySelector('#loadSettings'); const saveButton = document.querySelector('#saveSettings'); const applyButton = document.querySelector('#applySettings'); +const loadConfigFilesButton = document.querySelector('#loadConfigFiles'); +const saveConfigFileButton = document.querySelector('#saveConfigFile'); +const reloadConfigFileButton = document.querySelector('#reloadConfigFile'); +const fileTabs = document.querySelector('#fileTabs'); +const fileMeta = document.querySelector('#fileMeta'); +const fileEditor = document.querySelector('#fileEditor'); +const fileResult = document.querySelector('#fileResult'); +let configFiles = []; +let selectedFileKey = null; function token() { return tokenInput.value.trim(); @@ -67,6 +76,105 @@ loadButton?.addEventListener('click', loadSettings); saveButton?.addEventListener('click', () => saveSettings(false)); applyButton?.addEventListener('click', () => saveSettings(true)); +function selectedFile() { + return configFiles.find((file) => file.key === selectedFileKey) || null; +} + +function setFileResult(text) { + fileResult.textContent = text; +} + +function renderFileTabs() { + fileTabs.innerHTML = ''; + for (const file of configFiles) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = `file-tab${file.key === selectedFileKey ? ' active' : ''}`; + button.textContent = file.name; + button.addEventListener('click', () => selectFile(file.key)); + fileTabs.append(button); + } +} + +function renderFileMeta(file) { + if (!file) { + fileMeta.innerHTML = 'Файл не выбран'; + return; + } + + const writable = file.writable ? 'доступен для записи' : 'нет прав на запись'; + const exists = file.exists ? 'существует' : 'будет создан при сохранении'; + fileMeta.innerHTML = ` + ${file.path} + ${exists} + ${writable} + ${file.updated_at || 'без даты'} + `; +} + +function selectFile(key) { + selectedFileKey = key; + const file = selectedFile(); + renderFileTabs(); + renderFileMeta(file); + fileEditor.value = file?.content || ''; + setFileResult(file ? file.description : ''); +} + +async function loadConfigFiles() { + setFileResult('Загружаем файлы XLX...'); + const response = await fetch('/api/admin/xlxd/config-files', { + headers: { 'X-Admin-Token': token() }, + }); + const payload = await response.json(); + if (!payload.ok) { + setFileResult(payload.error || 'Ошибка загрузки файлов'); + return; + } + + configFiles = payload.data; + selectedFileKey = selectedFileKey || configFiles[0]?.key || null; + renderFileTabs(); + selectFile(selectedFileKey); +} + +async function saveConfigFile() { + const file = selectedFile(); + if (!file) { + setFileResult('Сначала выбери файл.'); + return; + } + + setFileResult(`Сохраняем ${file.filename}...`); + const response = await fetch('/api/admin/xlxd/config-files', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Admin-Token': token(), + }, + body: JSON.stringify({ file: file.key, content: fileEditor.value }), + }); + const payload = await response.json(); + if (!payload.ok) { + setFileResult(payload.error || 'Ошибка сохранения файла'); + return; + } + + configFiles = configFiles.map((item) => item.key === payload.data.key ? payload.data : item); + selectFile(payload.data.key); + setFileResult(`${payload.data.filename} сохранен.`); +} + +loadConfigFilesButton?.addEventListener('click', loadConfigFiles); +saveConfigFileButton?.addEventListener('click', saveConfigFile); +reloadConfigFileButton?.addEventListener('click', () => { + const file = selectedFile(); + if (file) { + fileEditor.value = file.content || ''; + setFileResult('Изменения в редакторе отменены.'); + } +}); + form?.addEventListener('input', () => { const data = formData(); const lines = { diff --git a/public/assets/styles.css b/public/assets/styles.css index 1567038..8f47238 100644 --- a/public/assets/styles.css +++ b/public/assets/styles.css @@ -207,7 +207,22 @@ input { font-size: 17px; } -input:focus { +textarea { + width: 100%; + min-height: 420px; + resize: vertical; + border: 1px solid var(--line); + border-radius: 8px; + padding: 16px; + background: #06070b; + color: var(--text); + font-family: Consolas, "Courier New", monospace; + font-size: 15px; + line-height: 1.55; +} + +input:focus, +textarea:focus { outline: 2px solid var(--accent); outline-offset: 2px; } @@ -230,14 +245,81 @@ input:focus { min-height: 420px; } +.muted-text { + max-width: 760px; + margin: 12px 0 0; + color: var(--muted); + font-size: 17px; + line-height: 1.45; +} + +.config-editor { + display: grid; + gap: 18px; +} + +.config-editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} + +.file-tabs { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.file-tab { + border: 1px solid var(--line); + border-radius: 8px; + padding: 12px 16px; + background: transparent; + color: var(--text); + font: inherit; + font-weight: 800; + cursor: pointer; +} + +.file-tab.active { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 18%, transparent); +} + +.file-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + color: var(--muted); + font-size: 14px; +} + +.file-meta span { + border: 1px solid var(--line); + border-radius: 999px; + padding: 8px 12px; + background: var(--panel-2); +} + +.editor-label { + color: var(--text); + font-size: 16px; +} + @media (max-width: 880px) { .hero, .grid, .panel.split, - .settings-form { + .settings-form, + .config-editor-head { grid-template-columns: 1fr; } + .config-editor-head { + display: grid; + } + h1 { font-size: 44px; } diff --git a/public/index.php b/public/index.php index bd427b4..dc13ae5 100644 --- a/public/index.php +++ b/public/index.php @@ -7,6 +7,7 @@ use Xlx\Domain\AccessExportService; use Xlx\Domain\IdAllocator; use Xlx\Domain\PaymentService; use Xlx\Domain\RegistrationService; +use Xlx\Domain\XlxdConfigFileService; use Xlx\Domain\XlxdSettingsService; use Xlx\Domain\YooKassaClient; use Xlx\Support\Database; @@ -117,6 +118,18 @@ try { Response::json(['ok' => true, 'data' => $service->save($pdo, $input['settings'] ?? [], (bool) ($input['apply'] ?? false))]); } + if ($method === 'GET' && $path === '/api/admin/xlxd/config-files') { + Security::requireAdminToken($config); + Response::json(['ok' => true, 'data' => (new XlxdConfigFileService($config))->list()]); + } + + if ($method === 'POST' && $path === '/api/admin/xlxd/config-files') { + Security::requireAdminToken($config); + $input = Input::json(); + $service = new XlxdConfigFileService($config); + Response::json(['ok' => true, 'data' => $service->save((string) ($input['file'] ?? ''), (string) ($input['content'] ?? ''))]); + } + Response::error('Route not found.', 404); } catch (Throwable $throwable) { Response::error($throwable->getMessage(), 400); diff --git a/public/views/admin.php b/public/views/admin.php index bd4fac8..13c69b4 100644 --- a/public/views/admin.php +++ b/public/views/admin.php @@ -81,6 +81,33 @@
+ +Файлы XLX
+Изменения сохраняются прямо в `/xlxd`. После правки перезапусти `xlxd`, если файл требует перечитывания сервисом.
+