app->singleton( \App\Services\Countries\CountryRemoteProviderInterface::class, \App\Services\Countries\CountryRemoteProvider::class, ); // Bind UploadDraftService interface to implementation $this->app->singleton(UploadDraftServiceInterface::class, function ($app) { return new UploadDraftService($app->make('filesystem')); }); // Bind vector adapter interface for similarity system (resolves via factory) $this->app->bind( \App\Services\Recommendations\VectorSimilarity\VectorAdapterInterface::class, fn () => \App\Services\Recommendations\VectorSimilarity\VectorAdapterFactory::make(), ); // EGS: bind SpotlightEngineInterface to the concrete SpotlightEngine $this->app->singleton( \App\Services\EarlyGrowth\SpotlightEngineInterface::class, \App\Services\EarlyGrowth\SpotlightEngine::class, ); } /** * Bootstrap any application services. */ public function boot(): void { $this->registerCpadMenuItems(); // Map the 'legacy' view namespace to resources/views/_legacy so all // view('legacy::foo') and @include('legacy::foo') calls resolve correctly // after the folder was renamed from legacy/ to _legacy/. View::addNamespace('legacy', resource_path('views/_legacy')); $this->configureAuthRateLimiters(); $this->configureUploadRateLimiters(); $this->configureMessagingRateLimiters(); $this->configureDownloadRateLimiter(); $this->configureArtworkRateLimiters(); $this->configureReactionRateLimiters(); $this->configureSocialRateLimiters(); $this->configureSettingsRateLimiters(); $this->configureMailFailureLogging(); ArtworkAward::observe(ArtworkAwardObserver::class); Artwork::observe(ArtworkObserver::class); ArtworkFavourite::observe(ArtworkFavouriteObserver::class); ArtworkComment::observe(ArtworkCommentObserver::class); ArtworkReaction::observe(ArtworkReactionObserver::class); // ── OAuth / SocialiteProviders ────────────────────────────────────── Event::listen( \SocialiteProviders\Manager\SocialiteWasCalled::class, \SocialiteProviders\Discord\DiscordExtendSocialite::class, ); // Apple provider removed — no listener registered // ── Posts / Feed System Events ────────────────────────────────────── Event::listen( \App\Events\Posts\ArtworkShared::class, \App\Listeners\Posts\SendArtworkSharedNotification::class, ); Event::listen( \App\Events\Posts\PostCommented::class, \App\Listeners\Posts\SendPostCommentedNotification::class, ); Event::listen( \App\Events\Posts\PostCommented::class, \App\Listeners\Posts\AwardXpForPostCommented::class, ); Event::listen( \App\Events\Achievements\AchievementCheckRequested::class, \App\Listeners\Achievements\CheckUserAchievements::class, ); Event::listen( \App\Events\Achievements\UserXpUpdated::class, \App\Listeners\Achievements\CheckUserAchievements::class, ); // Provide toolbar counts and user info to layout views (port of legacy toolbar logic) View::composer(['layouts.nova', 'layouts.nova.*'], function ($view) { $uploadCount = $favCount = $msgCount = $noticeCount = $receivedCommentsCount = 0; $avatarHash = null; $displayName = null; $userId = null; if (Auth::check()) { $userId = Auth::id(); try { $uploadCount = DB::table('artworks')->where('user_id', $userId)->count(); } catch (\Throwable $e) { $uploadCount = 0; } try { $favCount = DB::table('artwork_favourites')->where('user_id', $userId)->count(); } catch (\Throwable $e) { $favCount = 0; } try { $msgCount = (int) DB::table('conversation_participants as cp') ->join('messages as m', 'm.conversation_id', '=', 'cp.conversation_id') ->where('cp.user_id', $userId) ->whereNull('cp.left_at') ->whereNull('m.deleted_at') ->where('m.sender_id', '!=', $userId) ->where(function ($q) { $q->whereNull('cp.last_read_at') ->orWhereColumn('m.created_at', '>', 'cp.last_read_at'); }) ->count(); } catch (\Throwable $e) { $msgCount = 0; } try { $noticeCount = DB::table('notifications')->where('user_id', $userId)->whereNull('read_at')->count(); } catch (\Throwable $e) { $noticeCount = 0; } try { $receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class) ->unreadCountForUser(Auth::user()); } catch (\Throwable $e) { $receivedCommentsCount = 0; } try { $profile = DB::table('user_profiles')->where('user_id', $userId)->first(); $avatarHash = $profile->avatar_hash ?? null; } catch (\Throwable $e) { $avatarHash = null; } $displayName = Auth::user()->name ?: (Auth::user()->username ?? ''); } $view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName')); }); // Replace the framework HandleCors with our ConditionalCors so the // CP_ENABLE_CORS / config('cors.paths') toggle takes effect. try { $middlewareConfig = $this->app->make(\Illuminate\Foundation\Configuration\Middleware::class); $middlewareConfig->replace( \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\ConditionalCors::class ); } catch (\Throwable $_) { // Fallback: push to kernel if replace isn't available in this app instance $this->app->make(\Illuminate\Contracts\Http\Kernel::class) ->pushMiddleware(\App\Http\Middleware\ConditionalCors::class); } } private function configureAuthRateLimiters(): void { RateLimiter::for('register-ip', function (Request $request): Limit { $limit = max(1, (int) config('registration.ip_per_minute_limit', 3)); return Limit::perMinute($limit)->by('register:ip:' . $request->ip()); }); RateLimiter::for('register-ip-daily', function (Request $request): Limit { $limit = max(1, (int) config('registration.ip_per_day_limit', 20)); return Limit::perDay($limit)->by('register:ip:daily:' . $request->ip()); }); RateLimiter::for('register', function (Request $request): array { $emailKey = strtolower((string) $request->input('email', 'unknown')); $ipLimit = (int) config('registration.ip_per_minute_limit', 3); $emailLimit = (int) config('registration.email_per_minute_limit', 6); return [ Limit::perMinute($ipLimit)->by('register:ip:' . $request->ip()), Limit::perMinute($emailLimit)->by('register:email:' . $emailKey), ]; }); } private function configureMailFailureLogging(): void { Event::listen(JobFailed::class, function (JobFailed $event): void { if (! str_contains(strtolower($event->job->resolveName()), 'sendqueuedmailable')) { return; } Log::warning('mail delivery failed', [ 'transport' => config('mail.default'), 'job_name' => $event->job->resolveName(), 'queue' => $event->job->getQueue(), 'connection' => $event->connectionName, 'exception' => $event->exception->getMessage(), ]); }); } private function configureUploadRateLimiters(): void { RateLimiter::for('uploads-init', function (Request $request): array { return $this->buildUploadLimits($request, 'init'); }); RateLimiter::for('uploads-finish', function (Request $request): array { return $this->buildUploadLimits($request, 'finish'); }); RateLimiter::for('uploads-status', function (Request $request): array { return $this->buildUploadLimits($request, 'status'); }); } private function buildUploadLimits(Request $request, string $key): array { $config = (array) config('uploads.rate_limits.' . $key, []); $decay = (int) config('uploads.rate_limits.decay_minutes', 1); $perUser = (int) ($config['per_user'] ?? 0); $perIp = (int) ($config['per_ip'] ?? 0); $limits = []; if ($perUser > 0) { $userId = $request->user()?->id ?? 'guest'; $limits[] = Limit::perMinutes($decay, $perUser)->by('u:' . $userId); } if ($perIp > 0) { $limits[] = Limit::perMinutes($decay, $perIp)->by('ip:' . $request->ip()); } return $limits; } private function configureMessagingRateLimiters(): void { RateLimiter::for('messages-send', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(20)->by('messages:user:' . $userId), Limit::perMinute(40)->by('messages:ip:' . $request->ip()), ]; }); RateLimiter::for('messages-react', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(60)->by('messages:react:user:' . $userId), Limit::perMinute(120)->by('messages:react:ip:' . $request->ip()), ]; }); RateLimiter::for('messages-read', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(120)->by('messages:read:user:' . $userId), Limit::perMinute(240)->by('messages:read:ip:' . $request->ip()), ]; }); RateLimiter::for('messages-typing', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; $conversationId = (int) $request->route('conversation_id'); return [ Limit::perMinute(90)->by('messages:typing:user:' . $userId . ':conv:' . $conversationId), Limit::perMinute(180)->by('messages:typing:ip:' . $request->ip()), ]; }); RateLimiter::for('messages-recovery', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; $conversationId = (int) $request->route('conversation_id'); return [ Limit::perMinute(30)->by('messages:recovery:user:' . $userId . ':conv:' . $conversationId), Limit::perMinute(60)->by('messages:recovery:ip:' . $request->ip()), ]; }); RateLimiter::for('messages-presence', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(180)->by('messages:presence:user:' . $userId), Limit::perMinute(300)->by('messages:presence:ip:' . $request->ip()), ]; }); } private function configureDownloadRateLimiter(): void { RateLimiter::for('downloads', function (Request $request): array { $userId = $request->user()?->id; // Higher user-based allowance prevents false positives for active users, // while IP limit still protects guest endpoints from bursts. return [ Limit::perMinute(60)->by('downloads:user:' . ($userId ?? 'guest')), Limit::perMinute(120)->by('downloads:ip:' . $request->ip()), ]; }); } private function configureArtworkRateLimiters(): void { RateLimiter::for('artwork-awards', function (Request $request): array { $userId = $request->user()?->id; $artworkId = (int) $request->route('id'); return [ // Prevent burst spam on a single artwork while allowing normal exploration. Limit::perMinute(20)->by('awards:user:' . ($userId ?? 'guest') . ':art:' . $artworkId), // Global safety net for user/IP across all artworks. Limit::perMinute(120)->by('awards:user:' . ($userId ?? 'guest')), Limit::perMinute(180)->by('awards:ip:' . $request->ip()), ]; }); } private function configureReactionRateLimiters(): void { RateLimiter::for('reactions-read', function (Request $request): array { $userId = $request->user()?->id; return [ // Comment-heavy pages can trigger many reaction reads at once. Limit::perMinute(600)->by('reactions-read:user:' . ($userId ?? 'guest')), Limit::perMinute(900)->by('reactions-read:ip:' . $request->ip()), ]; }); RateLimiter::for('reactions-write', function (Request $request): array { $userId = $request->user()?->id; return [ Limit::perMinute(120)->by('reactions-write:user:' . ($userId ?? 'guest')), Limit::perMinute(180)->by('reactions-write:ip:' . $request->ip()), ]; }); } private function configureSocialRateLimiters(): void { RateLimiter::for('social-write', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(60)->by('social-write:user:' . $userId), Limit::perMinute(120)->by('social-write:ip:' . $request->ip()), ]; }); RateLimiter::for('social-read', function (Request $request): array { $userId = $request->user()?->id ?? 'guest'; return [ Limit::perMinute(240)->by('social-read:user:' . $userId), Limit::perMinute(480)->by('social-read:ip:' . $request->ip()), ]; }); } private function configureSettingsRateLimiters(): void { RateLimiter::for('username-check', function (Request $request): Limit { $key = 'username-check:ip:' . $request->ip(); if (method_exists(Limit::class, 'perSecond')) { return Limit::perSecond(5)->by($key); } return Limit::perMinute(300)->by($key); }); RateLimiter::for('email-change-request', function (Request $request): Limit { $userId = $request->user()?->id; $key = $userId !== null ? 'email-change-request:user:' . $userId : 'email-change-request:ip:' . $request->ip(); return Limit::perHour(1)->by($key); }); } private function registerCpadMenuItems(): void { if (! class_exists(Menu::class)) { return; } try { /** @var Menu $menu */ $menu = $this->app->make(Menu::class); } catch (\Throwable) { // Control panel menu registration should never block the app boot. } } }