presseportale/app/Providers/AppServiceProvider.php
Kevin Adametz d411e6ce7f admin: Verlauf protokolliert jede versendete E-Mail
Neuer Listener RecordMailActivity am MessageSent-Event erfasst zentral
alle Versandwege (Mailables, Notifications, Fortify, Queue-Mails beim
realen Versand im Worker). Neuer ActivityType "E-Mail" mit Empfänger,
Betreff und Mailable-Klasse in den Metadaten; bekannte Empfänger werden
mit ihrem User verknüpft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 09:13:43 +00:00

217 lines
9.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Providers;
use App\Contracts\NewsletterSyncClient;
use App\Helpers\ThemeHelper;
use App\Http\Middleware\BlockAdminDuringImpersonation;
use App\Http\Middleware\EnsureUserIsAdmin;
use App\Http\Middleware\EnsureUserIsCustomer;
use App\Http\Middleware\LogSlowAdminRequests;
use App\Http\Middleware\SetEdition;
use App\Listeners\ActivateUserAfterVerification;
use App\Listeners\RecordAuthActivity;
use App\Listeners\RecordMailActivity;
use App\Listeners\SyncCompanyMembershipsOnLogin;
use App\Models\AdminPreset;
use App\Models\Category;
use App\Models\CategoryTranslation;
use App\Models\Company;
use App\Models\Contact;
use App\Models\ContactMessage;
use App\Models\LegalRequest;
use App\Models\NewsletterSubscription;
use App\Models\PressRelease;
use App\Models\TerminationRequest;
use App\Models\User;
use App\Models\WithdrawalRequest;
use App\Observers\ActivityLogObserver;
use App\Observers\AdminPerformanceCacheObserver;
use App\Services\Admin\AdminRequestPerformanceMetrics;
use App\Services\Newsletter\NullNewsletterSyncClient;
use App\Services\PressRelease\PressReleaseService;
use App\Support\OperationalAlert;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Laravel\Cashier\Cashier;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->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->singleton(PressReleaseService::class);
$this->app->singleton(AdminRequestPerformanceMetrics::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(app()->isLocal());
$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.
Gate::before(fn (User $user, string $ability): ?bool => $user->is_super_admin ? 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 {
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']);
// 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);
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']);
}
}
}