diff --git a/docs/craftvia/lanes/qualitaet.md b/docs/craftvia/lanes/qualitaet.md new file mode 100644 index 0000000..91a26be --- /dev/null +++ b/docs/craftvia/lanes/qualitaet.md @@ -0,0 +1,103 @@ +# Lane L10a – Qualität & Abnahmetests (`lane/qualitaet`) + +Stand: 2026-09-14 · Basis `a7d4b02` (`feature/craftvia-mvp`, L1–L9 integriert) · Spec §27, §34, §37–§40, §42, §43 + +## 1. Umfang / erfüllte Spec-Punkte + +| Liefergegenstand | Umsetzung | +|---|---| +| 1 Demo-Seed | `prisma/seed.ts` ruft `scripts/lib/demo-seed.ts` (nur `SEED_DEMO=true` oder außerhalb Production, `SEED_DEMO=false` schaltet ab). Alles über die echten Fachservices (Statushistorie, Audit, Berichts-Snapshots konsistent), idempotent (Kundennummern `K-D…`, externe Nummern `DEMO-…`, feste Client-IDs beim Notdienst). Mandant `demo`: Team Nord (Tina Teamleiter, Max Monteur, Nora Nordmann), Team Süd (Sven Südfeld, Paul Petersen), 7 Kunden + 1 vorläufiger (Notdienst), 12 Objekte mit Zugangshinweisen (+ vorläufiges Notdienst-Objekt), Auftragsarten + 2 Checklisten-Vorlagen, 20 Aufträge über alle Statusgruppen (Neu, geplant/kommend, heute, unterwegs, in Arbeit/pausiert/Material fehlt, überfällig, mehrtägig mit Tagesbericht, Unterschrift ausstehend, zur Prüfung, technisch abgeschlossen, zur Abrechnung, abgerechnet, Notdienst), Materialvorgaben, Zeiten/Notizen/Material/Fotos, 2 freigegebene Abschlussberichte mit Unterschrift in der Historie von „Wohnanlage Elbblick – Haus A“, 1 Import in Prüfung (Musterdokument + Fake-Extraktion). Mandant `demo2`: 1 Kunde, 1 Objekt, 1 Auftrag. Neue Seed-Nutzer: `teamleiter2@`, `monteur2@`, `monteur3@demo.example`. Alle Personen/Adressen/Telefonnummern erfunden. | +| 2 E2E-Prozesstests §43.3 | 7 Skripte `scripts/test-e2e-*.ts` (Service-Ebene, eigene zz-Mandanten, Fixture `scripts/lib/e2e-fixture.ts`) – siehe §4 | +| 3 Sicherheitstests §43.4 | 4 Skripte `scripts/test-security-*.ts` inkl. HTTP-Test gegen den echten Server; RLS-Lauf dokumentiert (§5) | +| 4 Authentifizierter Durchstich | `scripts/smoke-auth.ts` auf alle Kernseiten je Rolle (Admin, Backoffice, Teamleiter, Monteur Nord/Süd, zweiter Mandant) erweitert: IDs aus den Demo-Daten, Status + erwarteter Text + keine Fehlerseite + keine Fremddaten; `PERF=1` misst die Render-Zeit | +| 5 Performance §34.1 | `scripts/seed-load.ts` (5 000 Aufträge, idempotent, `--reset`), Messung dev + Production-Build, Query-Pläne geprüft – keine Index-Migration nötig (§6) | +| Sicherheitsbefund behoben | Migration `20260914200000_qualitaet_audit_append_only`: `REVOKE UPDATE, DELETE, TRUNCATE ON audit_logs FROM craftvia_app` (§3) | + +## 2. Dateien + +- `prisma/seed.ts` (Demo-Nutzer + Aufruf Demo-Daten, expliziter Prozess-Exit wegen offener Queue-Handles) +- `scripts/lib/demo-seed.ts`, `scripts/lib/e2e-fixture.ts` (neu) +- `scripts/seed-load.ts` (neu), `scripts/smoke-auth.ts` (erweitert) +- `scripts/test-e2e-{regular-order,multiday-order,emergency,offline-sync,guards,import-duplicates,tenant-isolation}.ts` (neu) +- `scripts/test-security-{roles,uploads,auth,http}.ts` (neu) +- `prisma/migrations/20260914200000_qualitaet_audit_append_only/migration.sql` (neu) +- `docs/craftvia/lanes/qualitaet.md` + +**Keine Änderungen an Fachcode, Fundament, `src/server/api/**`, Docker/CI.** Keine Fremd-Einzeiler, keine Stubs, keine neuen npm-Abhängigkeiten, keine Schemaänderung. + +## 3. Migration (begründet) + +`20260914200000_qualitaet_audit_append_only`: Die App schreibt Audit-Einträge nur per INSERT (`writeAuditLog`, Owner-Client) und liest sie im Viewer; kein Service/keine Action/keine Route ändert oder löscht sie (statisch geprüft in `test-security-auth.ts`). Bisher hatte die RLS-App-Rolle `craftvia_app` über `enable_tenant_rls` volle DML-Rechte auf `audit_logs` – mit `RLS_ENFORCED=true` hätte ein kompromittierter Fachpfad eigene Audit-Einträge manipulieren können (§43.4 „Audit-Log-Manipulation“). Backup/Restore und DSGVO-Löschung laufen über die Owner-Rolle und sind nicht betroffen. **Hinweis:** Ein späteres erneutes `SELECT enable_tenant_rls('audit_logs')` würde die Rechte wieder vergeben – dann die REVOKE-Zeile wiederholen. Keine TENANT_MODELS-/Topologie-Änderung. + +## 4. Tests + +`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler, 2 bestehende Warnungen außerhalb L10a), build inkl. Modul-Guard-Check, **60/60 Testskripte** (davon 11 neu). Prüfungen der neuen Skripte (✓-Zeilen im Gate-Lauf): + +| Skript | ✓ | Inhalt | +|---|---|---| +| `test-e2e-regular-order` | 57 | §38 komplett: Import (Fake-Provider) → Dubletten-/Objektkandidat → Bestätigung → Zuweisung (+ Benachrichtigung) → Bundle (Zugangshinweis, internes Dokument verborgen) → Annehmen/Losfahren/Arbeit/Pause → Material Soll/Ist + Zusatz (ohne Grund abgelehnt) → Fotos (Pflichtfoto) → Checkliste → Blocker bei laufender Zeit → Zeiten berechnet → Abschlussbericht (Vorbelegung, Pflichtangabe) → Unterschrift → Teamleiter-/Backoffice-Freigabe (Bericht danach unveränderlich) → PDF (skip ohne Chromium; lokal erzeugt, `%PDF`, Checksumme) → Abrechnung → abgerechnet → vollständige Statushistorie + Audit → Objekt-Historie (Backoffice, Teamkollege). Mandant B und Monteur ohne Zuweisung an jedem Schritt abgewiesen | +| `test-e2e-multiday-order` | 22 | 2 Einsatztage, 2 Monteure, Tagesbericht je Tag nur mit Daten seines Tages (Zeiten 480/420 min, Fotos, Notizen, Material), zweiter Tagesbericht desselben Tages → conflict, daily_report_created → in_progress am Folgetag, Abschlussbericht über beide Tage (900 min), Freigaben, Abrechnung, Berichtsliste, Historie (3 freigegebene Berichte) | +| `test-e2e-emergency` | 31 | §39: Monteur legt Notdienst mit vorläufigem Kunden an (N-Nummer, laufende Session, Event), Dokumentation, Abschluss „Kunde abwesend“ (Grund Pflicht) → Zur Prüfung, `emergency.completed` + `signature_missing` + Pflichtmail; Rollen (Monteur/Teamleiter forbidden) + Mandant B; Backoffice: Dublettenkandidat, Abrechnung blockiert (Stammdaten/Bericht), Kunde zuordnen (vorläufiger → merged, Version +1), Objekt bestätigen, Auftrag ergänzen, Freigabe 5/5, abgerechnet, Historie | +| `test-e2e-offline-sync` | 40 | 12 Ops (2 Tage alt) in einem Batch → applied inkl. idMap und Gerätezeitstempeln, Wiederholung → duplicate ohne Doppelanlage, fremde clientOpId → rejected, Pflichtfoto fehlt → rejected blocked, Konflikt (Büroänderung) → conflict ohne Überschreiben + unabhängige Notiz applied + `sync.failed` an Monteur/Backoffice + Konfliktliste, Übernehmen (als Gerätenutzer) / Verwerfen, ungültige und nicht registrierte Ops, Scope und Mandant B | +| `test-e2e-guards` | 31 | Pflichtfoto + Checklistenpunkt „mit Foto“ blockieren Abschluss, Bericht und Sync-Op; Blocker verschwinden erst mit passenden Fotos; Nicht-Bild/fremdes Foto abgewiesen. Unterschrift: ohne → signature_pending (kein Weg zur Prüfung/Abrechnung), Nachreichen → in_review, erfasste Unterschrift unveränderlich, Verweigerung/Abwesenheit nur mit Grund (Snapshot, Hinweis ans Backoffice), Bild fehlt/fremd → invalid, „später“, „nicht erforderlich“ nur ohne Pflicht | +| `test-e2e-import-duplicates` | 24 | Kandidaten über Kundennummer, Firmenname, Adresse (Str./Straße), Telefon (anderes Format), E-Mail (Groß-/Kleinschreibung); nie automatische Zuordnung/Zusammenführung; Entscheidung „neu“ protokolliert, vergebene Nummer → conflict; manuelle Anlage „Mögliche Dublette“; Zusammenführen nur mit Bestätigung + Recht; Mandant B / Monteur | +| `test-e2e-tenant-isolation` | 159 | **Systematisch:** alle 35 Modelle aus `TENANT_MODELS` (Test scheitert bei neuem Modell ohne Fixture) – aus Mandant B findMany/findFirst/findUnique/count/update/updateMany/delete/deleteMany wirkungslos, Zeile unverändert, create mit fremder tenantId landet in B; **Postgres-RLS** je Tabelle als `craftvia_app` (Kontext B sieht/ändert/löscht nichts, Kontext A sieht, ohne Kontext 0 Zeilen); **67 Fachservice-Pfade** (Kunden, Objekte, Teams, Aufträge, Einsatz, Dokumente, Berichte, Import, Notdienst, Sync, Benachrichtigungen, Audit, Lotse) → not_found bzw. invalid bei Referenzen; 15 Listen/Suche/Bundle/Dashboard ohne A-Daten; A danach unverändert | +| `test-security-roles` | 113 | Rollen-Matrix aus `rbac.ts`: 26 Permission-Proben × 4 Rollen, Erwartung unabhängig aus `ROLE_DEFS` (fehlt → forbidden bzw. not_found bei Sichtbarkeitsrechten, vorhanden → nie forbidden); Abdeckungsprüfung aller Permissions (begründete Ausnahmen: Nutzer-/Rollenverwaltung → `test-tenant-users-authz.ts`, `document:read` → HTTP-Test); Sichtbarkeit read_team/Teamleiter-Dokumente | +| `test-security-uploads` | 41 | EXE/ELF/ZIP/HTML/SVG mit harmloser Endung → type_mismatch/unsupported_type, nichts gespeichert; Polyglot nur mit erkanntem Bildtyp; Übergröße je Art (15/25 MB), leer, Import; Dateinamen (Traversal, Backslash, CR/LF, reservierte Zeichen, Länge) → normalisiert, Key im Mandantenpräfix; Einsatz-Uploads; Sichtbarkeits-Eskalation; Malware-Befund und nicht erreichbarer ClamAV → fail closed; Audit nur für gespeicherte Dateien | +| `test-security-auth` | 31 | Login-Sperre nach 5 Fehlversuchen (auch richtiges Passwort abgewiesen, auditiert, Entsperren), kein Konto-Orakel, letzter Admin nicht aussperrbar; Rate-Limit Reset je IP/Konto; Session-Kill-Switch; Audit-Log: statischer Scan (keine Mutation in app/actions/services/api/jobs/lib), keine /api-Audit-Route, Viewer rein lesend, DB-Rechte `craftvia_app` (SELECT/INSERT ja, UPDATE/DELETE nein), Tenant-Guard, als `craftvia_app` UPDATE/DELETE → permission denied | +| `test-security-http` | 108 | Gegen `next start` (Produktions-Build aus dem Gate, freier Port) oder `SECURITY_BASE`: ohne Session 14 /api/v1-Routen → 401, Datei-Routen/Seiten → Login ohne Bytes; **manipulierte IDs: alle /api/v1-Routen mit ID (work-orders inkl. assign/transition/materials/documents/daily-/completion-report, reports approve/pdf/files, customers, sites history, imports + confirm, field documents, uploads) sowie `/files/` und `/imports//file` → 404** ohne Daten im Body, Sync → rejected not_found, Listen/Bundle ohne A-Daten, Monteur ohne Zuweisung → 404, unsinnige IDs → 404 statt 500; Rollenaktionen → 403 + Audit „denied“; Uploads über `/documents/upload`, `/api/v1/uploads`, `/api/v1/work-orders/import` (EXE, HTML, SVG, Polyglot mit Download `image/jpeg` + `attachment` + `nosniff` + CSP, Traversal, CR/LF ohne Header-Injection, > 25 MB, Server bleibt erreichbar); CSRF (fremder Origin → 403); Sessions (manipuliert, fremder Schlüssel, abgelaufen, Kill-Switch, deaktiviert); PATCH/PUT/DELETE auf Audit-Pfade ohne Wirkung; Login-Sperre über `/api/auth/callback/credentials` (Positivkontrolle); Sicherheits-Header | + +Statuscodes: Die HTTP-Tests akzeptieren für `invalid` 400 **oder** 422 und prüfen sonst 401/403/404 exakt – damit gültig vor und nach der Vereinheitlichung durch L10b. Die Offline-Prüfung „nicht registrierte Op“ ermittelt den Op-Typ dynamisch aus `EXTERNAL_OPS` (L10b registriert `report.submit`/`report.save_draft`). Schreibende Requests senden den eigenen Origin (L10b-Same-Origin-Prüfung). Je Nutzer weit unter 300 Requests/min (L10b-Rate-Limit). + +## 5. RLS-Lauf (`RLS_ENFORCED=true`, `RLS_DATABASE_URL` auf die Lane-DB) + +- **Alle neuen E2E- und Sicherheitsskripte: 11/11 grün** (inkl. Migration append-only). +- **Gesamte Testsuite: 58/60 grün.** Die zwei Abweichungen liegen in Fundament-Tests und sind keine Isolationslücken: + - `test-garage-storage.ts`: Abbruch „RLS_ENFORCED=true, aber RLS_DATABASE_URL fehlt“ – das Skript lädt die Umgebung offenbar erst nach dem Import von `db.ts` (fail-secure greift). Testumgebungsthema. + - `test-tenant-isolation.ts` (F-02): „findUnique über Compound-Key (fremd) → Throw“ erwartet den Owner-Pfad; mit scharfer RLS ist die fremde Zeile unsichtbar und das Ergebnis `null` – ebenfalls fail-closed, kein Datenabfluss. Test sollte beide Ausprägungen akzeptieren. + +## 6. Performance (§34.1) – 5 020 Aufträge im Mandanten `demo` + +`scripts/seed-load.ts` (3,1 s für 5 000 Aufträge inkl. Zuweisungen, Materialvorgaben, Historie, Berichte) → `PERF=1 scripts/smoke-auth.ts` (zweiter, warmer Request): + +| Seite | Dev-Server | Production-Build (`next start`) | +|---|---|---| +| `/dashboard` (Backoffice) | 233 ms | 30 ms | +| `/work-orders` (Karten / Tabelle / überfällig / Gruppe / Suche / Seite 3) | 252–319 ms | 33–40 ms | +| `/reports` | 467 ms | 53 ms | +| `/search?q=` | 206 ms | 30 ms | +| `/m` / `/m/orders` (Teamleiter, Monteur mit ~2 500 Aufträgen im Team) | 724 / 1 536 ms | 63–67 / 104–111 ms | + +Alle Seiten deutlich < 2 s. Query-Pläne (EXPLAIN ANALYZE) der Liste mit Monteur-Scope, Dashboard-Zählungen (überfällig, Berichte zur Prüfung), Paginierung und Berichtsliste: < 2 ms, Index-Scans auf den bestehenden Indizes (`work_orders(tenant_id, planned_start|status)` u. a.). Kein N+1: Liste und Dashboard laden mit festen Abfragen (Dashboard 11 parallele Counts + 2). **Keine Index-Migration nötig.** Last-Daten danach mit `--reset` wieder entfernt. + +## 7. Authentifizierter Durchstich (Dev-Server :3110, Demo-Seed) + +`BASE=http://localhost:3110 npx tsx scripts/smoke-auth.ts` → **OK, 94/94 Prüfungen**: +- **Admin:** Dashboard, Einstellungen (Nutzer, Audit, E-Mail, Lotse + Protokoll, Auftragsarten, Checklisten, Nummernkreise), Konto, Teams. +- **Backoffice:** Startseite → `/dashboard`, Aufträge (Karten, Tabelle, überfällig, Statusgruppe, Suche, Seite 3), Auftragsdetail mit allen 9 Tabs, Konflikte, Notdienst-Prüfung (Liste + Detail), Importe + **Prüfmaske**, Kunden (+ vorläufig, Detail), Objekte (+ Detail, **Historie** mit freigegebenem Auftrag), Berichte (+ freigegeben, zur Prüfung), Dokumente, Benachrichtigungen, Suche, Teams. +- **Teamleiter:** `/` → `/m`, Heute, Aufträge, mehrtägiger Auftrag, Auftrag zur Prüfung, Berichte; Auftrag von Team Süd → 404. +- **Monteur (Nord):** `/` → `/m`, Heute, Aufträge (Tab laufend), Auftragsdetail (Zugangshinweis), Fotos, Material, Notizen, Checkliste, Zeiten, Tagesbericht, Abschlussbericht, **Unterschrift**, Notdienst-Auftrag, Notdienst-Erfassung, Sync, Offline, Profil, eigenes Foto; fremder Auftrag/Kunde → 404, `/dashboard` → `/m`. +- **Monteur (Süd):** Heute, Unterschrift ausstehend; Auftrag von Team Nord → 404. +- **Zweiter Mandant (admin2@):** Dashboard/Aufträge/Kunden/Suche ohne Demo-Daten; Auftrag, Kunde, Objekt, Bericht, Notdienst, Import, Datei aus `demo` → 404. + +Gefundene Fehler im Durchstich: **keine** (kein Fachcode geändert). Visuelle Prüfung im Browser (375/768/1024 px) nicht durchgeführt: der Browser-Login würde eine Passworteingabe bzw. das Einschleusen eines Session-Tokens durch den Agenten erfordern; die mobilen Seiten wurden in L4/L7 visuell geprüft. + +## 8. Stubs / Abhängigkeiten + +Keine Stubs. Genutzt werden ausschließlich die integrierten Services von L1–L9. Kompatibilität mit L10b siehe §4 (Statuscodes, Same-Origin, Rate-Limit, registrierte Sync-Ops). + +## 9. Befunde, bekannte Lücken, Hinweise an den Architekten + +1. **Login ohne IP-basiertes Limit** (nur Sperre je Identität nach 5 Fehlversuchen; Reset/Einladung haben IP+Konto-Limits). Password-Spraying über viele Konten wird nicht gebremst → Kandidat für das L10b-Rate-Limit (auf `/api/auth/callback/credentials`). +2. **Polyglot-Dateien** (gültige Signatur + angehängter Fremdinhalt) passieren den Magic-Byte-Scanner. Mitigiert durch Speicherung mit erkanntem MIME-Typ, Auslieferung als `attachment` + `nosniff` + CSP (HTTP-Test); echter Inhaltsscan nur mit `CLAMAV_HOST`. +3. **Doppelte Audit-Einträge bei Einsatz-Uploads:** `storeFieldUpload` (L4) schreibt nach `storeFile` einen zweiten `document/create`-Eintrag. Harmlos, aber redundant. +4. **`confirmImport` validiert das Formular vor der Mandanten-/Existenzprüfung** → fremde ID mit ungültigem Formular ergibt `invalid` statt `not_found` (kein Datenabfluss; mit gültigem Formular 404). +5. **Mobile Listen ohne Paginierung** (`/m/orders` max. 200, „Heute“ max. 100 Aufträge je Abruf; §34.1 „Auftragslisten müssen paginiert werden“). Performance unkritisch; bei sehr großen Teams fallen Einträge aus der Liste. +6. **RLS-Fundament-Tests** (§5): `test-garage-storage.ts` (Env-Ladereihenfolge) und `test-tenant-isolation.ts` (Compound-Key erwartet Throw statt `null`) für `RLS_ENFORCED=true` anpassen. +7. **Demo-Termine sind relativ zum Seed-Zeitpunkt** („heute“, „überfällig“); der Seed ist idempotent und verschiebt bestehende Demo-Aufträge nicht. Für frische Demo-Daten Mandant neu aufsetzen. +8. **Seed in Production:** `prisma/seed.ts` lädt `scripts/lib/demo-seed.ts` nur bei aktivierten Demo-Daten dynamisch; ein Production-Image ohne `scripts/` braucht `SEED_DEMO` ungesetzt (Default in Production: aus). +9. Der HTTP-Sicherheitstest startet im Gate den Build per `next start` („does not work with output: standalone“ ist nur eine Warnung). Ohne Build und ohne `SECURITY_BASE` wird er übersprungen (Exit 0). + +## 10. Screens / Routen + +Keine neuen Routen. Geprüft (Smoke/HTTP): siehe §7 und §4 (`test-security-http`).