diff --git a/app/Mail/AuditAnfrage.php b/app/Mail/AuditAnfrage.php new file mode 100644 index 0000000..edba5b9 --- /dev/null +++ b/app/Mail/AuditAnfrage.php @@ -0,0 +1,47 @@ +anfrageEmail, $this->anfrageName)], + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'mail.audit-anfrage', + ); + } +} diff --git a/config/marketing.php b/config/marketing.php new file mode 100644 index 0000000..fe4a1e0 --- /dev/null +++ b/config/marketing.php @@ -0,0 +1,18 @@ + env('MARKETING_ANFRAGE_TO', 'info@markemacht.de'), + +]; diff --git a/dev/audit-formular-cookies/UMSETZUNGSPLAN.md b/dev/audit-formular-cookies/UMSETZUNGSPLAN.md new file mode 100644 index 0000000..f8d238a --- /dev/null +++ b/dev/audit-formular-cookies/UMSETZUNGSPLAN.md @@ -0,0 +1,164 @@ +# Umsetzungsplan — Audit-Formular (Laravel) + Cookie-/Datenschutz-Anpassung + +**Kontext:** Die statische markemacht.de wurde in die Laravel-App integriert. Zwei offene +Punkte: (1) das Anfrage-/Audit-Formular ist nach der Migration tot und muss als Laravel-Feature +wieder funktionieren, (2) Laravel setzt jetzt System-Cookies — der Hinweis „Keine Cookies" und +die Datenschutzerklärung müssen ehrlich werden. + +> Dieses Dokument ist **lebend**: Es wird nach jedem Checkpoint aktualisiert. Wer die Arbeit +> (auch an einem anderen Tag) wieder aufnimmt, liest zuerst **📍 Aktueller Stand** und macht +> beim ersten offenen Checkpoint weiter. Jeder Checkpoint ist so geschnitten, dass der Code +> danach in sich konsistent ist (Tests grün, nichts halb-fertig). + +--- + +## 📍 Aktueller Stand _(immer zuerst lesen)_ + +- **Phase:** ✅ 1 + 2 abgeschlossen. +- **Zuletzt erledigt:** **Phase 2 komplett & verifiziert** — mm-wirkung-Cookie entfernt (JS+Blade, + aus Build bestätigt), Footer „DSGVO-konform · keine Weitergabe", Datenschutz neu, 61/61 Tests grün, + Build neu (`markemacht-NNJhNDv3.js`). +- **Als Nächstes:** Phase 3 (Klarheits-Check) — eigener Plan/PR, separat. ODER offene Commit-Grenze klären. +- **Offen (Entscheidung User):** Commit-Grenze. Branch ist komplett WIP/uncommittet; die Phase-1/2- + Dateien sind NICHT separat committet, um die laufende Integration nicht zu fragmentieren. +- **Branch:** `feat/markemacht-laravel-migration` +- **Wiederaufnahme:** Diesen Block lesen → ersten offenen `[ ]`-Checkpoint suchen → dort + „Verifikation" am Ende ausführen, um den Ist-Zustand zu bestätigen, dann weiterarbeiten. + +--- + +## Gesperrte Entscheidungen (nicht mehr offen) + +- Audit-Formular wird **Livewire-SFC** `web.audit-form` (Projekt-Konvention: Single-File-Component + mit ⚡-Präfix unter `resources/views/components/web/⚡audit-form.blade.php`, **nicht** class-based). + Tag im Markup: ``. Modal-Chrome (öffnen/schließen/Fokus in + `markemacht.js`) bleibt Vanilla-JS unverändert. +- Mailversand über **Mailable** + `config/mail.php`. **Lokal: Mailpit** (Mutterschiff). Echte + SMTP-Creds kommen später in die Server-`.env` — Code liest nur aus `config('mail')`. +- Klarheits-Check ist **nicht** Teil dieses Plans (eigenes Feature/PR, Controller-basiert). +- Cookie-Strategie: nur technisch notwendige Cookies (`*_session`, `XSRF-TOKEN`). + `mm-wirkung`-Cookie wird **entfernt** → localStorage-only. **Kein Cookie-Banner** (kein Rechtsrat). +- Footer-Text statt „Keine Cookies": **„DSGVO-konform · keine Weitergabe"**. + +--- + +## Fortschritt (Überblick) + +### Phase 1 — Audit-Formular funktionsfähig (Livewire) +- [x] **CP 1.1** Mail-Fundament: `AuditAnfrage`-Mailable + Mail-View, Config/ENV geprüft +- [x] **CP 1.2** Livewire-Komponente `AuditForm` (Validierung, Honeypot, Throttle, Versand) +- [x] **CP 1.3** Modal-Markup auf `` umstellen; Livewire-Assets sicherstellen +- [x] **CP 1.4** Tests (`Mail::fake`, Validierung, Honeypot, Throttle) + Pint — 58/58 grün +- [x] **CP 1.5** Manuelle Verifikation in Mailpit (Commit: an User-Commit-Grenze gekoppelt) + +### Phase 2 — Cookies & Datenschutz ehrlich +- [x] **CP 2.1** `mm-wirkung`-Cookie entfernen (JS + Blade-Fallback) → localStorage-only +- [x] **CP 2.2** Footer-Text „Keine Cookies" → „DSGVO-konform · keine Weitergabe" +- [x] **CP 2.3** Datenschutz: Cookie-Absatz neu (technisch notwendige Cookies) + Aussage Datenweitergabe +- [x] **CP 2.4** Tests (Footer-Text, Datenschutz-Inhalt) + Pint — 61/61 grün, Build neu + +### Phase 3 — (separat, nur notiert) +- Klarheits-Check: Blade + JS + `POST /klarheits-check/auswerten` (Anthropic-Proxy), + inkl. KI-Datenschutz-Absatz. Eigener Plan/PR. + +--- + +## Phase 1 — Audit-Formular (Detail) + +**Altes Verhalten (Referenz):** `_markemacht.de/assets/php/send-anfrage.php` — POST, Felder +`name`, `email`, `message`, `privacy` (Pflicht-Checkbox), Honeypot `company`/`url`, Versand per +PHPMailer/SMTP, JSON-Antwort `{ok, message}`. Empfänger/Betreff aus `config.php`. + +### CP 1.1 — Mail-Fundament +- [ ] `php artisan make:mail AuditAnfrage --markdown=mail.audit-anfrage` (oder plain Blade-View) +- [ ] Mailable: Felder `name`, `email`, `message`; `Reply-To` = Absender; `Subject` + „Neue Anfrage über markemacht.de"; Empfänger aus `config` (z. B. `config('mail.from')` + oder neue `config('marketing.anfrage_empfaenger')`). +- [ ] Empfänger-Adresse: entscheiden, wo sie herkommt (ENV `MARKETING_ANFRAGE_TO`, Default + `info@markemacht.de`). In `config/` ablegen, nicht hardcoden. +- **Verifikation:** `php artisan test` läuft weiter grün; Mailable-Klasse existiert. +- **Wiederaufnahme-Notiz (erledigt 2026-06-18):** Mailable `App\Mail\AuditAnfrage` + (ctor: `anfrageName`, `anfrageEmail`, `anfrageMessage`; `replyTo` = Absender; Subject „Neue + Anfrage über markemacht.de"). View `resources/views/mail/audit-anfrage.blade.php`. Empfänger: + `config('marketing.anfrage_empfaenger')` ← `MARKETING_ANFRAGE_TO`, Default `info@markemacht.de` + (neue Datei `config/marketing.php`). Render + Inhalt verifiziert. + +### CP 1.2 — Livewire-Komponente +- [ ] `php artisan make:livewire Web/AuditForm` (Livewire-4-Konvention im Projekt prüfen) +- [ ] Properties + Rules: `name` required, `email` required/email, `message` required, + `privacy` accepted; Honeypot `company`/`url` (müssen leer sein). +- [ ] `submit()`: Honeypot-Check (still „erfolgreich" bei Befüllung), Validierung, + `RateLimiter` (z. B. 3 Anfragen / 10 Min pro IP), `Mail::to(...)->send(new AuditAnfrage(...))`, + Erfolg → `$this->dispatch('audit-sent')` (das hört `markemacht.js:139` bereits), + Felder leeren, Status-Property setzen. +- [ ] View `resources/views/livewire/web/audit-form.blade.php`: 1:1 die bestehenden Felder/Klassen + aus `footer.blade.php` übernehmen (Designsystem unverändert), `wire:model`, `wire:submit`, + `@error`, `wire:loading`, `aria-live`-Status. +- **Verifikation:** Komponente rendert isoliert (`Livewire::test`). +- **Wiederaufnahme-Notiz:** _(Throttle-Limits, Event-Name, Validierungsregeln dokumentieren)_ + +### CP 1.3 — Einbau ins Modal +- [ ] In `resources/views/web/partials/footer.blade.php` den `
`-Block + (Zeilen ~45–77) durch `` ersetzen. Hülle `[data-audit-form-panel]` + behalten oder in die Komponente ziehen — Modal-Chrome bleibt. +- [ ] Sicherstellen, dass Livewire-Assets auf Marketing-Seiten geladen werden (Default-Auto-Inject + prüfen; ggf. `@livewireScripts`/`@livewireStyles` ins Marketing-Layout). **Achtung:** vorher + lud die Marketing-Seite KEIN Livewire — hier genau testen (Konsole auf Fehler prüfen). +- **Verifikation:** Seite `/audit` und Footer-Modal öffnen, Formular sichtbar, keine JS-Fehler. +- **Wiederaufnahme-Notiz:** _(ob Layout angepasst werden musste)_ + +### CP 1.4 — Tests +- [ ] Feature-/Livewire-Test in `tests/Feature/Web/`: erfolgreicher Versand (`Mail::fake`, + `assertSent`), Validierungsfehler, Honeypot (kein Versand, aber „ok"), Throttle. +- [ ] `php artisan test --compact --filter=Audit` +- [ ] `vendor/bin/pint --dirty` +- **Verifikation:** alle neuen Tests grün, Gesamt-Suite grün. + +### CP 1.5 — Manuell + Commit +- [ ] Lokal echtes Absenden → Mail erscheint in **Mailpit**. +- [ ] Commit: `feat(marketing): Audit-Anfrageformular als Livewire-Komponente`. +- **Verifikation:** `git log` zeigt Commit; Arbeitsbaum sauber bzgl. Phase 1. + +--- + +## Phase 2 — Cookies & Datenschutz (Detail) + +### CP 2.1 — mm-wirkung-Cookie entfernen +- [ ] `resources/js/web/markemacht.js:28` (`document.cookie = …mm-wirkung…`) entfernen. +- [ ] `resources/views/web/layouts/marketing.blade.php:53` Cookie-Fallback im Inline-Script + entfernen (localStorage genügt, läuft vor erstem Paint). +- [ ] `npm run build` nötig (JS/Blade-Asset) — User bitten oder ausführen. +- **Verifikation:** Theme-Umschalter funktioniert weiter; im DevTools-Application-Tab kein + `mm-wirkung`-Cookie mehr, nur localStorage. + +### CP 2.2 — Footer-Text +- [ ] `resources/views/web/partials/footer.blade.php:6` „Keine Cookies" → + „DSGVO-konform · keine Weitergabe". +- **Verifikation:** Footer zeigt neuen Text. + +### CP 2.3 — Datenschutzerklärung +- [ ] `resources/views/web/datenschutz.blade.php:225-226`: „verwendet keine Cookies" ersetzen durch + Absatz **„Technisch notwendige Cookies"**: `markemacht_session` + `XSRF-TOKEN`, Zweck + (Bereitstellung/Absicherung der Formulare), Art. 6 Abs. 1 lit. f DSGVO / § 25 Abs. 2 TTDSG, + keine Einwilligung nötig, kein Tracking. +- [ ] localStorage-Absatz (228–233) bleibt korrekt → behalten. +- [ ] Aussage zur **Datenweitergabe** ergänzen/prüfen (Umami self-hosted, keine Dritten) — passt + zur Footer-Aussage und zum bestehenden Umami-Absatz (285+). +- **Verifikation:** Datenschutzseite konsistent, keine Aussage „keine Cookies" mehr. + +### CP 2.4 — Tests + Commit +- [ ] Test in `tests/Feature/Web/`: Footer enthält neuen Text / nicht mehr „Keine Cookies"; + Datenschutz enthält „Technisch notwendige Cookies". +- [ ] `php artisan test --compact` + `vendor/bin/pint --dirty` +- [ ] Commit: `feat(marketing): Cookie-Hinweis & Datenschutz an Laravel-Integration angepasst`. +- **Verifikation:** Suite grün, Commit gesetzt. + +--- + +## Changelog (Arbeitsprotokoll) + +| Datum | Checkpoint | Ergebnis / Notiz | +|---|---|---| +| 2026-06-18 | Setup | Plan angelegt, Entscheidungen gesperrt, Footer-Text = „DSGVO-konform · keine Weitergabe". | +| 2026-06-18 | Phase 1 | Mailable `AuditAnfrage` + Config `marketing.anfrage_empfaenger`, Livewire-SFC `web.audit-form`, Modal-Einbau, Tests, Mailpit-E2E. | +| 2026-06-18 | Phase 2 | mm-wirkung-Cookie raus (localStorage-only), Footer-Hinweis, Datenschutz „Technisch notwendige Cookies", Tests, Build. | diff --git a/resources/views/components/web/⚡audit-form.blade.php b/resources/views/components/web/⚡audit-form.blade.php new file mode 100644 index 0000000..58f588a --- /dev/null +++ b/resources/views/components/web/⚡audit-form.blade.php @@ -0,0 +1,140 @@ + + */ + protected function rules(): array + { + return [ + 'name' => 'required|string|max:120', + 'email' => 'required|email|max:190', + 'message' => 'required|string|max:5000', + 'privacy' => 'accepted', + ]; + } + + /** + * @return array + */ + protected function messages(): array + { + return [ + 'name.required' => 'Bitte geben Sie Ihren Namen an.', + 'email.required' => 'Bitte geben Sie Ihre E-Mail-Adresse an.', + 'email.email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.', + 'message.required' => 'Bitte beschreiben Sie kurz Ihr Anliegen.', + 'privacy.accepted' => 'Bitte akzeptieren Sie die Datenschutzerklärung.', + ]; + } + + public function submit(): void + { + // Spam-Schutz: Bots befüllen die versteckten Felder. Wir tun so, als sei alles gut. + if ($this->company !== '' || $this->url !== '') { + $this->markeAlsGesendet('Danke. Ihre Anfrage wurde aufgenommen.'); + + return; + } + + $this->validate(); + + $key = 'audit-anfrage:'.request()->ip(); + if (RateLimiter::tooManyAttempts($key, maxAttempts: 3)) { + $this->status = 'Zu viele Anfragen in kurzer Zeit. Bitte versuchen Sie es später erneut oder schreiben Sie an info@markemacht.de.'; + + return; + } + RateLimiter::hit($key, decaySeconds: 600); + + Mail::to(config('marketing.anfrage_empfaenger'))->send( + new AuditAnfrage($this->name, $this->email, $this->message), + ); + + $this->markeAlsGesendet('Danke. Ihre Anfrage wurde versendet.'); + } + + private function markeAlsGesendet(string $meldung): void + { + $this->reset(['name', 'email', 'message', 'privacy', 'company', 'url']); + $this->status = $meldung; + $this->sent = true; + + // markemacht.js lauscht auf dieses Event und schließt das Modal nach kurzer Zeit. + $this->dispatch('audit-sent'); + } +}; +?> + +
+ @if ($sent) +

{{ $status }}

+ @else + + {{-- Honeypot: außerhalb des sichtbaren Flusses, für Bots --}} + + +
+ + + @error('name') {{ $message }} @enderror +
+ +
+ + + @error('email') {{ $message }} @enderror +
+ +
+ + + @error('message') {{ $message }} @enderror +
+ + + @error('privacy') {{ $message }} @enderror + +

{{ $status }}

+ +
+ +
+ + @endif +
diff --git a/resources/views/mail/audit-anfrage.blade.php b/resources/views/mail/audit-anfrage.blade.php new file mode 100644 index 0000000..211ca1d --- /dev/null +++ b/resources/views/mail/audit-anfrage.blade.php @@ -0,0 +1,18 @@ + + + + + Neue Anfrage über markemacht.de + + +

Neue Anfrage über markemacht.de

+ +

+ Name: {{ $anfrageName }}
+ E-Mail: {{ $anfrageEmail }} +

+ +

Nachricht:

+

{{ $anfrageMessage }}

+ + diff --git a/resources/views/web/partials/footer.blade.php b/resources/views/web/partials/footer.blade.php new file mode 100644 index 0000000..1add4ed --- /dev/null +++ b/resources/views/web/partials/footer.blade.php @@ -0,0 +1,48 @@ +@php $sprite = '/web/markemacht/icons.svg'; @endphp +
+ +
+ + diff --git a/tests/Feature/Web/AuditFormTest.php b/tests/Feature/Web/AuditFormTest.php new file mode 100644 index 0000000..6c09ee4 --- /dev/null +++ b/tests/Feature/Web/AuditFormTest.php @@ -0,0 +1,92 @@ +assertOk() + ->assertSee('wire:submit', false) + ->assertSee('livewire', false); +}); + +test('leeres formular zeigt validierungsfehler und versendet nichts', function () { + Mail::fake(); + + Livewire::test('web.audit-form') + ->call('submit') + ->assertHasErrors(['name', 'email', 'message', 'privacy']); + + Mail::assertNothingSent(); +}); + +test('ungültige e-mail wird abgewiesen', function () { + Livewire::test('web.audit-form') + ->set('name', 'Max Muster') + ->set('email', 'keine-email') + ->set('message', 'Text') + ->set('privacy', true) + ->call('submit') + ->assertHasErrors(['email']); +}); + +test('gültige anfrage wird versendet und löst das schließen-event aus', function () { + Mail::fake(); + + Livewire::test('web.audit-form') + ->set('name', 'Max Muster') + ->set('email', 'max@example.com') + ->set('message', 'Wir brauchen Klarheit.') + ->set('privacy', true) + ->call('submit') + ->assertHasNoErrors() + ->assertSet('sent', true) + ->assertDispatched('audit-sent'); + + Mail::assertSent(AuditAnfrage::class, function (AuditAnfrage $mail) { + return $mail->hasTo(config('marketing.anfrage_empfaenger')) + && $mail->hasReplyTo('max@example.com'); + }); +}); + +test('honeypot-feld unterdrückt den versand, meldet aber erfolg', function () { + Mail::fake(); + + Livewire::test('web.audit-form') + ->set('name', 'Bot') + ->set('email', 'bot@example.com') + ->set('message', 'spam') + ->set('privacy', true) + ->set('company', 'ausgefuellt') + ->call('submit') + ->assertHasNoErrors() + ->assertSet('sent', true); + + Mail::assertNothingSent(); +}); + +test('zu viele anfragen werden gedrosselt', function () { + Mail::fake(); + RateLimiter::clear('audit-anfrage:127.0.0.1'); + + $sende = fn () => Livewire::test('web.audit-form') + ->set('name', 'Max Muster') + ->set('email', 'max@example.com') + ->set('message', 'Anfrage') + ->set('privacy', true) + ->call('submit'); + + // Drei erlaubte Versuche … + $sende(); + $sende(); + $sende(); + + // … der vierte wird gedrosselt: kein vierter Versand. + $sende()->assertSet('sent', false); + + Mail::assertSent(AuditAnfrage::class, 3); +});