app->bind(MalwareScanner::class, ClamDScanner::class); $this->app->bind(BusinessRepository::class, CompatibleBusinessRepository::class); } /** * Bootstrap any application services. */ public function boot(): void { $this->configureDefaults(); // Fortify calls this response AFTER invalidating/regenerating the server session. // Clear the client's encryption key too, so Back must reauthorize private pages. $this->app->bind(LogoutResponse::class, function () { return new class extends \Laravel\Fortify\Http\Responses\LogoutResponse { public function toResponse($request) { Inertia::clearHistory(); return parent::toResponse($request); } }; }); Event::listen(ValidTwoFactorAuthenticationCodeProvided::class, function ($event): void { // The authenticated installers also operate before the optional profile schema exists. // Record a completed Fortify challenge, never enrollment or a password-only login. session()->put(['portal.mfa_actor' => $event->user->id, 'portal.mfa_at' => time()]); }); Event::listen(Login::class, function ($event): void { if (app(PortalProfileAccess::class)->governanceAvailable()) { session()->put('portal.access_revision', (int) DB::table('users')->where('id', $event->user->id)->value('portal_access_revision')); } }); Gate::define('manage-portal', fn (User $user) => $user->hasVerifiedEmail() && in_array(strtolower($user->email), config('portal.managers', []), true)); foreach ([Sector::class, Opportunity::class] as $model) { $invalidate = function () { Cache::forget('portal_home_pt'); Cache::forget('portal_home_en'); Cache::forget('portal_home_v2_pt'); Cache::forget('portal_home_v2_en'); Cache::forget('portal_home_sector_organization_v1_pt'); Cache::forget('portal_home_sector_organization_v1_en'); Cache::forget('portal_home_territorial_v2_pt'); Cache::forget('portal_home_territorial_v2_en'); }; $model::saved($invalidate); $model::deleted($invalidate); } $requestLimitKey = static function (Request $request): string { if ($user = $request->user()) { return 'user:'.$user->getAuthIdentifier(); } $token = $request->header('X-Simulation-Token'); return is_string($token) && $token !== '' ? 'anonymous:'.hash('sha256', $token) : 'ip:'.$request->ip(); }; RateLimiter::for('api', function (Request $request) use ($requestLimitKey) { return Limit::perMinute(60)->by($requestLimitKey($request)) ->response(fn (Request $request, array $headers) => response()->json([ 'message' => 'Demasiadas solicitações. Aguarde um pouco e tente novamente.', ], 429, $headers)); }); RateLimiter::for('simulation-draft-writes', function (Request $request) use ($requestLimitKey) { return Limit::perMinute(30)->by('simulation-draft:'.$requestLimitKey($request)) ->response(fn (Request $request, array $headers) => response()->json([ 'message' => 'Foram guardadas demasiadas alterações em pouco tempo. Aguarde um minuto e tente novamente.', ], 429, $headers)); }); RateLimiter::for('simulation-calculations', function (Request $request) use ($requestLimitKey) { return Limit::perMinute(10)->by('simulation-calculation:'.$requestLimitKey($request)) ->response(fn (Request $request, array $headers) => response()->json([ 'message' => 'Foram solicitados demasiados cálculos em pouco tempo. Aguarde antes de tentar novamente.', ], 429, $headers)); }); RateLimiter::for('simulation-scenario-updates', function (Request $request) use ($requestLimitKey) { return Limit::perMinute(10)->by('simulation-scenario-update:'.$requestLimitKey($request)) ->response(fn (Request $request, array $headers) => response()->json([ 'message' => 'Foram solicitadas demasiadas alterações de cenário em pouco tempo. Aguarde antes de tentar novamente.', ], 429, $headers)); }); } /** * Configure default behaviors for production-ready applications. */ protected function configureDefaults(): void { Date::use(CarbonImmutable::class); DB::prohibitDestructiveCommands( app()->isProduction(), ); Password::defaults(fn (): ?Password => app()->isProduction() ? Password::min(12) ->mixedCase() ->letters() ->numbers() ->symbols() ->uncompromised() : null, ); } }