presseportale/dev/archive/2026-09-audit/AUDIT.md
Kevin Adametz 8e3554bf91 dev: Doku aufgeräumt, neue Roadmap als einzige Liste offener Punkte
- dev/roadmap.md neu: alle offenen Punkte aus Plänen, Audits und Protokollen,
  gegen Code und Git abgeglichen (Stand v1.7.0)
- Abgeschlossenes ins Archiv: 2026-09-01 → archive/2026-09-nutzergewinnung,
  2026-09-11-audit → archive/2026-09-audit, lagacy-rechnungen →
  archive/legacy-rechnungen, Deploy-Protokoll v1.1.0, Umami-Snippets,
  Go-Live-Roadmap vom 20.07.
- Aktiv bleiben eigene Ordner bildrechte/ (Plan Fassung 2, Decision-Update)
  und changelog/ (Decision-Update Changelog)
- dev/README.md als Ordnerkarte neu, Verweise und zwei Code-Kommentare auf
  die neuen Pfade

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

878 lines
81 KiB
Markdown
Raw Permalink 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.

# Code-, Projekt- und Entwicklungsaudit
**Projekt:** pressekonto / presseecho / businessportal24
**Auditdatum:** 11.09.2026
**Geprüfter Commit:** `5d0570bee0d94275465b19d6ecb9950cf4d1c1b1`
**Arbeitsverzeichnis:** `/var/www/html`
**Auftrag:** Umfangreicher Audit mit Fehleranalyse, Verbesserungsvorschlägen und priorisierten Optimierungen. Keine Änderungen am Anwendungscode.
**Ergebnis:** 28 priorisierte Befunde, ergänzt um Architekturbeurteilung, Prüfgrenzen, Optimierungsplan und konkrete Regressionstests.
## 1. Gesamtbeurteilung
Die Anwendung ist ein gewachsenes, fachlich anspruchsvolles Publishing-System. Sie verbindet öffentliche Nachrichtenportale mit Kunden- und Redaktionsverwaltung, Firmen- und Kontaktzuordnungen, mehreren Anmeldeverfahren, KI-Prüfung, Abonnements, Einmalkäufen, Credits, Legacy-Abrechnung, Newsletter und rechtlich motivierten Änderungsprozessen.
Die Grundlage ist deutlich weiter als ein unverändertes Starter-Kit: Es gibt zentralisierte Veröffentlichungs- und Abrechnungsservices, gezielte Datenbankindizes, eine ausgelagerte Volltextsuche, Transaktionen und Zeilensperren an wichtigen Geld- und Kontingentoperationen sowie einen großen Bestand an fachlichen Tests. Die jüngere Entwicklung enthält nachvollziehbare Verbesserungen an Sicherheit, Performance und Betrieb.
**Trotzdem bestehen wesentliche Fehler an den Übergängen zwischen diesen Teilbereichen.** Besonders relevant sind eine unerlaubte Vergabe von Firmenrechten über eine unbestätigte E-Mail-Adresse, unterschiedliche 2FA-Regeln je Anmeldeweg, übergangene Soft Deletes, fehlerhaftes Newsletter-Chunking, eine Abrechnung über ein explizites Vertragsende hinaus und die fehlende Bindung von KI-Ergebnissen an die analysierte Inhaltsversion.
**Empfehlung:** Die Befunde F01–F07 vor einer weiteren funktionalen Ausweitung bearbeiten. F01 ist innerhalb dieser Gruppe ein Sicherheits-Release-Blocker. Ein umfassender Neuaufbau ist nicht begründet; erforderlich sind gezielte Korrekturen an den bestehenden Sicherheits- und Zustandsgrenzen.
### 1.1 Wesentliche Ergebnisse auf einen Blick
| Priorität | Anzahl | Einordnung |
|---|---:|---|
| P1 | 7 | Hohe Sicherheits- oder Geschäftsauswirkung; vorrangig beheben |
| P2 | 19 | Relevante Funktions-, Betriebs-, Performance- oder Qualitätssicherungsfehler |
| P3 | 2 | Entwicklungs- und Dokumentationsverbesserungen |
Es wird kein P0 vergeben: Es wurde kein laufender Angriff oder eingetretener Produktionsschaden nachgewiesen. Die Vergabe P1 bedeutet bei F01 dennoch, dass die beschriebene Rechtevergabe vor dem nächsten regulären Release korrigiert werden sollte. Priorität bewertet die erforderliche Reaktion, nicht die Wahrscheinlichkeit eines bereits erfolgten Vorfalls.
### 1.2 Belastbarkeit des Ergebnisses
Der Audit kombiniert Quellcodeanalyse, Gegenprüfung des installierten Framework-Codes, Prüfung bestehender Tests, Git-Historie, Datenbankschema und ausschließlich lesende Datenbankabfragen. Der besonders relevante Authentifizierungsbefund wurde zusätzlich unabhängig anhand der vollständigen Aufrufkette gegengeprüft.
**Die Anwendungstests, PHPStan und Dependency-Audits konnten nicht ausgeführt werden:** Alle entsprechenden Sail-Aufrufe scheiterten mit `Docker or Podman is not running.` Auch `vendor/bin/sail up -d` scheiterte. Es wurde kein direkter PHP-/Node-Aufruf als Umgehung verwendet.
Damit sind die Fehler überwiegend **statisch belegt**, jedoch nicht als in dieser Sitzung dynamisch reproduzierte Angriffe oder Integrationstests zu verstehen. F22 ist zusätzlich durch tatsächliche MySQL-Query-Pläne gestützt. Die Datenbankverbindung des Boost-Servers funktionierte unabhängig von der nicht verfügbaren Sail-CLI. Ihre Betriebsrolle wurde nicht als Produktion unterstellt.
## 2. Prüfauftrag, Teilaufgaben und Vorgehen
Der Auftrag wurde in die folgenden kleinen Prüfpakete zerlegt und schrittweise bearbeitet. Für den Backend-Teil wurde entsprechend dem Laravel-Skill eine zusätzliche lesende Gegenprüfung eingesetzt.
| Schritt | Prüfpaket | Bearbeitet | Nachweis / Einschränkung |
|---|---|---|---|
| 1 | Bestandsaufnahme, Versionen, Umfang, Git-Zustand | Ja | Dateien, Composer-/npm-Konfiguration, Boost application-info, Git |
| 2 | Architektur, Domains, Portale, Routen und Zuständigkeiten | Ja | Routing, Provider, Context-Klassen, Services, Dokumentation |
| 3 | Anmeldung, Verifizierung, 2FA, Berechtigungen | Ja | Fortify, Socialite, Magic Links, Policies, Middleware, Login-Listener |
| 4 | Kunden-/Adminaktionen und Livewire-Zustand | Ja | Kritische öffentliche Actions, Locked-Properties, erneute Autorisierung |
| 5 | API-Verträge, Validierung, Eigentums- und Löschgrenzen | Ja | v1, Legacy-Aliasse, Requests, Resources, Scope-Verhalten |
| 6 | Veröffentlichung, KI-Prüfung, Quoten, Zeitsteuerung | Ja | Jobs, Veröffentlichungsservice, Zustandsübergänge und Scheduler |
| 7 | Geldflüsse und Legacy-Abrechnung | Ja | Wallet, Topups, Rechnungsnummern, MAN-Rechnungen, Käufe, Webhook-nahe Abläufe |
| 8 | Newsletter, Hintergrundverarbeitung, Wiederanlauf | Ja | Empfängerauswahl, Start, Versandjob und Fehlergrenzen |
| 9 | Datenbank, Suche, Performance und Migration | Ja | Migrations-/Indexprüfung, Query-Pläne, Importpfade |
| 10 | Medien, öffentliche Seiten, Cache, SEO und UI | Ja, statisch | Sichtbarkeitsregeln, Uploads, Rendering, Templates; kein visueller Browserlauf |
| 11 | Tests, CI, Entwicklungsworkflow und Betriebsdokumentation | Ja | Bestehende Testfälle gelesen, Gates und Runbook geprüft; Laufzeitchecks blockiert |
| 12 | Befunde zusammenführen, priorisieren, Maßnahmen und Regressionen | Ja | Dieser Bericht |
### 2.1 Was nicht verändert oder ausgeführt wurde
- Kein Anwendungscode, keine Tests, keine Migrationen, keine Konfiguration und keine Abhängigkeiten verändert.
- Keine Datenbankänderungen, keine Datenbereinigung, keine Rechnungen erzeugt, keine Newsletter gestartet und keine E-Mails versendet.
- Keine Authentifizierungs- oder Berechtigungsangriffe gegen reale Konten ausgeführt.
- Keine Deployments, keine Commits und kein Push.
- Keine produktiven Nutzdaten, Zugangsdaten oder Tokens in den Bericht übernommen.
- Ausschließlich diese angeforderte Markdown-Datei neu angelegt.
### 2.2 Geltungsbereich und Grenzen
Die Prüfung deckt alle wesentlichen technischen und fachlichen Bereiche ab. Bei über 116.000 Zeilen in den betrachteten Anwendungs-, View-, Test- und Migrationsdateien ist sie keine Zusicherung einer lückenlosen Zeilenprüfung jeder Datei. Kritische Pfade wurden vertieft untersucht; übrige Bereiche wurden strukturell und anhand von Querschnittssuchen bzw. repräsentativen Implementierungen geprüft.
Nicht nachgewiesen wurden: vollständige Testgrünheit, reale Browser-/Mobilansichten, tatsächliche Produktionskonfiguration, Infrastruktur-Firewallregeln, Backup-Wiederherstellbarkeit, Produktions-Queueparameter, Lastgrenzen, erfolgreiche echte Stripe-/Google-/SMTP-/KI-Interaktionen oder ein aktueller vollständiger CVE-Status. Ebenso stellt der Bericht keine juristische oder steuerrechtliche Freigabe dar; rechtlich benannte Funktionen werden technisch auf Konsistenz geprüft.
## 3. Projektbestand und Architektur
### 3.1 Tatsächlicher Technologiebestand
Versionen aus Boost `application-info` am Auditdatum, nicht lediglich die Mindestanforderungen aus `composer.json`:
| Baustein | Installierter Stand |
|---|---|
| PHP | 8.4 |
| Laravel | 12.63.0 |
| Fortify | 1.36.2 |
| Sanctum | 4.3.1 |
| Socialite | 5.27.0 |
| Cashier | 16.5.3 |
| Livewire / Volt | 4.2.4 / 1.10.5 |
| Flux / Flux Pro | jeweils 2.14.0 |
| Pest / PHPUnit | 3.8.6 / 11.5.50 |
| Larastan / Pint | 3.9.6 / 1.29.1 |
| Sail / Boost | 1.57.0 / 2.4.5 |
| Alpine / Tailwind CSS | 3.15.0 / 4.1.14 |
| Datenbank der verfügbaren Boost-Verbindung | MySQL |
Ergänzend sind unter anderem Spatie Permission und HTMLPurifier eingebunden. `composer.json` beschreibt das Projekt weiterhin als offizielles Livewire-Starter-Kit. Das ist kosmetisch, bildet die fachliche Eigenständigkeit aber nicht mehr ab.
### 3.2 Umfang
| Metrik | Ergebnis | Bedeutung |
|---|---:|---|
| PHP-Dateien unter `app/` | 363 | Einschließlich Concerns, Services, Commands etc. |
| Blade-Dateien unter `resources/views/` | 209 | UI und erhebliche Teile der Volt-Anwendungslogik |
| `*Test.php` unter `tests/` | 203 | Dateianzahl, keine Aussage über ausgeführte Tests |
| Statisch gezählte `test()`-/`it()`-Deklarationen | 1.481 | Datasets können die tatsächliche Zahl der Ausführungen erhöhen |
| Migrationsdateien | 112 | Hohe Schema- und Legacy-Komplexität |
| Zeilen in `app`, Views, Tests und Migrationen zusammen | 116.414 | Rohumfang einschließlich Kommentaren/Leerzeilen |
| PHPStan-Baseline | 166 Einträge / 188 unterdrückte Vorkommen | Keine neu gemessenen PHPStan-Fehler |
Besonders große Dateien:
| Datei | Zeilen | Konsequenz |
|---|---:|---|
| `resources/views/livewire/customer/press-releases/show.blade.php` | 1.997 | Viele unterschiedliche Käufe, Korrekturen und Anfragen in einem UI-Lebenszyklus |
| `app/Services/PressRelease/ProofPdfRenderer.php` | 1.476 | Eigenständige komplexe PDF-Erzeugung; hoher Spezialwissenbedarf |
| `resources/views/livewire/admin/press-releases/index.blade.php` | 1.401 | Tabellen-, Filter-, Status- und Verwaltungslogik gebündelt |
| `resources/views/livewire/admin/users/edit.blade.php` | 1.186 | Hohe Kopplung von Rollen, Firmen, Kontakten und Profilen |
| `resources/views/livewire/customer/bookings.blade.php` | 1.185 | Umfangreicher Bestellzustand und UI in einer Datei |
Große Dateien sind nicht automatisch fehlerhaft. Hier erschweren sie jedoch die Prüfung von Berechtigungswechseln und Zustandsübergängen. Die Zusammenführung vieler Funktionen sollte nicht mit einer Zusammenführung aller Verantwortlichkeiten verwechselt werden.
### 3.3 Fachliche Struktur
```mermaid
flowchart TD
Public[Öffentliche Portale: presseecho / businessportal24]
Hub[pressekonto: Anmeldung, Kundenbereich, Redaktion]
Api[v1-API und Legacy-Service-Aliasse]
Context[Portal- und Firmenkontext, Middleware, Policies]
Publishing[Veröffentlichungsservice, KI-Prüfung, Medien]
Billing[Cashier / Stripe, MAN-Rechnungen, Credits, Einzelkäufe]
CRM[Firmen, Kontakte, Mitgliedschaften]
Async[Queue, Newsletter, Scheduler, Importe]
Data[MySQL, private Dateien, Cache, Suchindex]
Public --> Context
Hub --> Context
Api --> Context
Context --> Publishing
Context --> Billing
Context --> CRM
Publishing --> Async
Billing --> Async
CRM --> Data
Publishing --> Data
Billing --> Data
Async --> Data
```
Es bestehen zwei voneinander unabhängige Zugriffsachsen: **Portal/Sprache** für die Veröffentlichung und **Benutzer/Firmenzuordnung** für die Verwaltung. Dazu kommt der Lebenszyklus eines Datensatzes: aktiv, archiviert, mit 410 entfernt oder soft-gelöscht. Mehrere Befunde entstehen dadurch, dass eine Abfrage nur eine dieser Achsen berücksichtigt.
### 3.4 Bereits gut gelöst
- Veröffentlichungen laufen zentral über `PressReleaseService`; Publikation prüft unter Sperren erneut Zustand, Quoten und verfügbare Käufe.
- Walletsaldo und Ledger werden transaktional geschrieben. Topup-Gutschriften prüfen `credited_at` unter Row Lock.
- Rechnungsnummern beruhen auf einer eindeutigen Sequenz und Zeilensperren.
- Boost-Inventar verwendet stabile Sperrzeilen und eine definierte Sperrreihenfolge.
- Die API besitzt Tokenfähigkeiten, aktive Benutzerprüfung und ein tokenbezogenes Rate Limit. Fremde Autorschaft wird in den untersuchten PM-Pfaden geprüft.
- Magic-Login-Tokens werden gehasht gespeichert und beim Verbrauch atomar beansprucht. Der Magic-Login respektiert bereits die lokale 2FA.
- Eigene Admin-/Customer-Middleware ist für Livewire-Folgeanfragen registriert. Viele Actions autorisieren zusätzlich explizit.
- Presse-HTML wird über einen zentralen Sanitizer ausgegeben; strukturierte Daten verwenden `JSON_HEX_TAG`. Unescaped-Ausgabe ist deshalb nicht pauschal als XSS eingestuft worden.
- Medien liegen konzeptionell auf privaten Disks und werden über Controller ausgeliefert. Das ist die richtige Grundlage, auch wenn F04 eine Lücke in der Sichtbarkeitsprüfung zeigt.
- Zahlreiche Scheduler-Tasks besitzen `withoutOverlapping()` und Failure-Hooks; es existiert ein Betriebs-Runbook.
- Die Suchindex-Auslagerung und zahlreiche zusammengesetzte Indizes zeigen eine bewusste Auseinandersetzung mit großen Beständen.
- Tests decken viele sensible Themen ausdrücklich ab: Rechteeskalation, Verifizierung, Billing, Scheduling, Compliance, Legacy-Kompatibilität und Performanceverhalten.
## 4. Befundregister
**Nachweisarten:** **S** = statisch anhand konkreter Aufrufkette belegt; **D** = durch lesende Datenbankprüfung ergänzt; **B** = Auswirkung hängt von einer ausdrücklich genannten Betriebs-/Parallelitätsbedingung ab. Ein B-Befund ist kein nachgewiesener Produktionsvorfall.
| ID | Priorität | Nachweis | Befund |
|---|---|---|---|
| F01 | P1 | S | Unbestätigte E-Mail kann dauerhafte fremde Firmenrechte auslösen |
| F02 | P1 | S | Google-Login umgeht lokale Zwei-Faktor-Authentifizierung |
| F03 | P1 | S | Sicherheits-UI umgeht erneute Identitätsprüfung; Recovery-Anzeige manipulierbar |
| F04 | P1 | S | Soft Deletes werden in API, Vorschau, Kundenlisten und Medien übergangen |
| F05 | P1 | S/D | Newsletter-Chunking dupliziert und überspringt Empfänger |
| F06 | P1 | S | MAN-Abrechnung ignoriert explizites Legacy-Vertragsende |
| F07 | P1 | S/B | KI-Ergebnisse sind nicht an die analysierte Inhaltsversion gebunden |
| F08 | P2 | S/B | Admin-Gutschriften autorisieren nur beim Öffnen der Komponente |
| F09 | P2 | S/B | Globaler Superadmin-Kurzschluss umgeht Kontodeaktivierung |
| F10 | P2 | S | Automatisch vergebene Firmenrechte werden bei Wegfall der Grundlage nicht entzogen |
| F11 | P2 | S | API-Pagination und Eingabeverträge unvollständig validiert |
| F12 | P2 | S/D | Meldeverfahren ordnen URLs nur anhand des Slugs zu |
| F13 | P2 | S/B | Firmenlöschung/-deaktivierung invalidiert öffentliche Seitencaches nicht zuverlässig |
| F14 | P2 | S/B | Newsletter-Start besitzt keinen sicheren Wiederanlauf |
| F15 | P2 | S/B | Newsletter-Job kann doppelt senden und Zähler inkonsistent hinterlassen |
| F16 | P2 | S | Nicht abrechenbare Altverträge können den Rechnungslauf blockieren |
| F17 | P2 | S/B | KI-Retrybudget und Worker-Timeout passen nicht zusammen |
| F18 | P2 | S/B | Monatskontingent-Reset ist nicht periodengebunden und nicht idempotent |
| F19 | P2 | S/B | Gleichzeitige Aufrufaggregatoren zählen Ereignisse mehrfach |
| F20 | P2 | S | Scheduler-Publikationsfehler liefern trotzdem Exit-Code Erfolg |
| F21 | P2 | S/B | Legacy-Import lädt große Gesamttabellen und hinterlässt Teilimporte |
| F22 | P2 | S/D | `LOWER(email)` verhindert den vorhandenen E-Mail-Index |
| F23 | P2 | S/B | Dateifehler können scheinbar erfolgreiche Uploads und verlorene Logos verursachen |
| F24 | P2 | S | Lint-CI formatiert, erzwingt aber keine saubere Formatierung im Commit |
| F25 | P2 | S | Test-/Analysekonfiguration lässt wesentliche Produktionspfade unbewiesen |
| F26 | P2 | S | Eigenes Anmeldemodal besitzt keine explizite Fokusführung |
| F27 | P3 | S | Standard-Entwicklungsstart startet keinen Vite-Server |
| F28 | P3 | S | Einstiegspunkte der Dokumentation zeigen widersprüchliche Entwicklungsstände |
## 5. Sicherheits- und Zugriffsprüfung
### F01 — Fremde Firmenrechte über eine unbestätigte E-Mail-Adresse
**Priorität:** P1, innerhalb dieses Audits Sicherheits-Release-Blocker.
**Belastbarkeit:** Vollständige statische Aufrufkette, unabhängig gegengeprüft; nicht gegen reale Konten ausgeführt.
**Fundstellen:**
- `config/fortify.php:150`: Profiländerung ist als Fortify-Feature aktiv.
- `app/Providers/FortifyServiceProvider.php:44`: eigene Profil-Action ist gebunden.
- `vendor/laravel/fortify/routes/routes.php:100–103`: `PUT /user/profile-information` benötigt Authentifizierung, keine verifizierte E-Mail.
- `app/Actions/Fortify/UpdateUserProfileInformation.php:48–56`: E-Mail wird sofort ersetzt, `email_verified_at` auf `null` gesetzt.
- `resources/views/livewire/auth/login.blade.php:62–69`: Login erfolgt vor der Verifizierungsprüfung.
- `app/Listeners/SyncCompanyMembershipsOnLogin.php:22–28` und `app/Services/Auth/ContactAccessService.php:104–119`: E-Mail-basierte Zuordnung ohne Verifizierungsprüfung.
- `app/Services/Auth/ContactAccessService.php:237–246`: Vergabe von `responsible` und additive Kontaktzuordnung.
- `app/Policies/CompanyPolicy.php:34–40,64–74`: diese Rolle gewährt Schreibzugriff.
**Fehlerszenario:** Ein Benutzer kontrolliert ein Passwortkonto und eine eigene E-Mail-Adresse. Eine fremde E-Mail steht in `companies.email` oder an einem zugeordneten Pressekontakt, gehört aber noch keinem anderen `users.email`-Datensatz. Der Benutzer ändert über den weiterhin aktiven Fortify-Endpunkt seine eigene Kontoadresse auf diese fremde Adresse. Nach Ab- und Anmeldung mit seinem unveränderten Passwort feuert der Login-Listener und trägt ihn bereits als Firmenverantwortlichen ein. Anschließend kann er über denselben Endpunkt auf seine eigene Adresse zurückwechseln und diese bestätigen. Die zuvor vergebenen Firmenmitgliedschaften bleiben erhalten.
**Auswirkung:** Dauerhafter Zugriff auf fremde Firmenstammdaten, Kontakte und entsprechend berechtigte Pressemitteilungen, ohne Besitznachweis des fremden Postfachs. Die `verified`-Middleware verhindert nur den unmittelbaren Zugang während des unverifizierten Zwischenzustands; sie verhindert nicht die Vergabe der Berechtigung.
**Abgrenzung:** Bestehende `users.email`-Einträge begrenzen durch Unique-Validierung die angreifbaren Zieladressen. CSRF ist keine Abwehr gegen einen Benutzer, der reguläre Änderungen am eigenen Konto ausführt. Ein fremdes Benutzerkonto wird durch diese Kette nicht unmittelbar übernommen; betroffen sind Firmenrechte.
**Empfehlung:** E-Mail-basierte Berechtigungen ausschließlich aufgrund einer aktuell nachgewiesenen Kontoadresse vergeben. Den Fortify-Endpunkt auf denselben `pending_email`-Prozess wie die reguläre Sicherheits-UI führen oder deaktivieren, wenn er nicht benötigt wird. Die Kontrolle muss im Zuordnungsservice liegen und alle Aufrufer erfassen. Bestehende automatisch entstandene Zuordnungen anschließend gezielt auf ihre Verifikationsgrundlage untersuchen; nicht pauschal alle Mitgliedschaften löschen.
**Erforderlicher Regressionstest:** Eigene bestätigte Adresse → fremde, nur am Kontakt gespeicherte Adresse → Passwortlogin → Rückwechsel und Bestätigung eigener Adresse. Während der gesamten Sequenz dürfen weder `company_user` noch `contact_user` unberechtigte Zuordnungen erhalten. Zusätzlich den direkten Serviceaufruf für unverifizierte und deaktivierte Benutzer prüfen.
### F02 — Google-Login überspringt lokale 2FA
**Priorität:** P1.
**Fundstellen:** `app/Http/Controllers/Auth/GoogleController.php:37–57`; als Gegenbeispiel `app/Http/Controllers/Auth/MagicLinkConsumeController.php:68–77` und `resources/views/livewire/auth/login.blade.php:49–59`.
Nach erfolgreicher Google-Auflösung ruft der Callback unmittelbar `Auth::login($user, remember: true)` auf. Eine Prüfung auf `hasEnabledTwoFactorAuthentication()` und die Übergabe in Fortifys Challenge fehlen. Passwort- und Magic-Login prüfen dies bereits.
**Trigger:** Ein aktives Bestandskonto mit bestätigter lokaler 2FA meldet sich über Google mit der passenden Adresse an. Die Anwendung erstellt sofort eine Sitzung. Dass der Google-Account eventuell selbst MFA verwendet, beweist nicht die Erfüllung der lokal konfigurierten TOTP-Anforderung.
**Auswirkung:** Die Sicherheitszusage „2FA aktiviert“ hängt vom gewählten Anmeldeweg ab. Das betrifft auch privilegierte Konten, sofern Google-Login für ihre Adresse funktioniert.
**Empfehlung:** Gemeinsame Login-Abschlusslogik für Passwort, Magic Link und Google: aktive Identität prüfen, gegebenenfalls Challengezustand setzen und erst nach erfolgreicher Challenge authentifizieren. Die Entscheidung über dauerhafte Anmeldung ebenfalls zentralisieren.
**Regression:** Google-Callback mit aktiviertem lokalen TOTP muss als Gast enden und zur Challenge führen. Ohne bestätigte 2FA bleibt das bisherige Verhalten. Deaktivierte Konten müssen gesperrt bleiben. `tests/Feature/Auth/GoogleLoginTest.php` deckt derzeit Neuanlage, Zuordnung, Deaktivierung und Fehler ab, jedoch diesen 2FA-Fall nicht.
### F03 — Sicherheitsaktionen ohne erneute Identitätsprüfung; Recovery-Anzeige clientseitig freischaltbar
**Priorität:** P1.
**Fundstellen:** `resources/views/livewire/customer/security.blade.php:39,74–109,120–164,179–186`; `routes/customer.php:28–31,73`; `config/fortify.php:152–155`; `vendor/laravel/fortify/routes/routes.php:142–172`.
Die Konfiguration verlangt `confirmPassword => true`. Fortifys eigene 2FA-Routen setzen dafür `password.confirm` ein. Die Volt-Komponente ruft die Fortify-Actions direkt auf, ohne diesen Schutz gleichwertig zu prüfen. Deaktivierung der 2FA, Erzeugung neuer Recovery-Codes und Start einer E-Mail-Änderung sind allein mit einer bestehenden Sitzung möglich.
Zusätzlich hängt die Anzeige entschlüsselter Recovery-Codes von der ungesperrten öffentlichen Property `showRecoveryCodes` ab. Diese Property ist vom Client veränderbar. „Nur einmal anzeigen“ ist dadurch lediglich das normale UI-Verhalten, keine serverseitige Sicherheitsgrenze.
**Trigger:** Eine bereits bestehende oder entwendete Sitzung ruft `disableTwoFactorAuthentication()` bzw. `regenerateRecoveryCodes()` auf oder setzt `showRecoveryCodes` auf `true`. Dafür ist keine erneute Kenntnis des Passworts oder eines anderen starken Nachweises erforderlich.
**Auswirkung:** Temporärer Sitzungszugriff kann zu dauerhaft geschwächter Kontosicherheit führen. Der Angreifer braucht eine Sitzung; es handelt sich nicht um einen anonymen Zugriff auf fremde Recovery-Codes.
**Empfehlung:** Einen zeitlich begrenzten, serverseitig geprüften erneuten Identitätsnachweis vor sicherheitskritischen Aktionen und vor Recovery-Ausgabe verlangen. Für passwortlose Konten einen ausdrücklich definierten sicheren Ersatz vorsehen. `showRecoveryCodes` mindestens gegen Clientmutation schützen; das ersetzt die erneute Identitätsprüfung nicht. Sicherheitsänderungen nachvollziehbar protokollieren und den Kontoinhaber benachrichtigen.
**Regression:** Ohne aktuellen Nachweis müssen direkte Action-Aufrufe und `set('showRecoveryCodes', true)` scheitern. Die Tests in `tests/Feature/CustomerProfileSecurityTest.php:209–270` prüfen zwar Einrichtung und normale Sichtbarkeit, erlauben die Regeneration bisher ohne erneuten Nachweis.
### F04 — Soft Deletes werden als Nebenwirkung der Portal-Entkopplung entfernt
**Priorität:** P1.
**Fundstellen:**
- `app/Http/Controllers/Api/V1/PressReleaseController.php:36,238,257`.
- `app/Http/Controllers/Api/V1/CompanyController.php:17,34`.
- `app/Http/Controllers/PressReleasePreviewController.php:39–49`.
- `app/Http/Controllers/Web/PressReleaseImageMediaController.php:23–26` und entsprechender Attachment-Controller.
- `app/Http/Controllers/Web/Concerns/AuthorizesPressReleaseMediaAccess.php:39–46`.
- `app/Models/PressRelease.php:537–542`.
- `resources/views/livewire/customer/press-kits/show.blade.php:233–260`.
`withoutGlobalScopes()` deaktiviert nicht nur den Portalfilter, sondern auch den Soft-Delete-Filter. Nachfolgende `ownedByApiUser()`-/`accessibleBy()`-Scopes stellen den Soft-Delete-Schutz nicht wieder her. Die betroffenen Modelle verwenden tatsächlich `SoftDeletes`.
**Konkrete Folgen:**
1. Eine über die API gelöschte eigene Entwurfs-PM kann weiterhin über Index/Show gefunden werden; auch das Update sucht ohne Soft-Delete-Scope.
2. Ein bestehender Vorschau-Token kann eine soft-gelöschte PM weiter laden.
3. Gelöschte Kontakte erscheinen weiterhin in der Firmenansicht, weil die Kontaktliste und der Einzelkontaktlookup alle Scopes entfernen.
4. Medien laden die zugehörige Firma ohne Scopes. Eine soft-gelöschte, zuvor aktive Firma bleibt auf dem geladenen Model `is_active=true`; eine veröffentlichte PM kann dadurch weiterhin als öffentlich gelten. Die Instanzprüfung berücksichtigt `deleted_at` nicht. Die normale Firmenlöschung in `resources/views/livewire/admin/companies/edit.blade.php:262` ändert diesen Aktivwert nicht.
**Wichtiger Testbefund:** `tests/Feature/PressReleasePreviewTest.php:237–247` heißt „preview link for soft deleted press release returns 404“, führt tatsächlich aber `$pr->forceDelete()` aus. Der Test beweist nur den Fall eines physisch entfernten Datensatzes und verdeckt genau diese Lücke.
**Empfehlung:** Portalübergreifende Abfragen mit `withoutGlobalScope(PortalScope::class)` formulieren. Gelöschte Datensätze nur in ausdrücklich dafür vorgesehenen Verwaltungsfunktionen über `withTrashed()` zulassen. Sichtbarkeitsregeln für PM und Firma einschließlich Soft Delete zentralisieren und sowohl in Query- als auch Instanzprüfungen gleich umsetzen.
**Regression:** Soft Delete separat für PM, Firma, Kontakt, Bild und Anhang testen. Bestehende Vorschau- und Medienlinks sowie API-GET/PATCH nach Löschung prüfen. Außerdem verifizieren, dass berechtigte portalübergreifende Zugriffe auf nicht gelöschte Daten erhalten bleiben.
### F08 — Admin-Gutschriften prüfen Rechte nur in `mount()`
**Priorität:** P2.
**Fundstellen:** `resources/views/livewire/admin/users/show.blade.php:25–27,39–82`; `app/Services/Billing/AdminGrantService.php:29–49,59–99`; `routes/admin.php`, Benutzer-Detailroute.
`mount()` verlangt `users:manage`; `grantCredits()` und `grantFreePressReleases()` prüfen diese Berechtigung nicht erneut. Auch der Service prüft den übergebenen Ausführenden nicht. Die persistente Admin-Middleware kontrolliert lediglich den Adminbereichszugang.
**Trigger:** Ein berechtigter Admin öffnet die Seite. Danach wird ihm `users:manage` entzogen oder er wird zum Editor herabgestuft. Als Editor erfüllt er weiterhin `access-admin` und kann aus dem alten Livewire-Zustand Gutschriften auslösen. Das gesperrte `$id` verhindert die Mutation des Zielbenutzers, aber nicht diesen Rechteentzugfall.
**Empfehlung:** Jede finanzwirksame Action erneut autorisieren; alternativ bzw. ergänzend den Aufruf zentral im Service absichern. Berechtigungen auf der Route als persistente Grenze setzen, wenn das gesamte Modul diese benötigt.
**Regression:** Komponente berechtigt öffnen, Rolle/Berechtigung während ihrer Lebensdauer entziehen, dann beide Actions aufrufen. Weder Ledger noch Käufe dürfen verändert werden. Der vorhandene Test `AdminGrantTest` prüft nur den initial gesperrten Seitenaufruf.
### F09 — Superadmin-Kurzschluss ignoriert deaktivierte Konten
**Priorität:** P2.
**Fundstellen:** `app/Providers/AppServiceProvider.php:114–119`; `app/Models/User.php:537–558`; `app/Http/Middleware/EnsureUserIsAdmin.php:21–25` und `EnsureUserIsCustomer.php`.
`Gate::before` liefert für jeden Superadmin `true`, bevor die Access-Gates überhaupt `canAccessAdmin()` oder `canAccessCustomer()` ausführen. Diese Methoden prüfen eigentlich zuerst `is_active`.
**Trigger:** Ein bereits angemeldetes Superadmin-Konto wird deaktiviert. Bei nachfolgenden Requests mit bestehender Sitzung kann der Gate-Kurzschluss die Sperre umgehen. Die gesonderten Login-Prüfungen schützen einen neuen Login, ersetzen aber nicht die Prüfung bereits bestehender Sitzungen.
**Empfehlung:** Kontodeaktivierung als vorrangige, nicht übersteuerbare Grenze behandeln. Superadmin-Erleichterungen erst danach anwenden. Bestehende Sitzungen und Tokens im Sperrprozess nach definierter Regel widerrufen.
**Regression:** Aktiven Superadmin anmelden, deaktivieren, Admin-/Customer-Route und Livewire-Folgeaktion aufrufen. Access-Gate und Model-Methode müssen denselben Ablehnungswert liefern.
### F10 — Automatisch verliehene Firmenrechte besitzen keinen Entzugslebenszyklus
**Priorität:** P2.
**Fundstellen:** `app/Services/Auth/ContactAccessService.php:104–119,225–246`; `resources/views/livewire/customer/press-kits/show.blade.php:193–209`; `app/Policies/CompanyPolicy.php:64–74`.
Die E-Mail-Synchronisation ist ausdrücklich rein additiv. Wird eine Kontaktadresse geändert, ein Pressekontakt gelöscht oder eine Firmenadresse ersetzt, bleibt ein zuvor automatisch vergebener `responsible`-Pivot bestehen. Die Policy benötigt später nur diesen Pivot und nicht mehr die ursprüngliche E-Mail-Übereinstimmung.
**Trigger:** Ein ehemaliger Ansprechpartner erhält zunächst legitimen Zugang, wird anschließend aus den Firmenkontakten entfernt und behält trotzdem Schreibrechte. Dies ist unabhängig vom missbräuchlichen Verifikationspfad F01.
**Empfehlung:** Herkunft automatisch verliehener Rechte speichern und einen eigenen Entzugsprozess implementieren. Manuell erteilte Team-/Eigentümerrechte dürfen dabei nicht pauschal mitgelöscht werden. Im Administrationsprozess sichtbar machen, dass Kontaktlöschung und Berechtigungsentzug aktuell verschiedene Vorgänge sind.
**Regression:** Automatische Zuordnung erzeugen, die letzte berechtigende E-Mail-Grundlage entfernen, anschließend Firmenzugriff prüfen. Manuell zusätzlich erteilte Rechte müssen nach der fachlich festgelegten Regel erhalten bleiben.
## 6. Veröffentlichung, API, Medien und öffentliche Auslieferung
### F07 — KI-Bewertung kann auf einer veralteten Inhaltsversion beruhen
**Priorität:** P1.
**Fundstellen:** `app/Jobs/ClassifyPressRelease.php:44–81`; `app/Jobs/ScorePressRelease.php:38–61`; `app/Services/PressRelease/PressReleaseService.php:123–160,177–189,214–240`.
Die Jobs kennen die PM-ID, jedoch keine Inhaltsversion. Sie laden den Inhalt, warten auf die externe Bewertung und speichern danach das Ergebnis ohne Vergleich mit dem inzwischen aktuellen Inhalt. Die Veröffentlichung lädt und sperrt zwar das aktuelle Model, prüft aber nicht, ob dieses den tatsächlich bewerteten Text enthält.
**Trigger:** Während Job A Version V1 bewertet, wird V2 gespeichert. Danach kommt A zurück und übernimmt sein Ergebnis für V2; ein jüngerer Job kann zusätzlich durch eine ältere Antwort überschrieben werden. Bei der Erstprüfung sind `classification`/`content_score` noch `null`; die Recheck-Helfer planen in diesem Zustand keinen Ersatzlauf ein. Administrative Änderungen und erlaubte Review-Bearbeitung bieten entsprechende Übergänge.
**Auswirkung:** Veröffentlichter Inhalt, Bewertung und KI-Audit können auseinanderfallen. Die erneute Blacklistprüfung im Veröffentlichungsservice kontrolliert eine andere, engere Regel und ersetzt keine Inhaltsversionsbindung.
**Empfehlung:** Monotone Inhaltsversion oder Hash bei Einplanung und Analyse speichern. Ergebnisübernahme und daraus abgeleitete Publikation nur bei übereinstimmender aktueller Version erlauben. Veraltete Antworten nachvollziehbar verwerfen und gegebenenfalls eine aktuelle Prüfung einplanen.
**Regression:** Fake-Treiber verändert den Text während der Analyse; zwei Jobs beenden sich in umgekehrter Reihenfolge; erste Analyse läuft parallel zu einer Bearbeitung. Ein altes Ergebnis darf weder neue Werte überschreiben noch V2 freigeben.
### F11 — API-Eingaben können Pagination und fachliche Verträge umgehen
**Priorität:** P2.
**Fundstellen:** `app/Http/Controllers/Api/V1/PressReleaseController.php:36–41`; `app/Http/Controllers/Api/V1/CompanyController.php:17–20`; `app/Http/Requests/Api/V1/StorePressReleaseRequest.php:33–46`; entsprechender Update-Request.
`paginate(min((int) per_page, 100))` begrenzt nur nach oben. Bei `per_page=-1` bleibt ein negativer Wert übrig. Der installierte Eloquent-Builder reicht diesen an `forPage()` weiter; `Query\Builder::limit()` setzt für negative Werte kein Limit (`vendor/.../Eloquent/Builder.php:1116–1126`, `vendor/.../Query/Builder.php:3110–3118`). Die Abfrage kann dadurch den vollständigen für den Benutzer sichtbaren Bestand einschließlich Relationen laden.
Weitere Vertragslücken: `language` verlangt lediglich zwei Zeichen statt der unterstützten Ausgaben `de|en`; `category_id` verlangt Existenz, aber keine aktive, zum Zielportal passende Kategorie. Der Statusfilter wird ungeprüft an einen auf `string` typisierten Callback übergeben; ein Arrayparameter kann statt einer kontrollierten 422-Antwort einen Typfehler auslösen.
**Auswirkung:** Unbegrenzte Ergebnismengen für große Konten, unnötige Speicher-/Datenbanklast und fachlich ungültige Datensätze. Dies ist keine nachgewiesene fremde Datenfreigabe: die Eigentumsbedingungen bleiben bestehen.
**Empfehlung:** Queryparameter explizit als Integer im Bereich 1–100, Status als erlaubten Enumwert und Sprache als unterstützte Ausgabe validieren. Kategorie und Portal zusammen validieren; Legacy-Verträge bewusst separat berücksichtigen. Maximale Textgröße definieren, wenn API- und Webeingaben dieselben fachlichen Grenzen haben sollen.
**Regression:** `per_page` mit -1, 0, 1, 100, 101, Text und Array; `status[]`; `language=zz`; inaktive bzw. portalunpassende Kategorie. Ungültige Eingaben kontrolliert ablehnen, SQL-Limit stets begrenzen.
### F12 — Melde-URL wird ohne Portal und Sprache einer PM zugeordnet
**Priorität:** P2.
**Fundstellen:** `app/Http/Controllers/Web/PressReleaseReportController.php:325–335`; `app/Http/Controllers/Web/ReportFormController.php:112–124`; `app/Models/PressRelease.php:43–45` und Schemaindex `press_releases_portal_language_slug_unique`.
Beide Controller extrahieren lediglich den Slug hinter `/press-release/`, entfernen den Portal-Scope und wählen die erste veröffentlichte PM. Host und Ausgabe der gemeldeten URL werden nicht berücksichtigt. Laut tatsächlich vorhandenem Unique-Index ist ein Slug nur innerhalb von Portal und Sprache eindeutig.
**Trigger:** Zwei Portale oder Ausgaben besitzen denselben Slug. Eine Meldung zur zweiten URL kann den zuerst gefundenen Datensatz der anderen Ausgabe treffen. Auch eine fremde Domain mit passendem Pfad wird nicht als fremd erkannt.
**Auswirkung:** Falsche Zuordnung im Bearbeitungsprozess und gegebenenfalls Versand des Verifikationslinks an den falschen hinterlegten Pressekontakt. Es wird nicht an eine beliebig eingegebene Angreiferadresse versendet; diese vorhandene Schutzmaßnahme bleibt wirksam.
**Empfehlung:** Gemeinsamen Resolver für eigene kanonische und ausdrücklich unterstützte Legacy-URLs verwenden: Host-Whitelist, Portal, Sprache und Slug zusammen auflösen; öffentliche Sichtbarkeit einheitlich berücksichtigen. Nicht zuordenbare Meldungen im allgemeinen Meldeweg bewusst ohne PM-Bezug behandeln.
**Regression:** Gleicher Slug in beiden Portalen und in `de/en`, URL einer fremden Domain, kodierter Slug, gelöschte bzw. künftig terminierte PM. Jeweils genau die beabsichtigte Zuordnung oder einen kontrollierten Nichttreffer erwarten.
### F13 — Öffentliche Caches überleben normale Firmenlöschung/-deaktivierung
**Priorität:** P2.
**Fundstellen:** `resources/views/livewire/admin/companies/edit.blade.php:121–186,250–265`; `app/Observers/AdminPerformanceCacheObserver.php:17–35`; `app/Services/Web/NewsroomDetailPageData.php:26–57,65–74`; `app/Support/WebPageCache.php:25–33,136–138`.
Der Firmenobserver invalidiert Admin-Statistiken. Die normale Firmenlöschung ruft lediglich `delete()` auf. Der öffentliche Newsroom prüft vor dem Seiten-Cache nur den Sonderfall `gone_at`, nicht die normale Löschung bzw. Deaktivierung. Beim Cachetreffer bleibt der vorher gespeicherte Inhalt verfügbar.
**Trigger:** Newsroomseite wird gecacht; danach wird die Firma normal gelöscht oder deaktiviert. Der nächste Aufruf erhält zunächst die gespeicherten Daten. Das öffentliche Cachesystem erlaubt ein Stale-Fenster bis 3.600 Sekunden. Das tatsächliche Ende hängt vom Neuaufbau und dessen Fehlerbehandlung ab; eine exakte maximale Sichtbarkeitsdauer wurde nicht gemessen.
**Abgrenzung:** Der spezielle 410-Entfernungsprozess besitzt bereits einen bewussten öffentlichen Cache-Flush. Gerade deshalb darf dieses Verhalten nicht auf die übrigen Löschwege übertragen werden.
**Empfehlung:** Alle öffentlichen Sichtbarkeitswechsel an denselben Cache-/Entfernungsprozess anschließen. Bei sensiblen Entfernungen den aktuellen Sichtbarkeitsstatus vor jedem Cachetreffer prüfen. Seiten-Cache und Browser-/CDN-Mediencache getrennt behandeln; ein Anwendungsflush löscht keine bereits extern gecachten Bilder.
**Regression:** Cache aufbauen, Firma löschen/deaktivieren, unmittelbar danach Newsroom, Kategorie, Startseite und Medien abrufen. Auch Kontaktänderungen mit gecachter Newsroomansicht prüfen.
### F23 — Dateisystemfehler können Erfolg vortäuschen oder bestehende Logos verlieren
**Priorität:** P2.
**Fundstellen:** `app/Services/PressRelease/PressReleaseAttachmentStorage.php:59–80`; `config/filesystems.php:33–47`; `resources/views/livewire/customer/press-kits/show.blade.php:115–130`; entsprechender Austausch in `resources/views/livewire/admin/companies/edit.blade.php:150–165`.
Der Attachment-Service ignoriert den Rückgabewert von `$disk->put()`. Gleichzeitig konfigurieren die Disks `throw => false` und `report => false`. Ein fehlgeschlagener Schreibvorgang kann somit normale Dateimetadaten zurückliefern, obwohl die Datei nicht gespeichert wurde.
Beim Logo-Austausch wird das bisherige Logo einschließlich Varianten vor der erfolgreichen Speicherung des neuen Logos gelöscht. Scheitert die neue Speicherung oder die anschließende Datenbankänderung, kann das alte Logo bereits unwiederbringlich fehlen.
**Trigger:** Voller Datenträger, Schreibrechteproblem oder fehlgeschlagener Storagezugriff beim Upload/Austausch.
**Empfehlung:** Storagefehler durch geprüften Rückgabewert bzw. geeignete Exceptionkonfiguration eindeutig behandeln. Neue Datei zuerst vollständig speichern, dann DB-Referenz wechseln und alte Datei erst anschließend entfernen. Neue Dateien bei gescheitertem DB-Commit aufräumen. Eine DB-Transaktion allein macht Dateisystemoperationen nicht atomar.
**Regression:** Storage-Fake/Mock liefert `false`; neue Datei kann nicht geschrieben werden; DB-Save nach erfolgreichem Upload schlägt fehl. Kein Erfolgshinweis ohne Datei und kein Verlust des bisherigen Logos.
### F26 — Anmeldemodal ohne Fokusführung
**Priorität:** P2, Bedienbarkeit/Barrierefreiheit.
**Fundstellen:** `resources/views/components/web/site-header.blade.php:102–105,158,189–236`.
Das eigene Modal verwendet `x-show`, `role="dialog"` und `aria-modal="true"`. Eine explizite Übernahme des Tastaturfokus, Fokusbegrenzung innerhalb des Modals und Rückgabe an den Auslöser sind im untersuchten Template nicht implementiert. Anders als ein Flux-Modal erhält dieser eigene Div-Aufbau solche Funktionen nicht allein durch ARIA-Attribute.
**Trigger:** Öffnen über Tastatur und anschließende Tab-Navigation. Der Fokus kann im Hintergrund bleiben bzw. außerhalb des sichtbaren Dialogs weiterlaufen. Escape ist implementiert und als positive Teilabsicherung zu berücksichtigen.
**Empfehlung:** Bestehende zugängliche Dialogkomponente verwenden oder Fokusübernahme, Fokusbegrenzung, Rückgabe und geeignete Hintergrund-Deaktivierung vollständig ergänzen. Kein pauschaler rechtlicher Konformitätsbefund: Ein visueller und assistiver Browserlauf fehlt.
**Regression:** Browserprüfung mit Tab/Shift-Tab, Escape, Öffnen/Schließen, Rückfokus und Screenreader-Beschriftung bei Desktop- und Mobilbreite.
## 7. Newsletter, Abrechnung und Hintergrundverarbeitung
### F05 — Newsletter dupliziert und überspringt Empfänger ab der zweiten Seite
**Priorität:** P1.
**Fundstellen:** `app/Services/Newsletter/CampaignSender.php:37–50`; `app/Services/Newsletter/CampaignRecipients.php:175–176`; `vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:3179–3190`; `database/migrations/2026_07_21_111646_create_newsletter_campaign_tables.php:36–46`.
Der Sender verwendet `chunkById(500)` auf einer Empfängerquery, die bereits nach `created_at` oder `last_login_at` sortiert. Laravel ersetzt dabei nur die Sortierung der ID-Spalte; die andere führende Sortierung bleibt bestehen.
**Deterministisches Beispiel:** 1.000 Benutzer mit zeitlich steigenden IDs und unterschiedlichen `created_at`-Werten, Sortierung `created_at DESC`. Die erste Seite liefert 1000 bis 501. Die zweite Seite setzt `id > 501`, sortiert aber weiterhin zeitlich absteigend und liefert 1000 bis 502 erneut. Die IDs 1 bis 500 werden nicht erreicht.
Die Migration und die tatsächlich abgefragten MySQL-Indizes enthalten keinen Unique-Constraint auf Kampagne/Benutzer oder Kampagne/E-Mail. Die wiederholten Empfängerzeilen können daher gespeichert und als eigene Jobs versendet werden.
**Auswirkung:** Doppelversand, ausgelassene Empfänger und falsche Kampagnenstatistiken. Das ist ein konkreter Grenzfehler, kein bloßer Performancevorschlag.
**Empfehlung:** Auswahlquery von der UI-Sortierquery trennen. Vor ID-Chunking eine eindeutige ID-Reihenfolge herstellen, beispielsweise über `reorder()`. Falls die Versandfolge fachlich relevant ist, einen passenden zusammengesetzten Cursor bzw. eine eingefrorene Versandposition einsetzen. Empfänger zusätzlich eindeutig absichern.
**Regression:** Mindestens 1.001 Benutzer, unterschiedliche Zeitwerte und alle angebotenen Sortierungen. Jede ausgewählte Benutzer-ID genau einmal und vollständige Abdeckung prüfen. Die vorhandenen Senderprüfungen in `tests/Feature/Admin/NewsletterCampaignTest.php:230–281` überschreiten die Chunk-Grenze nicht.
### F06 — MAN-Abrechnung läuft über das explizite Vertragsende hinaus
**Priorität:** P1.
**Fundstellen:** `app/Services/Billing/ManualInvoiceService.php:44–58,71–80,143–148`; `app/Models/UserPaymentOption.php:94–115`; `app/Console/Commands/GrandfatherLegacySubscriptions.php:99–100,207–209`.
Der Import übernimmt `valid_until_date` nach `grandfathered_until`. Die aktive Legacy-Berechtigungslogik berücksichtigt dieses Vertragsende. Weder die Rechnungsauswahl noch die erneute Prüfung unter dem Row Lock berücksichtigen es.
**Trigger:** Status bleibt `Grandfathered`, das explizite Vertragsende ist bereits überschritten, `current_period_end` ist fällig. Der Service erstellt weiterhin eine MAN-Rechnung und verlängert die Periode.
**Auswirkung:** Zugang und Abrechnung beurteilen denselben Vertrag widersprüchlich. Es können Rechnungen für einen technisch bereits als abgelaufen behandelten Vertrag entstehen. Ob solche Fälle im Bestand vorhanden sind, wurde nicht anhand personenbezogener Rechnungsdaten untersucht.
**Empfehlung:** Eine zentrale fachliche Abrechenbarkeitsregel verwenden und das Vertragsende sowohl bei der Auswahl als auch direkt vor dem Schreiben unter Sperre beachten. Behandlung des Endtags und einer gegebenenfalls abschließenden Abrechnung ausdrücklich festlegen.
**Regression:** Gestern beendeter, heute endender und unbefristeter Vertrag; auch direkter Aufruf von `invoiceFor()` muss dieselbe Grenze beachten.
### F14 — Newsletter-Start ist nicht gegen Parallelität und Teilabbrüche abgesichert
**Priorität:** P2.
**Fundstellen:** `app/Services/Newsletter/CampaignSender.php:29–75`; Empfängertabelle wie F05.
Der Draft-Guard prüft ein bereits geladenes Model. Empfänger werden ohne gemeinsamen Start-Lock eingefügt, anschließend wird `Sending` gesetzt und erst danach werden Jobs einzeln erzeugt.
**Fehlerfälle:** Zwei Requests starten denselben Entwurf; ein Start bricht mitten im Empfängerimport ab; oder der Queue-Dispatch scheitert nach einigen Jobs. Je nach Zeitpunkt bleiben doppelte Empfänger, eine teilweise befüllte Draft-Kampagne oder eine nicht mehr startbare `Sending`-Kampagne mit dauerhaft wartenden Empfängern zurück.
**Empfehlung:** Atomare Übernahme des Kampagnenstarts, eindeutige Empfänger und wiederaufnehmbare Einplanung aus persistentem Zustand. Beispielsweise vorbereitender Status plus Compare-and-swap/Row Lock und separater Dispatcher/Outbox. Eine große DB-Transaktion allein garantiert die Übergabe an eine externe Queue nicht.
**Regression:** Zwei unabhängig geladene Modelinstanzen derselben Kampagne; Ausnahme nach dem ersten Empfängerbatch; Ausnahme nach dem ersten Dispatch. Wiederanlauf muss alle Empfänger genau einmal einplanen.
### F15 — Versandjob kann doppelt senden und Fortschritt falsch zählen
**Priorität:** P2.
**Fundstellen:** `app/Jobs/SendNewsletterCampaignMail.php:32–57,80–102`.
Das Lesen von `Pending` ist kein atomarer Claim. Erst nach SMTP-Versand wird der Empfängerstatus geändert; Empfängerabschluss und Kampagnenzähler erfolgen in separaten Schreibvorgängen.
**Trigger:** SMTP nimmt eine Nachricht an, anschließend scheitert das DB-Update. Der Retry sieht weiterhin `Pending` und sendet nochmals. Bei einem Abbruch nach Empfängerupdate, aber vor Zählererhöhung bleibt der Fortschritt dagegen dauerhaft zu niedrig. Parallel laufende Instanzen können dieselbe Pending-Zeile gleichzeitig bearbeiten.
**Empfehlung:** Atomarer Versandclaim und idempotenter, transaktionaler DB-Abschluss. Für die Grenze SMTP/DB eine bewusste Zustellstrategie definieren: Provider-Idempotenz, falls verfügbar, oder sichtbarer Zustand „Zustellung unklar“. Exakt einmalige SMTP-Zustellung lässt sich nicht durch einen DB-Lock allein garantieren.
**Regression:** Fehler vor/nach SMTP-Annahme und zwischen Statusupdate und Zählerpflege; parallele Bearbeitung derselben Empfängerzeile. Wiederanlauf und Abschlussstatus prüfen.
### F16 — Die 50 ältesten nicht abrechenbaren Verträge können alles Weitere blockieren
**Priorität:** P2.
**Fundstellen:** `app/Console/Commands/GenerateManualInvoices.php:19–21,30,41–68`; `app/Services/Billing/ManualInvoiceService.php:44–58,87–107`; `routes/console.php:111–115`.
Der tägliche Lauf wählt standardmäßig die 50 ältesten fälligen Vereinbarungen. Nicht abrechenbare Fälle, beispielsweise ohne Rechnungsadresse oder Intervall, werden übersprungen und behalten ihre alte Fälligkeit. Ein nächster Batch wird nicht verarbeitet.
**Trigger:** Die ersten 50 fälligen Vereinbarungen sind unvollständig. Sie werden jeden Tag erneut ausgewählt, während die 51. vollständig abrechenbare Vereinbarung nie erreicht wird. Auch ohne ungültige Fälle ist der planmäßige Durchsatz auf 50 Vereinbarungen täglich begrenzt.
**Empfehlung:** Den fälligen Bestand mit begrenzten Cursor-Batches durchgehen; fehlerhafte Fälle separat markieren und erneut bearbeiten. Metriken für Rückstand und Alter des ältesten bearbeitbaren Falls ergänzen.
**Regression:** 50 unvollständige Vereinbarungen vor mindestens einer vollständigen. Die vollständige muss trotz der übersprungenen Fälle verarbeitet werden.
### F17 — KI-HTTP-Versuche können länger dauern als der Worker zulässt
**Priorität:** P2, bedingter Betriebsbefund.
**Fundstellen:** `app/Services/PressRelease/Classification/Drivers/OpenAiClassificationDriver.php:35–45`; `app/Services/PressRelease/ContentScore/Drivers/OpenAiContentScoreDriver.php:43–53`; `config/scoring.php:38,62`; `app/Jobs/ClassifyPressRelease.php:33`; `app/Jobs/ScorePressRelease.php:29`; `app/Console/Commands/RunClassificationQueue.php:23–34`; `config/queue.php:42,70`.
Der Content-Score-Treiber hat standardmäßig bis zu 30 Sekunden pro HTTP-Versuch; die Retry-Liste `[200, 500]` erlaubt drei Versuche. Sein mögliches Gesamtbudget liegt damit bei ungefähr 90,7 Sekunden zuzüglich sonstiger Arbeit. Die Klassifikation verwendet laut `config/scoring.php` dagegen standardmäßig 15 Sekunden und kommt auf ungefähr 45,7 Sekunden; das gleiche Überschreitungsrisiko entsteht dort erst bei entsprechend höherer Konfiguration oder zusätzlicher Laufzeit. Die Jobs besitzen kein eigenes Timeout; der Projekt-Worker setzt ebenfalls keines, sodass der Laravel-Standard von 60 Sekunden relevant ist. `retry_after` liegt standardmäßig bei 90 Sekunden.
**Trigger:** Mehrere langsame Transporttimeouts beim Content-Score. Der Worker kann den Prozess beenden, bevor der vorgesehene deterministische Fallback erreicht wird. Produktionsseitig anders konfigurierte Timeouts können das Verhalten ändern und wurden nicht verifiziert.
**Empfehlung:** HTTP-Gesamtbudget, Jobtimeout, Workerparameter und `retry_after` zusammen abstimmen. `retry_after` mit Sicherheitsabstand über dem maximalen Jobtimeout halten. Ein isoliertes Anheben nur des Worker-Timeouts kann Doppelverarbeitung begünstigen.
**Regression:** Kontrollierte langsame/fehlgeschlagene Treiberantworten mit realem Queue-Worker; Fallback und endgültiges Scheitern beobachten. Ein synchroner Jobtest beweist dieses Verhalten nicht.
### F18 — Monatskontingent-Reset ist nicht idempotent
**Priorität:** P2.
**Fundstellen:** `app/Console/Commands/ResetMonthlyPressReleaseQuota.php:30–34`; `routes/console.php:121–126`; `app/Services/PressRelease/PressReleaseService.php:291–296`.
Der Command setzt alle positiven Monatszähler auf null, ohne die bereits zurückgesetzte Periode zu speichern. Er ist am Monatsersten um 00:05 geplant.
**Trigger:** Publikationen zwischen Mitternacht und 00:05 werden kurz darauf wieder aus dem Verbrauch entfernt. Eine Wiederholung nach weiteren Publikationen löscht den Verbrauch nochmals. Wird der Schedulertermin verpasst, ist im Resetpfad kein automatischer Catch-up vorgesehen.
**Empfehlung:** Verbrauch an eine eindeutige Kalenderperiode binden und den Periodenwechsel atomar genau einmal vornehmen. Alternativ periodisierte Verbrauchsdatensätze verwenden. Nachholung beim ersten Zugriff definieren, damit ein einzelner Crontermin nicht die gesamte Kontingentlogik bestimmt.
**Regression:** Monatswechsel, zweimaliger Reset mit Publikation dazwischen, verspäteter Lauf. Kalenderkontingent nicht ohne fachliche Entscheidung auf Stripe-Aboperioden umstellen.
### F19 — Zwei Aufrufaggregatoren können dieselben Events doppelt zählen
**Priorität:** P2, Parallelitätsbedingung.
**Fundstellen:** `app/Console/Commands/AggregatePressReleaseViews.php:45–46,73–91,99–113`; Schedulerregistrierung in `routes/console.php`.
Counter und Fortschrittsmarke werden erfreulicherweise gemeinsam transaktional geschrieben. Die Fortschrittsmarke wird jedoch vorher gelesen und nicht unter einer gemeinsamen Sperre neu geprüft.
**Trigger:** Ein manueller Command-Lauf überlappt den Scheduler oder zwei unabhängig gestartete Prozesse laufen gleichzeitig. Beide lesen denselben Marker und addieren dieselben Ereignisse. `withoutOverlapping()` schützt den Scheduleraufruf, jedoch nicht automatisch jeden manuellen Einstieg.
**Empfehlung:** Einen für alle Einstiegspfade gemeinsamen Command-Lock verwenden oder die Watermark unter Row Lock lesen und den Batch innerhalb der geschützten Sequenz bestimmen. Laufzeitbegrenzung für dauerhaften Ereignisnachschub ergänzen.
**Regression:** Zwei DB-Verbindungen mit kontrolliertem Interleaving. Jeder Event darf nur einmal in `hits` eingehen; Wiederholung ohne neue Events muss wirkungslos sein.
### F20 — Geplante Publikationen melden trotz technischer Fehler Erfolg
**Priorität:** P2.
**Fundstellen:** `app/Console/Commands/PublishScheduledPressReleases.php:93–117`; `routes/console.php`, Failure-Hook für `press-releases:publish-scheduled`.
Technische Exceptions werden gezählt und über `report()` protokolliert. Der Command liefert anschließend trotzdem immer `SUCCESS`. Der konfigurierte `onFailure`-Alarm hängt jedoch vom fehlgeschlagenen Command ab.
**Trigger:** Einzelne oder alle fälligen PMs scheitern im Veröffentlichungsservice. Exit-Code-basierte Überwachung meldet einen erfolgreichen Lauf.
**Empfehlung:** Erwartete fachliche Ablehnungen von technischen Fehlern unterscheiden. Bei technischen Fehlern einen Fehler-Exit-Code liefern oder einen ausdrücklichen Alarm auslösen. Vorhandenes Fehlerlogging beibehalten; der Befund lautet nicht „Fehler werden überhaupt nicht protokolliert“.
**Regression:** Service wirft technische Exception: Fehler-Exit-Code bzw. Alarm nachweisen. Blacklistablehnung separat testen und fachlich definieren.
## 8. Datenbank, Importe und Performance
### F21 — Legacy-Import ist nicht durchgehend speicherbegrenzt oder atomar
**Priorität:** P2; Relevanz abhängig von weiteren Import-/Delta-Läufen.
**Fundstellen:** `app/Services/Import/PressReleaseImporter.php:34–58,159–170,182–265`; insbesondere die Identitätsübernahme bei `:189`.
Vor dem 200er-PM-Chunking werden die vollständigen Legacy-Bild- und Kontaktpivot-Tabellen geladen und gruppiert. Auch ein kleiner `--since`-Deltaimport benötigt dadurch Speicher abhängig vom Gesamtbestand.
Pro PM erfolgen Hauptdatensatz, Zeitstempel, Bilder, Kontakte und Importmap ohne gemeinsame Zieltransaktion. Scheitert der Import nach Speicherung der PM, aber vor dem Mapping, existiert ein Teilimport. Der Wiederanlauf identifiziert den bereits vorhandenen Datensatz nicht in allen Fällen unabhängig von der Importmap und kann seine öffentliche UUID-/Slug-Identität verändern.
**Empfehlung:** Relationen nur für die Legacy-IDs des aktuellen Chunks laden. Zieländerungen einschließlich Mapping je PM transaktional speichern. Vorhandene Zielidentität auch ohne Mapping anhand des stabilen Legacy-Schlüssels erkennen. Delta-Wiederanlauf ausdrücklich als fachlichen Vertrag testen.
**Regression:** Ausnahme nach Hauptdatensatz, aber vor Mapping; Wiederholung muss dieselbe UUID und denselben Slug behalten. Großer Mediengesamtbestand mit kleinem PM-Delta darf den Speicherbedarf nicht linear zum Gesamtbestand anwachsen lassen.
### F22 — E-Mail-Suche umgeht den bereits angelegten Index
**Priorität:** P2.
**Fundstellen:** `app/Services/Auth/ContactAccessService.php:128–146`; `app/Models/User.php:186`; Indexmigration für `companies.email` und tatsächlich vorhandener `companies_email_idx`.
Die Firmenzuordnung fragt `LOWER(email) = ?` ab. Ein normaler Index auf `email` kann diese Funktionsbedingung in der beobachteten Query nicht verwenden. Das wurde mit zwei ausschließlich lesenden `EXPLAIN`-Abfragen überprüft; als Vergleichswert wurde eine synthetische Adresse unter `example.invalid` verwendet.
| Vergleich | MySQL-Zugriff | Index | Geschätzte Zeilen |
|---|---|---|---:|
| `LOWER(email) = ... AND deleted_at IS NULL` | `ALL` | keiner | 110.291 |
| `email = ... AND deleted_at IS NULL` | `ref` | `companies_email_idx` | 1 |
Die verfügbare Datenbank verwendet für `companies.email` `utf8mb4_unicode_ci`, also eine bereits case-insensitive Kollation. Die Zahlen sind **Optimizerschätzungen**, keine gemessenen Laufzeiten und keine exakt gelesenen Datenmengen. Für `users` und `contacts` wurde in diesem Audit kein eigener EXPLAIN-Vergleich vorgenommen.
**Auswirkung:** Ein vorhandener Performanceindex bleibt gerade in einem häufigen Login-/Zuordnungspfad ungenutzt. Mehrere Loginverfahren und erneute Zuordnungen können diese Suche wiederholt auslösen.
**Empfehlung:** E-Mail-Normalisierung und Kollationsvertrag festlegen. Unter der vorhandenen MySQL-Kollation einen direkten Vergleich nutzen und Groß-/Kleinschreibungsfälle testen; bei abweichenden Datenbanken gegebenenfalls eine indizierte normalisierte Spalte verwenden. Nicht ungeprüft die gleiche Annahme auf alle Tabellen/Umgebungen übertragen.
**Regression:** Groß-/Kleinschreibung, führende/nachfolgende Leerzeichen und Legacy-Adressen; zusätzlich EXPLAIN für den finalen Querypfad. Erfolgskriterium ist korrekte Zuordnung mit Indexzugriff, nicht nur eine kürzere Query.
### 8.1 Beobachtete Größenordnung und Konsequenzen
Lesend aus `information_schema.TABLES` entnommene InnoDB-Schätzwerte der verfügbaren Verbindung:
| Tabelle | Geschätzte Zeilen |
|---|---:|
| `press_releases` | 134.979 |
| `companies` | 110.291 |
| `users` | 65.572 |
| `contacts` | 19.018 |
| `press_release_events` | 952.754 |
| `activity_events` | 5.316 |
Diese Werte sind ungefähre Schema-/Optimierermetadaten. Es wurden dafür keine einzelnen Personen-, Kunden- oder Nachrichtendatensätze ausgelesen. Sie belegen, dass Vollscans, unbegrenzte Collection-Ladevorgänge und kleine Batch-Grenzfehler praktisch relevante Größenordnungen erreichen können.
### 8.2 Weitere Performanceverbesserungen
1. **Cacheinvalidierung gezielter machen:** `AdminPerformanceCacheObserver` verwirft bei jeder beobachteten Änderung die gesamte Liste der Admin-Cachekeys. Bei Importen oder vielen einzelnen Änderungen entstehen unnötige Löschoperationen. Zuerst Anzahl und Dauer messen; danach nach Domäne bzw. Batch invalidieren.
2. **Abfragebudget pro Seite definieren:** Für PM-Listen, Firmenansicht und Dashboard sowohl Abfrageanzahl als auch geladene Zeilen messen. Eine konstante Queryanzahl allein schließt übergroße Relationen nicht aus.
3. **Kein zusätzlicher Index ohne Query-Plan:** Bereits viele kombinierte Indizes sind vorhanden. Weitere Indizes erhöhen Schreib- und Migrationskosten. Kandidaten erst anhand realer WHERE-/ORDER-BY-Pfade bewerten.
4. **Öffentliche Suche begrenzen:** Kurze Suchbegriffe fallen bewusst auf LIKE zurück. Eingabelänge, Wiederholrate, teure Sortierungen und tiefe Seiten bei großen Beständen beobachten. Keine unbelegte SQL-Injection behaupten: die betrachteten Rohbedingungen verwenden Parameterbindung.
5. **Firmen-/Kontaktzuordnung bündeln:** Die Zuordnung führt pro Firma weitere Existenz-/Pivotabfragen aus. Nach F01/F10 kann dieser Pfad auf konsistente, gebündelte Berechtigungsdaten ausgerichtet werden; Sicherheit geht vor Queryverkürzung.
## 9. Tests, CI und Entwicklungsqualität
### F24 — Lint-Workflow prüft den eingecheckten Stil nicht verbindlich
**Priorität:** P2.
**Fundstellen:** `.github/workflows/lint.yml:13–14,31–46`.
Der Workflow führt `vendor/bin/pint` im schreibenden Modus aus. Die nachfolgende Commit-Stufe ist auskommentiert; ein Diff-/Fehlergate fehlt. Der CI-Checkout kann daher erfolgreich formatiert werden, während der eigentliche Commit unverändert unformatiert bleibt. Ein erfolgreicher Workflow ist kein Nachweis, dass der eingecheckte Code bereits dem Stil entspricht.
**Empfehlung:** Einen bewusst prüfenden CI-Mechanismus verwenden: beispielsweise nach dem erforderlichen Formatierungslauf einen verbleibenden Git-Diff als Fehler werten. Die für dieses Projekt geltende lokale Pint-Regel und den CI-Vertrag ausdrücklich unterscheiden. Schreibrechte `contents: write` sind für einen rein prüfenden Job nicht notwendig.
**Regression/Abnahme:** Absichtlich unformatierter Probe-Commit muss den erforderlichen CI-Check rot machen; sauberer Commit grün. In diesem Audit wurde kein Formatter ausgeführt, da keine PHP-Datei geändert werden sollte.
### F25 — Hohe Testzahl, aber Lücken bei produktionsnahen und sicherheitskritischen Zuständen
**Priorität:** P2.
**Fundstellen:** `phpunit.xml:7–14,29–43`; `phpstan.neon:16–27`; `.github/workflows/tests.yml:35–61`; `tests/Feature/PressReleasePreviewTest.php:237–247`; `tests/Feature/CustomerProfileSecurityTest.php:253–270`.
Die Testkonfiguration erzwingt SQLite `:memory:` und synchrone Queues. Das ist für viele schnelle Featuretests sinnvoll, beweist aber keine MySQL-Zeilensperren, Fulltext-Ausführung, Kollationsdetails, Queue-Timeouts oder echte konkurrierende Transaktionen. Die CI-Datei zeigt keine ergänzende MySQL-/Queue-Matrix.
PHPStan arbeitet auf Level 4 mit Baseline. Die Analysepfade umfassen `app`, Bootstrap, Config, Factories, Seeder und Routen, jedoch nicht die umfangreiche PHP-Logik in Volt-Blade-Dateien. Diese Logik ist nicht schon dadurch statisch analysiert, dass das Projekt Larastan verwendet.
Hinzu kommen konkret irreführende bzw. zu kleine Tests: Der Soft-Delete-Test benutzt Force Delete (F04); Newslettertests überschreiten die Chunk-Grenze nicht (F05); die Recovery-Tests prüfen normale UI-Sichtbarkeit, nicht die Manipulation der öffentlichen Property (F03). Ein JS-/Browser-Testlauf ist in den betrachteten Paket- und CI-Dateien nicht eingerichtet.
**Empfehlung:** Bestehende schnelle Tests behalten und gezielt ergänzen: MySQL-Integration für Locks/Fulltext, echte Workerprüfung für Zeitgrenzen, Browserfälle für Fokus/Livewire-Navigation und negative Lifecycle-Tests für Rechte-/Identitätsänderungen. Geschäftskritische Logik schrittweise aus großen Volt-Dateien in bereits etablierte analysierbare Services/Actions verlagern. Baseline nur begründet verändern; sie nicht als allgemeine Fehlerablage behandeln.
**Abnahme:** F01–F07 erhalten jeweils einen zuvor fehlschlagenden Regressionstest; mindestens ein produktionsnaher Lauf beweist die DB-/Queue-spezifischen Fälle. Eine Code-Coverage-Prozentzahl wurde nicht ermittelt und wird nicht aus der Testanzahl abgeleitet.
### F27 — `composer run dev` startet standardmäßig keinen Vite-Server
**Priorität:** P3.
**Fundstellen:** `composer.json`, Script `dev`; `package.json`, Scripts `dev`, `dev:portal`, `dev:web`, `dev:all`.
Composer startet neben Server, Queue und Logs `npm run dev`. Dieses npm-Script gibt jedoch nur einen Hinweistext aus und beendet sich. Die tatsächlichen Vite-Server werden ausschließlich durch die spezialisierten Scripts gestartet.
**Auswirkung:** Der übliche Entwicklungsstart wirkt aktiv, liefert aber keine laufende Asset-Neukompilierung. Vorhandene Builddateien können den Fehler zunächst verdecken. Ohne Assets können zusätzlich Manifest-/Darstellungsfehler auftreten.
**Empfehlung:** Einen funktionierenden Standardstart definieren und die bewusste Auswahl einzelner Bundles zusätzlich anbieten. Den Bedarf an externem MySQL, Redis, Mailpit und Traefik im Startprozess klar dokumentieren: `docker-compose.yml` referenziert diese Infrastruktur, stellt sie selbst aber nicht vollständig bereit.
**Abnahme:** Aus einer vorbereiteten frischen Entwicklungsumgebung den dokumentierten Hauptbefehl starten; beide gewünschten Hotfiles/Ports und eine sichtbare Assetänderung prüfen. Kein Dependency-Upgrade dafür erforderlich.
### F28 — Entwicklungsdokumentation enthält widersprüchliche aktuelle Zustände
**Priorität:** P3.
**Fundstellen:** `docs/README.md:3–6,24–45`; `dev/roadmap.md:3–6`; `dev/README.md`; `resources/views/admin/README.md`; `resources/views/admin/BACKEND_STATUS.md`.
`docs/README.md` bezeichnet den Stand vom 12.06.2026 als Einstieg und führt inzwischen vorhandene Funktionen als noch nicht umgesetzt. `dev/roadmap.md` ist als Stand vom 20.07.2026 formuliert, während Entwicklung und Berichte unter `dev/2026-09-01` bis September weiterlaufen. Mindestens der Link auf `dev/frontend/hub-flux/PROGRESS.md` verweist auf die alte, inzwischen archivierte Struktur. Admin-Statusdateien enthalten ebenfalls ältere TODO-Stände.
**Auswirkung:** Neue Entwickler können vorhandene Funktionen nochmals planen, veraltete Entscheidungen als verbindlich ansehen oder wichtige offene Betriebsaufgaben übersehen. Umfangreiche Dokumentation hilft nur, wenn Gegenwart, historische Entscheidung und Archiv eindeutig unterscheidbar sind.
**Empfehlung:** Einen einzigen aktuellen Einstieg mit Datum, Verantwortlichkeit und Links auf Code-/Betriebsreferenzen festlegen. Historische Dokumente als solche markieren und verschobene Verweise korrigieren. Für jede neue Entscheidung den betroffenen bisherigen Entscheidungsstand ausdrücklich ersetzen.
**Abnahme:** Ein neuer Entwickler kann aus dem Einstieg Architektur, lokalen Start, Prüfkommandos, aktuellen Backlog und Deploymentablauf ohne widersprüchliche Statusangaben finden. Links automatisiert auf vorhandene lokale Ziele prüfen.
### 9.1 Entwicklungsverlauf
Die betrachtete lokale Git-Historie zeigt seit April 2026 folgende Commitverteilung: Mai 7, Juni 110, Juli 222, September 14. Diese Zahlen sind nur die sichtbare Historie des Checkouts, keine Produktivitätsmessung oder vollständige Teamhistorie.
Die jüngsten Änderungen sind fachlich fokussiert: Firmenkonsolidierungs-Trockenlauf, Konversionsmessung, API-Portalauflösung, Newsletter-Bounces, E-Mail-Index, Wiederherstellung der statischen Analyse und Entfernung mit 410. Das spricht für eine aktive Nachbesserungsphase nach dem Launch.
Das wiederkehrende Entwicklungsrisiko ist **teilweise Zentralisierung**: Neue Services und abgesicherte Pfade existieren, alte Alternativwege bleiben gleichzeitig aktiv. Beispiele sind der sichere `pending_email`-Dialog neben der sofortigen Fortify-Änderung, 2FA im Magic-Login neben dem direkten Google-Login und ein korrekter 410-Cacheflush neben normalem `delete()`. Künftige Änderungen sollten deshalb immer alle Einstiegspfade einer fachlichen Operation erfassen.
## 10. Weitere Optimierungen und gezielt offene Prüfungen
Die folgenden Punkte sind bewusst nicht als zusätzliche bestätigte Sicherheitslücken gezählt. Sie benötigen Messung, eine fachliche Entscheidung oder einen produktionsnahen Nachweis.
| Bereich | Beobachtung | Nächster sinnvoller Schritt |
|---|---|---|
| Authentifizierungsarchitektur | Mehrere Login- und Profiländerungspfade sind auseinanderentwickelt | Gemeinsame Identitäts-/Loginabschlusslogik und einheitlicher E-Mail-Änderungsservice |
| Google-Identität | `SocialAuthService` verknüpft primär per E-Mail und überschreibt Providerdaten | Umgang mit stabiler Provider-ID, verifizierter Adresse, Providerwechsel und Account-Linking ausdrücklich definieren; nicht als nachgewiesene Google-Tokenfälschung einstufen |
| Rollen/Presets | Preset-Routen sind nicht mit einer eigenen Managementpermission eingeschränkt | Entscheiden, ob Editoren globale Voreinstellungen ändern dürfen; dann Route und Actions symmetrisch absichern |
| MAN-Perioden | `addMonth()` / `addYear()` können an Monats-/Schaltjahresgrenzen überlaufen | Vertraglichen Periodenanker definieren; 31. Januar und 29. Februar testen |
| MAN-Steuerdaten | MAN-Resolver und Checkout nutzen unterschiedliche Prüftiefe bei USt-IDs | Technischen Datenvalidierungsvertrag angleichen und fachlich freigeben; keine Steuerrechtsbewertung aus diesem Audit ableiten |
| Legacy-Aufrufzahlen | Reimport schreibt absolute Legacy-`hits`, neuer Aggregator addiert eigene Events | Bei weiteren Delta-Läufen Eigentümerschaft des Zählers definieren; Verlust bereits aggregierter neuer Aufrufe verhindern |
| Mediencache | Öffentliche Medien erhalten `public, max-age=86400` | Anforderungen an Entfernungsfristen, CDN-Purge und versionierte URLs prüfen; gespeicherte Browserkopien sind nicht durch DB-Änderung widerrufbar |
| Bildverarbeitung | MIME-/Bytegrenzen vorhanden, GD-Verarbeitung im Uploadpfad | Maximale Pixelmaße und Dekompressions-/Speicherbudget prüfen; Tests mit großen stark komprimierten Bildern |
| Newsletter-Anmeldung | API erzeugt unbestätigte Einträge; Sync-Provider ist teilweise Platzhalter | End-to-End bestätigen, wer Bestätigungsmail, DOI-Abschluss und externen Sync tatsächlich übernimmt; keine funktionierende Integration aus einem 201 allein ableiten |
| Queue-Priorisierung | Klassifikation und Default-Jobs teilen laut Runbook Worker | Bei Last separate Kapazität für Transaktionsmails/Newsletter/KI erwägen; Wartezeiten zuerst messen |
| Retention | Events/Magic Links besitzen eigene Bereinigung; weitere Protokolle wachsen | Aufbewahrungs- und Archivregeln pro Datentyp festlegen, einschließlich API-Logs, KI-Audits und Versandprotokollen |
| Abhängigkeiten | `npm i` statt `npm ci` in Test-CI; kein Dependency-Auditgate in den betrachteten Workflows | Reproduzierbare Installation und gesonderte Security-Prüfung einrichten; Updates nur nach Kompatibilitätsprüfung |
| Sicherheitsheader | Grundheader vorhanden; keine CSP in `SecurityHeaders` | Eine zu Livewire/Alpine und bewusst erlaubten Footer-Scripts passende CSP zunächst beobachten; fehlende CSP allein ist kein XSS-Nachweis |
| Proxy/Hosts | `trustProxies('*')` und Host-Whitelist vorhanden, einschließlich IP-/localhost-Ausnahmen | Reale Proxykette und Erreichbarkeit des Appports prüfen; Schutz vor manipulierten Forwarded-Headern nicht allein aus lokaler Config ableiten |
| Formulare/Assets | Zwei Vite-Bundles und teilweise historische Kommentare zu Alpine | Browser-Smokes für Bundlewechsel, Livewire-Navigation, Fehlerscroll und erneute Initialisierung |
| Große Volt-Komponenten | UI und Geschäftslogik eng gekoppelt | Schrittweise entlang tatsächlicher Aktionen zerlegen: Teilen, Boost, Nachweis, Korrektur, DSGVO-Anfrage, Abrechnung |
| PDF-Erzeugung | Umfangreicher eigener Renderer | Repräsentative Unicode-, Umbruch-, Mehrseiten- und Randfall-PDFs visuell vergleichen; Parser-/Layouttests ergänzen |
| Betriebliche Nachweise | Runbook vorhanden, tatsächliche Cron-/Worker-/Backupzustände nicht geprüft | Wiederherstellungstest und Alarmtest mit dokumentiertem Ergebnis; RTO/RPO und Zuständigkeit festlegen |
## 11. Priorisierter Umsetzungsplan
Alle folgenden Schritte sind **Empfehlungen für Folgearbeit**. In diesem Audit wurde keiner dieser Eingriffe umgesetzt.
### Paket A — Identitäts- und Veröffentlichungsgrenzen schließen
**Reihenfolge:** F01 → F02/F03 → F04 → F07.
1. Unverifizierte E-Mail als Grundlage für Berechtigungen ausschließen; alten Fortify-Profilpfad konsolidieren.
2. Google in die gleiche 2FA-Abschlusslogik führen; Sicherheitsaktionen mit erneutem Identitätsnachweis schützen.
3. Portalübergreifende Abfragen von Soft-Delete-Ausnahmen trennen; Medien-/Vorschauzugriff mitprüfen.
4. KI-Ergebnisse und Freigaben an eine Inhaltsversion binden.
**Abnahmekriterium:** Die jeweiligen negativen Regressionstests schlagen vor dem Fix fehl und danach fehlerspezifisch nicht mehr an. Bestehende reguläre Passwort-, Magic-, Google- und Veröffentlichungsflüsse funktionieren weiter. Für F01 zusätzlich eine gezielte Bestandsprüfung automatisch verliehener Rechte planen.
### Paket B — Versand und Rechnungslogik stabilisieren
**Reihenfolge:** F05/F06 → F16 → F14/F15.
1. Newsletter-Chunking und Eindeutigkeit korrigieren, bevor größere Kampagnen erneut gestartet werden.
2. Vertragsende im MAN-Auswahl- und Schreibpfad beachten.
3. Rechnungslauf gegen blockierende unvollständige Altfälle absichern.
4. Versandstart und Zustellung wiederaufnehmbar machen; Zustellunsicherheit offen behandeln.
**Abnahmekriterium:** Über 1.000 Empfänger werden vollständig und eindeutig eingeplant; beendete Verträge erzeugen keine unzulässige Folgeperiode; Fehler in einem Datensatz blockieren keine anderen. Keine Tests mit echten Mailadressen oder echten Zahlungen ausführen.
### Paket C — Rechtewechsel, Cache und Eingabegrenzen
**Umfang:** F08–F13 sowie F23.
Rechte erneut pro Action prüfen, Superadmin-Deaktivierung wirksam machen, automatischen Rechteentzug definieren, API-Parameter validieren, Melde-URL korrekt auflösen, öffentliche Caches konsistent invalidieren und Uploadfehler sauber behandeln.
**Abnahmekriterium:** Tests ändern Rechte, Datenzustand und Storageverfügbarkeit zwischen zwei Requests. Ein ausschließlich erfolgreicher Erstdurchlauf genügt nicht.
### Paket D — Scheduler, Queue und Importwiederanlauf
**Umfang:** F17–F21.
Zeitbudgets harmonisieren, Monatsverbrauch periodisieren, Aggregation gegen gleichzeitige Einstiege schützen, technische Publikationsfehler alarmierbar machen und Importtransaktionen/Batchspeicher korrigieren.
**Abnahmekriterium:** Wiederholung und Unterbrechung sind ausdrücklich getestete Normalfälle. Ein versäumter oder doppelt gestarteter Hintergrundlauf darf keine stillen fachlichen Mehrfachwirkungen verursachen.
### Paket E — Schnelle Optimierungen und Qualitätsgates
**Umfang:** F22, F24–F28 und ausgewählte Punkte aus Abschnitt 10.
| Maßnahme | Erwarteter Nutzen | Relativer Aufwand / Risiko |
|---|---|---|
| Direkter indizierter Firmen-E-Mail-Vergleich nach Kollationsprüfung | Konkrete Vermeidung eines Vollscans | Klein; Normalisierungsregeln testen |
| Unter-/Obergrenze für API-Pagination | Begrenzter Speicher- und Queryaufwand | Klein; API-Fehlervertrag beachten |
| Technische Schedulerfehler mit Fehlerstatus melden | Vorhandene Alarme werden aussagekräftig | Klein; fachliche Ablehnungen trennen |
| Gutschriften erneut autorisieren | Rechteentzug sofort wirksam | Klein; Lifecycle-Test erforderlich |
| Lintjob mit verbindlichem Diff-Gate | Formatierung des tatsächlichen Commits gesichert | Klein |
| Funktionierender Standard-Devstart | Weniger Asset-/HMR-Verwirrung | Klein; beide Bundles prüfen |
| Dokumentationseinstieg aktualisieren | Schnellere Orientierung und weniger Doppelarbeit | Klein |
| MySQL-/Worker-/Browserchecks ergänzen | Produktionsrelevante Regressionen auffindbar | Mittel; gezielt beginnen |
| Große Volt-Dateien fachlich zerlegen | Bessere Prüfbarkeit und statische Analyse | Mittel bis groß; kein pauschaler Umbau |
Die Aufwandsklassen sind relative Planungswerte, keine belastbaren Zeitschätzungen. Sicherheits- und Datenbestandsfolgen können den Aufwand wesentlich erhöhen.
## 12. Konkreter Test- und Abnahmeplan
### 12.1 Höchste Priorität
| Testpaket | Prüffälle | Passende vorhandene Anknüpfung |
|---|---|---|
| Identität/Firmenrechte | Unverifizierte E-Mail, Rückwechsel, legitime Verifikation, Kontaktentzug | `Auth/ContactAccessTest.php`, `CustomerProfileSecurityTest.php`, `Auth/AccessGatesTest.php` |
| Login/2FA | Google mit TOTP, Magic mit TOTP, Passwort mit TOTP, inaktive Konten | `Auth/GoogleLoginTest.php`, `Auth/TwoFactorLoginTest.php`, `Auth/MagicLinkLoginTest.php` |
| Sensitive Actions | Recovery-Propertymutation, Disable/Regenerate ohne erneuten Nachweis, Rechteentzug | `CustomerProfileSecurityTest.php`, `Admin/AdminGrantTest.php` |
| Löschen/Sichtbarkeit | Soft Delete versus Force Delete; PM/Firma/Kontakt; Vorschau/Medien/API | `PressReleasePreviewTest.php`, `SecurityHardeningTest.php`, `Api/V1/PressReleaseApiTest.php` |
| Newsletter | Mehrere Chunks, Eindeutigkeit, Parallelstart, Teilabbruch, SMTP-/DB-Fehler | `Admin/NewsletterCampaignTest.php` |
| MAN-Rechnung | Vertragsende, Blockade durch 50 ungültige Fälle, Monatsende/Schaltjahr | `Billing/ManualInvoiceGenerationTest.php` |
| KI-Inhaltsversion | Textänderung während Prüfung, alte Antwort nach neuer Antwort | `PressReleaseClassificationJobTest.php`, `PressReleaseContentScoreTest.php` |
### 12.2 Produktionsnahe Ergänzungen
- **MySQL:** Parallelität mit zwei Verbindungen, echte Zeilensperren, Unique-Konflikte, Slug-/E-Mail-Kollation, Fulltext und EXPLAIN. SQLite-Tests parallel behalten.
- **Queue:** Reale Worker mit den vorgesehenen Timeouts und `retry_after`, künstliche Treiberverzögerung, Prozessabbruch und Wiederanlauf; keine echten externen Kosten verursachen.
- **Browser:** Login-/Kontowechsel, Domain-/Bundlewechsel, Mobilnavigation, Fokus im Dialog, Validierungsanzeige und Tastaturbedienung.
- **Betrieb:** Testalarm bei technischem Schedulerfehler; Nachweis, dass ein älterer Pending-/Sending- oder Abrechnungsrückstand sichtbar wird.
- **Dateien:** Fehlgeschlagene Writes, DB-Abbruch nach Write, sehr große Pixelmaße, abgelaufene/gelöschte Medienlinks.
### 12.3 Reihenfolge der Prüfungen nach Korrekturen
Zuerst die jeweils betroffenen konkreten Pest-Dateien über Sail ausführen. Danach die betroffenen Fachgruppen, PHPStan und schließlich vor Freigabe die gesamte Suite sowie die ergänzten MySQL-/Worker-/Browserprüfungen. Formatierung nur im Rahmen einer autorisierten Codeänderung durchführen.
Beispielhafte vorhandene Dateiaufrufe:
```bash
vendor/bin/sail artisan test --compact tests/Feature/Auth/GoogleLoginTest.php
vendor/bin/sail artisan test --compact tests/Feature/Auth/ContactAccessTest.php
vendor/bin/sail artisan test --compact tests/Feature/CustomerProfileSecurityTest.php
vendor/bin/sail artisan test --compact tests/Feature/PressReleasePreviewTest.php
vendor/bin/sail artisan test --compact tests/Feature/Admin/NewsletterCampaignTest.php
vendor/bin/sail artisan test --compact tests/Feature/Billing/ManualInvoiceGenerationTest.php
vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress
vendor/bin/sail composer audit --locked --format=json
vendor/bin/sail npm audit --json
```
Diese Liste ist ein Folgeplan und keine Behauptung, dass die Befehle hier erfolgreich liefen.
## 13. Durchgeführte Prüfungen und Ausführungsprotokoll
| Prüfung | Tatsächliches Ergebnis |
|---|---|
| Git-Ausgangszustand | `git status --short` zu Beginn leer; geprüfter HEAD oben festgehalten |
| Dateiinventar und Größen | Erfasst; siehe Abschnitt 3.2 |
| Boost `application-info` | Erfolgreich; tatsächliche Paketversionen erfasst |
| Boost `database-schema`, Summary | Erfolgreich; Tabellen und Spalten lesend betrachtet |
| Index-/Kollationsmetadaten | Erfolgreich; u. a. Empfängertabelle, PM-Slugindex und Firmen-E-Mail |
| Zwei Firmen-E-Mail-EXPLAINs | Erfolgreich; Fullscan versus Indexzugriff, siehe F22 |
| InnoDB-Tabellenschätzungen | Erfolgreich; keine exakten Nutzdatencounts, siehe 8.1 |
| Framework-/Livewire-/Fortify-Dokumentation | Über Boost abgefragt; sicherheitsrelevante Aussagen zusätzlich am installierten Framework-Code geprüft |
| Bestehende relevante Tests | Lesend geprüft, nicht als bestanden gewertet |
| Audit-Dokumentprüfung | 28 eindeutige Detailbefunde und passende Prioritätszählung; Dateireferenzen und Zeilengrenzen geprüft. Der in F28 bewusst beanstandete historische Pfad ist erwartungsgemäß nicht vorhanden. |
| Abschließender Git-Zustand | Ausschließlich `dev/2026-09-11-audit/` neu; keine Änderung an versionierten Anwendungsdateien |
| `vendor/bin/sail artisan route:list --except-vendor --json --no-interaction` | Exit 1: Docker/Podman nicht verfügbar |
| `vendor/bin/sail up -d` | Exit 1: Docker/Podman nicht verfügbar |
| `vendor/bin/sail artisan test --compact --filter='GoogleLogin\|TwoFactorLogin\|ContactAccess\|PressReleasePreview\|NewsletterCampaign\|ManualInvoice'` | Exit 1 vor Teststart: Docker/Podman nicht verfügbar |
| `vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress` | Exit 1 vor Analyse: Docker/Podman nicht verfügbar |
| `vendor/bin/sail composer audit --locked --format=json` | Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar |
| `vendor/bin/sail npm audit --json` | Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar |
| Frontend-Build / Browserlauf | Nicht durchgeführt; kein erfolgreicher Laufzeit-/Darstellungsnachweis |
| Formatter / Migration / Versand / Abrechnung | Nicht ausgeführt; kein Anwendungscode verändert |
**Interpretation:** Die Umgebung verhindert dynamische Validierung, nicht die oben beschriebenen statischen Befunde. Umgekehrt beweisen ein großer Testbestand, vorhandene CI-Dateien und installierte Pakete keine aktuelle fehlerfreie Ausführung. Weder „alle Tests grün“ noch „keine Sicherheitslücken in Abhängigkeiten“ wäre auf Basis dieser Sitzung eine zulässige Aussage.
## 14. Quellen und Nachvollziehbarkeit
### 14.1 Primäre Projektquellen
- Routen: `bootstrap/app.php`, `routes/domains.php`, `routes/auth.php`, `routes/admin.php`, `routes/customer.php`, `routes/api.php`, `routes/legacy-api.php`, `routes/web.php`, `routes/legal.php`, `routes/console.php`.
- Zugriffsgrenzen: `app/Authorization/Permission.php`, `app/Policies/*`, Auth-/Panel-/API-Middleware, Auth-Services und Login-/Verifizierungslistener.
- Geschäftslogik: `app/Services/PressRelease/*`, `app/Services/Billing/*`, `app/Services/Newsletter/*`, `app/Services/Import/*`, Jobs und zugehörige Commands.
- Oberfläche: kritische Volt-Komponenten unter `resources/views/livewire`, öffentliche Layouts/Komponenten, Vite-Konfiguration, JavaScript und CSS-Einstiegspunkte.
- Datenmodell: betreffende Eloquent-Modelle, Migrationsdateien und lesend abgefragte MySQL-Metadaten.
- Entwicklungsqualität: `composer.json`, `package.json`, Lockdateien als Bestand, `phpunit.xml`, `phpstan.neon`, `phpstan-baseline.neon`, `tests/`, `.github/workflows/`, Git-Historie.
- Projektentwicklung: `dev/README.md`, `dev/roadmap.md`, `dev/2026-09-01/`, `dev/betrieb/deploy-runbook.md`, `docs/README.md` und einschlägige Statusreferenzen.
Datei-/Zeilenverweise gelten für den oben genannten Commit. Historische Kommentare und frühere Auditnummern im Code wurden nicht ungeprüft als aktuelle Tatsachen übernommen.
### 14.2 Dokumentation zur Gegenprüfung
- [Livewire 4: Security](https://livewire.laravel.com/docs/4.x/security) — öffentliche Actions/Properties und serverseitige Autorisierung.
- [Livewire 4: Locked](https://livewire.laravel.com/docs/4.x/attribute-locked) — Schutz von Properties; ersetzt keine fachliche Berechtigungsprüfung.
- [Laravel 12: Authorization](https://laravel.com/docs/12.x/authorization) — Gates, Policies und `before`-Prüfungen.
- [Laravel 12: Fortify](https://laravel.com/docs/12.x/fortify) — Loginpipeline, 2FA und Passwortbestätigung.
- [Laravel 12: Queues](https://laravel.com/docs/12.x/queues) — Worker-Timeouts und `retry_after`.
- [Laravel 12: Database Testing](https://laravel.com/docs/12.x/database-testing) — Testisolation und Datenbanktests.
Die Boost-Suche lieferte bei einer Fortify-Abfrage einen als `latest` bezeichneten Auszug aus dem 13.x-Dokumentationszweig. Dieser wurde nicht als Beleg einer Laravel-12-spezifischen API übernommen; entscheidend waren für F01/F03 die tatsächlich installierten Fortify-Routen und Actions. Entsprechend wurden auch Pagination und Chunking direkt am installierten Laravel-Code gegengeprüft.
## 15. Freigabebild nach diesem Audit
**Anwendungscode unverändert. Optimierungen wurden ausschließlich beschrieben.**
Für eine belastbare technische Freigabe fehlen die Korrektur bzw. explizite Bearbeitung der P1-Befunde und die anschließend erfolgreichen Regressionen in einer ausführbaren Umgebung. Die größte Verbesserung ist nicht zusätzliche Funktionsmenge, sondern ein einheitlicher Vertrag pro fachlicher Operation: eine Identitätsgrundlage, eine Berechtigungsentscheidung, eine Inhaltsversion, eine Löschsemantik und ein wiederholbarer Hintergrundprozess.
Die vorhandenen zentralen Services, Datenbankstrukturen und Tests bieten dafür eine gute Ausgangsbasis. Der empfohlene Weg ist eine Reihe kleiner, fachlich abgegrenzter Änderungen mit jeweils konkretem Fehlernachweis und Regressionstest.