L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job

- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt,
  Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status
- /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung),
  Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot,
  Enumeration-Schutz, Slug-Kollisionen
- Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern,
  umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit)
- Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi),
  Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil
- Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand
- Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding
- Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 19:01:47 +02:00
co-authored by Claude Opus 5
parent 6b8cdf543b
commit d9290a187c
79 changed files with 4576 additions and 19 deletions
+6
View File
@@ -96,3 +96,9 @@ Die Pfade sind relativ zu `/api/v1`. „Recht“ nennt das Gate der Route. Mit
| GET | `/planning/recommendations` | work_orders | `work_order:assign` | Einsatz-Empfehlungen (Luftlinie + freie Kapazität, Top 5) + nahe ungeplante Aufträge; nur Vorschläge |
| GET | `/planning/live` | work_orders | Service: `work_order:read_all` oder Teamleiter | Live-Lage: Status aus aktiver WorkSession, Standort = Objekt des Auftrags, keine Geräte-Koordinaten |
| GET | `/openapi.json` | – | angemeldet | OpenAPI-3.1-Dokument |
## Testphase: Nur-Lesen nach Ablauf (L15)
Für abgelaufene Testmandanten beantworten alle nicht lesenden Anfragen (POST/PUT/PATCH/DELETE, inkl.
`/sync` und `/uploads`) mit **422** `{ "error": { "code": "blocked", "message": "trial_expired", "details": { "readOnly": true, "message": "…", "deletionDueAt": "…" } } }`.
GET-Anfragen, Datei-Downloads und PDFs bleiben erlaubt. Siehe [TESTPHASE.md](TESTPHASE.md).
+9
View File
@@ -208,6 +208,15 @@ Metadaten (Art, Modell, Tokens, Zeitpunkt, Bezug) bleiben für Kosten- und Nachv
je Mandant wird ein Audit-Eintrag `ai_generation_retention` geschrieben. Die Frist mit dem DSB abstimmen.
Die Variable muss im `craftvia-worker` gesetzt sein.
### 7.4 Testphase (L15)
Öffentliche Selbstanmeldung unter `/testen`, Plattform-Wizard unter `/admin/trial`. Variablen
`TRIAL_MAX_DAYS` (Default 30) und `TRIAL_CONTACT_EMAIL`. Der `craftvia-worker` legt beim Start den
täglichen Job-Scheduler `trial-lifecycle-daily` an (Erinnerungen, Ablauf, Löschung nach 30 Tagen) und
verarbeitet `tenant-export` (Datenexport). Details, Schreibsperre und manueller Lauf:
[TESTPHASE.md](TESTPHASE.md). Vor dem Go-live die Platzhaltertexte für Nutzungsbedingungen und
Datenschutz (`messages/*/trial.json` → `legal.*`) ersetzen.
## 8. Rate Limits
| Bereich | Env | Default | Zählung |
+67
View File
@@ -0,0 +1,67 @@
# Craftvia – Testphase & Onboarding (Betrieb)
Stand: 2026-09-16 · Lane L15 · Lane-Bericht: [lanes/testphase.md](lanes/testphase.md)
Craftvia lässt sich als zeitlich begrenzte Testversion anbieten – per öffentlicher Selbstanmeldung
(`/testen`) oder durch den Plattform-Admin (`/admin/trial`).
## 1. Lebenszyklus
| Phase | Zustand | Was gilt |
|---|---|---|
| Anmeldung | `TrialSignup.status = pending` | Nichts provisioniert. Bestätigungslink 24 h gültig, einmal verwendbar. |
| Testphase | `Tenant.plan = TRIAL`, `now < trialEndsAt` | Voll nutzbar. Banner „Testphase endet in X Tagen“ ab 7 Tagen vor Ende. |
| Abgelaufen | `now ≥ trialEndsAt` | **Nur lesen + Export.** Login, Lesen, Datei-Downloads, PDFs, Export bleiben möglich. |
| Löschung | `now ≥ deletionDueAt` (Standard: Ende + 30 Tage) | Täglicher Job löscht den Mandanten (Offboarding aller Mandanten-Tabellen + Speicher `<tenantId>/`), Mandant `ARCHIVED`. |
| Umgewandelt | `plan = FULL`, `convertedAt` | Nie Nur-Lesen, nie automatische Löschung. |
`trialEndsAt` ist das **exklusive** Ende: Beginn des Folgetags des gewählten Datums in Europe/Berlin.
Die Sperre hängt nur an diesem Zeitpunkt, nicht am Job – sie greift sekundengenau.
Plattform-Aktionen (Mandantendetail, Karte „Testphase“, jeweils mit Bestätigung und Plattform-Audit):
Enddatum ändern/verlängern (auch nach Ablauf → sofort wieder schreibbar, Erinnerungen neu geplant),
in Vollversion umwandeln, Testphase sofort beenden, Löschung vormerken (Ende + 30 Tage, frühestens in
7 Tagen) bzw. abbrechen. Nur Plattform-**Voll**-Admins; Mandanten-Admins haben keinen Weg dorthin.
## 2. Konfiguration
| Variable | Default | Bedeutung |
|---|---|---|
| `TRIAL_MAX_DAYS` | `30` | Längste selbst gewählte Testphase (1–365). Vorbelegung im Wizard: heute + 14 (höchstens `TRIAL_MAX_DAYS`). Der Plattform-Admin darf bis 365 Tage setzen. |
| `TRIAL_CONTACT_EMAIL` | leer | Kontakt in Banner und Testphasen-Mails („Vollversion oder Verlängerung: …“). Leer = allgemeiner Hinweis. |
Weitere Voraussetzungen: `APP_BASE_URL` (Links in Mails), funktionierender Mailversand (Double-Opt-in),
S3/Garage (Export-Dateien), `REDIS_URL` + `craftvia-worker` (Jobs). Rate-Limits der öffentlichen
Endpunkte sind fest (je App-Instanz, siehe `src/server/rate-limit.ts`): Anmeldung 5/h je IP und je
E-Mail, Bestätigung 20/15 min je IP, Schrittprüfung 120/15 min je IP.
## 3. Jobs (`npm run worker:craftvia`)
| Queue | Auslöser | Inhalt |
|---|---|---|
| `trial-lifecycle` | Job-Scheduler `trial-lifecycle-daily` (alle 24 h, beim Worker-Start angelegt) | Erinnerungen 7/3/1 Tage vorher, Ablaufmail, Löschhinweis 7 Tage vor Löschung, Löschung, Aufräumen alter Anmeldungen (7 Tage nach Link-Ablauf). Idempotent: jede Mail wird vor dem Versand über eine bedingte Aktualisierung „beansprucht“. |
| `tenant-export` | „Export erstellen“ unter `/settings/export` | ZIP (CSV + JSON + Dateien) nach `<tenantId>/uploads/…`, Download 7 Tage über `/settings/export/<id>` (Sitzung + `tenant:manage`). Ohne Redis läuft der Export inline. |
Manueller Lauf (z. B. nach Ausfall des Workers), im App- oder Worker-Container:
```bash
node --import tsx -e "import('./src/server/services/trial/lifecycle.ts').then(m => m.runTrialLifecycle()).then(console.log)"
```
Im Worker werden Jobs, die im Namen von Nutzern schreiben (`import-extraction`, `transcription`), für
abgelaufene Testmandanten übersprungen. PDFs/Vorschaubilder bereits gespeicherter Daten laufen weiter.
## 4. Schreibsperre – wo sie greift
- Server Actions der Module: `moduleGuard` (`src/server/action-guard.ts`) → `ServiceError("blocked", "trial_expired")`.
- `/api/v1`: `requireApiContext` sperrt jede nicht lesende Anfrage, die über `withApi` läuft (Sync, Uploads, alle Mutationen) → HTTP 422 `{ error: { code: "blocked", message: "trial_expired", details: { readOnly: true, message, deletionDueAt } } }`. Die Offline-Outbox behandelt 422 beim Sync-Batch als vorübergehend und wiederholt später.
- Routen außerhalb von `withApi`: Backoffice-Upload `/documents/upload`, `POST /api/v1/work-orders/[id]/documents` (explizit).
- Einstellungen, Nutzer-/Rollenverwaltung, Lotse-Einstellungen (EXEMPT-Actions, explizit).
- Nicht gesperrt: Login, Konto (eigenes Passwort, MFA, Sprache), Mandantenwechsel, Lesen, `/files/<id>`, Export, Plattform-Aktionen.
## 5. Datenschutz
- Anmeldung speichert Passwort nur als Argon2id-Hash (mit Pepper), Link-Token nur als SHA-256, IP nur als HMAC. Der Passwort-Hash wird nach Bestätigung/Ablauf aus der Anmeldung entfernt; Anmeldungen werden 7 Tage nach Link-Ablauf gelöscht.
- Enumeration-Schutz: Für bereits registrierte Adressen gleiche Antwort, aber Hinweis-Mail statt Link.
- Nutzungsbedingungen und Datenschutzhinweise unter `/testen/nutzungsbedingungen` bzw. `/testen/datenschutz` sind **Platzhalter** (Texte in `messages/<locale>/trial.json` → `legal.*`) und vor dem Go-live vom Betreiber zu ersetzen.
- Löschung über das bestehende DSGVO-Offboarding (Löschnachweis `DeletionCertificate`), zusätzlich Objektspeicher-Präfix `<tenantId>/`.