AppFactory.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\App;
  4. use App\Auth\LocalLoginController;
  5. use App\Auth\LogoutController;
  6. use App\Auth\OidcController;
  7. use App\Controllers\AllowlistController;
  8. use App\Controllers\AuditController;
  9. use App\Controllers\CategoriesController;
  10. use App\Controllers\ConsumersController;
  11. use App\Controllers\DashboardController;
  12. use App\Controllers\HealthzController;
  13. use App\Controllers\HomeController;
  14. use App\Controllers\IpsController;
  15. use App\Controllers\ManualBlocksController;
  16. use App\Controllers\MeController;
  17. use App\Controllers\NoAccessController;
  18. use App\Controllers\PoliciesController;
  19. use App\Controllers\ReportersController;
  20. use App\Controllers\SearchController;
  21. use App\Controllers\SettingsController;
  22. use App\Controllers\TokensController;
  23. use App\Http\AuthRequiredMiddleware;
  24. use App\Http\CsrfMiddleware;
  25. use App\Http\JsonExceptionHandler;
  26. use App\Http\SessionMiddleware;
  27. use App\Http\TwigGlobalsMiddleware;
  28. use Psr\Container\ContainerInterface;
  29. use Psr\Log\LoggerInterface;
  30. use Slim\App;
  31. use Slim\Factory\AppFactory as SlimAppFactory;
  32. use Slim\Routing\RouteCollectorProxy;
  33. /**
  34. * Builds the configured Slim app for the UI.
  35. *
  36. * Middleware order (Slim is LIFO; bottom = outermost):
  37. * 1. Session — always start the PHP session first.
  38. * 2. Csrf — needs a session to read/write the token.
  39. * 3. TwigGlobals — needs both above.
  40. * 4. Routing + body parsing.
  41. *
  42. * `/app/*` routes get an additional AuthRequiredMiddleware on the
  43. * route group so anonymous users bounce to /login.
  44. */
  45. final class AppFactory
  46. {
  47. /**
  48. * @return App<ContainerInterface|null>
  49. */
  50. public static function build(ContainerInterface $container): App
  51. {
  52. SlimAppFactory::setContainer($container);
  53. $app = SlimAppFactory::create();
  54. $app->addRoutingMiddleware();
  55. /** @var LoggerInterface $logger */
  56. $logger = $container->get(LoggerInterface::class);
  57. /** @var bool $isDev */
  58. $isDev = $container->get('settings.is_dev');
  59. $errorMiddleware = $app->addErrorMiddleware($isDev, true, true, $logger);
  60. /** @var JsonExceptionHandler $handler */
  61. $handler = $container->get(JsonExceptionHandler::class);
  62. $errorMiddleware->setDefaultErrorHandler($handler);
  63. // Slim middleware is LIFO — the last `add()` call runs first.
  64. // Order on the incoming request:
  65. // 1. Session (needs to start before anything reads $_SESSION)
  66. // 2. BodyParsing (so CSRF can read form fields)
  67. // 3. CSRF (reads from session + parsed body)
  68. // 4. TwigGlobals (after CSRF token is set on the request attr)
  69. // 5. AuthRequired (per /app/* group)
  70. /** @var TwigGlobalsMiddleware $globals */
  71. $globals = $container->get(TwigGlobalsMiddleware::class);
  72. /** @var CsrfMiddleware $csrf */
  73. $csrf = $container->get(CsrfMiddleware::class);
  74. /** @var SessionMiddleware $session */
  75. $session = $container->get(SessionMiddleware::class);
  76. $app->add($globals);
  77. $app->add($csrf);
  78. $app->addBodyParsingMiddleware();
  79. $app->add($session);
  80. /** @var HomeController $home */
  81. $home = $container->get(HomeController::class);
  82. $app->get('/', $home);
  83. /** @var HealthzController $healthz */
  84. $healthz = $container->get(HealthzController::class);
  85. $app->get('/healthz', $healthz);
  86. /** @var LocalLoginController $local */
  87. $local = $container->get(LocalLoginController::class);
  88. $app->get('/login', [$local, 'showLogin']);
  89. $app->post('/login/local', [$local, 'postLocal']);
  90. /** @var OidcController $oidc */
  91. $oidc = $container->get(OidcController::class);
  92. $app->get('/login/oidc', [$oidc, 'initiate']);
  93. $app->get('/oidc/callback', [$oidc, 'callback']);
  94. /** @var LogoutController $logout */
  95. $logout = $container->get(LogoutController::class);
  96. $app->post('/logout', $logout);
  97. /** @var NoAccessController $noAccess */
  98. $noAccess = $container->get(NoAccessController::class);
  99. $app->get('/no-access', $noAccess);
  100. /** @var AuthRequiredMiddleware $authRequired */
  101. $authRequired = $container->get(AuthRequiredMiddleware::class);
  102. $app->group('/app', function (RouteCollectorProxy $group) use ($container): void {
  103. /** @var MeController $me */
  104. $me = $container->get(MeController::class);
  105. $group->get('/me', $me);
  106. /** @var DashboardController $dashboard */
  107. $dashboard = $container->get(DashboardController::class);
  108. $group->get('/dashboard', $dashboard);
  109. /** @var IpsController $ips */
  110. $ips = $container->get(IpsController::class);
  111. $group->get('/ips', [$ips, 'index']);
  112. // {ip:.+} so v6 colons don't break Slim's default segment regex.
  113. $group->get('/ips/{ip:.+}', [$ips, 'show']);
  114. /** @var ManualBlocksController $manualBlocks */
  115. $manualBlocks = $container->get(ManualBlocksController::class);
  116. $group->get('/manual-blocks', [$manualBlocks, 'index']);
  117. $group->post('/manual-blocks', [$manualBlocks, 'create']);
  118. $group->post('/manual-blocks/{id}/delete', [$manualBlocks, 'delete']);
  119. // /app/subnets is an alias filtered to kind=subnet so the
  120. // sidebar's "Subnets" link lands on a focused list.
  121. $group->get('/subnets', [$manualBlocks, 'subnetsIndex']);
  122. /** @var AllowlistController $allowlist */
  123. $allowlist = $container->get(AllowlistController::class);
  124. $group->get('/allowlist', [$allowlist, 'index']);
  125. $group->post('/allowlist', [$allowlist, 'create']);
  126. $group->post('/allowlist/{id}/delete', [$allowlist, 'delete']);
  127. /** @var PoliciesController $policies */
  128. $policies = $container->get(PoliciesController::class);
  129. $group->get('/policies', [$policies, 'index']);
  130. $group->post('/policies', [$policies, 'create']);
  131. $group->get('/policies/{id}', [$policies, 'edit']);
  132. $group->post('/policies/{id}', [$policies, 'update']);
  133. $group->post('/policies/{id}/delete', [$policies, 'delete']);
  134. // GET-only XHR proxy used by the edit page's preview pane.
  135. $group->get('/policies/{id}/preview-proxy', [$policies, 'previewProxy']);
  136. $group->get('/policies/{id}/score-distribution-proxy', [$policies, 'scoreDistributionProxy']);
  137. /** @var ReportersController $reporters */
  138. $reporters = $container->get(ReportersController::class);
  139. $group->get('/reporters', [$reporters, 'index']);
  140. $group->post('/reporters', [$reporters, 'create']);
  141. $group->get('/reporters/{id}', [$reporters, 'edit']);
  142. $group->post('/reporters/{id}', [$reporters, 'update']);
  143. $group->post('/reporters/{id}/delete', [$reporters, 'delete']);
  144. /** @var ConsumersController $consumers */
  145. $consumers = $container->get(ConsumersController::class);
  146. $group->get('/consumers', [$consumers, 'index']);
  147. $group->post('/consumers', [$consumers, 'create']);
  148. $group->get('/consumers/{id}', [$consumers, 'edit']);
  149. $group->post('/consumers/{id}', [$consumers, 'update']);
  150. $group->post('/consumers/{id}/delete', [$consumers, 'delete']);
  151. /** @var TokensController $tokens */
  152. $tokens = $container->get(TokensController::class);
  153. $group->get('/tokens', [$tokens, 'index']);
  154. $group->post('/tokens', [$tokens, 'create']);
  155. $group->post('/tokens/{id}/delete', [$tokens, 'delete']);
  156. /** @var CategoriesController $categories */
  157. $categories = $container->get(CategoriesController::class);
  158. $group->get('/categories', [$categories, 'index']);
  159. $group->post('/categories', [$categories, 'create']);
  160. $group->get('/categories/{id}', [$categories, 'edit']);
  161. $group->post('/categories/{id}', [$categories, 'update']);
  162. $group->post('/categories/{id}/delete', [$categories, 'delete']);
  163. /** @var AuditController $audit */
  164. $audit = $container->get(AuditController::class);
  165. $group->get('/audit', [$audit, 'index']);
  166. /** @var SearchController $search */
  167. $search = $container->get(SearchController::class);
  168. $group->get('/search', [$search, 'index']);
  169. /** @var SettingsController $settings */
  170. $settings = $container->get(SettingsController::class);
  171. $group->get('/settings', [$settings, 'index']);
  172. $group->post('/settings/jobs/trigger/{name}', [$settings, 'trigger']);
  173. $group->post('/settings/maintenance/purge', [$settings, 'purge']);
  174. $group->post('/settings/maintenance/seed-demo', [$settings, 'seedDemo']);
  175. $group->post('/settings/audit-toggles', [$settings, 'updateAuditToggles']);
  176. })->add($authRequired);
  177. $app->map(
  178. ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  179. '/{routes:.+}',
  180. function ($request, $response) {
  181. return $response->withStatus(404)->withHeader('Content-Type', 'text/plain');
  182. }
  183. );
  184. return $app;
  185. }
  186. }