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:
23
docs/legal/data-processing-inventory.md
Normal file
23
docs/legal/data-processing-inventory.md
Normal 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.
|
||||||
44
docs/legal/incident-response.md
Normal file
44
docs/legal/incident-response.md
Normal 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.
|
||||||
45
docs/legal/licenses-and-attributions.md
Normal file
45
docs/legal/licenses-and-attributions.md
Normal 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.
|
||||||
37
docs/legal/retention-and-deletion.md
Normal file
37
docs/legal/retention-and-deletion.md
Normal 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.
|
||||||
33
docs/legal/subprocessors.md
Normal file
33
docs/legal/subprocessors.md
Normal 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.
|
||||||
59
docs/legal/technical-organizational-measures.md
Normal file
59
docs/legal/technical-organizational-measures.md
Normal 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
1138
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lageplan",
|
"name": "lageplan",
|
||||||
"version": "1.7.3",
|
"version": "1.7.4",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
@@ -88,7 +90,8 @@
|
|||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 Chrome versions",
|
"last 2 Chrome versions",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react'
|
|||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { useAuth } from '@/components/providers/auth-provider'
|
import { useAuth } from '@/components/providers/auth-provider'
|
||||||
import { Loader2, ExternalLink } from 'lucide-react'
|
import { Loader2, ExternalLink } from 'lucide-react'
|
||||||
|
import { ORG_CONFIRMATIONS } from '@/lib/legal-content'
|
||||||
|
|
||||||
interface PendingDoc { type: string; version: string; title: string; url: string | null }
|
interface PendingDoc { type: string; version: string; title: string; url: string | null }
|
||||||
|
|
||||||
@@ -15,16 +16,6 @@ const ALLOWLIST = [
|
|||||||
'/konto', '/forgot-password', '/reset-password',
|
'/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() {
|
export function LegalConsentGate() {
|
||||||
const { user, loading, logout } = useAuth()
|
const { user, loading, logout } = useAuth()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|||||||
32
src/config/__tests__/legal-config.test.ts
Normal file
32
src/config/__tests__/legal-config.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
63
src/lib/__tests__/legal-rules.test.ts
Normal file
63
src/lib/__tests__/legal-rules.test.ts
Normal 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
14
src/lib/legal-content.ts
Normal 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
44
src/lib/legal-rules.ts
Normal 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}`))
|
||||||
|
}
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
import { prisma } from '@/lib/db'
|
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). */
|
// Re-Export für bestehende Importe
|
||||||
export type LegalDocumentType =
|
export {
|
||||||
| 'TERMS'
|
REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN, requiredTypesFor, computePendingDocuments,
|
||||||
| 'PRIVACY'
|
}
|
||||||
| 'ORGANIZATION_DECLARATION'
|
export type { LegalDocumentType, LegalAcceptanceContext }
|
||||||
| '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']
|
|
||||||
|
|
||||||
export interface ActiveDocument {
|
export interface ActiveDocument {
|
||||||
type: string
|
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.
|
* 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[]> {
|
export async function getPendingAcceptances(userId: string, isOrgAdmin: boolean): Promise<ActiveDocument[]> {
|
||||||
const required = new Set<string>([
|
const required = requiredTypesFor(isOrgAdmin)
|
||||||
...REQUIRED_FOR_ALL,
|
|
||||||
...(isOrgAdmin ? REQUIRED_FOR_ORG_ADMIN : []),
|
|
||||||
])
|
|
||||||
const active = (await getActiveDocuments()).filter(d => required.has(d.type))
|
const active = (await getActiveDocuments()).filter(d => required.has(d.type))
|
||||||
if (active.length === 0) return []
|
if (active.length === 0) return []
|
||||||
|
|
||||||
@@ -66,8 +51,8 @@ export async function getPendingAcceptances(userId: string, isOrgAdmin: boolean)
|
|||||||
},
|
},
|
||||||
select: { documentType: true, documentVersion: true },
|
select: { documentType: true, documentVersion: true },
|
||||||
})
|
})
|
||||||
const acceptedKeys = new Set(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`))
|
const acceptedKeys = new Set<string>(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`))
|
||||||
return active.filter(d => !acceptedKeys.has(`${d.type}@${d.version}`))
|
return computePendingDocuments(active, required, acceptedKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True, wenn der Benutzer alle erforderlichen aktiven Dokumente akzeptiert hat. */
|
/** True, wenn der Benutzer alle erforderlichen aktiven Dokumente akzeptiert hat. */
|
||||||
|
|||||||
14
vitest.config.ts
Normal file
14
vitest.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user