A3 des Umbaus Agentur-Attribution → User-Profil: Eager Loading der
Detailseite/Vorschau lädt user:id,agency_name; die Legacy-Relation
PressRelease::submittingAgency() (Pseudo-Firma type=agency via
owner_user_id) entfällt. Tests decken Verteiler-Fall, Namensgleichheit
und PM ohne Übermittler ab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Klick auf "Kostenpflichtig depublizieren" legte sofort einen
Pending-Kauf an; wer bei Stripe abbrach, sah danach dauerhaft "liegt
bereits ein Depublikations-Vorgang vor". Jetzt sperrt nur ein bezahlter
Vorgang — ein offener Pending-Kauf wird beim nächsten Klick
wiederverwendet (kein Duplikat), und die Detailseite zeigt das Angebot
weiterhin an.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA-Entscheidung 07.07.: Eine Buchung ist nicht rückabwickelbar — es darf
nie eine negative Proration-Rechnung oder ein Stripe-Guthaben entstehen.
- StripeCheckoutService::isDowngrade: sofortiger Wechsel nur, wenn der
Periodenpreis des Ziel-Tarifs den aktuellen erreicht (Wechsel-Rechnung
damit garantiert >= 0); sonst Downgrade
- scheduleDowngrade: Stripe Subscription Schedule stellt das Abo zum Ende
der bezahlten Periode um (proration_behavior none), hebt eine
vorgemerkte Kündigung auf und spiegelt die Vormerkung lokal
(neue Spalten scheduled_stripe_price/scheduled_change_at/
stripe_schedule_id an subscriptions)
- Sofort-Upgrade löst eine bestehende Vormerkung vorher auf (release)
- Webhook customer.subscription.updated räumt die Vormerkung nach
Vollzug ab
- Buchungs-Modal unterscheidet Upgrade (sofort, Differenz-Rechnung) und
Downgrade (Vormerkung, keine Zahlung heute); Banner + Hinweis auf der
Paket-Karte zeigen den vorgemerkten Wechsel mit Datum
- Kontingent-Sperre aus 4b7f6d3 entfernt — ohne Gutschrift gegenstandslos
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA-Befund 07.07.: Der Wechsel zurück in einen kleineren Tarif erzeugte
eine Proration-Gutschrift (negative Stripe-Rechnung) — die STR-Spiegelung
crashte an den unsigned-Spalten (Webhook-500), und der Wechsel war trotz
verbrauchtem Monatskontingent möglich.
- ProcessStripeWebhook: Belege ohne positiven Rechnungsbetrag
(Gutschrift/Null-Rechnung) werden geloggt übersprungen statt gespiegelt
- CheckoutController::swap: Wechsel gesperrt, wenn der Monatsverbrauch
das Kontingent des Ziel-Tarifs übersteigt (möglich ab Monatserstem)
- Buchungs-Modal: erklärt die Sperre inkl. Datum statt Bestätigungs-
Button; Proration-Hinweis deckt jetzt auch die Guthaben-Richtung ab
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Vorschau-Link (/pm-vorschau/{token}) renderte eine schlichte
Standalone-Seite, die dem spaeteren Erscheinungsbild nicht entsprach.
Jetzt rendert er die echte Detailseite (web.release-detail) im Theme
des Zielportals:
- Controller wendet Domain-/Theme-Konfig des Zielportals an (Sprache
der PM, Links zeigen auf die Portal-Domain), kein Hit-Zaehler
- Gelbes Vorschau-Banner mit Status und Link-Gueltigkeit; noindex per
Meta-Tag + X-Robots-Tag-Header, Canonical entfaellt bei noindex
(neue robots-Section im Web-Layout)
- Teilen-Buttons teilen den Vorschau-Link selbst (oeffentliche URL
existiert vor Veroeffentlichung noch nicht)
- PMs fuer beide Portale: ?portal=... waehlt die Ansicht, Umschalter
im Banner; im Kundenpanel je Portal ein Vorschau-Button, sonst ein
einzelner "Vorschau oeffnen"-Button (Link-Feld jetzt kopierbar)
- Alte press-release-preview-View entfernt
Co-authored-by: Cursor <cursoragent@cursor.com>
Tarif-Karten bieten bei bestehendem Abo jetzt 'Zu diesem Tarif
wechseln' (pro Intervall, aktueller Tarif deaktiviert). Der Wechsel
laeuft ueber das bekannte Bestaetigungs-Modal (T1neu-Checkbox) als
POST auf die neue checkout.swap-Route: Cashier swapAndInvoice stellt
sofort um, verrechnet anteilig und hebt eine vorgemerkte Kuendigung
auf. SCA-Faelle leiten auf die Cashier-Bezahlseite. QA-Befund 07.07.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Kundenseite /admin/me/invoices zeigte bisher nur das Legacy-Archiv.
Neue Sektion "Aktuelle Rechnungen" mit Netto/USt/Gesamt und Status;
STR-Rechnungen oeffnen die gehostete Stripe-Rechnungsansicht ueber
einen Redirect-Endpunkt (URL wird je Aufruf frisch geholt, Zugriff nur
fuer den Eigentuemer). KPIs zaehlen neu + Archiv zusammen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fortifys FailedTwoFactorLoginResponse leitet auf die Route
two-factor.login um, die bei views => false nie registriert wird
(RouteNotFoundException). Eigener Response leitet zurück auf die
Volt-Challenge-Seite und meldet den Fehler auf Deutsch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 2FA: Einrichtung verlangt jetzt die Bestätigung mit einem App-Code
(Fortify confirm-Semantik); vorher blieb two_factor_confirmed_at
leer und der Login fragte nie nach der Challenge.
- Recovery-Codes nur noch einmalig sichtbar (nach Bestätigung bzw.
Neu-Erzeugung), nicht mehr bei jedem Seitenaufruf.
- E-Mail-Änderung als Pending-Flow: Bestätigungslink geht an die NEUE
Adresse, die alte bleibt bis zum Klick aktiv — kein Aussperren durch
Tippfehler und keine Registrierungs-Notice mehr nach dem Ändern.
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>
Die Scope-Doku fuehrte den /thumbnails-Redirect als "ANGEPASST", im Code
existierte er nicht — alte Symfony-Bild-URLs (/thumbnails/..., /uploads/
...) liefen ins 404. Neuer LegacyMediaRedirectController mappt den
Original-Dateinamen (bleibt beim Medien-Import erhalten) auf die neue
Storage-URL fuer PM-Bilder und Firmenlogos; unbekannte Dateien bleiben
404. Gefunden bei der R6.1-Vorpruefung.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kunden koennen eigene veroeffentlichte PMs kostenpflichtig aus der
oeffentlichen Ansicht nehmen: Panel auf der PM-Detailseite (inkl.
T1neu-Unternehmer-Checkbox und Doppelausweis netto/brutto), Stripe-
Ad-hoc-Checkout wie Credit-Topups, Erfuellung im Webhook — die PM
wechselt auf archived + depublished_at, die oeffentliche URL zeigt
dauerhaft einen Tombstone (410 Gone, ohne Inhalte, mit ehrlichem
Drittarchive-Hinweis). Doppelbuchungs-Bremse pro PM; Status-Log mit
Quelle "depublication". Gate: ohne BILLING_DEPUBLICATION_PRICE_CENTS
bleibt der Weg deaktiviert und /melden zeigt die neutrale
Preisformulierung [EXTERN: Preis festlegen, O6].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zwei eigenstaendige Formulare unter /melden/rechtswidrig (DSA Art. 16:
URL, Name, E-Mail, Begruendung, Wahrheitsversicherung) und
/melden/kontaktdaten (DSGVO-Antrag) auf allen drei Domains. Gemeldete
URLs werden automatisch der passenden veroeffentlichten PM zugeordnet;
ohne Treffer bleibt die URL im payload. Jede Meldung erhaelt eine
Vorgangsnummer (M-{id}, analog K-{id} der Kuendigung), die meldende
Person unverzueglich eine Eingangsbestaetigung (DSA Art. 16 Abs. 4) und
das Melde-Postfach einen internen Hinweis — auch im bestehenden
Per-PM-Flow. Admin-Queue zeigt Vorgangsnummer, Name und gemeldete URL;
/melden verlinkt die neuen Formulare als ersten Weg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET kontakt/ueber-uns -> 301 auf die Ausgaben-Startseite;
kontakt.store-POST bleibt fuer das Hub-Kontakt-Modal
- Legacy-Aliases (contact/kontakt/ueber-uns) buendeln auf die Startseite
- Spoke-Footer: Kontakt als mailto (Betreiber-Fallback bis R2.1),
Ueber-uns-Link raus, Selfservice-Kuendigungslink ergaenzt
- Sitemap-Konstanten bereinigt, Tests angepasst
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>
Oeffentliche Bestaetigungsseite /de/kuendigung ohne Login: Art der
Kuendigung (Grund nur bei ausserordentlich), Zuordnungsdaten,
Beendigungszeitpunkt; Honeypot + Rate-Limit wie beim Widerruf.
Sofortige automatische Zugangsbestaetigung per E-Mail (Par. 312k
Abs. 4 BGB) + Betreiber-Mail. Kuendigungen laufen in der
Recht-&-Compliance-Queue unter den Widerrufen auf (K-Vorgaenge,
eigener Filter, Erledigt/Wieder-oeffnen, vierte KPI-Karte).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /widerruf (host-agnostisch, throttled, Honeypot, eigener
withdrawal-Error-Bag): persistiert nur Zuordnungsdaten (Name, E-Mail,
optionale Bestellnummer - KEIN Grund, darf nicht verlangt werden) und
versendet sofort die gesetzlich vorgeschriebene automatische
Eingangsbestaetigung mit Zeitstempel an den Verbraucher plus eine
interne Benachrichtigung an die Brand-Kontaktadresse des Hosts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der WebPortalContext kennt nur die beiden Spokes - Anfragen von
pressekonto.test waeren sonst bei businessportal24 gelandet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die size:0-Regeln liefen vor dem Honeypot-Zweig: ein gefuelltes Feld
produzierte einen Validierungsfehler (verraet die Falle), der stille
done()-Pfad war tot. Honeypot-Check jetzt vor der Validierung; Test
schaerft das Soll (kein Fehler, normaler Erfolgs-Redirect).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Startseiten-Datenaufbereitung ($webHomeData) in HomePageData; die Edition-Home-
Route nutzt Web\HomeController (pressekonto-Redirect + Portal->View-Mapping).
Damit ist routes/web.php von ~1080 auf ~230 Zeilen geschrumpft; alle
datengetriebenen Seiten laufen jetzt ueber schlanke Invokable Controller +
Page-Data-Services. Verhalten unveraendert, 92 Web-Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Erste Seite des Web-Routen-Umbaus: die Preise-Closure aus routes/web.php in
PreisePageData (Datenaufbereitung) + Web\PreiseController (schlanker Invokable
Controller) verschoben. Route nutzt jetzt die Controller-Klasse. Verhalten
unveraendert, Web-Tests bleiben gruen (92).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gate::define('access-admin'|'access-customer') als Single Source of Truth im
AppServiceProvider (auch via @can/Policies nutzbar). EnsureUserIsAdmin und
EnsureUserIsCustomer pruefen jetzt ueber diese Gates.
- Impersonation-Redirect aus EnsureUserIsAdmin in die eigene Middleware
BlockAdminDuringImpersonation ausgelagert (Sitzungs-Bedingung statt
Rollenfrage); laeuft vor der Zugriffspruefung und ist zusaetzlich als
persistente Livewire-Middleware registriert.
- Routen-Alias 'can:' bewusst NICHT verwendet: die Admin-Middleware ist
persistente Livewire-Middleware und braucht eine konkrete Klasse; der
parametrisierte Alias kann dort nicht stehen. EnsureApiUserIsActive bleibt
eigenstaendig (is_active-Pruefung mit getesteter JSON-Antwort, kein Gate).
- Tests: Auth/AccessGatesTest; UserImpersonationTest bleibt gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- VatResolver::normalizeCountryCode()/isEuCountry() als Single Source of Truth;
resolve(), VatIdValidationService und StripeCheckoutService nutzen jetzt die
zentrale EU-Pruefung statt eigener config-Lookups.
- User::findByEmail() buendelt die LOWER(email)-Lookups (ContactAccessService,
SocialAuthService). Bleibt bewusst case-insensitiv, da E-Mails nicht
normalisiert gespeichert werden; der indexfreundliche Direktvergleich
braucht erst eine Schreib-Normalisierung (offen, dokumentiert).
- config('billing.default_currency') ersetzt 8 verstreute 'EUR'/'eur'-Literale
(Stripe-Calls via strtolower). Vereinheitlicht nebenbei die zuvor uneinheitlich
gespeicherte SinglePurchase.currency.
- Neuer CustomerAccountProvisioner::provisionVerifiedCustomer() ist die
gemeinsame Provisioning-Methode (verifiziert + aktiv + customer-Rolle) fuer
ContactAccessService und SocialAuthService.
- Tests: UserFindByEmailTest, erweiterte VatResolverTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- LogApiUsage als terminable Middleware: der ApiUsageLog-INSERT laeuft nach
gesendeter Antwort und verlaengert nicht mehr die Request-Dauer. Startzeit
wird ueber ein Request-Attribut uebergeben (terminate() laeuft auf frischer
Instanz).
- User::openSinglePurchaseCount() memoisiert die offenen Einmalkaeufe pro
Request. Vorher wurde derselbe grantingSubmission-Count im Status-Widget
mehrfach (Quota-Total, Quota-Remaining, openSinglePurchases) plus ein
exists() in hasActiveBooking abgesetzt -> jetzt eine Query.
- CustomerCompanyContext::selectedCompany() nutzt eine einzige first()-Query
statt exists()+first(); das Verwerfen nicht (mehr) erreichbarer Auswahlen
bleibt erhalten.
- LegacySubscriptionService::cancelBundleFor() ist ein einziges Bulk-Update
statt Update-pro-Iteration (UserPaymentOption hat keine Observer/Events).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SetDomainUrl war nirgends registriert und duplizierte die URL-Forcing-
Logik (forceRootUrl/forceScheme/useAssetOrigin/config) aus
ThemeServiceProvider::boot(). Ersatzlos geloescht. Die Host->Config-
Aufloesung ist bereits zentral in DomainAssetContext::resolve gebuendelt
(ThemeMiddleware wurde in 0.4 entfernt, SetCurrentPortal nutzt seit 0.4
Portal::fromTheme).
Bewusst NICHT angefasst: CurrentPortalContext (mutable static) ->
request-scoped Binding - Tests koppeln an ::clear(), Umbau beruehrt viele
Call-Sites (eigener, riskanter Schritt fuer spaeter).
Tests: volle Suite gruen (802 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PressRelease::scopeOwnedByApiUser() (bewusst eng: nur user_id-Autorschaft,
anders als das breitere scopeAccessibleBy) ersetzt die byte-gleiche
findOwnedPressRelease-Query in PressReleaseController + ImageController.
- apiResourceRelations()-Helfer ersetzt den 5x identischen Eager-Load-Set
(company/category.translations/images) in Index/Show/Create/Update/Reclassify.
Bewusst NICHT umgesetzt: Slug-Reimplementierung (uniqueSlug) durch
HasUniqueSlug ersetzen - der API-Fallback Str::random(8) weicht vom
Trait-slugFallback() ab (Verhaltensaenderung bei leer-slugifizierten Titeln).
Tests: neuer Scope-Test; volle Suite gruen (802 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Die 4x identische Zugriffs-Closure (owner_user_id == me OR whereHas('users',
me)) in CompanyController (index/show), PressReleaseController::
findOwnedCompany und CustomerCompanyContext::accessibleCompanyQuery durch
einen Company::scopeAccessibleBy() ersetzt; das Scope-Verhalten
(withoutGlobalScopes) bleibt beim Aufrufer.
Bewusst unangetastet: User::accessibleCompanyIds() (relationenbasiert MIT
globalen Scopes - anderes Scope-Verhalten) und PressReleaseImageController
(prueft PR-Autorschaft user_id, nicht Firmenzugriff).
Tests: neuer Scope-Test; volle Suite gruen (801 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
End-to-end-Aufladung der Credit-Wallet, spiegelt den Einzel-PM-Fluss:
- Paket-Staffel mit Bonus in config/credits.php (10->10, 25->27, 50->55,
100->115 Credits; price_cents netto, Stripe Tax ergaenzt USt.)
- credit_topups (Pending->Paid) + CreditTopupService: startTopup legt den
Pending-Kauf an, fulfill() schreibt die Wallet idempotent gut (credited_at)
- StripeCheckoutService::forCreditTopup via checkoutCharge + credit_topup_id
Metadata; ProcessStripeWebhook schreibt bei checkout.session.completed gut
- Checkout-Route /admin/me/checkout/credits/{pack} mit Rechnungsadress-Gate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ProofPdfService: Veroeffentlichungsnachweis 3 Credits pauschal, einmal pro
PM (Zweitdownload kostenfrei); ProofPdfRenderer erzeugt das PDF on-demand
aus vorhandenen PM-Daten (kein externer Renderer); GET-Download-Endpoint
/admin/me/press-releases/{id}/nachweis hinter downloadProof-Policy + Kauf-Gate
- ExtraPmPurchaseService: tier-gestaffelter Nachkauf (19/15/12/10/8) aus der
Wallet; verbucht als bezahlter SinglePurchase(ExtraPm) und greift damit in
die bestehende Kontingent-/Slot-Mechanik. InsufficientCreditsException
liefert das Mini-Checkout-Signal (required/available/shortfall)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Launch-pflichtiger Compliance-Slice: öffentliche Anfrage zu einer PM speist eine
manuelle Admin-Queue (keine KI).
- Migration legal_requests + Model + Enums (Type: dsgvo/personal_rights/report,
Status: open/in_progress/resolved/rejected) + Factory.
- Öffentliches Formular /release/{slug}/rechtliches (LegalRequestController +
web/legal-request.blade.php): typ-abhängiger Hinweistext (Alpine), E-Mail bei
DSGVO/Persönlichkeitsrecht erforderlich, zwei versteckte Honeypot-Felder,
Rate-Limit + Bremse "1 offene Anfrage pro PM/Typ". Regeltexte als Entwurf mit
TODO für rechtliche Finalisierung markiert.
- Routen bewusst in eigener routes/legal.php (entkoppelt vom laufenden Web-Umbau),
host-agnostisch via domains.php eingebunden.
- Admin-Bereich "Recht & Compliance": Sidebar-Nav mit Offen-Zähler, Volt-Queue
index/show (in Bearbeitung/erledigt/abgelehnt/wieder öffnen + interne Notiz).
- Tests: je Typ, Honeypots (Dataset), Bremse, Admin-Queue + Status-Übergänge.
- Doku: Detailplan WS-3-Status + Deployment-Migrationsreihenfolge ergänzt.
Hinweis: Der "Melden"-/E&F-Button auf der PM-Detailseite (release-detail.blade.php)
wird mit dem separaten Web-Frontend-Commit verdrahtet; Ziel ist legal-request.create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RoleAwareLoginResponse gab bei wantsJson() sofort 204 zurück – VOR den
Sicherheitschecks. Ein XHR/JSON-Login eines verifiziert-inaktiven Accounts
erhielt damit eine Session ohne Logout. Checks laufen jetzt zuerst:
verifiziert-inaktiv → Logout + Session-Invalidate + 403 (JSON) bzw. Login mit
Fehler (HTML); unverifiziert → 204 (JSON) bzw. Notice (HTML); danach der
Erfolgsfall.
Tests: JSON-Login eines inaktiven Accounts (403, guest), JSON-Login eines
aktiven Users (204, authentifiziert).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Befund (Review 16.06.): Der Volt-Login machte direkt Auth::attempt() und umging
Fortifys 2FA-Pipeline (2FA-Bypass); zusätzlich existierte der Fortify-POST /login
parallel mit schwächeren Post-Login-Regeln.
Fix (Volt-nativ):
- Volt-Login prüft Credentials ohne sofortiges Login; bei aktivem 2FA wird der
Session-Vertrag login.id/login.remember gesetzt und auf eine neue Volt-
2FA-Challenge-Seite (/two-factor-challenge) geleitet, die an Fortifys
bestehenden Controller postet (TOTP + Recovery-Code).
- Gemeinsame Post-Login-Logik in App\Support\LoginRedirect (rollengerechtes
Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND Response.
- RoleAwareLoginResponse implementiert jetzt LoginResponse UND
TwoFactorLoginResponse und erzwingt einheitlich: unverifiziert → Notice,
verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist
auch der direkte Fortify-POST-Pfad gehärtet.
Tests: 2FA-Übergabe, Challenge-Guard, voller TOTP-Flow, Fortify-POST blockt
inaktive User und hält Customer aus dem Admin-Bereich.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Magic-Link-Versand im Login rate-limited (E-Mail+IP 3/h und IP-only 15/h);
verhindert Mail-Fluten und das Entwerten aktiver Links.
- Inaktive (aber verifizierte) User werden beim Passwort-Login zentral
blockiert (Auth::logout + Fehler) – sichert nur-auth/verified-Routen ab.
- Rollensicherer Login-Redirect: gemerkte intended-Admin-URLs schicken einen
Customer nicht mehr in den 403, sondern auf das rollengerechte Ziel.
- ContactAccess prüft is_active vor jeder Mutation: deaktivierte Bestands-
Accounts werden durch eine Anfrage weder verändert noch angemailt.
- Magic-Link-Verbrauch atomar (UPDATE … whereNull(consumed_at)) – Single-Use
auch bei parallelen Requests.
- Sicherheits-Doku um diese Härtungen + Captcha-Empfehlung ergänzt.
Tests: Rate-Limit, intended-Admin-URL für Customer, inaktiver Login,
ContactAccess ohne Mutation inaktiver Accounts.
Co-Authored-By: Claude Fable 5 <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>