Commit graph

30 commits

Author SHA1 Message Date
c751d4e9d2 security: Andere Geräte abmelden, IP-weites Login-Limit, dauerhafte Sperrmarke, Passwortregeln, CSP im Melde-Modus
Restliche Phase 4 aus dem Sicherheitsaudit 07.10.2026:

- Sicherheitsseite: „Andere Geräte abmelden“ rotiert remember_token und
  beendet alle anderen Sessions, die aktuelle bleibt (nicht während einer
  Impersonation).
- LoginIpThrottle: 20 Fehlversuche je IP in 15 Minuten über alle Konten.
  Gezählt über das Failed-Event, damit Livewire-Login und Fortifys
  POST /login denselben Zähler teilen; Erfolg setzt ihn nicht zurück.
- Sperrmarke für Sessions in users.sessions_revoked_at_ms statt nur im
  Cache — übersteht cache:clear. Die alte Cache-Marke zählt übergangsweise
  mit und kann im nächsten Release entfallen.
- Neue und geänderte Passwörter: mindestens 12 Zeichen, in Produktion
  zusätzlich uncompromised() (HIBP, k-Anonymität). Bestehende bleiben gültig.
- Content-Security-Policy-Report-Only in Produktion; Meldungen über
  /csp-report nach storage/logs/csp-*.log, je Meldung höchstens einmal pro
  Stunde, ohne Query und tokenartige Pfadsegmente.

Migration: users.sessions_revoked_at_ms (users hat FULLTEXT, 25 MB, Sekunden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:02:49 +00:00
1c07fcb84b security: Sicherheitsaudit 07.10.2026 (S-01–S-06) behoben, Passkeys und Zweitfaktor-Pflicht für Admins
Audit: dev/sicherheit/SICHERHEITSAUDIT-2026-10-07.md, Plan und Entscheidungen:
dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:09:35 +00:00
f235156de9 web/api/ki: Soft Deletes wieder wirksam, Caches bei Firmenwechsel, KI-Ergebnis an Inhalt gebunden (Audit Block 3+4, Deploy D)
Block 3 — Sichtbarkeit, Löschung, Cache, Eingaben

F04: withoutGlobalScopes() ohne Argument entfernte neben dem PortalScope
auch den Soft-Delete-Filter. Öffentliche, Kunden-, API-, Vorschau- und
Medienpfade sowie die PressRelease-Helfer schreiben jetzt
withoutGlobalScope(PortalScope::class). Zwei Stellen, die der Audit nicht
kannte: die öffentliche Meldungsseite zeigte gelöschte Firmenkontakte mit
Name, Mail und Telefon; das Pressefach listete gelöschte Kontakte weiter
und ließ sie bearbeiten. Ein Architekturtest friert die 39 bewusst
verbliebenen Stellen (Admin, Importer, Befehle, Slug-Eindeutigkeit) ein.
Der Vorschau-Test prüfte bisher forceDelete statt delete.

F22: LOWER(email) machte auf companies (110k Zeilen) bei jedem Login einen
Vollscan; die Kollation ist ohnehin case-insensitiv. EmailQuery vergleicht
auf MySQL direkt über den Index, auf SQLite (Tests) weiter per LOWER.
Neuer Index contacts_email_idx.

F13: Firmen-Observer verwirft die öffentlichen Seiten-Caches bei
Löschung, Wiederherstellung und is_active-Wechsel auf jedem Weg; die
Newsroom-Seite prüft Sichtbarkeit vor dem Cache-Treffer; Kontakt-
Änderungen im Pressefach flushen. Der Tombstone-Test wärmte den Cache
vorher nicht und traf den Fehler nie.

F12: Gemeldete URLs werden über Host, Ausgabe und Slug aufgelöst
(ReportedUrlResolver); fremde Hosts zählen nicht, bei Doppelveröffentlichung
gewinnt die Erstquelle.

F11: per_page 1–100, status als Enum, language nur de/en, Kategorie muss
aktiv sein und zum Ziel-Portal passen. Legacy-Aliasse behalten ihren
Vertrag.

F23: Rückgabewert von put() wird geprüft; Logo erst speichern, dann
Datenbank, dann altes Logo löschen. Fehler enden als Feldfehler, nicht
als verlorenes Logo.

Block 4 — KI-Prüfung und Scheduler

F07: Klassifikation und Score tragen die Prüfsumme des Inhalts bei
Einplanung. Weicht sie beim Speichern ab, wird das Ergebnis verworfen
(ki_audits.discarded_at) und ein neuer Lauf eingeplant. Der Scheduler
veröffentlicht keine Meldung, deren jüngste Klassifikation nicht zum
aktuellen Text passt, sondern plant eine Prüfung mit Routing ein.

F18: Kontingent an den Kalendermonat gebunden (users.press_release_quota_period).
Verbrauch aus einem früheren Monat zählt null, auch ohne Reset-Cron; der
Reset ist idempotent und lässt den laufenden Monat in Ruhe.

F20: Der Publikations-Scheduler liefert bei technischen Fehlern einen
Fehler-Exit-Code, damit der Failure-Hook greift.

F17: KI-Jobs mit $timeout = 120, retry_after 150; Runbook ergänzt.

F19: Aggregation der Aufrufe unter einem Cache-Lock für alle Einstiege.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:40:48 +00:00
62202a4b13 auth: automatische Firmenrechte mit Herkunft und Entzug, Nachweis vor Sicherheitsaktionen (Audit Block 1, Deploy C)
F10: company_user.granted_via unterscheidet automatisch vergebene
Rechte (E-Mail-Sync) von manuellen und aus dem Altbestand. Beim Login
senkt der Sync automatische responsible-Rechte ohne aktuellen E-Mail-
Treffer auf member (E2) — Mitarbeiterwechsel nehmen den Schreibzugriff
mit, Lesezugriff bleibt. Das Admin-Formular markiert nur neue oder
geänderte Zeilen als manual, unveränderte behalten ihre Herkunft.

Bestandsprüfung: php artisan company-memberships:audit listet
responsible-Zuordnungen ohne E-Mail-Grundlage (Tabelle ab Go-Live, CSV
vollständig) und markiert mit --classify die erklärbaren als auto. Auf
Live nach der Migration einmal mit --classify laufen lassen; nicht
erklärbare Zeilen bleiben manual und damit geschützt.

F03 (E3): Zwei-Faktor deaktivieren, Wiederherstellungs-Codes erneuern
und die E-Mail-Änderung verlangen einen aktuellen App-Code oder einen
Wiederherstellungs-Code (der dabei verbraucht wird); ohne 2FA das
Passwort. Reine Magic-Link-Konten ohne Passwort (der Altbestand) können
die Adresse weiter ändern — die neue Adresse muss ihren Bestätigungslink
ohnehin einlösen. Fünf Fehlversuche pro Minute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:19:28 +00:00
e746deda4f technik: Statische Analyse wieder aussagekräftig machen
phpstan lief mit 390 Meldungen außerhalb der Baseline, der größte Teil davon
Falschmeldungen. Ursache: Larastan löste keinen einzigen Cast auf, weil alle
Models ihre Casts über die casts()-METHODE deklarieren (Laravel-11-Stil).
Laravel führt die erst zur Laufzeit mit $casts zusammen, Larastan instanziiert
Models aber ohne Konstruktor — $pressRelease->status galt deshalb als string
und jeder Enum-Vergleich als "immer falsch". Dafür gibt es den Schalter
parseModelCastsMethod, dessen Larastan-Default false ist.

Zweite Sammelursache waren fehlende Generics an den Relations-Methoden: Ohne
sie landete phpstan auf Illuminate\Database\Eloquent\Model und meldete jede
Modell-Eigenschaft und jeden eigenen Scope als unbekannt. Alle 102
Relationen tragen jetzt ihr @return <Related, $this>; das verbessert
nebenbei die Vervollständigung in der IDE.

Zusammen fällt die Zahl von 390 auf 169. Die neu erzeugte Baseline umfasst 166
Einträge, überwiegend Closure-Parameter, die als blankes Builder typisiert
sind, sowie zwei nachgeprüfte Falschmeldungs-Gruppen: die Concerns unter
app/Livewire werden ausschließlich aus Volt-Komponenten eingebunden, die
phpstan nicht analysiert, und Cashier typisiert findBillable() gegen den TRAIT
Billable, was phpstan nicht mit User vereinbaren kann — die Laufzeitprüfung
dort ist richtig und bleibt.

Was die Analyse an echten toten Zweigen zutage gefördert hat, ist mit
entfernt: eine doppelte Null-Prüfung im Nachweis-PDF, Prüfungen gegen
NOT-NULL-Spalten in der Rechnungsstellung und im Vorschau-Controller, eine
immer wahre Bedingung im Logo-Pfad und in RetireAgencyCompanyType, ein
überflüssiges ?? [] auf einer Symfony-Methode, die immer ein Array liefert,
sowie der unerreichbare null-Arm in publicPortalLinks(). MigrateLegacyMedia
deklariert seine Medientypen jetzt als Literal-Union, womit das match ohne
default nachweislich vollständig ist statt nur faktisch.

Damit das nicht wieder abdriftet — die Baseline stand seit Mai unverändert —
läuft die Analyse ab jetzt als eigener Schritt in der Test-Pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:07:51 +00:00
5585a26380 billing: Stripe-Belege auf Deutsch und sprechende Produktnamen
Drei Stellschrauben für die Stripe-Rechnung (Befund Test-Buchung:
englischer Beleg, Position nur 'Starter'):
- stripePreferredLocales() am User: Stripe rendert Rechnung/Quittung
  in der Kontosprache (de-DE, bei language=en englisch)
- Checkout-Session mit locale in der Kontosprache statt Auto-Erkennung
- billing:sync-stripe-plans vergibt sprechende Produktnamen inkl.
  PM-Kontingent und zieht sie bei bereits verknüpften Plänen an den
  bestehenden Stripe-Produkten nach (Preise bleiben unverändert)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:25:07 +00:00
40783192d0 security: Rechteausweitung schließen & Admin-Panel feingranular gaten (K1, H1, H2, M8)
- users:manage/roles:manage auf allen User-/Rollen-Masken erzwungen;
  is_super_admin aus $fillable entfernt und nur per autorisiertem
  forceFill setzbar; zentraler Gate::before-Super-Admin-Kurzschluss (K1/M8).
- Nicht-Super-Admins können Super-Admin-Konten nicht mehr bearbeiten (Fix#2);
  Impersonation mit Rangprüfung, keine Token-Erstellung während Impersonation (H1).
- Destruktive/verwaltende Admin-Aktionen (CRM/Billing/Compliance/Struktur)
  hängen an neuen admin-only Permissions; Editor behält reine Content-Rechte (H2).
- Kanonischer Permission-Katalog (App\Authorization\Permission) als Single
  Source of Truth für Seeder und Monitoring.

Deployment: `php artisan db:seed --class=RolesAndPermissionsSeeder` auf allen
Umgebungen ausführen (neue Permissions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:48:50 +00:00
51da374173 E-Mail-Praeferenzen: Opt-out je Mail-Typ + One-Click-Unsubscribe ohne Login
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 17:15:05 +00:00
30c407f571 Profilfeld users.agency_name für Übermittler-Attribution
Erster Schritt des Umbaus Agentur-Attribution → User-Profil
(dev/agentur-attribution/entwicklungsplan.md, A1): Die Übermittler-
Angabe wird Profil-Eigenschaft des einliefernden Accounts statt
Pseudo-Firma vom Typ „agency".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:20:32 +00:00
6eedaa474e Checkout: Profil-Rechnungsadresse als Stripe-Rechnungsempfaenger
Die lokal gepflegte Rechnungsadresse stand nur bei der Erstanlage am
Stripe-Customer; der Checkout erzwang die Neueingabe und ueberschrieb
sie — auf der Rechnung stand die getippte Adresse statt der Profildaten
(QA-Befund 07.07.). Jetzt: Customer-Sync (Name/Firma, Adresse, USt-ID)
vor jedem Checkout, stripeName()-Hook liefert Firma bzw. Rechnungsname
statt Login-Anzeigename, billing_address_collection auf 'auto' (keine
Zwangs-Neueingabe bei hinterlegter Adresse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:09:05 +00:00
9e6f775e1b 5.2: gemeinsamen Firmen-Sync-Kern als User::syncCompaniesWithRoles() gezogen
Analyse der beiden ~40/160-Zeilen-save()-Methoden (users/create vs edit): sie
teilen nur den User-Attribut-Payload und den byte-identischen Firmen-Sync-Loop
(Bearbeiten hat zusaetzlich Profil/Kontakte/volle Billing-CRUD). Ein voller
UserUpsertService waere Over-Engineering (ueberwiegend Branches) — daher nur
der wirklich gemeinsame Persistenz-Kern:

User::syncCompaniesWithRoles(array $companyIds, array $roles) baut das
Pivot-Payload (fehlende Rolle => 'member') und synct. Beide Formulare nutzen
es; im Bearbeiten kommen die linkedCompanyIds nun direkt aus $validated.

Test: UserSyncCompaniesTest (Rollen-Zuordnung, member-Default, Detach).
29 User-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:47:23 +00:00
38884a53d6 Perf-Fix: User::accessibleCompanyIds() hydratisiert keine Company-Models mehr
Auf /admin/me/press-releases wurden bei einem User mit sehr vielen Firmen
~107.000 Company-Models geladen. Ursache: accessibleCompanyIds() nutzte
das Eloquent-Relation-pluck() (ownedCompanies()->pluck() / companies()->
pluck()), das pro ID ein vollstaendiges Model hydratisiert. Die Methode
wird ueber PressRelease::scopeAccessibleBy() pro Seitenaufruf ~2x
ausgewertet (Status-Count- + Listen-Query) -> ~2x 53k Models.

Fix:
- toBase()->pluck() statt Relation-pluck(): liefert nur die ID-Werte ohne
  Hydration, wendet die Global Scopes (PortalScope) weiterhin an.
- Ergebnis pro Request memoisiert (cachedAccessibleCompanyIds), da der Scope
  die Methode mehrfach aufruft.

Messung (User 14016, 26.772 Firmen): 107.238 -> 24 hydratisierte
Company-Models auf der PM-Liste. Ergebnis-IDs unveraendert.

Regressionstests: accessibleCompanyIds liefert owned+member ohne Dubletten
und hydratisiert 0 Company-Models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:18:51 +00:00
4466858aac Optimierung 3.5: Kleinere Zentralisierungen (VAT, findByEmail, Waehrung, Provisioning)
- 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>
2026-06-19 11:14:05 +00:00
9ed429fe74 Optimierung 3.4: Performance (Logging terminable, Single-Purchase memoisiert)
- 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>
2026-06-19 11:05:07 +00:00
a82964df2b Optimierung Phase 0: Korrektheit & Security
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>
2026-06-19 08:09:02 +00:00
3da335388e WS-8: N+1 entschaerfen und PM-Zugriffsmuster konsolidieren
- User::currentPlan() und hasUnlimitedPressReleaseQuota() pro Request
  memoisiert. Beide werden je Render mehrfach abgefragt (Status-Widget auf
  jeder Seite, Buchungsseite, Quota-Methoden) und loesten bisher jeweils
  eigene subscription-/Plan-/payment-option-Queries aus. fresh() setzt den
  Cache zurueck.
- show.blade.php: getMyPR() war NICHT memoisiert und wurde pro Request bis
  zu 7x mit voller Eager-Load-Query aufgerufen -> jetzt memoisiert. Firmen-
  Kennzahlen direkt per withCount() eager geladen statt nachtraeglichem
  loadCount().
- Dupliziertes Zugriffsmuster (withoutGlobalScopes + user_id/company_id) aus
  edit/show/index in PressRelease::scopeAccessibleBy(User) zentralisiert
  (Single Source of Truth fuer Kunden-PM-Zugriff), mit Tests abgesichert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:27:39 +00:00
344aac0740 Wallet-Topup ueber Stripe (Credit-Pakete mit Bonus-Staffel)
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>
2026-06-17 14:56:23 +00:00
3e8844245d Pruefzaehler + Pruefkontingent (Decision-Update Phase-2 vorgezogen)
Eigener Pruef-Zaehler, getrennt von der Credit-Wallet (Paragraph 4.2/4.3):

- review_checks Ledger (eine Zeile je Pruefung, source free|credit,
  charged_credits), aggregiert pro Account/Monat statt pro PM
- ReviewCheckService: Tageslimit (harte Bremse, nicht freikaufbar) ->
  Monats-Freikontingent (tier-gestaffelt 4/12/30/60/120) -> Overflow
  zieht 1 Credit/Pruefung aus der Wallet
- ReviewLimitException fuer das erreichte Tageslimit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:19:37 +00:00
b63cd26326 Credit-Wallet + Ledger + Tier-Preisableitung (Fundament)
Echte Credit-Wallet (1 Credit = 1 EUR) mit append-only Ledger als Basis fuer
die Credit-Oekonomie aus dem Decision-Update (Rev. 4):

- credit_wallets (denormalisierter Saldo) + credit_transactions (Ledger,
  vorzeichenbehaftet, balance_after, polymorphe reference)
- CreditWalletService: einziger Schreibpfad, atomar mit Row-Lock,
  InsufficientCreditsException mit shortfall fuer den Mini-Checkout
- Tier-Enum (Einzel/Starter/Business/Pro/Agency) + User::currentTier()
- CreditPricingService: tier-gestaffelte Ableitung aus config/credits.php
  (Extra-PM 19/15/12/10/8, Boost 12/20/35, PDF 3, Depublish 25, Pruef-Quota)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:16:43 +00:00
068a5a4b49 WS-6: Google-Login via Laravel Socialite
- Socialite installiert; oauth_provider/oauth_provider_id an users (Migration).
- GoogleController (redirect/callback) + SocialAuthService: De-Dup über E-Mail,
  neuer User aktiv + verifiziert + customer (Verifizierung über den Google-
  Kanal), offener Selbst-Registrierer wird onboardet, deaktivierter Account wird
  NICHT reaktiviert. Abschluss über die gemeinsame LoginRedirect-Logik
  (rollengerecht, 403-sicher).
- Routen /auth/google/redirect + /auth/google/callback (guest), "Mit Google
  anmelden/registrieren"-Buttons auf Login und Register.
- config/services.php google + .env.example-Keys; Sicherheits-/Deployment-Doku
  ergänzt (Keys, Redirect-URI, Migration).

Tests: neuer User, De-Dup bestehender User, deaktivierter Account blockiert,
unverifizierter Registrierer onboardet, fehlgeschlagener Callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 10:39:19 +00:00
980763c362 WS-2: Firmen-Scope für PMs & Magic-Link-Zugang für Pressekontakte
Firmen-Scope (Fundament):
- PM-Zugriff war hart an user_id (Autor) gebunden. Jetzt additiv: Autor ODER
  Mitglied der zugeordneten Firma (Owner via owner_user_id oder company_user-
  Pivot). Geändert in PressReleasePolicy (canManage) sowie den Queries der
  Listen-, Show- und Edit-Komponenten. Helfer User::accessibleCompanyIds()/
  canAccessCompany(). Solo-Owner unverändert; Firmenmitglieder sehen/bearbeiten
  alle PMs ihrer Firma.

Magic-Link-Zugang für Pressekontakte (ContactAccessService):
- Öffentliches, enumeration-sicheres Formular (/pressekontakt-zugang) mit
  Honeypot + Rate-Limit. Eine hinterlegte Kontakt-E-Mail führt zu einem lazy
  angelegten, de-duplizierten customer-Account (aktiv, verifiziert über den
  Magic-Link-Kanal), der den Firmen seiner Kontakte als Mitglied zugeordnet
  wird. Versand über den bestehenden Login-Magic-Link (Generator + Consume
  wiederverwendet) – keine Schema-Änderung, kein paralleles System.
- Dezenter Einstiegslink von der Login-Seite (PM-Frontend-Wiring später).

Tests: PressReleaseCompanyScopeTest (3), ContactAccessTest (6, inkl. De-Dup,
Enumeration-Sicherheit, Honeypot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 08:33:12 +00:00
94cb209a9f WS-6: E-Mail-Verifizierung, Auth-Flow-Fixes & Legacy-Rollen-Sicherheitsfix
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>
2026-06-16 08:16:41 +00:00
afcca34f91 User-Panel-Restarbeiten: PM-Guard, Profil-Rework, USt-ID-Prüfung, Buchungspflicht-Adresse
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:36:18 +00:00
8f3261d0b4 Checkout: Stripe-Tax-Adressanforderung erfüllen
Stripe Tax verlangt eine gültige Kundenadresse. Beide Checkout-Sessions
erfassen jetzt die Rechnungsadresse verpflichtend und speichern sie am
Stripe-Customer (customer_update address/name = auto; Name ist Pflicht
bei aktivierter USt-ID-Abfrage). Zusätzlich liefert User::stripeAddress()
die lokale Rechnungsadresse bei der Customer-Anlage mit (Cashier-Hook).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:33:58 +00:00
c8dc99c3c8 Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
  und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
  Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
  Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
  bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
  Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
  PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
  Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
  (STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
  CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
  Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E ✅, Checkliste,
  STATUS-ABGLEICH, PROGRESS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:10:32 +00:00
d548f4b235 Phase 9D: Tarif-Datenmodell, Cashier und hybride Rechnungskreise STR-/MAN-
Tarif-Datenmodell (Decision-Update):
- plans: Starter/Business/Pro/Agency mit Monats-/Jahrespreis (Jahres =
  10 x Monat), PM-Kontingent, Tageslimit, Stripe-IDs; idempotenter Seeder
- single_purchases: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit
  Status-Lifecycle und Stripe-Checkout-Referenzen
- laravel/cashier ^16.5 installiert (freigegeben); User ist Billable,
  Cashier-Migrationen published + ausgefuehrt; lokale invoices()-Relation
  ueberschreibt bewusst die Cashier-Methode

Hybride Rechnungskreise (Entscheidung 12.06.2026):
- invoice_number_sequences + InvoiceNumberGenerator: atomare fortlaufende
  Nummern pro Kreis (STR- fuer den neuen Stripe-Shop, MAN- fuer den
  manuellen Legacy-Kreis); Alt-Archiv legacy_invoices bleibt unveraendert
- ManualInvoiceService + billing:generate-manual-invoices (Scheduler
  taeglich 04:30): prueft aktive/grandfathered user_payment_options ohne
  Stripe-Subscription auf erreichtes Periodenende, friert die
  Rechnungsadresse als Snapshot ein, stellt die MAN-Rechnung aus
  (Zahlungsziel billing.manual_due_days) und schaltet die Periode weiter;
  Konditions-Overrides via legacy_conditions, sonst Netto-Preis +
  billing.vat_rate; nicht abrechenbare Faelle werden geloggt und
  beim naechsten Lauf erneut geprueft

Submit-Gate:
- User::hasActiveBooking() prueft jetzt echt (hinter
  billing.enforce_booking): Cashier-Abo, bezahlter Einzel-/Extra-PM-Kauf
  oder laufende Legacy-Vereinbarung (MAN-Kreis)

Suite: 468 passed, 4 skipped (17 neue Billing-Tests). Pint clean.
Offen fuer 9E: Stripe-Checkout/Webhooks, STR-Spiegelung, Slot-Logik auf
Plan-Kontingent, Migration der aktiven Legacy-Zahlungen in
user_payment_options.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:15:46 +00:00
4419d9ff43 Phase 9 Block 1: Gelb-Routing Direkt-Live, Slot-Verbrauch bei Veroeffentlichung, Submit-Gate
9A — Gelb geht direkt live (Entscheidung 12.06.2026):
- routeByClassification(): Gelb durchlaeuft denselben Auto-Publish-Pfad
  wie Gruen (autoPublishApproved); nur Rot wird abgelehnt
- Scheduler publiziert faellige gelbe + gruene PMs; unklassifizierte
  bleiben als Fallback in der manuellen Queue

9B — Slot-Verbrauch bei Veroeffentlichung (Decision-Update 3.2):
- Increment aus submitForReview() entfernt; publish() und
  changeStatusFromAdmin() zaehlen idempotent beim ersten
  published-Uebergang (Pruefung ueber Status-Logs); Rot kostet nichts
- Submit-Guard: Einreichen erfordert freien Slot
  (QuotaExceededException, API 422)

9C — Submit-Gate vorbereitet (Decision-Update 5.1):
- User::hasActiveBooking()-Stub hinter config/billing.php
  (enforce_booking, Default aus); Tarif-Modul ersetzt nur den Rumpf
- Einreichungs-Modal zeigt ohne Buchung einen Buchungs-Hinweis;
  Server-Guard (BookingRequiredException), API antwortet 402
- Fix: Customer-Create legte PMs bei "Zur Pruefung senden" direkt mit
  Status review an (vorbei an Blacklist/Quota/KI/Status-Log) — laeuft
  jetzt immer ueber submitForReview()

Suite: 451 passed, 4 skipped (9 neue Tests). Pint clean.
Plan: docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md (Block 2 nach Review-Stopp).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:47:06 +00:00
a000238ca8 User Panel: Phase-8-Abschluss, Titelbild/Lizenzen/Zeitzonen und KI-Pruef-Pipeline
Phase 8 (Rest) + Umbauten vom 10./11.06.:
- Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker,
  PressReleaseCoverImage-Resolver
- Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen,
  Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise)
- Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt),
  geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE)
- Quota-Stub (users.press_release_quota) + monatlicher Reset-Command
- Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf
  filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout)

KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans):
- API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route
  durch denselben Funnel (Blacklist, Quota, Status-Log)
- Klassifikation Rot/Gelb/Gruen asynchron (Queue classification,
  OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log
- Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen ->
  Auto-Publish; Scheduler publiziert nur gruene faellige PMs
- Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl.
  Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung
- Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override

Suite: 442 passed, 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:30:13 +00:00
5b8bdf4182 12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
2026-05-12 18:32:33 +02:00
405df0a122 first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-10-20 17:53:02 +02:00