# Mitglieder-System (Accounts + Content-Gating) — Design-Spec

**Datum:** 2026-07-02
**Branch:** `feat/wissen-subdomain`
**Status:** Freigegeben (User: „passt, leg los")

---

## Ziel

Ein Konto-/Login-System für kotsch.tech, das zwei Inhaltsbereiche hinter eine Anmeldung stellt:

1. **Wissen-Artikel** (`wissen.kotsch.tech`) — ohne Login nur das **erste Drittel** lesbar, danach Login-Wand.
2. **Webtools** — die **309 stärksten Tools** (13 Profi-Kategorien) nur mit Login nutzbar; die restlichen ~341 Alltags-Tools bleiben frei.

Nicht angemeldete Besucher sehen also überall nur den Anfang und müssen für den vollen Zugang ein Konto anlegen (E-Mail-Bestätigung Pflicht).

---

## Leitentscheidungen (vom User bestätigt)

| Frage | Entscheidung |
|-------|--------------|
| Login-Art | **Beides**: Passwort **und** passwortlos (Code per Mail) |
| Wissen-Vorschau | **Erstes Drittel** des Artikels frei |
| Webtools-Modell | **309 gesperrt, ~341 frei** (kein Tageslimit) |
| Tool-Auswahl | **Kategorie-basiert**: 13 Profi-Kategorien gesperrt |

**Gesperrte Kategorien (~309):** Developer (75), Finanzen (76), Business (31), Marketing (27), KI (23), Steuer (19), SEO (17), Design (10), Entwickler (9), Security (7), Recht (7), Converter (5), Social Media (3).

**Freie Kategorien (~341):** Gesundheit, Mathematik, Alltag, Haushalt, Energie, Text, Haus & Garten, Auto, Reise, Küche, Sport, Nachhaltigkeit, Küche & Alltag, Arbeit, Wetter, Haustiere, Schule, Immobilien, Hosting.

---

## Architektur-Überblick

```
                    ┌─────────────────────────┐
                    │   Konto-System (Auth)   │  Phase 1 — Fundament
                    │  users, Login, Verify,  │
                    │  Passwort-Reset, /konto │
                    └───────────┬─────────────┘
                        ┌───────┴────────┐
                        ▼                ▼
              ┌──────────────┐   ┌──────────────────┐
              │ Wissen-Gate  │   │  Webtools-Gate   │
              │ Phase 2      │   │  Phase 3         │
              │ 1/3 frei     │   │ 309 gesperrt     │
              └──────────────┘   └──────────────────┘
```

**Zwei getrennte, koexistierende Systeme:**

- **`leads`** (bestehend, unverändert) — Newsletter-/Magnet-Trichter für `/gratis`. Nur E-Mail, kein Login, stateless HMAC-Cookie `kt_sub`.
- **`users`** (neu) — echte Konten mit Laravel-Standard-Auth (Session-Guard). Schaltet Wissen + Webtools frei.

**Gate-Prüfung überall:** `Auth::check() && Auth::user()->hasVerifiedEmail()`. Ein verifizierter, eingeloggter User erfüllt alle Gates (inkl. der bestehenden `/gratis`-Wand — kleine Brücke in `LeadGate`).

---

## Phase 1 — Konto-System (Fundament)

### Datenmodell

**`users`-Tabelle** (Laravel-Basis + Anpassungen):

| Feld | Typ | Zweck |
|------|-----|-------|
| `id` | bigint PK | |
| `name` | string, nullable | optional |
| `email` | string, unique | Login-Kennung |
| `email_verified_at` | timestamp, nullable | Zugang erst nach Verifizierung |
| `password` | string, nullable | **leer = nur passwortlos** |
| `remember_token` | string, nullable | „Eingeloggt bleiben" (30 Tage) |
| `consent_at` | timestamp, nullable | DSGVO |
| `consent_text` | text, nullable | Einwilligungs-Snapshot |
| `newsletter_opt_in` | bool, default false | verknüpft optional mit `leads` |
| `timestamps` | | |

**`login_codes`-Tabelle** (passwortloser Code, analog `lead_otps`):

| Feld | Zweck |
|------|-------|
| `id`, `user_id` (FK) | |
| `code_hash` | SHA-256 des 6-stelligen Codes |
| `expires_at` | 15 Minuten |
| `attempts` | max 5 |
| `consumed_at` | einmalige Verwendung |

**`password_reset_tokens`** — Laravel-Standardtabelle (via `php artisan`).

### Flows

**Registrierung** (`GET/POST /registrieren`):
- Felder: E-Mail (Pflicht), Passwort (**optional**), Name (optional), Datenschutz-Häkchen (Pflicht), Newsletter-Häkchen (optional).
- Anlegen mit `email_verified_at = null`. Verifizierungs-Mail raus (Link **und** 6-stelliger Code).
- Newsletter-Häkchen gesetzt → parallel `LeadService::subscribe()` mit Quelle `account:register`.

**E-Mail-Verifizierung:**
- **Link:** Laravel signierte URL (`GET /email/verify/{id}/{hash}`) → `email_verified_at` setzen → eingeloggt.
- **Code:** `POST /email/verify-code` (6-stellig) → verifiziert + eingeloggt. Derselbe Code-Mechanismus wie passwortloser Login.

**Login — zwei Wege** (`GET /login`):
- **Passwort:** `POST /login` mit E-Mail + Passwort. Bei nicht-verifiziert → Hinweis + neuer Verify-Link.
- **Passwortlos:** `POST /login/code-anfordern` (E-Mail) → `login_codes`-Eintrag + Mail mit Code → `POST /login/code` (Code) → eingeloggt (verifiziert die E-Mail zugleich, falls noch offen).
- „Eingeloggt bleiben"-Häkchen → Laravel `remember` (30 Tage).

**Passwort-Reset** (Laravel-Standard): `/passwort/vergessen` → Mail → `/passwort/reset/{token}`. Nutzer ohne Passwort können hier erstmalig eins setzen.

**Konto-Seite** (`GET /konto`, `auth`-Middleware):
- E-Mail anzeigen, Passwort setzen/ändern, Newsletter an/aus, **Konto löschen** (DSGVO — löscht User + login_codes; verknüpfter Lead bleibt/wird separat behandelt).

**Logout:** `POST /logout`.

### Admin

`GET /admin/users` (bestehende `admin.auth`-Middleware): Liste (E-Mail, Name, verifiziert, Registrierdatum, Newsletter), Suche/Filter, Anzahl-Stats, User löschen. Sidebar-Eintrag ergänzen.

### Bausteine Phase 1

- Migrationen: `users` (anpassen/erstellen), `login_codes`, `password_reset_tokens`.
- Model `App\Models\User` (`Authenticatable`, `MustVerifyEmail`), `App\Models\LoginCode`.
- `App\Support\Auth\LoginCodeService` (create/verify/consume, Transaktion, Null-Guards — analog `LeadService`/OTP).
- Controller: `Auth\RegisterController`, `Auth\LoginController`, `Auth\VerifyController`, `Auth\PasswordResetController`, `AccountController`, `Admin\AdminUserController`.
- Mail-Templates (Markendesign): `VerifyEmailMail`, `LoginCodeMail`, `ResetPasswordMail`.
- Views: `auth/register`, `auth/login` (Tab Passwort ⇄ Code), `auth/verify`, `auth/passwort-vergessen`, `auth/passwort-reset`, `konto/index`, `admin/users/index`.
- Routen in `routes/web.php` (Haupt-Host; deutsche Pfade).
- `config/auth.php` — Standard-`users`-Provider (schon vorhanden).

### Sicherheit / DSGVO

- Passwörter: Laravel `bcrypt`. Codes: nur SHA-256-Hash gespeichert, 15 Min, max 5 Versuche, einmalig.
- Throttling: `throttle` auf allen POST-Auth-Routen (Login, Code, Register, Reset).
- Einwilligungs-Snapshot bei Registrierung (`consent_at`/`consent_text`).
- Selbst-Löschung + Admin-Löschung (Recht auf Vergessenwerden).
- Datenschutz-Seite: Abschnitt „Benutzerkonto" (welche Daten, warum, Löschung).
- HTTPS-Pflicht (bestehende Middleware deckt das ab).

---

## Phase 2 — Wissen-Gating

**Betroffen:** nur die Artikel-Ansicht `WissenController::site($topic, $slug)`. Hub (`index`) und Themen-Listen (`topic`) bleiben **komplett öffentlich** (nur Kartenlisten).

**Logik:**
- `parseSite()` liefert den Artikel-Body bereits als Block-Struktur (Absätze/Überschriften/Listen via DOM/XPath).
- **Anonym:** nur das **erste Drittel der Block-Knoten** rendern (aufrunden, min. 1 Block), danach Blur-Fade + Login-Wand-Component. Die restlichen zwei Drittel werden **serverseitig gar nicht erst ausgegeben** → kein View-Source-/Scraper-Bypass.
- **Eingeloggt + verifiziert:** voller Body.

**PDFs** (`WissenController::pdf()`): **Voll-Gate** — Anonyme werden zur Login-Wand geleitet (ein PDF lässt sich nicht sauber dritteln). Themen-Seite zeigt PDF-Karten weiterhin an (mit Schloss-Hinweis), aber `pdf()`/`asset()` liefern nur eingeloggt aus.

**SEO:**
- Paywall-JSON-LD wie bei `/gratis`: `isAccessibleForFree: false`, `hasPart` mit `cssSelector` auf den gegateten Bereich.
- Das erste Drittel bleibt indexierbarer Teaser (Google „flexible sampling").

**Bausteine:** `<x-login-wall>`-Blade-Component (wiederverwendbar für Phase 3), Anpassung `WissenController::site()` + `pdf()`, `site.blade.php` (Drittel-Rendering + Wall), Helfer `firstThird(array $blocks): array`.

---

## Phase 3 — Webtools-Gating

**Premium-Definition — kategorie-basiert, DRY:**
- Neue `config/premium_tools.php`: Liste der 13 gesperrten Kategorien (+ optionale Slug-Overrides `force_free` / `force_premium`).
- Ein Tool ist premium, wenn `tool['cat']` in der Liste steht. **Keine 309 Config-Einträge anfassen.**
- Helfer `App\Support\Webtools\PremiumTools::isPremium(array $tool): bool`.

**Gate-Punkt:** die Webtools-Route-Closure in `routes/web.php` (`/webtools/{slug}`, rendert `tools.quick`). Vor dem View-Return: Premium-Status ermitteln, `$gated = isPremium && !Auth::check()` an den View geben.

**Rendering (`tools/quick.blade.php`):**
- **Anonym + Premium:** SEO-Kopf (Titel, Intro, `meta_description`, FAQ, Keywords) bleibt **sichtbar & indexierbar**. Statt Eingabefelder + Rechen-Button → **Login-Wand** (`<x-login-wall>`). **Kein Compute-JS** wird ausgegeben → kein Bypass über DevTools.
- **Eingeloggt oder freies Tool:** unverändert voll nutzbar.

**Kategorie-Übersichten / Header-Dropdown:** Premium-Tools bleiben gelistet (SEO + Entdeckung), bekommen ein Schloss-Icon. Klick → Tool-Seite mit Login-Wand.

**Standalone-Views** (`tools.{slug}`, hartcodierte Views ohne Config-`cat`): Premium-Status über optionale Slug-Liste in `config/premium_tools.php`. Gros der Premium-Tools sind Config-Tools; Standalone-Edge-Cases im Plan behandeln.

**Bausteine:** `config/premium_tools.php`, `PremiumTools`-Helfer, Anpassung Webtools-Route + `tools/quick.blade.php`, Schloss-Icon in Kategorie-/Dropdown-Views.

---

## Testing (pro Phase)

**Phase 1:** Registrierung (mit/ohne Passwort), Verify per Link, Verify per Code, Login Passwort (verifiziert/unverifiziert), Login passwortlos, Remember-Cookie, Passwort-Reset, Konto-Löschung, Admin-User-Liste, Throttling. Ziel: alle grün via `composer test`.

**Phase 2:** Anonym sieht nur erstes Drittel + Wall (und Rest-HTML ist **nicht** im Response), eingeloggt voller Artikel, PDF anonym → Wall, PDF eingeloggt → Auslieferung, Paywall-JSON-LD vorhanden, Hub/Themen öffentlich.

**Phase 3:** Premium-Tool anonym → Wall + kein Compute-JS, Premium-Tool eingeloggt → voll, freies Tool anonym → voll, `PremiumTools::isPremium()` je Kategorie korrekt, Schloss-Icon in Liste.

**Test-Disziplin:** immer `composer test` (config:clear vorweg), danach `config:cache` wiederherstellen. 54 vorbestehende Fehler im Suite sind unrelated.

---

## Reihenfolge & Abhängigkeiten

1. **Phase 1** zuerst (ohne Konto kein Gate). Eigener Plan, eigener Build.
2. **Phase 2** + **Phase 3** danach (unabhängig voneinander, beide hängen an Phase 1). Je eigener Plan.

Diese Spec deckt alle drei Phasen ab; die Implementierungspläne werden **pro Phase** geschrieben (Phase-2/3-Interfaces hängen an Phase-1-Ergebnissen).

---

## Angenommene Defaults (User-bestätigt)

1. PDFs in Wissen = Voll-Login (kein Drittel-Preview).
2. Name bei Registrierung optional, nur E-Mail Pflicht.
3. Newsletter = optionales Häkchen bei Registrierung (Brücke zu `leads`).
4. „Eingeloggt bleiben" = 30 Tage.
5. Eingeloggte User umgehen auch die `/gratis`-Wand.
