configureRateLimiting(); if (config('app.force_https')) { \Illuminate\Support\Facades\URL::forceScheme('https'); } // Links und Weiterleitungen nie aus dem Host-Header bauen: sonst landen in synchron // versandten Mails (z. B. Login-Links) Adressen eines fremden Hosts. Eine lokale APP_URL // (häufig unverändert aus der .env.example) würde dagegen jede Weiterleitung kaputt machen. if ($this->app->isProduction() && $this->hasPublicAppUrl()) { \Illuminate\Support\Facades\URL::forceRootUrl(config('app.url')); } $this->routes(function (): void { // Vor routes/api.php registriert, damit die versionierten Pfade zuerst greifen. Route::prefix('api/v1') ->middleware('api.machine') ->group(base_path('routes/api_v1.php')); Route::prefix('api') ->middleware('api') ->group(base_path('routes/api.php')); Route::middleware('web') ->group(base_path('routes/web.php')); // External-access routes carry their own middleware groups (declared per-group in the files). Route::group([], base_path('routes/external-guest.php')); Route::group([], base_path('routes/external.php')); Route::prefix('api/app/v1') ->middleware('api.app') ->name('app.v1.') ->group(base_path('routes/app-api.php')); }); Route::bind('projects', function ($value) { return Project::withTrashed()->whereKey($value)->firstOrFail(); }); } protected function configureRateLimiting(): void { RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Login-Discovery der App: fragt jede bekannte Instanz nach einem E-Mail-Hash. Das // Limit bremst das Abklopfen fremder Adressen, normale Logins brauchen eine Anfrage. RateLimiter::for('app-discover', function (Request $request) { // Die App fragt pro Anmeldung einmal; die Stundengrenze bremst das Durchprobieren von Adressen. return [ Limit::perMinute(10)->by('app-discover:ip:' . $request->ip()), Limit::perHour(60)->by('app-discover:ip-hour:' . $request->ip()), ]; }); RateLimiter::for('app-login', function (Request $request) { // Läuft vor der Request-Validierung: Bots posten "email" auch als Array (sonst 500) $email = $request->input('email'); $email = Str::lower(is_string($email) ? $email : ''); $ip = (string) $request->ip(); return [ Limit::perMinute(10)->by('app-login:ip:' . $ip), Limit::perMinute(5)->by('app-login:identity:' . hash('sha256', $email . '|' . $ip)), // Pro Konto unabhängig von der IP (wie der Web-Login): bremst Password-Spraying und das // Durchprobieren von 2FA-Codes über viele Adressen; höchstens 15 Minuten Wartezeit. Limit::perMinutes(15, 10)->by('app-login:account:' . hash('sha256', trim($email))), ]; }); RateLimiter::for('api-token', function (Request $request) { $token = $request->user()?->token(); $key = $token instanceof AccessToken ? $token->oauth_access_token_id : $request->ip(); return Limit::perMinute(120)->by($key); }); } /** * Nur Loopback-/Entwicklungsadressen gelten als „nicht gesetzt“; Intranet-Namen und -IPs sind legitim. */ private function hasPublicAppUrl(): bool { $host = strtolower((string) parse_url((string) config('app.url'), PHP_URL_HOST)); return $host !== '' && !in_array($host, ['localhost', '127.0.0.1', '::1', '[::1]'], true) && !str_ends_with($host, '.localhost') && !str_ends_with($host, '.test'); } }