withRouting( web: __DIR__.'/../routes/domains.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', then: function (): void { // Legacy-API-Aliasse (/service/… ohne /api-Präfix) in der // api-Middleware-Gruppe — Übernahme bestehender Integrationen. Route::middleware('api')->group(__DIR__.'/../routes/legacy-api.php'); // Tiefer Health-Check für den externen Monitor (Monitoring-Plan // 1.5) — ohne web-/api-Gruppe: keine Session, kein API-Log. Route::get('/health', HealthController::class) ->middleware('throttle:30,1') ->name('health.deep'); }, ) ->withMiddleware(function (Middleware $middleware) { // Kanonische Hosts: www. → 301 auf die Apex-Domain, so früh // wie möglich (vor Session/Theme), damit www-Requests gar nicht erst // gerendert werden. $middleware->prepend(RedirectWwwToApex::class); // Security-Header auf allen Antworten (Web + API), Proxy-unabhängig. $middleware->append(SecurityHeaders::class); // Fehler ohne Exception (tote interne Links, 403/429 für Angemeldete, // 419) als GlitchTip-Warnung — global, weil 404 vor jedem Routing // entsteht (Monitoring-Plan 1.3). $middleware->append(ReportErrorSignals::class); // Portal-Kontext nach dem Theme-Provider setzen (liest config('app.theme')) $middleware->append(SetCurrentPortal::class); // Stufe 1 der Konversionsleiter: Klick aus einer unserer Mails // (`?src=`). Bewusst global statt je Route — die Mail-Links zeigen auf // Hub, Spokes und öffentliche Detailseiten gleichermaßen, und eine // vergessene Route wäre eine unsichtbare Lücke in der Messung. $middleware->append(RecordMailLinkClick::class); // Ausgabe/Sprache (de|en) aus dem URL-Präfix ableiten und URL-Defaults setzen. $middleware->append(SetEdition::class); // Portalseiten auf dem Hub (Anhang 2, E10): /de/preise & Co. gibt es // nur auf den Portalen — 301 statt einer Fassung mit fremdem Branding // unter pressekonto.com. In der web-Gruppe, nicht global: die // Middleware liest den Routennamen, und der steht erst nach dem // Routing fest. $middleware->web(append: [RedirectPortalOnlyPagesFromHub::class]); // Gesperrte Sessions (Audit K-04): nach einer Kontoübernahme per // OAuth-Verifizierung fliegen ältere Sessions beim nächsten Request // raus — nach StartSession, deshalb in der web-Gruppe. $middleware->web(append: [EnsureSessionIsNotRevoked::class]); // Wohin eingeloggte User von Gast-Routen (/login, /register) gelenkt // werden: rollen- und verifizierungsbewusst statt fix auf /dashboard, // sonst landet ein Customer dort im 403 und sitzt fest. $middleware->redirectUsersTo(function (Request $request) { $user = $request->user(); if ($user && ! $user->hasVerifiedEmail()) { return route('verification.notice'); } if ($user?->canAccessAdmin()) { return route('dashboard'); } return $user?->canAccessCustomer() ? route('me.dashboard') : '/'; }); $middleware->api(prepend: [ LogApiUsage::class, RejectLegacyApiKeys::class, ]); // One-Click-Unsubscribe (RFC 8058): Mail-Clients POSTen ohne Session/ // CSRF-Token auf die signierte Abmelde-URL — die Signatur ersetzt den // CSRF-Schutz. $middleware->validateCsrfTokens(except: [ 'e-mail-benachrichtigungen/abmelden/*', 'e-mail-benachrichtigungen/abmelden-adresse', ]); $middleware->alias([ 'abilities' => CheckAbilities::class, 'ability' => CheckForAnyAbility::class, ]); // BasicAuth ganz am Ende, nach Session-Middleware. Immer anhängen; // ob erzwungen wird, entscheidet die Middleware zur Laufzeit anhand // von config('auth.basic.*') (greift auch unter `config:cache`). $middleware->append(BasicAuthMiddleware::class); // Trust all proxies (for Traefik/Docker setup) $middleware->trustProxies( at: '*', headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO ); // Host-Whitelist: Da allen Proxies inkl. X-Forwarded-Host vertraut wird, // muss die App fremde Host-Header verwerfen — sonst kann ein Client per // X-Forwarded-Host die URL-Generierung vergiften (Passwort-Reset-/Verify- // Mails, signierte Links, Cache-Poisoning). Nur die konfigurierten // Domains (und deren Subdomains) sowie lokale Aufrufe (Health-Checks per // localhost/IP) sind zulässig. Laravel erzwingt dies nur außerhalb von // `local`/Tests (siehe TrustHosts::shouldSpecifyTrustedHosts). $middleware->trustHosts(at: function (): array { $domainPatterns = array_map( static fn (string $host): string => '^(.+\.)?'.preg_quote($host, '#').'$', array_filter([ config('domains.domain_portal'), config('domains.domain_presseecho'), config('domains.domain_businessportal'), ], static fn (?string $host): bool => $host !== null && $host !== ''), ); // Health-Checks/Uptime-Monitore erreichen den Container oft per // localhost oder direkter IP (kein DNS-Name) — diese dürfen keinen // 400 bekommen. Ein IP-/localhost-Host kann keine Phishing-Domain in // signierte Links schleusen, daher unbedenklich. $localPatterns = [ '^localhost$', '^\d{1,3}(\.\d{1,3}){3}$', '^\[?[0-9a-fA-F:]+\]?$', ]; return array_values(array_merge($domainPatterns, $localPatterns)); }, subdomains: false); }) ->withExceptions(function (Exceptions $exceptions) { // Gemeldete Exceptions zusätzlich an GlitchTip (Monitoring-Plan 1.1). // Ohne SENTRY_LARAVEL_DSN ein No-op; Laravels dontReport-Liste (404, // Validierung, Auth) gilt weiter — diese Fälle meldet // ReportErrorSignals gezielt als Warnung. Integration::handles($exceptions); // Vertippte artisan-Befehle sind kein Anwendungsfehler (GlitchTip // server-pressekonto, Issue 9: „config:cachels“ auf Live). $exceptions->dontReport(CommandNotFoundException::class); })->create();