| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- <?php
- declare(strict_types=1);
- namespace App\Controllers;
- use App\ApiClient\AdminClient;
- use App\ApiClient\ApiAuthException;
- use App\ApiClient\ApiException;
- use App\Auth\SessionManager;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Slim\Views\Twig;
- /**
- * `/app/settings` — admin-only effective config + jobs status.
- *
- * Three sections render: Configuration (from `/admin/config`), Jobs
- * (from `/admin/jobs/status`, with overdue badges + manual-trigger
- * buttons), GeoIP (parsed out of the config payload).
- *
- * The trigger handler is a server-rendered POST → 303 → re-render
- * pattern: forwards to the api's `POST /admin/jobs/trigger/{name}` and
- * stashes the outcome in flash. We deliberately avoid an XHR + spinner
- * to keep the no-JS path working; the admin can wait the few seconds
- * a job takes.
- *
- * RBAC: Viewer/Operator get a no-access page; Admin only.
- */
- final class SettingsController
- {
- use CrudControllerSupport;
- public function __construct(
- private readonly Twig $twigEngine,
- private readonly SessionManager $sessionManager,
- private readonly AdminClient $admin,
- ) {
- }
- protected function twig(): Twig
- {
- return $this->twigEngine;
- }
- protected function sessions(): SessionManager
- {
- return $this->sessionManager;
- }
- public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- if (($redirect = $this->requireUser($request, $response)) !== null) {
- return $redirect;
- }
- $user = $this->sessions()->getUser();
- if ($user === null) {
- return $response->withStatus(302)->withHeader('Location', '/login');
- }
- if (!$this->userIs($user, 'admin')) {
- return $response->withStatus(303)->withHeader('Location', '/no-access');
- }
- $config = null;
- $jobs = null;
- $error = null;
- try {
- $config = $this->admin->getConfig($user->userId);
- $jobs = $this->admin->getJobsStatus($user->userId);
- } catch (ApiAuthException) {
- return $response->withStatus(303)->withHeader('Location', '/no-access');
- } catch (ApiException $e) {
- $error = 'API error: ' . $e->getMessage();
- }
- return $this->twigEngine->render($response, 'pages/settings/index.twig', [
- 'active_section' => 'settings',
- 'config' => $config,
- 'jobs' => $jobs,
- 'error' => $error,
- ]);
- }
- /**
- * @param array{name: string} $args
- */
- public function trigger(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
- {
- if (($redirect = $this->requireUser($request, $response)) !== null) {
- return $redirect;
- }
- $user = $this->sessions()->getUser();
- if ($user === null) {
- return $response->withStatus(302)->withHeader('Location', '/login');
- }
- if (!$this->userIs($user, 'admin')) {
- return $response->withStatus(303)->withHeader('Location', '/no-access');
- }
- $name = $args['name'];
- $body = $this->formBody($request);
- $params = [];
- if (isset($body['full'])) {
- $params['full'] = $this->formBool($body['full']);
- }
- if (isset($body['reenrich'])) {
- $params['reenrich'] = $this->formBool($body['reenrich']);
- }
- try {
- $result = $this->admin->triggerJob($user->userId, $name, $params);
- $status = (string) ($result['status'] ?? 'unknown');
- $items = (int) ($result['items_processed'] ?? 0);
- $duration = (int) ($result['duration_ms'] ?? 0);
- $this->sessions()->flash(
- $status === 'success' ? 'success' : 'error',
- sprintf('%s — %s (items=%d, duration=%dms)', $name, $status, $items, $duration),
- );
- } catch (ApiException $e) {
- $this->flashFromException($e);
- }
- return $response->withStatus(303)->withHeader('Location', '/app/settings');
- }
- /**
- * Wipe operational data. The API requires the literal `PURGE` string;
- * we additionally require the user to have typed it in the form, both
- * to avoid drive-by clicks and to keep curl-style misuse loud.
- */
- public function purge(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- if (($redirect = $this->requireUser($request, $response)) !== null) {
- return $redirect;
- }
- $user = $this->sessions()->getUser();
- if ($user === null) {
- return $response->withStatus(302)->withHeader('Location', '/login');
- }
- if (!$this->userIs($user, 'admin')) {
- return $response->withStatus(303)->withHeader('Location', '/no-access');
- }
- $body = $this->formBody($request);
- $confirm = isset($body['confirm']) && is_string($body['confirm']) ? trim($body['confirm']) : '';
- if ($confirm !== 'PURGE') {
- $this->sessions()->flash('error', 'Type PURGE exactly to confirm the wipe.');
- return $response->withStatus(303)->withHeader('Location', '/app/settings');
- }
- try {
- $result = $this->admin->purgeData($user->userId);
- $deleted = is_array($result['deleted'] ?? null) ? $result['deleted'] : [];
- $total = array_sum(array_map('intval', $deleted));
- $this->sessions()->flash('success', sprintf(
- 'Database purged. %d rows deleted across %d tables.',
- $total,
- count($deleted),
- ));
- } catch (ApiException $e) {
- $this->flashFromException($e);
- }
- return $response->withStatus(303)->withHeader('Location', '/app/settings');
- }
- /**
- * Load the demo dataset. Idempotent on the api side — repeats return
- * 409 which we surface as an info-level flash, not an error.
- */
- public function seedDemo(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- if (($redirect = $this->requireUser($request, $response)) !== null) {
- return $redirect;
- }
- $user = $this->sessions()->getUser();
- if ($user === null) {
- return $response->withStatus(302)->withHeader('Location', '/login');
- }
- if (!$this->userIs($user, 'admin')) {
- return $response->withStatus(303)->withHeader('Location', '/no-access');
- }
- try {
- $result = $this->admin->seedDemo($user->userId);
- $summary = is_array($result['summary'] ?? null) ? $result['summary'] : [];
- $this->sessions()->flash('success', sprintf(
- 'Demo data loaded — %d reporters, %d consumers, %d IPs, %d reports.',
- (int) ($summary['reporters'] ?? 0),
- (int) ($summary['consumers'] ?? 0),
- (int) ($summary['ips'] ?? 0),
- (int) ($summary['reports'] ?? 0),
- ));
- } catch (ApiException $e) {
- $this->flashFromException($e);
- }
- return $response->withStatus(303)->withHeader('Location', '/app/settings');
- }
- }
|