| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- <?php
- declare(strict_types=1);
- namespace App\Application\Admin;
- use App\Domain\Audit\AuditAction;
- use App\Domain\Audit\AuditEmitter;
- use App\Domain\Settings\AppSettings;
- use Doctrine\DBAL\Connection;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- /**
- * `GET/PATCH /api/v1/admin/app-settings` — runtime-mutable feature flags.
- *
- * Currently exposes the two audit-emission toggles (report.received and
- * blocklist.requested) which are surfaced as switches on the settings
- * page so an admin can silence the high-volume audit rows without
- * restarting the api container.
- *
- * RBAC: Admin only — same trust level as the rest of the settings page.
- */
- final class AppSettingsController
- {
- use AdminControllerSupport;
- private const KEYS = [
- AppSettings::KEY_AUDIT_REPORT_RECEIVED_ENABLED,
- AppSettings::KEY_AUDIT_BLOCKLIST_REQUEST_ENABLED,
- ];
- public function __construct(
- private readonly AppSettings $settings,
- private readonly AuditEmitter $audit,
- private readonly Connection $connection,
- ) {
- }
- public function show(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- return self::json($response, 200, $this->snapshot());
- }
- public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- $body = self::jsonBody($request);
- $errors = [];
- $changes = [];
- foreach (self::KEYS as $key) {
- if (!array_key_exists($key, $body)) {
- continue;
- }
- $raw = $body[$key];
- if (!is_bool($raw) && !in_array($raw, [0, 1, '0', '1', 'true', 'false'], true)) {
- $errors[$key] = 'must be a boolean';
- continue;
- }
- $value = is_bool($raw) ? $raw : in_array($raw, [1, '1', 'true'], true);
- $current = $this->settings->getBool($key, true);
- if ($current !== $value) {
- $changes[$key] = ['from' => $current, 'to' => $value];
- }
- }
- if ($errors !== []) {
- return self::validationFailed($response, $errors);
- }
- if ($changes !== []) {
- $auditCtx = self::auditContext($request);
- $this->connection->transactional(function () use ($changes, $auditCtx): void {
- foreach ($changes as $key => $diff) {
- $this->settings->setBool($key, (bool) $diff['to']);
- }
- $this->audit->emitOrThrow(
- AuditAction::APP_SETTINGS_UPDATED,
- 'app_settings',
- null,
- ['changes' => $changes],
- $auditCtx,
- 'audit-toggles',
- );
- });
- }
- return self::json($response, 200, $this->snapshot());
- }
- /**
- * @return array<string, bool>
- */
- private function snapshot(): array
- {
- return [
- AppSettings::KEY_AUDIT_REPORT_RECEIVED_ENABLED => $this->settings->getBool(
- AppSettings::KEY_AUDIT_REPORT_RECEIVED_ENABLED,
- true,
- ),
- AppSettings::KEY_AUDIT_BLOCKLIST_REQUEST_ENABLED => $this->settings->getBool(
- AppSettings::KEY_AUDIT_BLOCKLIST_REQUEST_ENABLED,
- true,
- ),
- ];
- }
- }
|