app->singleton(PireonClient::class, fn () => PireonClient::fromConfig()); $this->app->scoped(TokenStore::class, fn ($app) => new TokenStore( $app['session.store'], $app->make(PireonClient::class), )); // Resolves git hosting drivers; see config/git.php. $this->app->singleton(GitManager::class); } public function boot(): void { // Backs the "api" guard declared in config/auth.php. Auth::viaRequest('pireon', fn (Request $request) => $this->app ->make(PireonTokenGuard::class)($request)); // Drop the OAuth token set whenever a session ends, no matter which // route did it — ours, or Filament's own logout button. Event::listen(Logout::class, function (): void { if ($this->app->bound('session') && $this->app['session']->isStarted()) { $this->app->make(TokenStore::class)->forget(); } }); // Backs `throttle:api` on the REST routes. Authenticated callers are // budgeted per user so one noisy client cannot exhaust the allowance // of everyone sharing an IP. RateLimiter::for('api', fn (Request $request) => Limit::perMinute(120) ->by($request->user()?->id ?: $request->ip())); // The public API is a different problem. The public frontend renders // server-side, so every visitor's page view arrives from the same IP: // a per-IP budget sized for one person would throttle the entire // website as though it were a single abusive client. Hence a generous // limit, and an explicit bypass for the frontend's own address. RateLimiter::for('public-api', function (Request $request) { if (in_array($request->ip(), (array) config('site.trusted_ips'), true)) { return Limit::none(); } return Limit::perMinute((int) config('site.rate_limit', 600))->by($request->ip()); }); // Submissions are the opposite problem again. Reads are throttled per // IP; writes cannot be, because SSR means every submission on the site // arrives from the frontend's single address — which is in // `trusted_ips` and would bypass the limit anyway. // // So the budget is spent per e-mail address, with a site-wide hourly // ceiling behind it as the actual protection against a flood. RateLimiter::for('public-write', fn (Request $request) => [ Limit::perMinute((int) config('site.submissions.rate_limit', 3)) ->by('submission:'.sha1(Str::lower((string) $request->input('email')))), Limit::perHour((int) config('site.submissions.hourly_limit', 60)) ->by('submissions'), ]); // Telemetry is a third case again. It is one server reporting to // another, authenticated by a shared secret, so the address it comes // from is stable and a per-IP budget means what it says — unlike the // submissions, where the same address stands for every visitor. // // Trusted IPs deliberately do *not* bypass this one: the frontend is // the only caller there will ever be, so an exemption would leave the // endpoint with no ceiling at all. RateLimiter::for('public-telemetry', fn (Request $request) => Limit::perMinute( (int) config('site.telemetry.rate_limit', 60) )->by($request->ip())); // Fail loudly in development when a relation is used without being // loaded, or a non-fillable attribute is assigned. Model::shouldBeStrict($this->app->isLocal()); Model::unguard(false); // The OAuth redirect URI is registered as https on the identity // server, so generated URLs must match it in deployed environments. if ($this->app->environment('production') || str_starts_with((string) config('app.url'), 'https://')) { URL::forceScheme('https'); } } }