presseportale/routes/admin.php
Kevin Adametz 1c07fcb84b security: Sicherheitsaudit 07.10.2026 (S-01–S-06) behoben, Passkeys und Zweitfaktor-Pflicht für Admins
Audit: dev/sicherheit/SICHERHEITSAUDIT-2026-10-07.md, Plan und Entscheidungen:
dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md.

- S-01 Legacy-Rechnungen fremder Kunden nur mit billing:manage (auch Kundenroute).
- S-02 Kontaktzuordnung auf der Firmenanzeige verlangt companies:write, $id gesperrt.
- S-03 CompanyMembershipObserver: automatische Firmenrechte werden bei jeder
  Adress-/Zuordnungsänderung sofort neu bewertet; memberships:resync-auto.
- S-04 Passwortwechsel beendet alle anderen Sessions (PasswordChanger), Reset
  alle Sessions und offene Login-Links; Fortify PUT /user/password abgeschaltet.
  API-Tokens bleiben bestehen.
- S-05 Google: email_verified Pflicht, feste Bindung über die Provider-ID;
  Erstverknüpfung bestehender Konten außerhalb gmail/hd nur per Bestätigungslink
  in derselben Browser-Session.
- S-06 Frischer Identitätsnachweis (Passwort, App-Code, E-Mail-Code oder
  Passkey) vor E-Mail-Wechsel, erster 2FA-Einrichtung und Token-Erzeugung;
  Hinweis-Mails an die (bisherige) Kontoadresse.
- Entscheidung 5: Passkeys (Fortify, @laravel/passkeys) für alle; Super-Admins
  und Admins bestätigen die Session für den Admin-Bereich per Passkey oder
  App-Code (EnsureStrongAdminAuthentication, Notfallschalter
  AUTH_REQUIRE_STRONG_ADMIN); Reset des zweiten Faktors durch Super-Admins.

Neue Migration: create_passkeys_table. Suite 2.403 grün (1 übersprungen),
Larastan ohne Meldung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:09:35 +00:00

167 lines
11 KiB
PHP

<?php
use App\Http\Controllers\Admin\ArchiveEntryController;
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\LeaveImpersonationController;
use App\Http\Controllers\Admin\PressReleaseImageEvidenceController;
use App\Http\Controllers\InvoicePdfController;
use App\Http\Controllers\LegacyInvoicePdfController;
use App\Http\Middleware\BlockAdminDuringImpersonation;
use App\Http\Middleware\EnsureStrongAdminAuthentication;
use App\Http\Middleware\EnsureUserIsAdmin;
use App\Http\Middleware\LogSlowAdminRequests;
use Illuminate\Support\Facades\Route;
// Hinweis: Die Root-Route (`/`) auf der Portal-Domain rendert seit dem
// Hub-Launch die öffentliche Publisher-Landing (web/pressekonto.blade.php).
// Eingeloggte Admins werden über die Auth-Pipeline direkt nach /dashboard
// geleitet (siehe FORTIFY_HOME / LoginResponseContract).
Route::get('dashboard', DashboardController::class)
->middleware(['auth', 'verified', BlockAdminDuringImpersonation::class, EnsureUserIsAdmin::class, EnsureStrongAdminAuthentication::class, LogSlowAdminRequests::class])
->name('dashboard');
// ========================================
// Nutzer-eigene Einstellungen (kein Admin erforderlich)
// ========================================
Route::middleware(['auth', 'verified'])->group(function () {
Route::redirect('settings', '/admin/me/profile');
Route::redirect('settings/profile', '/admin/me/profile')->name('settings.profile');
Route::livewire('settings/password', 'settings.password')->name('settings.password');
Route::livewire('settings/appearance', 'settings.appearance')->name('settings.appearance');
});
Route::post('admin/impersonate/leave', LeaveImpersonationController::class)
->middleware(['auth', 'throttle:10,1'])
->name('admin.impersonate.leave');
// ========================================
// Admin-Bereich (nur Rollen admin/editor)
// ========================================
// Starke Anmeldung für Admins (Entscheidung 5, 07.10.2026): einmalige
// Bestätigung der Session per Passkey oder Authenticator-Code, bevor der
// Admin-Bereich öffnet (EnsureStrongAdminAuthentication).
Route::livewire('admin/anmeldung-bestaetigen', 'auth.strong-confirmation')
->middleware(['auth', 'verified', BlockAdminDuringImpersonation::class, EnsureUserIsAdmin::class])
->name('strong-auth.confirm');
Route::middleware(['auth', 'verified', BlockAdminDuringImpersonation::class, EnsureUserIsAdmin::class, EnsureStrongAdminAuthentication::class, LogSlowAdminRequests::class])->group(function () {
// Content Management
Route::livewire('admin/press-releases', 'admin.press-releases.index')->name('admin.press-releases.index');
Route::livewire('admin/press-releases/create', 'admin.press-releases.create')->name('admin.press-releases.create');
Route::livewire('admin/press-releases/{id}', 'admin.press-releases.show')->name('admin.press-releases.show');
Route::livewire('admin/press-releases/{id}/edit', 'admin.press-releases.edit')->name('admin.press-releases.edit');
// Beweisoriginal und veröffentlichte Fassung (Bildkennzeichnung E.4):
// nur hier, nur mit Zweck (per POST, Audit A-08), jeder Abruf
// protokolliert; Restnachweise entfernter Bilder nur mit
// compliance:manage (Audit A-11, im Controller).
Route::match(['get', 'post'], 'admin/press-releases/{id}/images/{image}/original', [PressReleaseImageEvidenceController::class, 'original'])
->whereNumber(['id', 'image'])->name('admin.press-releases.images.original');
Route::match(['get', 'post'], 'admin/press-releases/{id}/images/{image}/published', [PressReleaseImageEvidenceController::class, 'published'])
->whereNumber(['id', 'image'])->name('admin.press-releases.images.published');
// Rubriken-Taxonomie: portalweite Struktur — nur admin (Befund H2).
Route::middleware('can:categories:manage')->group(function (): void {
Route::livewire('admin/categories', 'admin.categories.index')->name('admin.categories.index');
Route::livewire('admin/categories/create', 'admin.categories.create')->name('admin.categories.create');
Route::livewire('admin/categories/{id}/edit', 'admin.categories.edit')->name('admin.categories.edit');
});
Route::middleware('can:footer-codes:manage')->group(function (): void {
Route::livewire('admin/footer-codes', 'admin.footer-codes.index')->name('admin.footer-codes.index');
Route::livewire('admin/footer-codes/create', 'admin.footer-codes.create')->name('admin.footer-codes.create');
Route::livewire('admin/footer-codes/{id}/edit', 'admin.footer-codes.edit')->name('admin.footer-codes.edit');
});
// CRM
// Lesen (companies:read) auch für Editoren; Schreiben nur admin (companies:write, Befund H2).
Route::livewire('admin/companies', 'admin.companies.index')->name('admin.companies.index');
Route::livewire('admin/companies/{id}', 'admin.companies.show')->where('id', '[0-9]+')->name('admin.companies.show');
Route::livewire('admin/contacts', 'admin.contacts.index')->name('admin.contacts.index');
Route::middleware('can:companies:write')->group(function (): void {
Route::livewire('admin/companies/create', 'admin.companies.create')->name('admin.companies.create');
Route::livewire('admin/companies/{id}/edit', 'admin.companies.edit')->name('admin.companies.edit');
Route::livewire('admin/companies/{companyId}/contacts/create', 'admin.contacts.create')->name('admin.companies.contacts.create');
Route::livewire('admin/contacts/create', 'admin.contacts.create')->name('admin.contacts.create');
Route::livewire('admin/contacts/{id}/edit', 'admin.contacts.edit')->name('admin.contacts.edit');
});
// Recht & Compliance (WS-3): Queue für DSGVO-/Persönlichkeitsrechts-/Meldungen
// — sensible Vorgänge, nur admin (Befund H2).
Route::middleware('can:compliance:manage')->group(function (): void {
Route::livewire('admin/legal-requests', 'admin.legal-requests.index')->name('admin.legal-requests.index');
Route::livewire('admin/legal-requests/{id}', 'admin.legal-requests.show')->name('admin.legal-requests.show');
// Kontakt: Queue für Anliegen aus dem Kontakt-Widget (Kontaktaufnahme,
// Fehlermeldung, PM-Meldung, Feedback)
Route::livewire('admin/contact-messages', 'admin.contact-messages.index')->name('admin.contact-messages.index');
Route::livewire('admin/contact-messages/{id}', 'admin.contact-messages.show')->name('admin.contact-messages.show');
});
// Billing: Rechnungen, Zahlungen, Tarife, Gutscheine — nur admin (Befund H2).
Route::middleware('can:billing:manage')->group(function (): void {
Route::livewire('admin/invoices', 'admin.invoices.index')->name('admin.invoices.index');
Route::get('admin/legacy-invoices/{legacyInvoice}/pdf', LegacyInvoicePdfController::class)->name('admin.legacy-invoices.pdf');
Route::get('admin/invoices/{invoice}/pdf', InvoicePdfController::class)->name('admin.invoices.pdf');
Route::livewire('admin/payments', 'admin.payments.index')->name('admin.payments.index');
Route::livewire('admin/payments/plans', 'admin.payments.plans')->name('admin.payments.plans');
Route::livewire('admin/coupons', 'admin.coupons.index')->name('admin.coupons.index');
Route::livewire('admin/newsletter-sync', 'admin.newsletter.sync')->name('admin.newsletter.sync');
});
// Administration
Route::livewire('admin/presets', 'admin.presets.index')->name('admin.presets.index');
Route::livewire('admin/presets/create', 'admin.presets.create')->name('admin.presets.create');
Route::livewire('admin/presets/{id}/edit', 'admin.presets.edit')->name('admin.presets.edit');
Route::livewire('admin/users', 'admin.users')->name('admin.users.index');
Route::livewire('admin/users/create', 'admin.users.create')->name('admin.users.create');
// Gutschriften auf der Detailseite sind finanzwirksam: die Berechtigung
// hängt an der Route (persistente Authorize-Middleware, greift auch für
// Livewire-Folge-Requests) und nicht nur an mount() (Audit F08).
Route::livewire('admin/users/{id}', 'admin.users.show')->middleware('can:users:manage')->name('admin.users.show');
Route::livewire('admin/users/{id}/edit', 'admin.users.edit')->name('admin.users.edit');
// Interner Newsletter: Kampagnen an Bestands-User (Filter + Protokoll).
// Massenversand an die User-Datenbank → nur mit users:manage (admin).
Route::middleware('can:users:manage')->group(function (): void {
// Mail-Sperrliste (Bounce-Suppression, Roadmap 1b.6)
Route::livewire('admin/mail/sperrliste', 'admin.mail.suppressions')->name('admin.mail.suppressions');
Route::livewire('admin/newsletter', 'admin.newsletter.campaigns.index')->name('admin.newsletter.campaigns.index');
Route::livewire('admin/newsletter/abmeldungen', 'admin.newsletter.unsubscribes')->name('admin.newsletter.unsubscribes');
Route::livewire('admin/newsletter/create', 'admin.newsletter.campaigns.edit')->name('admin.newsletter.campaigns.create');
Route::livewire('admin/newsletter/{id}', 'admin.newsletter.campaigns.show')->where('id', '[0-9]+')->name('admin.newsletter.campaigns.show');
Route::livewire('admin/newsletter/{id}/edit', 'admin.newsletter.campaigns.edit')->where('id', '[0-9]+')->name('admin.newsletter.campaigns.edit');
});
Route::livewire('admin/roles', 'admin.roles.index')->name('admin.roles.index');
Route::livewire('admin/roles/create', 'admin.roles.create')->name('admin.roles.create');
Route::livewire('admin/roles/{id}/edit', 'admin.roles.edit')->name('admin.roles.edit');
// Bilder (Bildkennzeichnung): alle Titelbilder mit Nachweis, Bestätigung und KI-Vorab-Check.
Route::livewire('admin/images', 'admin.images.index')->name('admin.images.index');
// Beweisarchiv (Bildkennzeichnung Block F): Liste, Legal Hold, Öffnen/Export mit Zweck.
Route::middleware('can:compliance:manage')->group(function (): void {
Route::livewire('admin/archive', 'admin.archive.index')->name('admin.archive.index');
Route::match(['get', 'post'], 'admin/archive/{entry}/download', [ArchiveEntryController::class, 'download'])->whereNumber('entry')->name('admin.archive.download');
Route::match(['get', 'post'], 'admin/archive/{entry}/export', [ArchiveEntryController::class, 'export'])->whereNumber('entry')->name('admin.archive.export');
});
// Reports
Route::livewire('admin/reports/activity', 'admin.reports.activity')->name('admin.reports.activity');
Route::livewire('admin/reports/slow-requests', 'admin.reports.slow-requests')->name('admin.reports.slow-requests');
// Einlieferer-Bericht: unzustellbare Kontaktdaten und abgelehnte
// Meldungen je API-Einlieferer (Kontakterreichbarkeit).
Route::livewire('admin/reports/einlieferer', 'admin.reports.submitter-contacts')->name('admin.reports.submitter-contacts');
// Indexierungsstatus aus der Search Console (Roadmap 1c.4): Verteilung
// nach Direkteinreichung/Syndikation — Grundlage für die Kundenansicht.
Route::livewire('admin/reports/indexierung', 'admin.reports.index-status')->name('admin.reports.index-status');
Route::middleware('can:settings:manage')->group(function (): void {
Route::livewire('admin/reports/system-health', 'admin.reports.system-health')->name('admin.reports.system-health');
});
});