*/ public static function build(ContainerInterface $container): App { SlimAppFactory::setContainer($container); $app = SlimAppFactory::create(); $app->addRoutingMiddleware(); /** @var LoggerInterface $logger */ $logger = $container->get(LoggerInterface::class); /** @var bool $isDev */ $isDev = $container->get('settings.is_dev'); $errorMiddleware = $app->addErrorMiddleware($isDev, true, true, $logger); /** @var JsonExceptionHandler $handler */ $handler = $container->get(JsonExceptionHandler::class); $errorMiddleware->setDefaultErrorHandler($handler); // Slim middleware is LIFO — the last `add()` call runs first. // Order on the incoming request: // 1. Session (needs to start before anything reads $_SESSION) // 2. BodyParsing (so CSRF can read form fields) // 3. CSRF (reads from session + parsed body) // 4. TwigGlobals (after CSRF token is set on the request attr) // 5. AuthRequired (per /app/* group) /** @var TwigGlobalsMiddleware $globals */ $globals = $container->get(TwigGlobalsMiddleware::class); /** @var CsrfMiddleware $csrf */ $csrf = $container->get(CsrfMiddleware::class); /** @var SessionMiddleware $session */ $session = $container->get(SessionMiddleware::class); $app->add($globals); $app->add($csrf); $app->addBodyParsingMiddleware(); $app->add($session); /** @var HomeController $home */ $home = $container->get(HomeController::class); $app->get('/', $home); /** @var HealthzController $healthz */ $healthz = $container->get(HealthzController::class); $app->get('/healthz', $healthz); /** @var LocalLoginController $local */ $local = $container->get(LocalLoginController::class); $app->get('/login', [$local, 'showLogin']); $app->post('/login/local', [$local, 'postLocal']); /** @var OidcController $oidc */ $oidc = $container->get(OidcController::class); $app->get('/login/oidc', [$oidc, 'initiate']); $app->get('/oidc/callback', [$oidc, 'callback']); /** @var LogoutController $logout */ $logout = $container->get(LogoutController::class); $app->post('/logout', $logout); /** @var NoAccessController $noAccess */ $noAccess = $container->get(NoAccessController::class); $app->get('/no-access', $noAccess); /** @var AuthRequiredMiddleware $authRequired */ $authRequired = $container->get(AuthRequiredMiddleware::class); $app->group('/app', function (RouteCollectorProxy $group) use ($container): void { /** @var MeController $me */ $me = $container->get(MeController::class); $group->get('/me', $me); /** @var DashboardController $dashboard */ $dashboard = $container->get(DashboardController::class); $group->get('/dashboard', $dashboard); /** @var IpsController $ips */ $ips = $container->get(IpsController::class); $group->get('/ips', [$ips, 'index']); // {ip:.+} so v6 colons don't break Slim's default segment regex. $group->get('/ips/{ip:.+}', [$ips, 'show']); /** @var ManualBlocksController $manualBlocks */ $manualBlocks = $container->get(ManualBlocksController::class); $group->get('/manual-blocks', [$manualBlocks, 'index']); $group->post('/manual-blocks', [$manualBlocks, 'create']); $group->post('/manual-blocks/{id}/delete', [$manualBlocks, 'delete']); // /app/subnets is an alias filtered to kind=subnet so the // sidebar's "Subnets" link lands on a focused list. $group->get('/subnets', [$manualBlocks, 'subnetsIndex']); /** @var AllowlistController $allowlist */ $allowlist = $container->get(AllowlistController::class); $group->get('/allowlist', [$allowlist, 'index']); $group->post('/allowlist', [$allowlist, 'create']); $group->post('/allowlist/{id}/delete', [$allowlist, 'delete']); /** @var PoliciesController $policies */ $policies = $container->get(PoliciesController::class); $group->get('/policies', [$policies, 'index']); $group->post('/policies', [$policies, 'create']); $group->get('/policies/{id}', [$policies, 'edit']); $group->post('/policies/{id}', [$policies, 'update']); $group->post('/policies/{id}/delete', [$policies, 'delete']); // GET-only XHR proxy used by the edit page's preview pane. $group->get('/policies/{id}/preview-proxy', [$policies, 'previewProxy']); /** @var ReportersController $reporters */ $reporters = $container->get(ReportersController::class); $group->get('/reporters', [$reporters, 'index']); $group->post('/reporters', [$reporters, 'create']); $group->get('/reporters/{id}', [$reporters, 'edit']); $group->post('/reporters/{id}', [$reporters, 'update']); $group->post('/reporters/{id}/delete', [$reporters, 'delete']); /** @var ConsumersController $consumers */ $consumers = $container->get(ConsumersController::class); $group->get('/consumers', [$consumers, 'index']); $group->post('/consumers', [$consumers, 'create']); $group->get('/consumers/{id}', [$consumers, 'edit']); $group->post('/consumers/{id}', [$consumers, 'update']); $group->post('/consumers/{id}/delete', [$consumers, 'delete']); /** @var TokensController $tokens */ $tokens = $container->get(TokensController::class); $group->get('/tokens', [$tokens, 'index']); $group->post('/tokens', [$tokens, 'create']); $group->post('/tokens/{id}/delete', [$tokens, 'delete']); /** @var CategoriesController $categories */ $categories = $container->get(CategoriesController::class); $group->get('/categories', [$categories, 'index']); $group->post('/categories', [$categories, 'create']); $group->get('/categories/{id}', [$categories, 'edit']); $group->post('/categories/{id}', [$categories, 'update']); $group->post('/categories/{id}/delete', [$categories, 'delete']); /** @var AuditController $audit */ $audit = $container->get(AuditController::class); $group->get('/audit', [$audit, 'index']); /** @var SearchController $search */ $search = $container->get(SearchController::class); $group->get('/search', [$search, 'index']); /** @var SettingsController $settings */ $settings = $container->get(SettingsController::class); $group->get('/settings', [$settings, 'index']); $group->post('/settings/jobs/trigger/{name}', [$settings, 'trigger']); $group->post('/settings/maintenance/purge', [$settings, 'purge']); $group->post('/settings/maintenance/seed-demo', [$settings, 'seedDemo']); })->add($authRequired); $app->map( ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], '/{routes:.+}', function ($request, $response) { return $response->withStatus(404)->withHeader('Content-Type', 'text/plain'); } ); return $app; } }