Files
2026-07-04 16:00:04 +02:00

225 lines
10 KiB
PHP

<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
if (!$ctx->settings->allowRemoteHosts()) {
$ctx->renderFatal('Zarządzanie hostami zdalnymi jest wyłączone (`allow_remote_hosts=0`).', 403);
return;
}
$ok = [];
$errors = [];
$lastSyncOutput = '';
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$postgresPort = $ctx->pg->serverPort();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
if (empty($dbNames)) {
$ctx->renderPage('Hosty Zdalne Użytkownika', '<div class="panel"><p class="muted">Brak baz danych do zarządzania hostami.</p></div>', $ok, $errors);
return;
}
$selectedDb = $ctx->postString('db_name');
if ($selectedDb === '') {
$selectedDb = $ctx->queryString('db');
}
if ($selectedDb === '') {
$selectedDb = $dbNames[0];
}
try {
$selectedDb = NamePolicy::normalizeDatabaseName($selectedDb, $prefix);
} catch (Throwable $e) {
$selectedDb = $dbNames[0];
}
if (!in_array($selectedDb, $dbNames, true)) {
$selectedDb = $dbNames[0];
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
$selectedRole = $ctx->queryString('role');
if ($ctx->isPost()) {
$selectedRole = $ctx->postString('role_name');
try {
$ctx->requireCsrf('manage_hosts_submit');
$selectedDb = NamePolicy::normalizeDatabaseName($ctx->postString('db_name', $selectedDb), $prefix);
if (!in_array($selectedDb, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $selectedDb);
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
if (!in_array($selectedRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie ma dostępu do wybranej bazy: ' . $selectedRole);
}
$currentEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $selectedDb);
$hostMap = [];
foreach ($currentEntries as $entry) {
$hostMap[$entry['host']] = $entry['note'];
}
$removeHosts = $ctx->postArray('remove_hosts');
$removeHosts = array_values(array_unique($removeHosts));
foreach ($removeHosts as $rawHost) {
$host = trim($rawHost);
if ($host === '') {
continue;
}
if (array_key_exists($host, $hostMap)) {
unset($hostMap[$host]);
}
}
$addHostRaw = trim($ctx->postString('add_host'));
if ($addHostRaw !== '') {
$addHost = NamePolicy::normalizeRemoteIpv4AccessPattern($addHostRaw);
$addComment = NamePolicy::normalizeHostComment($ctx->postString('add_comment'));
$hostMap[$addHost] = $addComment;
}
$allowAllHosts = $ctx->postString('allow_all_hosts') === '1';
if ($allowAllHosts) {
$allHostsComment = NamePolicy::normalizeHostComment($ctx->postString('allow_all_comment'));
$hostMap[NamePolicy::ALL_IPV4_HOST_PATTERN] = $allHostsComment !== '' ? $allHostsComment : 'Dostęp z każdego adresu IPv4';
} else {
unset($hostMap[NamePolicy::ALL_IPV4_HOST_PATTERN]);
}
if (count($hostMap) > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla wybranej bazy i użytkownika PostgreSQL.');
}
$finalEntries = [];
foreach ($hostMap as $host => $note) {
$finalEntries[] = ['host' => $host, 'note' => $note];
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $finalEntries, $selectedDb);
$restartPostgresql = $ctx->postString('restart_postgresql') === '1';
$sync = $ctx->pg->syncHbaRules($restartPostgresql);
$lastSyncOutput = trim((string)($sync['output'] ?? ''));
if (!$sync['ok']) {
$errors[] = 'Hosty zapisane, ale synchronizacja konfiguracji hostów zdalnych nie powiodła się: ' . $sync['output'];
} elseif ($restartPostgresql) {
$ok[] = 'Synchronizacja konfiguracji PostgreSQL zakończona i usługa PostgreSQL została zrestartowana.';
} elseif (array_key_exists(NamePolicy::ALL_IPV4_HOST_PATTERN, $hostMap)) {
$ok[] = 'Synchronizacja PostgreSQL zakończona. Dla dostępu z każdego hosta port PostgreSQL do otwarcia w CSF: ' . $postgresPort . '.';
} else {
$ok[] = 'Synchronizacja konfiguracji PostgreSQL i CSF zakończona.';
}
$ok[] = 'Zaktualizowano hosty dostępu dla użytkownika ' . $selectedRole . '.';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
if ($selectedRole !== '') {
try {
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
} catch (Throwable $e) {
$selectedRole = '';
}
}
if ($selectedRole === '' && !empty($roleNames)) {
$selectedRole = $roleNames[0];
}
$currentEntries = [];
if ($selectedRole !== '') {
$currentEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $selectedDb);
}
$allHostsEnabled = false;
$allHostsComment = '';
foreach ($currentEntries as $entry) {
if ($entry['host'] === NamePolicy::ALL_IPV4_HOST_PATTERN) {
$allHostsEnabled = true;
$allHostsComment = $entry['note'];
break;
}
}
$dbOptions = '';
foreach ($dbNames as $dbName) {
$sel = ($dbName === $selectedDb) ? ' selected' : '';
$dbOptions .= '<option value="' . AppContext::e($dbName) . '"' . $sel . '>' . AppContext::e($dbName) . '</option>';
}
$roleOptions = '<option value="">-- wybierz użytkownika --</option>';
foreach ($roleNames as $roleName) {
$sel = ($roleName === $selectedRole) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $sel . '>' . AppContext::e($roleName) . '</option>';
}
$hostRows = '';
if (empty($currentEntries)) {
$hostRows = '<tr><td colspan="3" class="muted">Brak zapisanych hostów.</td></tr>';
} else {
foreach ($currentEntries as $entry) {
$host = $entry['host'];
$note = $entry['note'];
$hostRows .= '<tr>';
$hostRows .= '<td>' . AppContext::e($host) . '</td>';
$hostRows .= '<td>' . AppContext::e($note) . '</td>';
$hostRows .= '<td><label class="inline"><input type="checkbox" name="remove_hosts[]" value="' . AppContext::e($host) . '"> usuń</label></td>';
$hostRows .= '</tr>';
}
}
$body = '';
if ($allHostsEnabled) {
$body .= '<div class="alert alert-err" style="border:1px solid #b42318;background:#fff1f0;color:#7a1b13;margin-bottom:12px">';
$body .= '<strong>Uwaga:</strong> dla bazy <code>' . AppContext::e($selectedDb) . '</code> i użytkownika <code>' . AppContext::e($selectedRole) . '</code> włączono dostęp z każdego adresu IPv4. Plugin nie otwiera portu PostgreSQL w CSF dla tego trybu. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.';
$body .= '</div>';
}
$body .= '<div class="panel">';
$body .= '<h3>Status konfiguracji hostów zdalnych</h3>';
$body .= '<p class="muted">Hosty zdalne są włączone przez <code>allow_remote_hosts=1</code>. Zapis hostów uruchamia synchronizację <code>pg_hba</code>, <code>postgresql.conf</code> oraz CSF.</p>';
if ($lastSyncOutput !== '') {
$body .= '<p><strong>Ostatni wynik synchronizacji:</strong></p>';
$body .= '<pre class="muted">' . AppContext::e($lastSyncOutput) . '</pre>';
} else {
$body .= '<p class="muted">Ostatni wynik synchronizacji będzie widoczny po zapisaniu zmian hostów.</p>';
}
$body .= '</div>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('manage_hosts_submit');
$body .= '<div class="row">';
$body .= '<div><label>Baza danych</label><select name="db_name" required>' . $dbOptions . '</select></div>';
$body .= '<div><label>Użytkownik PostgreSQL</label><select name="role_name" required>' . $roleOptions . '</select></div>';
$body .= '</div>';
$body .= '<div class="row">';
$body .= '<div><label>Dodaj IPv4/CIDR</label><input type="text" name="add_host" placeholder="198.51.100.12 lub 198.51.100.0/24"></div>';
$body .= '<div><label>Komentarz do nowego hosta</label><input type="text" name="add_comment" placeholder="np. serwer aplikacyjny PROD"></div>';
$body .= '</div>';
$body .= '<div class="panel" style="margin-top:12px;border-color:#f0b4a7;background:#fff8f6">';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center;margin-top:0">';
$body .= '<label class="inline" style="margin:0"><input type="checkbox" name="allow_all_hosts" value="1"' . ($allHostsEnabled ? ' checked' : '') . '> Dostęp dla wszystkich hostów IPv4 (<code>0.0.0.0/0</code>)</label>';
$body .= '<button class="danger" type="submit" name="restart_postgresql" value="1">Zapisz i restartuj PostgreSQL</button>';
$body .= '</div>';
$body .= '<p class="muted">Ta opcja dotyczy tylko wybranej bazy i użytkownika. Plugin skonfiguruje PostgreSQL, ale nie otworzy portu PostgreSQL w CSF. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.</p>';
$body .= '<label>Komentarz dla trybu wszystkich hostów</label><input type="text" name="allow_all_comment" value="' . AppContext::e($allHostsComment) . '" placeholder="np. VPS klienta - port ' . AppContext::e((string)$postgresPort) . ' otwierany ręcznie w CSF">';
$body .= '</div>';
$body .= '<div class="panel"><h3>Aktualne hosty dla wybranej bazy</h3>';
$body .= '<table><thead><tr><th>Host/CIDR</th><th>Komentarz</th><th>Akcja</th></tr></thead><tbody>' . $hostRows . '</tbody></table>';
$body .= '</div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zapisz hosty</button></div>';
$body .= '</form>';
$ctx->renderPage('Hosty Zdalne Użytkownika', $body, $ok, $errors);
};