18 KiB
Lane L15 – Testphase & Onboarding (lane/testphase)
Stand: 2026-09-16 · Basis 6b8cdf5 (feature/craftvia-mvp, L1–L14 integriert) · Betriebsdoku: ../TESTPHASE.md
1. Umfang / erfüllte Punkte
| Punkt | Umsetzung |
|---|---|
| 1 Datenmodell | Migration 20260916100000_testphase: Tenant.plan (FULL/TRIAL), trialSource, trialStartedAt, trialEndsAt (exklusives Ende = Beginn des Folgetags in Europe/Berlin), convertedAt, readOnlySince, deletionDueAt, trialDeletedAt, Versandmarker trialReminder7/3/1At, trialExpiredNoticeAt, trialDeletionNoticeAt (Plattform-Daten, keine RLS). TrialSignup (Plattform-Tabelle ohne tenant_id): Firmendaten, E-Mail, Argon2id-Hash (+ Pepper, wird nach Bestätigung/Ablauf geleert), Enddatum, Beispieldaten/Module, Token-SHA-256, Ablauf 24 h, IP-HMAC, Status. TenantExport (Mandanten-Tabelle, enable_tenant_rls, beide TENANT_MODELS, requestedById in pii-fields.ts). TenantSettings.onboarding (Checkliste). |
2 Öffentlicher Wizard /testen |
5 Schritte (Betrieb · Admin-Konto · Testzeitraum · Einrichtung · Zusammenfassung), Fortschrittsanzeige, alle Werte in einem State (Zurück ohne Datenverlust), Validierung je Schritt im Browser und per Server-Action (checkTrialStepAction, gleiche Regeln aus lib/trial/signup.ts), finale Prüfung serverseitig. Branche: Auswahlliste + Freitext, Betriebsgröße optional, Passwort-Policy wie Identity, Enddatum per Datumsfeld (Vorbelegung heute + 14, erlaubt morgen … heute + TRIAL_MAX_DAYS, Anzeige „Testphase bis TT.MM.JJJJ (X Tage)“), Beispieldaten ja/nein, Module vorausgewählt (abwählbar), Pflicht-Checkboxen mit Platzhalter-Seiten /testen/nutzungsbedingungen, /testen/datenschutz. Double-Opt-in-Mail → /testen/bestaetigen zeigt die Anmeldung, Einrichten erst per POST (Mail-Scanner verbrauchen nichts) → provisionTrialTenant (tenant-admin, TRIAL, Ende des Tages Berlin, Löschung +30 Tage) → direkt angemeldet über den bestehenden login-ticket-Provider → /dashboard?welcome=1 mit „Erste Schritte“ (Firmendaten, Team, Monteur einladen, erster Auftrag, mobile App; automatisch erkannt oder abhakbar, ausblendbar). Missbrauchsschutz: Rate-Limit je IP und je E-Mail (neue Scopes trialSignup, trialConfirm, trialStepCheck), Honeypot, identische Antwort bei vorhandener E-Mail (+ Hinweis-Mail, gleicher Hash-Aufwand gegen Timing), Slug-Kollisionen (Reservierung per create, -2 … -20, Zufallssuffix), ältere Links derselben Adresse entwertet. |
| 3 Plattform-Admin | /admin/trial „Testmandant anlegen“ (Firma, Branche, Admin, Enddatum bis 1 Jahr, Beispieldaten) → Einladung über issueToken("invitation") + sendUserInvitationMail (bestehende Person: nur verknüpft). Mandantenliste: Spalte „Version“ mit Badges „Test bis …“ / „abgelaufen – nur lesen“ / „Löschung am …“ / „gelöscht“ / „Vollversion“, Filter Alle/Test/Voll. Mandantendetail: Karte „Testphase“ mit Enddatum ändern/verlängern (auch nach Ablauf), umwandeln, sofort beenden, Löschung vormerken/abbrechen – jeweils Popup mit Bestätigungs-Checkbox, nur Voll-Admins, Audit (scope platform, am Mandanten) mit before/after. |
| 4 Nur-Lesen nach Ablauf | services/trial/state.ts#assertTenantWritable → ServiceError("blocked", "trial_expired", { readOnly, message, deletionDueAt }), zeitgenau über trialEndsAt. Zentral in moduleGuard (alle Modul-Actions inkl. Lotse), requireApiContext für jede nicht lesende /api/v1-Anfrage (Methode aus withApi, AsyncLocalStorage → 422; Sync, Uploads eingeschlossen), explizit in /documents/upload, POST /api/v1/work-orders/[id]/documents (ohne withApi), Einstellungen, Nutzer-/Rollenverwaltung, Lotse-Einstellungen, Onboarding. Worker überspringt import-extraction/transcription abgelaufener Mandanten. Offen bleiben Login, Konto, Lesen, /files, PDFs, Export. Lesepfade, die moduleGuard nutzen (mobile Seitenkontexte, Import-Datei-Download), verwenden moduleGuard(key, { read: true }); der Guard-Check verbietet diesen Modus in Actions. Banner in Backoffice und mobiler App: „Testphase endet in X Tagen“ (ab 7 Tagen) bzw. „Testphase abgelaufen – nur Lesezugriff. Daten werden am … gelöscht.“ + Kontakt (TRIAL_CONTACT_EMAIL) + Export-Link (Admins). |
| 5 Export | /settings/export (tenant:manage, auch im Nur-Lesen-Zustand): Worker-Job tenant-export baut ZIP mit csv/ (Semikolon, BOM, Formel-Injektion entschärft) + json/ für Kunden, Ansprechpartner, Objekte, Teams, Nutzer (ohne Secrets), Aufträge, Checklisten, Material (Vorgabe/Verbrauch), Einsätze, Zeiten, Notizen, Berichte, Unterschriften, Fotos, Dokumente, Meilensteine, Abrechnung + dateien/ (alle gespeicherten Dateien inkl. Berichts-PDFs, Limit 500 MB) + LIESMICH.txt; Ablage über storage.put unter <tenantId>/uploads/, Download /settings/export/<id> (Session + DB-autoritatives tenant:manage, 7 Tage, Audit). Der DSGVO-/Backup-Export ist ein Betreiber-Werkzeug (alle Tabellen, JSON, Plattform-Portal) – wiederverwendet wurde sein ZIP-Writer (backup/zip.ts). |
| 6 Lebenszyklus-Job | Queue trial-lifecycle, Job-Scheduler trial-lifecycle-daily (24 h) in scheduleRecurringJobs. Erinnerungen 7/3/1 Tage (verpasster Lauf → nur die nächstliegende, ältere als erledigt markiert), Ablaufmail, Löschhinweis 7 Tage vorher, Löschung an deletionDueAt über dsgvo/deletion.ts#offboardTenant (Topologie aller TENANT_MODELS, Identities ohne Rest-Mitgliedschaft, Löschnachweis) + Objektspeicher-Präfix <tenantId>/ + Plattform-Audit. Jede Mail wird per bedingtem Update beansprucht (genau einmal, auch parallel). Doppelprüfung + Claim unmittelbar vor dem Löschen (TRIAL, nicht umgewandelt, abgelaufen, fällig → SUSPENDED, danach ARCHIVED). Anmeldungen werden 7 Tage nach Link-Ablauf gelöscht. |
| 7 i18n/Doku | Namespace trial (de/en), nav.dataExport; Mail-Vorlagen trial_confirm, trial_existing_account, trial_reminder, trial_expired, trial_deletion_notice (de/en, TRIAL_TEMPLATE_KEYS). docs/craftvia/TESTPHASE.md (Lebenszyklus, env, Jobs, Sperre, Datenschutz), Abschnitte in DEPLOY.md (7.4) und API.md, env-Beispiele. |
Entscheidungen
- Beispieldaten:
scripts/lib/demo-seed.tssetzt sieben Nutzer mit festen Rollen sowie Foto-/PDF-/Import-Pipeline voraus. Ein Testmandant startet mit genau einem Admin →services/trial/sample-data.tsist eine gekürzte Variante nach demselben Prinzip (ausschließlich echte Fachservices: 3 Kunden, 3 Objekte, „Beispielteam“, 4 Aufträge in Entwurf/Geplant/Zugewiesen/Prüfung; KennungBEISPIEL-, zählt nicht für die Checkliste). - Provisionierung:
provisionTenantupsertet per Slug. Damit zwei gleichnamige Anmeldungen nie im selben Mandanten landen, reserviertprovisionTrialTenantden Slug percreate(inkl. Testphasen-Feldern) und ruft erst dannprovisionTenantauf. - Sync im Nur-Lesen-Zustand: Der Batch wird mit 422 abgewiesen, bevor eine Operation angewendet wird; die Outbox behandelt Nicht-OK beim Batch als vorübergehend und behält die Operationen (kein Datenverlust nach Verlängerung/Umwandlung).
services/sync/apply.tsblieb unverändert. - Plattform-Wizard als eine Seite mit vier nummerierten Abschnitten (Betreiber-Werkzeug, Einladung statt Passwort).
2. Dateien
Neu
- Migration
prisma/migrations/20260916100000_testphase/ src/lib/trial/{dates,signup}.tssrc/server/services/trial/{config,state,signup,abuse,provision,sample-data,admin,lifecycle,export,onboarding,mail,jobs,storage-purge}.tssrc/server/actions/trial-{signup,platform,tenant}.tssrc/server/jobs/processors/{trial-lifecycle,tenant-export}.tssrc/app/testen/{layout,page}.tsx,src/app/testen/{bestaetigen,nutzungsbedingungen,datenschutz}/page.tsxsrc/app/(app)/settings/export/page.tsx,src/app/(app)/settings/export/[id]/route.ts,src/app/(platform)/admin/trial/page.tsxsrc/components/trial/{signup-wizard,confirm-form,legal-placeholder,trial-banner,getting-started,trial-badge,trial-admin-card,platform-forms}.tsxmessages/{de,en}/trial.json,docs/craftvia/TESTPHASE.md, dieser Bericht- Tests/Smoke:
scripts/test-testphase-{signup,readonly,lifecycle}.ts,scripts/lib/testphase-fixture.ts,scripts/smoke-testphase.ts
Fundament-/Fremdeingriffe (laut Auftrag erlaubt, jeweils minimal)
| Datei | Eingriff | Grund |
|---|---|---|
prisma/schema.prisma |
Tenant-Felder, Enum TenantPlan, TenantSettings.onboarding, Modelle TrialSignup, TenantExport |
Punkt 1 |
src/server/provision.ts |
optional admin.passwordHash, admin.mustChangePassword, modules; Rückgabe um adminUserId, identityId, identityCreated ergänzt; Identity per findUnique+create statt upsert (Verhalten für Bestandsaufrufer gleich) |
Hash aus der Anmeldung übernehmen, Modulauswahl, Einladung nur für neue Identity |
src/server/action-guard.ts |
assertTenantWritable nach Modul-Check; Option { read: true } |
zentrale Sperre; Lesepfade |
src/server/api/respond.ts |
withApi legt die Methode in AsyncLocalStorage ab (currentApiMethod, isMutatingApiRequest) |
Sperre nur für nicht lesende Anfragen |
src/server/api/context.ts |
assertApiWriteAllowed(tenantId) in requireApiContext |
zentrale API-Sperre |
src/app/(app)/documents/upload/route.ts, src/app/api/v1/work-orders/[id]/documents/route.ts |
je 1 Zeile assertTenantWritable |
Upload-Routen ohne withApi |
src/server/actions/{tenant-settings,tenant-users,lotse-settings}.ts |
je 1 Zeile Sperre (+ Klartext in tenant-users#actionError) |
Einstellungen/Nutzerverwaltung gesperrt |
src/server/services/field/page-context.ts, src/app/(field)/m/emergency/page.tsx, src/app/(app)/imports/[id]/file/route.ts |
moduleGuard(key, { read: true }) |
Lesepfade ohne Sperre (Smoke-Befund: sonst 500 auf /m) |
src/proxy.ts |
/testen in PUBLIC_PATHS |
öffentliche Anmeldung |
src/server/rate-limit.ts |
Scopes trialSignup, trialConfirm, trialStepCheck |
Missbrauchsschutz |
src/server/mail/templates.ts |
5 Vorlagen de/en, TRIAL_TEMPLATE_KEYS |
Mails |
src/server/jobs/{queues,processors/index}.ts, scripts/craftvia-worker.ts |
2 Queues + Scheduler, 2 Registry-Zeilen, Sperrprüfung vor jedem Job | Punkt 5/6 |
src/app/(app)/layout.tsx, src/app/(field)/m/layout.tsx |
<TrialBanner> |
Banner |
src/app/(app)/dashboard/page.tsx (L2) |
1 Zeile <GettingStarted> |
Checkliste im Dashboard |
src/app/(platform)/admin/page.tsx, admin/[id]/page.tsx |
Badge-Spalte, Filter, Button; Karte TrialAdminCard |
Punkt 3 |
src/lib/nav.ts, messages/{de,en}/nav.json |
Eintrag „Datenexport“ | Einzeiler |
src/server/db.ts, src/server/backup/topology.ts, src/server/dsgvo/pii-fields.ts |
TenantExport |
Pflicht bei neuer Tenant-Tabelle |
scripts/check-module-guards.ts |
3 Einträge, Kategorie PUBLIC (jede Action braucht Rate-Limit), Verbot des Lese-Modus in Actions |
Top-Level-Actions |
scripts/test-e2e-tenant-isolation.ts (L10) |
1 Fixture-Zeile TenantExport |
Test verlangt Abdeckung aller Tenant-Modelle |
.env.example, .env.prod.example, .env.coolify.example, docs/craftvia/{DEPLOY,API}.md |
Testphase-Abschnitte | Betrieb |
Neue env-Variablen: TRIAL_MAX_DAYS (Default 30), TRIAL_CONTACT_EMAIL (optional). Keine neuen npm-Abhängigkeiten.
3. Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
test-testphase-signup.ts |
76 | Grenzen (morgen … heute + 30, Vorbelegung), Pflichtfelder, Passwort-Policy, Modul-/Checkbox-Pflicht, Normalisierung, TRIAL_MAX_DAYS, Ende des Tages Berlin (Winter/Sommer/Umstellung); ungültig/Honeypot ohne Seiteneffekt; Double-Opt-in (nur Token-Hash, Argon2id, IP-HMAC, 24 h, kein Mandant vor Bestätigung, ältere Links entwertet, Ablauf → expired + Hash geleert, Einmalverwendung); Provisionierung (TRIAL, Enddatum, Löschtermin, Slug, tenant-admin, Login-Passwort = Wizard-Passwort, Modulauswahl, Beispieldaten über Fachservices, Audit); Enumeration (identische Antwort, Hinweis-Mail ohne Link, keine offene Anmeldung); Rate-Limit je IP/E-Mail inkl. Retry-After, jede öffentliche Action limitiert, Proxy; ohne Beispieldaten; Slug-Kollision -2; Mandant B liest/ändert keine Kunden von A |
test-testphase-readonly.ts |
59 | Checkliste (automatisch erkannt, Beispieldaten zählen nicht, manuell, Monteur forbidden/unsichtbar, Vollversion ohne Checkliste); Ablauf → blocked trial_expired mit Klartext; withApi+Sperre: POST/DELETE 422, GET 200, Mandant B 200; Sync-Batch 422 ohne angewendete Operation; statisch: moduleGuard, requireApiContext, alle 24 mutierenden /api/v1-Routen, Upload-Routen, Einstellungen, Nutzerverwaltung, Lotse, Worker, Lese-Modus nur in 3 Lesepfaden und nie in Actions; Jobs (Import/Transkription übersprungen, PDF/Export/Mandant B nicht); Lesen erlaubt; Export im Nur-Lesen-Zustand (ZIP, CSV mit BOM, JSON, Datei byte-identisch, keine Daten von B, keine Hashes, Formel-Injektion, paralleler Export conflict, Mandant B not_found, Monteur forbidden, Download-Ablauf, Audit); Verlängern hebt die Sperre auf; Mandant B unverändert |
test-testphase-lifecycle.ts |
67 | Erinnerungen 7/3/1 genau einmal, verpasster Lauf; Ablaufmail einmal + readOnlySince; Löschhinweis einmal; Löschung nur fällig (Tabellen leer, Identity, Speicherobjekt, Nachweis, Plattform-Audit, keine Wiederholung); Vollversion B und nicht fälliger Test B2 unberührt; Umwandeln/Verlängern/Abbrechen verhindern Löschung; Vormerken ≥ 7 Tage, folgt neuem Ende; Audit before/after je Aktion; Plattform-Rechte (Mandanten-Admin, Read-only-Admin → forbidden, Action ohne Plattform-Session abgewiesen, Datumsgrenzen, Vollversion invalid, Mandanten-Actions ohne Testphasen-Änderung); Wizard (freies Enddatum, Einladung, Passwortzwang, Token, Beispieldaten, bestehende Person ohne neue Einladung, Audit); Vorlagen de/en; Processor + Scheduler |
Gate (npm run gate) grün: prisma generate, tsc, lint (0 Fehler; 3 vorbestehende Warnungen in fremden Dateien), build inkl. Guard-Check (39 Action-Dateien), 79/79 Testskripte. Lane-DB craftvia_testphase, RLS_DATABASE_URL auf dieselbe DB. Ein Lauf mit RLS_ENFORCED=true wurde nicht durchgeführt.
HTTP-Smoke (next build && next start -p 3115, Session-Cookies ohne Passworteingabe): scripts/smoke-testphase.ts 32/32 – anonym /testen (Schritt 1 von 5), Bestätigung mit ungültigem Token, Rechtstexte, Redirects; abgelaufener Admin: Banner + Löschdatum + Export-Link, Lesen, Exportseite, GET /api/v1/customers 200, 422 trial_expired für POST /api/v1/customers, POST /api/v1/work-orders/[id]/documents, POST /documents/upload; abgelaufener Monteur: /m, /m/orders, /m/emergency mit Banner, Auftrag ohne Zuweisung 404, Bundle 200, 422 für /api/v1/sync und /api/v1/uploads, keine Exportseite; laufender Test: „Testphase endet in 3 Tagen.“, „Erste Schritte“, Willkommen; Plattform: Liste mit Badges/Filtern, Wizard, Detail mit Karte und Popups. Zusätzlich scripts/smoke-auth.ts 137/137 (keine Regression). Der erste Smoke-Lauf fand den 500 auf /m (Lese-Kontext über moduleGuard) → behoben in 273a453.
Visuell: nicht geprüft – die Navigation des Browser-Panels auf localhost wurde abgelehnt. Bitte /testen (Wizard, 375/768/1024 px), Banner und Plattform-Karte manuell ansehen.
4. Stubs / Abhängigkeiten
Keine Stubs. Genutzt: provisionTenant, issueToken/sendUserInvitationMail, login-ticket/signIn, offboardTenant + Topologie, storage/readStoredBytes, buildZip, enqueueMail, enqueueJob/Scheduler, Fachservices L1/L2 (Kunden, Objekte, Teams, Aufträge, Zuweisung).
5. Bekannte Lücken / offene Punkte
- Offline-Fotos nach Ablauf:
/api/v1/uploadsantwortet 422; die Outbox (L7) wertet 422 beim Upload als endgültig ungültig und verwirft das Blob auf dem Gerät. Sync-Operationen bleiben erhalten. Vorschlag L7:blocked/trial_expiredals vorübergehend behandeln. - Fehlertexte in fremden Formularen: Actions anderer Lanes zeigen bei der Sperre ihre eigene Fehlerdarstellung (Code
blocked/trial_expired, teils generisch); der Banner erklärt den Zustand. Eine einheitliche Klartext-Zuordnung je Lane steht aus. - Rate-Limits je App-Instanz (In-Memory wie SEC2/L10b).
- Rechtstexte sind Platzhalter (
messages/*/trial.json→legal.*). - Fallback nach Bestätigung: Scheitert die direkte Anmeldung (z. B. künftige MFA-Pflicht), leitet die Action auf
/login?trial=ready; die Login-Seite (Fundament) zeigt dazu keinen eigenen Hinweis. - Plattform-Einladung an bestehende Personen: wird nur verknüpft (wie
createTenantUser), ohne Hinweis-Mail. - Export im Speicher gebaut (Dateien bis 500 MB, kein ZIP64/Streaming); alte Export-Dateien werden nicht automatisch aus dem Speicher entfernt (nur mit dem Mandanten).
- Worker-Sperre nur für
import-extractionundtranscription; Benachrichtigungs-Mails aus Ereignissen vor dem Ablauf laufen weiter. - Slug eines gelöschten Testmandanten bleibt belegt (Zeile
ARCHIVEDmit Löschnachweis); neue Anmeldungen erhalten ein Suffix. - Checkliste: „Mobile App öffnen“ nur manuell abhakbar; Checkliste nur für Mandanten, die als Testphase gestartet sind.
- RLS-Modus (
RLS_ENFORCED=true) für die neuen Tests nicht gelaufen.
6. Screens / Routen
| Route | Zugriff | Inhalt |
|---|---|---|
/testen |
öffentlich | Wizard „Kostenlos testen“ |
/testen/bestaetigen?token= |
öffentlich | Anmeldung prüfen, „Jetzt einrichten“ (POST) → Dashboard |
/testen/nutzungsbedingungen, /testen/datenschutz |
öffentlich | Platzhalter-Rechtstexte |
/dashboard |
Backoffice | Karte „Erste Schritte“ (Admins, Testphasen-Mandanten) |
alle (app)- und /m-Seiten |
angemeldet | Testphasen-Banner |
/settings/export, /settings/export/<id> |
tenant:manage |
Datenexport anfordern/herunterladen |
/admin |
Plattform | Spalte „Version“, Filter Alle/Test/Voll, Button „Testmandant anlegen“ |
/admin/trial |
Plattform (Voll-Admin für Aktion) | Wizard „Testmandant anlegen“ |
| `/admin/?trial=extend | convert | end |