AuditController.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controllers;
  4. use App\ApiClient\AdminClient;
  5. use App\ApiClient\ApiException;
  6. use App\Auth\SessionManager;
  7. use Psr\Http\Message\ResponseInterface;
  8. use Psr\Http\Message\ServerRequestInterface;
  9. use Slim\Views\Twig;
  10. /**
  11. * `/app/audit` — filterable audit log list. Filters are passed
  12. * through to the api as-is; pagination is done page+page_size in
  13. * the query string. The page tolerates a partial outage by
  14. * rendering an empty state with the api error message.
  15. */
  16. final class AuditController
  17. {
  18. use CrudControllerSupport;
  19. private const ALLOWED_ACTIONS = [
  20. 'reporter.created', 'reporter.updated', 'reporter.deleted',
  21. 'consumer.created', 'consumer.updated', 'consumer.deleted',
  22. 'token.created', 'token.revoked',
  23. 'policy.created', 'policy.updated', 'policy.deleted',
  24. 'category.created', 'category.updated', 'category.deleted',
  25. 'manual_block.created', 'manual_block.deleted',
  26. 'allowlist.created', 'allowlist.deleted',
  27. 'job.triggered',
  28. ];
  29. private const ALLOWED_KINDS = ['user', 'admin-token', 'reporter', 'consumer', 'system'];
  30. public function __construct(
  31. private readonly Twig $twigEngine,
  32. private readonly SessionManager $sessionManager,
  33. private readonly AdminClient $admin,
  34. ) {
  35. }
  36. protected function twig(): Twig
  37. {
  38. return $this->twigEngine;
  39. }
  40. protected function sessions(): SessionManager
  41. {
  42. return $this->sessionManager;
  43. }
  44. public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  45. {
  46. if (($redirect = $this->requireUser($request, $response)) !== null) {
  47. return $redirect;
  48. }
  49. $user = $this->sessions()->getUser();
  50. // Guard for static analysis — requireUser bounced if null already.
  51. if ($user === null) {
  52. return $response->withStatus(302)->withHeader('Location', '/login');
  53. }
  54. $params = $request->getQueryParams();
  55. $filters = [
  56. 'actor_kind' => self::clean($params['actor_kind'] ?? null),
  57. 'actor_id' => self::clean($params['actor_id'] ?? null),
  58. 'action' => self::clean($params['action'] ?? null),
  59. 'entity_type' => self::clean($params['entity_type'] ?? null),
  60. 'entity_id' => self::clean($params['entity_id'] ?? null),
  61. 'subject_kind' => self::clean($params['subject_kind'] ?? null),
  62. 'subject_id' => self::clean($params['subject_id'] ?? null),
  63. 'from' => self::clean($params['from'] ?? null),
  64. 'to' => self::clean($params['to'] ?? null),
  65. ];
  66. $page = isset($params['page']) && ctype_digit((string) $params['page']) ? max(1, (int) $params['page']) : 1;
  67. $pageSize = 50;
  68. $list = null;
  69. $error = null;
  70. try {
  71. $list = $this->admin->listAuditLog($user->userId, $filters, $page, $pageSize);
  72. } catch (ApiException $e) {
  73. $error = 'API error: ' . $e->getMessage();
  74. }
  75. return $this->twigEngine->render($response, 'pages/audit/index.twig', [
  76. 'active_section' => 'audit',
  77. 'list' => $list,
  78. 'page' => $page,
  79. 'page_size' => $pageSize,
  80. 'filters' => $filters,
  81. 'allowed_actions' => self::ALLOWED_ACTIONS,
  82. 'allowed_kinds' => self::ALLOWED_KINDS,
  83. 'error' => $error,
  84. ]);
  85. }
  86. private static function clean(mixed $v): ?string
  87. {
  88. if (!is_string($v)) {
  89. return null;
  90. }
  91. $trimmed = trim($v);
  92. return $trimmed === '' ? null : $trimmed;
  93. }
  94. }