docs(legal)+test: Compliance-Dokumentation & Consent-Unit-Tests (v1.7.4)

Teil 5 des Compliance-Updates:

- docs/legal/: data-processing-inventory.md, subprocessors.md,
  technical-organizational-measures.md, retention-and-deletion.md, incident-response.md,
  licenses-and-attributions.md — Ist-Zustand aus der Analyse, offene Punkte als Platzhalter
- Reine Consent-Regeln nach src/lib/legal-rules.ts ausgelagert (kein Prisma) + Texte nach
  src/lib/legal-content.ts (7 Pflichtbestätigungen) — für Testbarkeit
- Vitest eingerichtet (vitest.config.ts, test-Script); 13 Tests grün:
  Pflicht-Dokumenttypen, Re-Consent bei neuer Version, normale Benutzer ≠ Org-Vertreter,
  7 Org-Bestätigungen, Platzhalter-Erkennung, keine erfundenen Subprozessoren

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 12:19:01 +02:00
parent 235959101b
commit a943db9c54
15 changed files with 1534 additions and 69 deletions

View File

@@ -0,0 +1,23 @@
# Dateninventar / Verzeichnis der Bearbeitungstätigkeiten — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung.
Abgeleitet aus dem tatsächlichen Code-Stand. Offene Fristen/Standorte als Platzhalter `[…]`.
| Datenkategorie | Zweck | Betroffene | Herkunft | Speicherort | Empfänger/Drittanbieter | Auslandbekanntgabe | Aufbewahrung | Löschprozess | Schutzmassnahmen |
|---|---|---|---|---|---|---|---|---|---|
| Kontodaten (Name, E-Mail, Rolle) | Authentifizierung, Verwaltung | Benutzer | Eingabe bei Registrierung | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | Selfservice/Antrag; Backups bis Zyklusende | Rollen/Mandanten, TLS, Zugriffskontrolle |
| Passwort (bcrypt-Hash) | Authentifizierung | Benutzer | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | mit Konto | Einweg-Hash (bcrypt cost 12) |
| Einsatz-/Inhaltsdaten (Pläne, Journale, Zeichnungen, Koordinaten) | Kernfunktion | Org-Mitglieder, ggf. Dritte | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Löschung durch Org | Projekt-/Orglöschung; Backups | Mandantentrennung, Zugriffskontrolle |
| Hochgeladene Dateien (Logos, Planbilder, Symbole) | Darstellung | Org | Upload | MinIO | — | `[SERVERSTANDORT]` | bis Löschung | mit Projekt/Org | Zugriffsschutz, Validierung |
| Zustimmungsnachweise (Doc-Typ/Version/Zeitpunkt/Kontext) | Nachweis Einwilligung/Vertrag | Benutzer | System | PostgreSQL | — | `[SERVERSTANDORT]` | Aufbewahrung zu Nachweiszwecken `[FRIST]` | revisionssicher, nicht überschrieben | minimale Daten (keine roh-IP) |
| Protokoll-/Sicherheitsdaten | Betrieb, Sicherheit | Benutzer | System | Server-Logs | — | `[SERVERSTANDORT]` | `[FRIST]` | rotierend | Minimierung sensibler Inhalte |
| Session-Cookie `auth-token` | Anmeldung | Benutzer | System | Client-Cookie | — | — | 24 h / 14 Tage | Ablauf/Logout | httpOnly, secure (prod), sameSite=lax |
| E-Mail-Versand (Verifizierung, Reset, Kontakt, Einladung) | Kommunikation | Benutzer/Empfänger | System/Eingabe | SMTP-Anbieter | E-Mail-Anbieter | `[ABHÄNGIG]` | beim Anbieter | — | TLS, nur bei Bedarf |
| Unterstützungsbeiträge (Betrag, Name/Nachricht, payment_intent) | Zahlungsabwicklung | Spender | Eingabe/Stripe | Stripe + `SystemSetting` | Stripe | ja (möglich) | `[FRIST]` | `[PROZESS]` | Zahlungsdaten nur bei Stripe |
| Karten-/Geo-Anfragen | Karten/Adresse/Wetter | Benutzer | Client | (extern) | OSM/Esri/swisstopo/Nominatim/Open-Meteo | möglich | keine Speicherung durch App | — | nur Kachel/Koordinaten |
| localStorage (Zeichnungen, Sync-Queue, Theme, Onboarding, letztes Projekt, Cookie-Hinweis) | Funktion/Offline | Benutzer | Client | Gerät des Nutzers | — | — | bis lokal gelöscht | Browser | verbleibt lokal |
## Offene Entscheidungen
- Konkrete Aufbewahrungsfristen (Logs, Zustimmungen, Zahlungsdaten) → siehe `retention-and-deletion.md`.
- Serverstandorte bestätigen.

View File

@@ -0,0 +1,44 @@
# Incident-Response — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. **Manueller Entscheidungsprozess** — es erfolgt KEINE
automatische Meldung an Behörden. Meldepflichten werden im Einzelfall geprüft.
## Kontakte (auszufüllen)
- Verantwortlich (Betreiber): `[NAME]` · `[KONTAKT]`
- Datenschutzkontakt: siehe `src/config/legal.ts` (`privacyContactEmail`)
- Technischer Notfallkontakt: `[NAME/TELEFON]`
- Zuständige Aufsichtsbehörde (CH): EDÖB — `[LINK/KONTAKT]`
## Ablauf
1. **Erkennung** — Quelle (Monitoring, Meldung, Nutzerhinweis), Zeitpunkt und Symptome festhalten.
2. **Interne Eskalation** — Verantwortliche informieren; Vorfall im Vorfallprotokoll eröffnen.
3. **Eindämmung** — betroffene Systeme/Zugänge isolieren, ggf. Dienste temporär sperren.
4. **Beweissicherung** — Logs/Artefakte sichern, bevor Änderungen erfolgen.
5. **Bewertung betroffener Daten** — welche Datenkategorien/Personen sind betroffen?
6. **Risikobewertung** — Eintritts-/Schadenswahrscheinlichkeit für Betroffene.
7. **Information betroffener Organisationen** — ohne unangemessene Verzögerung, sachlich.
8. **Prüfung Behördenmeldung** — Meldepflicht (z.B. EDÖB) im Einzelfall prüfen und **manuell** entscheiden/dokumentieren.
9. **Passwort-/Schlüsselrotation** — Secrets, Tokens, ggf. Nutzer-Passwörter rotieren.
10. **Wiederherstellung** — aus sauberem Stand/Backup; Integrität prüfen.
11. **Nachbearbeitung** — Ursachenanalyse, Massnahmen, Lessons Learned.
## Vorfallprotokoll (Vorlage)
| Feld | Inhalt |
|---|---|
| Vorfall-ID / Datum | |
| Entdeckt durch / wann | |
| Beschreibung | |
| Betroffene Daten/Personen | |
| Sofortmassnahmen | |
| Risikobewertung | |
| Information an Organisationen (wann/wie) | |
| Behördenmeldung (ja/nein, Begründung) | |
| Rotation durchgeführt | |
| Wiederherstellung | |
| Abschluss / Massnahmen | |
## Vom Betreiber zu treffende Entscheidungen
- Verbindliche Kontaktliste und Erreichbarkeit.
- Melde-Schwellen und interne Fristen.
- Aufbewahrung des Vorfallprotokolls.

View File

@@ -0,0 +1,45 @@
# Lizenzen und Attributionen — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. Eine Quellenangabe ersetzt NICHT automatisch eine
erforderliche Nutzungslizenz. Offene Freigabefragen sind markiert.
## Karten & Geodaten
| Quelle | Nutzung | Lizenz/Bedingungen | Attribution | Offene Frage |
|---|---|---|---|---|
| OpenStreetMap | Basiskarte, Nominatim | ODbL (Daten) | "© OpenStreetMap-Mitwirkende" (vorhanden im Impressum/Karte) | Tile-Usage-Policy einhalten (Fair Use) |
| Esri World Imagery | Satellitenlayer | Esri-Nutzungsbedingungen | "Esri World Imagery" | ⚠️ kommerzielle/Nutzungsbedingungen prüfen |
| swisstopo (geo.admin.ch) | Landeskarte | swisstopo-Bedingungen | "© swisstopo" empfohlen | ⚠️ Attribution/Bedingungen bestätigen |
| Open-Meteo | Wind/Höhe | Open-Meteo Terms (frei, Attribution empfohlen) | "Open-Meteo" | Attribution ergänzen? |
| Open-Elevation | Höhe (Fallback) | offen/community | — | ⚠️ Bedingungen prüfen |
## Symbole / Icons
| Quelle | Nutzung | Lizenz | Attribution | Offene Frage |
|---|---|---|---|---|
| FKS/BABS taktische Zeichen | Einsatzsymbole | `[NUTZUNGSRECHT ZU KLÄREN]` | "Bildquelle Feuerwehr Koordination Schweiz FKS" (vorhanden) | ⚠️ **Nutzungsrecht/Freigabe FKS bestätigen** |
| lucide-react | UI-Icons | ISC | — (im Code) | ok |
## Schriftarten
| Quelle | Lizenz | Hinweis |
|---|---|---|
| Barlow (Google Fonts, via next/font) | SIL Open Font License 1.1 | build-time self-hosted, kein Laufzeit-Request zu Google |
## Wesentliche Bibliotheken (Auszug)
| Paket | Lizenz |
|---|---|
| next / react | MIT |
| maplibre-gl | BSD-3-Clause |
| jspdf | MIT (+ Teile Apache-2.0) |
| qrcode | MIT |
| stripe (SDK) | MIT |
| bcryptjs, jose, zod, socket.io | MIT |
| @prisma/client, prisma | Apache-2.0 |
## Bereits umgesetzte sichtbare Attributionen
- Karten-Attribution "© OpenStreetMap contributors | MapLibre" auf der Karte.
- "Taktische Symbole: Bildquelle Feuerwehr Koordination Schweiz FKS" im Footer.
- Kartendaten-Hinweis im Impressum (OpenStreetMap, Nominatim).
## Offene Freigabefragen (Betreiber)
1. **FKS/BABS-Symbole**: Nutzungsrecht/Freigabe für die konkrete Verwendung schriftlich bestätigen.
2. Esri/swisstopo-Nutzungsbedingungen für den vorgesehenen (auch nicht-kommerziellen) Einsatz prüfen.
3. Vollständige Lizenzliste (z.B. `license-checker`) generieren und Drittlizenzen bündeln, falls verteilt.

View File

@@ -0,0 +1,37 @@
# Aufbewahrungs- und Löschkonzept — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. **Keine Fristen erfunden** — offene Fristen als `[FRIST — zu
entscheiden]`. Der Betreiber muss die markierten Entscheidungen treffen.
| Datenkategorie | Aufbewahrung | Löschung / Prozess |
|---|---|---|
| Aktive Benutzerkonten | solange Konto besteht | Selfservice/Antrag; kaskadierende Löschung zugehöriger Daten |
| Deaktivierte Konten (`isActive=false`) | `[FRIST]` | `[PROZESS — automatische Löschung nach X?]` |
| Organisationen (Tenant) | solange aktiv | Löschung durch Org-Admin (Einstellungen) nach Berechtigungsprüfung |
| Pläne / Projekte | bis Löschung durch Org | mit Projekt-/Orglöschung (Cascade) |
| Dateiuploads (MinIO) | bis Löschung des Bezugsobjekts | mit Projekt/Org/Icon |
| Audit-/Aktionslogs (falls geführt) | `[FRIST]` | `[PROZESS]` |
| Sicherheits-/Server-Logs | `[FRIST]` | Rotation `[KONFIG]` |
| Einladungen | `[FRIST]` | nach Annahme/Ablauf |
| Passwort-Reset-Tokens | Ablauf (`resetTokenExpiry`) | mit Verwendung/Ablauf |
| E-Mail-Verifizierungs-Tokens | bis Verifizierung | mit Verifizierung |
| E-Mails (beim SMTP-Anbieter) | beim Anbieter | `[ABHÄNGIG]` |
| Zahlungsinformationen (Stripe + `SystemSetting/donations`) | `[FRIST]` (ggf. gesetzliche Aufbewahrung) | `[PROZESS]`; Zahlungsdaten liegen bei Stripe |
| Supportanfragen (Kontaktformular) | `[FRIST]` | `[PROZESS]` |
| Backups | `[BACKUP-AUFBEWAHRUNGSFRIST]` | mit Ablauf des Backup-Zyklus |
| Zustimmungshistorie (LegalAcceptance) | zu Nachweiszwecken `[FRIST]` | revisionssicher, i.d.R. nicht gelöscht solange Konto besteht |
## Grundsätze
- Produktivdaten werden nach Abschluss des Löschvorgangs entfernt.
- Daten in Sicherungskopien können bis zum Ablauf des regulären Backup-Zyklus fortbestehen.
- Organisationsrelevante Daten werden nicht ohne Berechtigungsprüfung sofort gelöscht.
- Betroffenenrechte (Auskunft, Berichtigung, Löschung, Export) über `/konto/datenschutz` bzw.
Datenschutzkontakt; Eingang wird bestätigt.
## Vom Betreiber zu treffende Entscheidungen
1. Aufbewahrungsfrist deaktivierter Konten und automatischer Löschlauf?
2. Log-Aufbewahrung (Server-/Sicherheitslogs) und Rotation?
3. Aufbewahrung Zahlungs-/Spendendaten (gesetzliche Pflichten?).
4. Backup-Aufbewahrungsfrist (`LEGAL_BACKUP_RETENTION`).
5. Aufbewahrung Zustimmungsnachweise nach Kontolöschung.
6. Support-/Kontaktanfragen-Frist.

View File

@@ -0,0 +1,33 @@
# Unterauftragnehmer / Subprozessoren — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung.
Nur tatsächlich im Code verwendete bzw. optional konfigurierbare Dienste. Nicht verwendete Dienste
sind NICHT aufgeführt. Regionen/Verträge, die nicht sicher bekannt sind, tragen Platzhalter `[…]`.
| Anbieter | Zweck | Verarbeitete Daten | Sitz / Region | Datenschutzhinweis | Auslandbearbeitung | Vertragsstatus |
|---|---|---|---|---|---|---|
| OpenStreetMap Foundation (Tiles) | Basiskarte | Kachel-Koordinaten, IP (Client) | `[REGION — UK/EU]` | osmfoundation.org/wiki/Privacy_Policy | möglich | `[ZU PRÜFEN]` |
| Esri (ArcGIS World Imagery) | Satellitenbilder | Kachel-Koordinaten, IP (Client) | `[REGION — USA/global]` | esri.com/legal | wahrscheinlich | `[ZU PRÜFEN]` |
| swisstopo (geo.admin.ch) | Schweizer Landeskarte | Kachel-Koordinaten, IP (Client) | Schweiz | geo.admin.ch | nein | `[ZU PRÜFEN]` |
| Nominatim (OSM) | Adress-/Reverse-Geocoding | Suchbegriff/GPS, IP (Client) | `[REGION — OSMF]` | osmfoundation.org | möglich | `[ZU PRÜFEN]` |
| Open-Meteo | Wind-/Höhendaten | Koordinaten, IP (Client) | `[REGION — EU/DE]` | open-meteo.com/en/terms | möglich | `[ZU PRÜFEN]` |
| Open-Elevation | Höhendaten (Fallback) | Koordinaten, IP (Client) | `[REGION]` | open-elevation.com | möglich | `[ZU PRÜFEN]` |
| Stripe | Zahlungsabwicklung (freiwillige Beiträge) | Betrag, freiwilliger Name/Nachricht, Zahlungsdaten (direkt bei Stripe) | `[REGION — USA/EU]` | stripe.com/privacy | ja (USA möglich) | `[DPA PRÜFEN]` · nur wenn konfiguriert |
| SMTP-/E-Mail-Anbieter (konfigurierbar) | System-E-Mails | Empfängeradresse, Mailinhalt | `[ABHÄNGIG VOM ANBIETER]` | `[ANBIETER]` | abhängig | `[ZU PRÜFEN]` · nur wenn konfiguriert |
| MinIO (Objektspeicher, selbst gehostet) | Datei-/Bild-Uploads | Pläne, Logos, Symbole | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb |
| PostgreSQL (Datenbank, selbst gehostet) | Primäre Datenhaltung | Konto-, Org-, Plan-, Journaldaten | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb |
## Hinweise
- **Client-seitige Dienste** (Tiles, Nominatim, Open-Meteo/Elevation): werden direkt aus dem Browser
des Nutzers aufgerufen; dabei kann die IP-Adresse an den Anbieter gelangen.
- **Stripe** und **SMTP** sind nur aktiv, wenn in den Systemeinstellungen (DB `SystemSetting`) konfiguriert.
- **MapTiler / Mapbox / Google Maps**: NICHT genutzt (MapTiler nur in der CSP-Allowlist vorbereitet).
- **Analytics / Error-Tracking (Sentry, GA, Plausible, PostHog)**: NICHT vorhanden.
## Offene Entscheidungen des Betreibers
- Serverstandort für PostgreSQL, MinIO, Backups und Logs abschliessend bestätigen.
- DPA/AVV mit Stripe und dem gewählten E-Mail-Anbieter abschliessen und Status hier eintragen.
- Verarbeitungsregionen der Kartendienste konkretisieren.

View File

@@ -0,0 +1,59 @@
# Technische und organisatorische Massnahmen (TOM) — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. Ist-Zustand aus dem Code; offene Punkte klar markiert.
Legende: ✅ vorhanden · ⚠️ teilweise / zu bestätigen · ❌ offen / nicht umgesetzt.
## Zugriff & Berechtigungen
- ✅ Mandantentrennung je Organisation (Tenant), `getProjectWithTenantCheck`.
- ✅ Rollenkonzept: SERVER_ADMIN, TENANT_ADMIN, OPERATOR, VIEWER.
- ⚠️ MFA für System-/Org-Admins: ❌ nicht vorhanden → **Empfehlung offen**.
- ✅ Admin-APIs serverseitig mit `isAdmin`/`isServerAdmin` geschützt.
## Authentifizierung & Sessions
- ✅ Passwort-Hashing: bcrypt (cost 12), nie Klartext.
- ✅ JWT-Session (jose, HS256), Cookie `auth-token` httpOnly, secure (prod), sameSite=lax, 24 h/14 Tage.
- ✅ Passwort-Reset mit kryptographischem Token (32 Bytes), Ablauf.
- ✅ E-Mail-Verifizierung.
## Transport & Web-Härtung
- ✅ HTTPS/TLS; HSTS (max-age 2 Jahre, preload).
- ✅ Sicherheits-Header: CSP (mit `frame-ancestors 'self'`), X-Content-Type-Options nosniff,
Referrer-Policy, Permissions-Policy, COOP, X-XSS-Protection.
- ⚠️ CSP enthält `unsafe-inline`/`unsafe-eval` (für MapLibre gelockert) → langfristig verschärfen.
- ✅ Private Routen (/app,/admin,/settings,/konto): `X-Robots-Tag: noindex` + `Cache-Control: no-store` (Middleware).
- ⚠️ Socket.io-Server (`server-custom.js`): CORS `origin:'*'`**einschränken empfohlen**.
## Eingaben & Uploads
- ✅ Rate-Limiting: Login, Registrierung, Passwort-Reset, Verifizierung, Kontakt, Konto-Löschung, Share-PIN.
- ⚠️ Rate-Limiter ist In-Memory (pro Instanz) → bei Mehr-Instanz-Betrieb geteilten Store nutzen.
- ⚠️ Upload-Validierung (MIME/Grösse): vorhanden für Icons/Logos/Planbilder → **Umfang bestätigen**.
- ✅ Zugriffsschutz auf Dateien (serve-Routen mit Tenant-Check).
## Speicherung & Verschlüsselung
- ✅ Übertragung verschlüsselt (TLS).
- ⚠️ Verschlüsselung ruhender Daten (DB/MinIO/Backups): `[ZU BESTÄTIGEN]`.
- ⚠️ Secrets-Verwaltung: ENV (`NEXTAUTH_SECRET`, MinIO) + DB (`SystemSetting`: SMTP/Stripe) → **Absicherung der DB-Secrets bestätigen**.
## Backup & Wiederherstellung
- ⚠️ Backups: `[VORHANDEN? FREQUENZ? AUFBEWAHRUNG?]` (siehe `docs/backup-und-seeding.md`).
- ❌ Restore-Tests: `[DOKUMENTIEREN]`.
## Logging & Monitoring
- ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**.
- ❌ Zentrales Monitoring/Alerting: `[OFFEN]`.
## Betrieb & Prozesse
- ✅ Idempotente Migrationen (`prisma/migrate.js`) beim Container-Start.
- ⚠️ Dependency-/Patch-Management: `[PROZESS DOKUMENTIEREN]`.
- ⚠️ Trennung Entwicklung/Test/Produktion: `[BESTÄTIGEN]`; Umgang mit Testdaten `[BESTÄTIGEN]`.
- ✅ Löschkonzept/Datenexport: siehe `retention-and-deletion.md`; Betroffenenrechte via `/konto/datenschutz`.
- ✅ Berechtigungsentzug: Benutzer sperren (`isActive`), aus Organisation entfernen.
## Direkt umgesetzte risikoarme Verbesserungen (dieses Update)
- Private Routen auf `noindex`/`no-store` gesetzt.
- Rechtliche Prüf-Hinweise in Exporten und Berechnungen.
- Revisionssichere Zustimmungsprotokollierung ohne roh-IP.
## Empfohlene, noch offene Massnahmen
- MFA für Admins; CSP verschärfen; Socket.io-CORS einschränken; geteilter Rate-Limit-Store;
Verschlüsselung ruhender Daten + Backups bestätigen; Restore-Tests; Monitoring.

1138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "lageplan",
"version": "1.7.3",
"version": "1.7.4",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {
@@ -8,6 +8,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"test:watch": "vitest",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
@@ -88,7 +90,8 @@
"sharp": "^0.34.5",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^4.1.10"
},
"browserslist": [
"last 2 Chrome versions",

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { useAuth } from '@/components/providers/auth-provider'
import { Loader2, ExternalLink } from 'lucide-react'
import { ORG_CONFIRMATIONS } from '@/lib/legal-content'
interface PendingDoc { type: string; version: string; title: string; url: string | null }
@@ -15,16 +16,6 @@ const ALLOWLIST = [
'/konto', '/forgot-password', '/reset-password',
]
const ORG_CONFIRMATIONS = [
'Ich bestätige, dass ich berechtigt bin, für diese Organisation ein Administratorkonto einzurichten oder diese Bestätigung abzugeben.',
'Unsere Organisation versteht, dass Lageplan ein unterstützendes Werkzeug und kein Alarmierungs-, Leitstellen- oder Einsatzleitsystem ist.',
'Unsere Organisation prüft Lagepläne, Adressen, Symbole, Distanzen, Berechnungen und sonstige Inhalte vor der operativen Verwendung durch fachkundige Personen.',
'Unsere Organisation hält unabhängig von Lageplan aktuelle Offline-, Notfall- und Rückfallunterlagen bereit.',
'Unsere Organisation verwendet Lageplan nicht als einzige Informationsquelle für operative Entscheidungen.',
'Unsere Organisation speichert ohne separate schriftliche Vereinbarung keine Patienten-, Gesundheits-, Polizei-, Passwort-, Zugangs-, Schlüsselcode- oder anderen besonders schützenswerten Daten in Lageplan.',
'Ich habe die Nutzungsbedingungen und die Datenschutzerklärung gelesen und akzeptiere die für die Nutzung geltenden Bestimmungen.',
]
export function LegalConsentGate() {
const { user, loading, logout } = useAuth()
const pathname = usePathname()

View File

@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest'
import { isPlaceholder, legal } from '@/config/legal'
describe('isPlaceholder', () => {
it('erkennt Platzhalter im Format [NAME]', () => {
expect(isPlaceholder('[POSTADRESSE]')).toBe(true)
expect(isPlaceholder('[RECHTLICHER NAME DES BETREIBERS]')).toBe(true)
})
it('erkennt echte Werte als Nicht-Platzhalter', () => {
expect(isPlaceholder('Musterstrasse 1, 8000 Zürich')).toBe(false)
expect(isPlaceholder('app@lageplan.ch')).toBe(false)
})
it('leere Werte gelten als Platzhalter (fehlend)', () => {
expect(isPlaceholder('')).toBe(true)
expect(isPlaceholder(null)).toBe(true)
expect(isPlaceholder(undefined)).toBe(true)
})
})
describe('legal config', () => {
it('führt genau 5 Rechtsdokumenttypen', () => {
expect(Object.keys(legal.documents).sort()).toEqual(
['dataProcessingAgreement', 'organizationDeclaration', 'privacy', 'responsibleUse', 'terms']
)
})
it('listet nur real genutzte Subprozessoren (keine erfundenen wie Google Analytics)', () => {
const names = legal.processors.map(p => p.name.toLowerCase())
expect(names.some(n => n.includes('stripe'))).toBe(true)
expect(names.some(n => n.includes('analytics'))).toBe(false)
expect(names.some(n => n.includes('mapbox'))).toBe(false)
})
})

View File

@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest'
import { requiredTypesFor, computePendingDocuments, REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN } from '@/lib/legal-rules'
import { ORG_CONFIRMATIONS } from '@/lib/legal-content'
const active = [
{ type: 'TERMS', version: '1.0.0' },
{ type: 'PRIVACY', version: '1.0.0' },
{ type: 'ORGANIZATION_DECLARATION', version: '1.0.0' },
]
describe('requiredTypesFor', () => {
it('verlangt für alle Benutzer TERMS + PRIVACY', () => {
const req = requiredTypesFor(false)
expect(req.has('TERMS')).toBe(true)
expect(req.has('PRIVACY')).toBe(true)
expect(req.has('ORGANIZATION_DECLARATION')).toBe(false)
})
it('verlangt für Org-Admins zusätzlich die Organisationsbestätigung', () => {
const req = requiredTypesFor(true)
expect(req.has('ORGANIZATION_DECLARATION')).toBe(true)
})
})
describe('computePendingDocuments', () => {
it('meldet alle Pflichtdokumente offen, wenn nichts akzeptiert wurde', () => {
const pending = computePendingDocuments(active, requiredTypesFor(true), new Set())
expect(pending.map(d => d.type).sort()).toEqual(['ORGANIZATION_DECLARATION', 'PRIVACY', 'TERMS'])
})
it('meldet nichts offen, wenn aktive Versionen akzeptiert wurden', () => {
const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0', 'ORGANIZATION_DECLARATION@1.0.0'])
const pending = computePendingDocuments(active, requiredTypesFor(true), accepted)
expect(pending).toHaveLength(0)
})
it('löst Re-Consent aus, wenn eine neue Version veröffentlicht wird', () => {
// Nutzer hat 1.0.0 akzeptiert, aktiv ist jetzt 2.0.0
const activeV2 = [{ type: 'TERMS', version: '2.0.0' }, { type: 'PRIVACY', version: '1.0.0' }]
const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0'])
const pending = computePendingDocuments(activeV2, requiredTypesFor(false), accepted)
expect(pending.map(d => d.type)).toEqual(['TERMS'])
expect(pending[0].version).toBe('2.0.0')
})
it('normale Benutzer werden nicht als Org-Vertreter behandelt (keine Org-Bestätigung verlangt)', () => {
const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0'])
const pending = computePendingDocuments(active, requiredTypesFor(false), accepted)
expect(pending).toHaveLength(0)
})
})
describe('Konstanten & Inhalte', () => {
it('REQUIRED-Mengen sind korrekt', () => {
expect(REQUIRED_FOR_ALL).toEqual(['TERMS', 'PRIVACY'])
expect(REQUIRED_FOR_ORG_ADMIN).toEqual(['ORGANIZATION_DECLARATION'])
})
it('Organisationsbestätigung hat genau 7 Pflichtaussagen', () => {
expect(ORG_CONFIRMATIONS).toHaveLength(7)
expect(ORG_CONFIRMATIONS.every(s => s.trim().length > 0)).toBe(true)
})
})

14
src/lib/legal-content.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Reine Textinhalte für den Consent-Flow (ohne UI-Abhängigkeiten, damit testbar).
*/
/** Die 7 einzeln zu bestätigenden Pflichtaussagen der Organisationsbestätigung. */
export const ORG_CONFIRMATIONS: string[] = [
'Ich bestätige, dass ich berechtigt bin, für diese Organisation ein Administratorkonto einzurichten oder diese Bestätigung abzugeben.',
'Unsere Organisation versteht, dass Lageplan ein unterstützendes Werkzeug und kein Alarmierungs-, Leitstellen- oder Einsatzleitsystem ist.',
'Unsere Organisation prüft Lagepläne, Adressen, Symbole, Distanzen, Berechnungen und sonstige Inhalte vor der operativen Verwendung durch fachkundige Personen.',
'Unsere Organisation hält unabhängig von Lageplan aktuelle Offline-, Notfall- und Rückfallunterlagen bereit.',
'Unsere Organisation verwendet Lageplan nicht als einzige Informationsquelle für operative Entscheidungen.',
'Unsere Organisation speichert ohne separate schriftliche Vereinbarung keine Patienten-, Gesundheits-, Polizei-, Passwort-, Zugangs-, Schlüsselcode- oder anderen besonders schützenswerten Daten in Lageplan.',
'Ich habe die Nutzungsbedingungen und die Datenschutzerklärung gelesen und akzeptiere die für die Nutzung geltenden Bestimmungen.',
]

44
src/lib/legal-rules.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* Reine Consent-Regeln OHNE Laufzeit-Abhängigkeiten (kein Prisma) — damit unit-testbar.
*/
export type LegalDocumentType =
| 'TERMS'
| 'PRIVACY'
| 'ORGANIZATION_DECLARATION'
| 'RESPONSIBLE_USE'
| 'DATA_PROCESSING_AGREEMENT'
export type LegalAcceptanceContext =
| 'REGISTRATION'
| 'LOGIN_RECONSENT'
| 'ORGANIZATION_CREATION'
| 'ADMIN_ROLE_ACCEPTANCE'
| 'DONATION'
/** Für ALLE Benutzer erforderliche Dokumente. */
export const REQUIRED_FOR_ALL: LegalDocumentType[] = ['TERMS', 'PRIVACY']
/** Zusätzlich für Organisationsadministratoren erforderlich. */
export const REQUIRED_FOR_ORG_ADMIN: LegalDocumentType[] = ['ORGANIZATION_DECLARATION']
/** Pflicht-Dokumenttypen für einen Benutzer. */
export function requiredTypesFor(isOrgAdmin: boolean): Set<string> {
return new Set<string>([...REQUIRED_FOR_ALL, ...(isOrgAdmin ? REQUIRED_FOR_ORG_ADMIN : [])])
}
/**
* Kernregel: welche aktiven Pflichtdokumente sind für den Benutzer noch offen?
* @param active aktive Dokumente (eine Version je Typ)
* @param required Menge erforderlicher Dokumenttypen
* @param acceptedKeys bereits akzeptierte Schlüssel `TYPE@version`
*/
export function computePendingDocuments<T extends { type: string; version: string }>(
active: T[],
required: Set<string>,
acceptedKeys: Set<string>,
): T[] {
return active
.filter(d => required.has(d.type))
.filter(d => !acceptedKeys.has(`${d.type}@${d.version}`))
}

View File

@@ -1,26 +1,14 @@
import { prisma } from '@/lib/db'
import {
REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN, requiredTypesFor, computePendingDocuments,
type LegalDocumentType, type LegalAcceptanceContext,
} from '@/lib/legal-rules'
/** Dokumenttypen (mit Prisma-String-Feldern konsistent). */
export type LegalDocumentType =
| 'TERMS'
| 'PRIVACY'
| 'ORGANIZATION_DECLARATION'
| 'RESPONSIBLE_USE'
| 'DATA_PROCESSING_AGREEMENT'
/** Zustimmungskontexte. */
export type LegalAcceptanceContext =
| 'REGISTRATION'
| 'LOGIN_RECONSENT'
| 'ORGANIZATION_CREATION'
| 'ADMIN_ROLE_ACCEPTANCE'
| 'DONATION'
/** Für ALLE Benutzer erforderliche Dokumente. */
export const REQUIRED_FOR_ALL: LegalDocumentType[] = ['TERMS', 'PRIVACY']
/** Zusätzlich für Organisationsadministratoren erforderlich. */
export const REQUIRED_FOR_ORG_ADMIN: LegalDocumentType[] = ['ORGANIZATION_DECLARATION']
// Re-Export für bestehende Importe
export {
REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN, requiredTypesFor, computePendingDocuments,
}
export type { LegalDocumentType, LegalAcceptanceContext }
export interface ActiveDocument {
type: string
@@ -52,10 +40,7 @@ export async function getActiveDocument(type: LegalDocumentType): Promise<Active
* Ein Dokument gilt als offen, wenn für die aktive Version keine Zustimmung des Benutzers existiert.
*/
export async function getPendingAcceptances(userId: string, isOrgAdmin: boolean): Promise<ActiveDocument[]> {
const required = new Set<string>([
...REQUIRED_FOR_ALL,
...(isOrgAdmin ? REQUIRED_FOR_ORG_ADMIN : []),
])
const required = requiredTypesFor(isOrgAdmin)
const active = (await getActiveDocuments()).filter(d => required.has(d.type))
if (active.length === 0) return []
@@ -66,8 +51,8 @@ export async function getPendingAcceptances(userId: string, isOrgAdmin: boolean)
},
select: { documentType: true, documentVersion: true },
})
const acceptedKeys = new Set(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`))
return active.filter(d => !acceptedKeys.has(`${d.type}@${d.version}`))
const acceptedKeys = new Set<string>(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`))
return computePendingDocuments(active, required, acceptedKeys)
}
/** True, wenn der Benutzer alle erforderlichen aktiven Dokumente akzeptiert hat. */

14
vitest.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config'
import path from 'path'
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.{test,spec}.{ts,tsx}'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})