Files
craftvia/docs/craftvia/lanes/testphase.md
T

18 KiB
Raw Blame History

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.ts setzt sieben Nutzer mit festen Rollen sowie Foto-/PDF-/Import-Pipeline voraus. Ein Testmandant startet mit genau einem Admin → services/trial/sample-data.ts ist 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; Kennung BEISPIEL-, zählt nicht für die Checkliste).
  • Provisionierung: provisionTenant upsertet per Slug. Damit zwei gleichnamige Anmeldungen nie im selben Mandanten landen, reserviert provisionTrialTenant den Slug per create (inkl. Testphasen-Feldern) und ruft erst dann provisionTenant auf.
  • 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.ts blieb 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}.ts
  • src/server/services/trial/{config,state,signup,abuse,provision,sample-data,admin,lifecycle,export,onboarding,mail,jobs,storage-purge}.ts
  • src/server/actions/trial-{signup,platform,tenant}.ts
  • src/server/jobs/processors/{trial-lifecycle,tenant-export}.ts
  • src/app/testen/{layout,page}.tsx, src/app/testen/{bestaetigen,nutzungsbedingungen,datenschutz}/page.tsx
  • src/app/(app)/settings/export/page.tsx, src/app/(app)/settings/export/[id]/route.ts, src/app/(platform)/admin/trial/page.tsx
  • src/components/trial/{signup-wizard,confirm-form,legal-placeholder,trial-banner,getting-started,trial-badge,trial-admin-card,platform-forms}.tsx
  • messages/{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

  1. Offline-Fotos nach Ablauf: /api/v1/uploads antwortet 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_expired als vorübergehend behandeln.
  2. 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.
  3. Rate-Limits je App-Instanz (In-Memory wie SEC2/L10b).
  4. Rechtstexte sind Platzhalter (messages/*/trial.json → legal.*).
  5. 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.
  6. Plattform-Einladung an bestehende Personen: wird nur verknüpft (wie createTenantUser), ohne Hinweis-Mail.
  7. 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).
  8. Worker-Sperre nur für import-extraction und transcription; Benachrichtigungs-Mails aus Ereignissen vor dem Ablauf laufen weiter.
  9. Slug eines gelöschten Testmandanten bleibt belegt (Zeile ARCHIVED mit Löschnachweis); neue Anmeldungen erhalten ein Suffix.
  10. Checkliste: „Mobile App öffnen“ nur manuell abhakbar; Checkliste nur für Mandanten, die als Testphase gestartet sind.
  11. 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