E9: Kategorie-Zuordnung wie vorgeschlagen übernommen, Zuordnung ist
standardmäßig an (CANONICAL_ASSIGNMENT_ENABLED). Auf der Dev-Kopie
verifiziert: Kopie trägt rel=canonical auf die Erstquelle und fällt aus der
Sitemap ihres Portals.
E10: faq, hilfe, gute-pressemitteilung, die vier Transparenzseiten und die
API-Doku lagen auf allen drei Hosts mit identischem Text und je eigenem
Self-Canonical. Jetzt führt presseecho die Informationsseiten, der Hub die
API-Doku; Sitemap und llms.txt nennen nur die führende Adresse.
Rechtstexte (Impressum, Datenschutz, AGB, Richtlinien, Meldeweg,
Kündigung) bleiben bewusst je Host — jede Domain ist ein eigenes Angebot
mit eigener Anbieterkennzeichnung.
Bei der Erreichbarkeitsprüfung des Hubs gefunden und behoben:
/de/preise, /de/veroeffentlichen, /de/newsrooms und /de/suche wurden unter
pressekonto.com mit dem Branding von businessportal24 ausgeliefert (der
Portal-Kontext fällt dort darauf zurück) — jetzt 301 auf presseecho, die
Transparenz-Navigation verlinkt direkt dorthin. /de/kontakt und
/de/ueber-uns sprangen über /de auf die Startseite, jetzt in einem Schritt.
Der Self-Canonical kommt jetzt aus dem aufgerufenen Host; Portal-Inhalte,
die über den Hub laufen, behalten ihr Portal (webPortalContextApplied).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Abmeldelink in API-Benachrichtigungen zeigte auf das Konto des
Einlieferers - ein Klick des Pressekontakts haette connektar abgemeldet.
Jetzt traegt der Link die Empfaengeradresse (signiert), der Zustand liegt
als Unsubscribed in der Sperrliste. Anmeldelinks, die die Person selbst
anfordert, gehen weiter durch; harte Bounces sperren alles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die zu messende Strecke führt von einer Mail über presseecho.de per Magic Link
auf pressekonto.com und endet in einer Buchung — drei Wechsel, zwei Domains. Ein
cookiefreies Analytics-Tool verliert die Sitzung genau dort, wo die interessante
Konversion passiert. Claim, Registrierung und Buchung sind außerdem keine
Seitenaufrufe, sondern Zustandsänderungen in dieser Datenbank, bei denen die
Identität ohnehin bekannt ist. Deshalb eigene Ereignisdaten statt Umami; Umami
behält Seitenaufrufe und Suchbegriffe.
`conversion_events` hält Ereignistyp, Herkunft sowie Firmen- und Meldungsbezug
— keine Personen, keine Klartextadressen, keine IP. Der rote Faden durch den
Trichter ist die Firma; für die Frage "an welcher Stufe bricht es ab" reicht das
vollständig.
Der Kern ist die Herkunft auf dem Token (neue Spalte magic_links.source). Ohne
sie weiß die Auswertung am Ende, dass jemand gebucht hat, aber nicht, ob er aus
einer Ablehnungsmail kam. Eine Session überlebt den Domainwechsel nicht, das
Token schon. Die Mail-Links tragen zusätzlich `?src=` und `&pr=`; die
Meldungs-ID ist fälschbar, schaltet aber nichts frei und verfälscht im
schlimmsten Fall nur unsere eigene Statistik. Mit dem Magic-Link-Zugang aus 1b.2
entfällt sie.
Fünf Stufen statt gleichrangiger Punkte, jede mit ihrer Diagnose am Enum: Vier
beliebige Konversionspunkte sagen nicht, WO es hakt. Geschrieben wird an Stufe 1
(globale Middleware — die Mail-Links zeigen auf Hub, Spokes und öffentliche
Seiten, eine vergessene Route wäre eine unsichtbare Lücke), Stufe 2
(Magic-Link-Einlösung), Stufe 4 (Registrierung) und Stufe 5 (beide
Zahlungspfade, je Firma dedupliziert — eine zweite Buchung ist keine erste).
Stufe 3 ist definiert und wird ausgewertet; ihre Schreibstelle kommt mit der
Profilpflege aus 1b.3.
Für alle Schreibstellen gilt: Fehler werden geschluckt. Die Messung begleitet
einen Vorgang, sie ist nicht der Vorgang — eine kaputte Statistik darf keine
Anmeldung und erst recht keine Buchung scheitern lassen.
`conversion:funnel` zählt je Stufe die eindeutigen Firmen, nicht die Ereignisse:
Ein doppelter Klick ist kein doppelter Interessent, und nur mit derselben
Einheit auf allen Stufen ist ein Übergang überhaupt eine Quote. Ereignisse ohne
Firmenbezug stehen daneben statt im Nenner.
Aufbewahrung 395 Tage, zusammen mit der Tabelle festgelegt statt nachgetragen,
täglich im Scheduler bereinigt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ploi kann nur Pfad-Redirects, daher als App-Middleware (früh im globalen
Stack): GET/HEAD auf www.<domain> → 301 auf die Apex-Domain mit Pfad und
Query. Unsichere Methoden werden nicht umgeleitet (301 verwirft den Body)
— die beantwortet die App dank Host-Normalisierung weiterhin selbst.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- trustHosts erlaubt nur die konfigurierten Domains (plus localhost/IP
für Health-Checks) und verwirft fremde Host-Header, sodass
X-Forwarded-Host keine URL-Generierung mehr vergiften kann (H5).
- Legacy-API-Keys nur noch per Header (Bearer/X-ApiKey); die alte
Query-/Body-Übergabe wird nicht mehr zur Auth akzeptiert, erhält aber
weiterhin den 410-Migrationshinweis statt eines opaken 401 (M11).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bestehende Kunden-Integrationen der Altportale laufen nach dem Relaunch
weiter, ohne dass Kunden etwas umstellen muessen:
1. legacy:import-api-keys uebernimmt die api_keys aus den Legacy-DBs
(sf_guard_user_profile) als Sanctum-Tokens mit identischem Secret
(Sanctum loest Tokens ohne id|-Praefix per sha256-Hash auf) — nur
fuer berechtigte Nutzer (aktiver Zahlungsstatus/Bestandsschutz,
ApiAccessEligibilityService); idempotent, mit Dry-Run. Probe gegen
die lokalen Snapshots: 406 Keys wuerden uebernommen.
2. Die Auth-Middleware uebersetzt die alten Uebergabeformen (X-ApiKey-
Header, api_key-Parameter) fuer gueltige Keys in Bearer; unbekannte
Keys erhalten weiterhin 410 mit Migrationshinweis.
3. Die dokumentierten /service/...-Pfade der Alt-API sind als Aliasse
auf die v1-Controller gelegt (pressreleases inkl. Bilder, companies
lesend); nicht uebernommene Alt-Endpoints (Pressemappen/Kontakte
anlegen) antworten 410 mit Hinweis auf Kundenkonto und neue Doku.
API-Seite + OpenAPI entsprechend aktualisiert; Go-Live-Checkliste um den
Import-Schritt (nach dem Grandfathering) ergaenzt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Security-Header-Middleware (nosniff, X-Frame-Options, Referrer-/
Permissions-Policy) global, proxy-unabhaengig; CSP bewusst separat.
- Firmenlogo-Upload im Create-Formular durch den ImageService-Pfad
ersetzt (Mime-Whitelist ohne SVG, Re-Encode, UUID-Name) - vorher
rohe store()-Ablage ohne Server-Validierung (SVG/XSS moeglich).
- Rate-Limits ergaenzt: Passwort-Reset (3/E-Mail + 15/IP pro Std.),
Registrierung (10/IP pro Std.), throttle auf magic-login- und
pm-vorschau-Consume-Routen (Defense-in-Depth).
- EnsureUserIsCustomer als persistente Livewire-Middleware
(symmetrisch zum Admin-Gate, greift auch bei Folge-Requests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Konsolidierter Entwicklungsplan aus den zwei Analyse-Dokumenten plus
Umsetzung der Phase-0-Massnahmen (Korrektheit/Security, je mit Tests):
0.1 BasicAuth: timing-sicherer Vergleich (hash_equals, beide Felder ohne
Short-Circuit); Enabled-Check zur Laufzeit via config('auth.basic.*')
statt env() im Bootstrap-Closure (greift jetzt unter config:cache);
Config-Block + .env.example. Fund: auth.basic.* war nie als Config
definiert, Gate war faktisch unkonfigurierbar.
0.2 Legacy-Invoice-"latest"-Abfrage zentralisiert (LegacyInvoice::
scopeLatestFirst + User::latestLegacyInvoice); beide Consumer N+1-frei
umgestellt; unterschiedliches null-Verhalten bewusst erhalten.
0.3 Payment-Option-Eligibility vereinheitlicht (period-aware, strikt):
UserPaymentOption::scopeActiveNow/grandfatheredNow + User::
hasActivePaymentOption; hasActiveBooking/hasUnlimitedPressReleaseQuota
und ApiAccessEligibilityService darauf umgestellt; $hasGrandfathered-
Mislabel behoben.
0.4 Theme->Portal-Vokabular: totes, projektfremdes ThemeMiddleware
geloescht; Mapping in Portal::fromTheme() zentralisiert; fremden
b2in-Default im ThemeServiceProvider auf Portal-Default korrigiert.
0.5 InvoiceNumberGenerator: Erst-Insert race-sicher via insertOrIgnore
(Unique-Index auf circle existiert bereits). CreditWalletService war
bereits abgesichert (Unique-Index + createOrFirst).
Tests: 4 neue Dateien, 5 bestehende auf gueltiges grandfathered_until
angepasst. Volle Suite gruen (793 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E-Mail-Verifizierung (Entscheidung 15.06.):
- User implementiert MustVerifyEmail; Registrierung legt inaktives, rollenloses
Konto an und leitet auf die Danke-/Notice-Seite; Registered-Event versendet die
Verifizierungsmail. Bestätigter Link aktiviert das Konto + vergibt customer-Rolle
(ActivateUserAfterVerification). Backfill-Migration setzt email_verified_at für
alle Bestands-User (sonst würde die verified-Middleware ~59k aktive Legacy-User
aussperren). Seeder-User verifiziert.
Auth-Flow-Korrekturen:
- Magic-Link-Consume: rollensicherer Redirect ohne intended() (Customer landete
sonst per stale intended=/dashboard im 403-Admin-Bereich).
- Guest-Redirect (bootstrap/app.php) rollen-/verifizierungsbewusst statt fix
/dashboard – schließt die 403-Sackgasse auf /login und /register.
- Logout auf der Notice-Seite via echtes POST-Formular statt Livewire-Action
(behebt 419 beim Session-Invalidate).
- Magic-Link-Anforderung über eigenes Modal mit separater E-Mail-Eingabe.
- Unverifizierte Login-Versuche landen auf der Notice-Seite.
Sicherheitsfix Legacy-Rollen:
- UserImporter mappte Alt-Gruppe 2 (Self-Publisher) auf editor (= Admin-Zugriff).
Mapping auf customer korrigiert; Daten-Migration stuft die 65.950 fälschlichen
Legacy-Editoren auf customer herab. Echte admin/api-only bleiben unberührt.
Tests: Registration, EmailVerification, Authentication (Guest-Redirect),
MagicLinkLogin (Modal/Redirect/Regression), Legacy-Import (Gruppen-Mapping).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>