app->bind(NewsletterSyncClient::class, function (): NewsletterSyncClient { $provider = (string) config('newsletter.sync.provider', 'placeholder'); // Es existiert bislang nur der No-Op-Client. Ein aktivierter, aber // nicht implementierter Provider soll laut schlagen statt stumm // ins Leere zu synchronisieren. if (config('newsletter.sync.enabled') && $provider !== 'placeholder') { throw new \RuntimeException( "Newsletter-Sync-Provider [{$provider}] ist nicht implementiert – es existiert nur der Platzhalter-Client." ); } return new NullNewsletterSyncClient; }); $this->app->bind(DnsResolver::class, SystemDnsResolver::class); $this->app->singleton(PressReleaseService::class); $this->app->singleton(AdminRequestPerformanceMetrics::class); } /** * Bootstrap any application services. */ public function boot(): void { Model::preventLazyLoading(app()->isLocal()); // Die Generations-Nummer der Seiten-Caches wird prozessintern gemerkt. // Ein Queue-Worker lebt über beliebig viele Jobs hinweg und würde sonst // dauerhaft mit dem Stand seines ersten Zugriffs arbeiten — Keys einer // längst verworfenen Generation lesen und löschen. Queue::looping(static fn () => WebPageCache::resetMemo()); $scheme = request()->header('X-Forwarded-Proto') ?? request()->server('HTTP_X_FORWARDED_PROTO') ?? (request()->secure() ? 'https' : 'http'); if ($scheme === 'https') { URL::forceScheme('https'); } // Baseline für das Ausgabe-Präfix (de|en). Die SetEdition-Middleware // überschreibt dies pro Request mit der tatsächlichen Ausgabe; der // Default hier stellt sicher, dass route('…')-Aufrufe das {edition}- // Segment auch außerhalb der HTTP-Middleware erhalten (Livewire-Tests, // Konsole, Mails). URL::defaults(['edition' => SetEdition::DEFAULT_EDITION]); // Zugriffs-Gates als Single Source of Truth (auch in Blade via @can und in // Policies nutzbar). Dieselbe Definition, die die Panel-Middleware prüfen. // Super-Admins dürfen alles: zentraler Kurzschluss vor allen Gates und // Permission-Checks (analog zu den Policy-`before()`-Methoden). Damit // greifen feingranulare `can('users:manage')`-Prüfungen auch für // Super-Admins, die keine Spatie-Rolle mit der Permission tragen. // Deaktivierung geht vor (Audit F09): ein gesperrter Super-Admin fällt // auf die regulären Gates zurück, die `is_active` prüfen – sonst wäre // die Sperre für bestehende Sessions wirkungslos. // Ausnahme: die Kundenbestätigung eines Admin-Uploads ist an die // Identität des Kontoinhabers gebunden (PressReleasePolicy, Audit N05). Gate::before(fn (User $user, string $ability): ?bool => $ability !== 'confirmImageRights' && $user->is_super_admin && $user->is_active ? true : null); Gate::define('access-admin', fn (User $user): bool => $user->canAccessAdmin()); Gate::define('access-customer', fn (User $user): bool => $user->canAccessCustomer()); // Betriebs-Alert bei fehlgeschlagenem Queue-Job (R5.3): die KI-Jobs // laufen auf der `classification`-Queue — ein stiller Stau in // `failed_jobs` würde jede Einreichung in der Prüfung hängen lassen. Queue::failing(function (JobFailed $event): void { // Bounce-Pfad 2: SMTP-Ablehnung im Worker → Sperre bzw. Circuit // Breaker (Roadmap 1b.6). Läuft VOR dem Alert, damit der Breaker // seinen eigenen, einmaligen Alert setzen kann. app(RecordSmtpFailure::class)->record($event); // Der Breaker hat den Versand angehalten: Das ist EIN Alert (beim // Öffnen), nicht einer je liegengebliebener Mail. if ($event->exception instanceof MailDeliveryPausedException) { return; } OperationalAlert::send("Queue-Job fehlgeschlagen: {$event->job->resolveName()}", [ 'connection' => $event->connectionName, 'queue' => $event->job->getQueue(), 'exception' => $event->exception->getMessage(), ]); }); // Registrierung → Verifizierungsmail; bestätigter Klick → Aktivierung. Event::listen(Registered::class, SendEmailVerificationNotification::class); Event::listen(Verified::class, ActivateUserAfterVerification::class); // Aktivitäts-Verlauf (Admin ▸ Reports ▸ Verlauf): Registrierung und // Login; Formular-Eingänge laufen über den ActivityLogObserver unten. Event::listen(Registered::class, [RecordAuthActivity::class, 'recordRegistered']); Event::listen(Login::class, [RecordAuthActivity::class, 'recordLogin']); // Das eine Gate vor jeder ausgehenden Mail: Suppression, Return-Path, // Circuit Breaker (Roadmap 1b.6). Gibt false zurück → Mail geht nicht raus. Event::listen(MessageSending::class, [GateOutgoingMail::class, 'gate']); // Jede tatsächlich versendete E-Mail landet ebenfalls im Verlauf. Event::listen(MessageSent::class, [RecordMailActivity::class, 'recordSent']); // Framework-Auth-Mails (Passwort-Reset, E-Mail-Verifizierung) im // pressekonto-CI rendern statt im Laravel-Standard-Markdown-Theme — // gleiches Layout wie alle übrigen System-Mails (emails/layout). ResetPassword::toMailUsing(function (User $user, string $token): MailMessage { $resetUrl = url(route('password.reset', [ 'token' => $token, 'email' => $user->getEmailForPasswordReset(), ], false)); return (new MailMessage) ->subject('Passwort zurücksetzen für Ihr pressekonto') ->view('emails.auth.reset-password', [ 'user' => $user, 'resetUrl' => $resetUrl, 'expiresInMinutes' => (int) config('auth.passwords.'.config('auth.defaults.passwords').'.expire'), ]); }); VerifyEmail::toMailUsing(function (User $user, string $verificationUrl): MailMessage { return (new MailMessage) ->subject('E-Mail-Adresse bestätigen für Ihr pressekonto') ->view('emails.auth.verify-email', [ 'user' => $user, 'verificationUrl' => $verificationUrl, ]); }); // Bei jedem Login Firmen-/Kontakt-Zuordnungen (gleiche E-Mail) auffrischen. Event::listen(Login::class, SyncCompanyMembershipsOnLogin::class); // Stripe Tax berechnet die USt im Checkout automatisch nach den // gleichen Regeln wie der VatResolver im MAN-Kreis (DE mit Steuer, // EU nur mit USt-ID befreit, Drittland befreit). Aktiviert zugleich // die USt-ID-Abfrage im Stripe Checkout. Cashier::calculateTaxes(); // Formular-Eingänge in den Aktivitäts-Verlauf schreiben. ContactMessage::observe(ActivityLogObserver::class); LegalRequest::observe(ActivityLogObserver::class); WithdrawalRequest::observe(ActivityLogObserver::class); TerminationRequest::observe(ActivityLogObserver::class); NewsletterSubscription::observe(ActivityLogObserver::class); // Hält die Suchtabelle synchron — sie trägt die Volltext-Indizes, // damit press_releases ohne teuren Rebuild erweitert werden kann. PressRelease::observe(PressReleaseSearchObserver::class); // Bestätigungsprotokoll je Bild bleibt insert-only (Bildkennzeichnung A.2). ImageRightsConfirmation::observe(ImageRightsConfirmationObserver::class); // Kontakterreichbarkeit: DNS-Befund der Mail-Domain nachziehen, // sobald eine Adresse entsteht oder sich ändert. Company::observe(EmailDomainObserver::class); Contact::observe(EmailDomainObserver::class); // Öffentliche Seiten-Caches bei Löschung/Deaktivierung einer Firma // verwerfen — auf jedem Löschweg, nicht nur im Admin-Formular (F13). Company::observe(CompanyPublicCacheObserver::class); AdminPreset::observe(AdminPerformanceCacheObserver::class); Category::observe(AdminPerformanceCacheObserver::class); CategoryTranslation::observe(AdminPerformanceCacheObserver::class); Company::observe(AdminPerformanceCacheObserver::class); Contact::observe(AdminPerformanceCacheObserver::class); NewsletterSubscription::observe(AdminPerformanceCacheObserver::class); PressRelease::observe(AdminPerformanceCacheObserver::class); User::observe(AdminPerformanceCacheObserver::class); Permission::observe(AdminPerformanceCacheObserver::class); Role::observe(AdminPerformanceCacheObserver::class); DB::listen(fn (QueryExecuted $query) => app(AdminRequestPerformanceMetrics::class)->record($query)); // Auch bei Livewire-Folge-Requests erneut prüfen — sonst gälten die // Panel-Gates nur für den initialen Seitenaufruf (R6.2: Customer-Gate // symmetrisch zum Admin-Gate persistieren). Livewire::addPersistentMiddleware([ BlockAdminDuringImpersonation::class, EnsureUserIsAdmin::class, EnsureUserIsCustomer::class, LogSlowAdminRequests::class, ]); } /** * Set the asset URL dynamically based on the current theme */ protected function setDynamicAssetUrl(): void { try { $assetUrl = ThemeHelper::getAssetUrl(); config(['app.asset_url' => $assetUrl]); } catch (\Exception $e) { // Fallback to default if theme detection fails config(['app.asset_url' => 'https://assets.pressekonto.test']); } } }