DB 047: legal_documents (versioniert, draft/published/archived) +
legal_document_audit (Änderungslog); Partial-Unique-Index garantiert
max. ein published-Dokument pro document_type.
Backend: GET /api/legal-documents/{type}/published (kein Auth);
Superadmin-CRUD + Publish/Archive + Audit unter /api/admin/legal-documents.
Frontend: LegalPage lädt aus API mit Platzhalter-Fallback;
AdminLegalDocumentsPage (/admin/legal-documents) mit Tab-Navigation,
Versionsliste, Entwurf-Editor, Publish/Archive-Workflow, Änderungslog.
AdminPageNav: Link „Rechtstexte" ergänzt.
version: 0.8.71 (backend + frontend)
module: legal_documents 1.0.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
Compliance-Implementierung – Umsetzungsbericht
Erstellt: 2026-05-09
Zuletzt aktualisiert: 2026-05-10
Audit-Basis: docs/compliance-audit.md
App-Version nach Umsetzung: 0.8.71
Freigegebene Pakete und Umsetzungsstatus
P-01 – Rechtstexte ⚠️ (technischer Teil umgesetzt; juristische Inhalte offen)
Status: Technisch teilweise umgesetzt (2026-05-10, Version 0.8.69)
Betroffene Dateien:
frontend/src/pages/LegalPage.jsx(neu) — Platzhalter-Komponente für alle vier Seitenfrontend/src/App.jsx— 4 neue öffentliche Routenfrontend/src/pages/LoginPage.jsx— Rechtstext-Links im Card-Footerfrontend/src/components/DesktopSidebar.jsx— Rechtstext-Links im Sidebar-Footer
Technische Änderung:
Vier öffentliche Routen angelegt, kein Auth erforderlich, kein Redirect für nicht eingeloggte Nutzer:
/impressum→<LegalPage type="impressum" />/datenschutz→<LegalPage type="datenschutz" />/nutzungsbedingungen→<LegalPage type="nutzungsbedingungen" />/medienrichtlinie→<LegalPage type="medienrichtlinie" />
Jede Seite enthält:
- Deutlich sichtbaren Platzhalterhinweis: „MUSTER / PLATZHALTER – Inhalt wird vor Produktivbetrieb juristisch geprüft und durch den Betreiber ergänzt."
- Strukturierte Pflichtfelder je Rechtstext (Betreiber, Rechtsgrundlagen, Betroffenenrechte etc.)
- Querlinks zu den anderen drei Rechtstextseiten
Links zu allen vier Seiten sind in der Login-/Registrierungsseite (Card-Footer) und im Desktop-Sidebar-Footer sichtbar ohne Anmeldung erreichbar.
Nicht umgesetzt (außerhalb Freigabe):
Juristische Texte, inhaltliche Überprüfung, Admin-konfigurierbare Inhalte, Einwilligungs-Checkboxen.
KRIT-01 Blocking-Status:
Der Blocker KRIT-01 bleibt offen bis juristisch geprüfte Texte durch den Betreiber eingepflegt sind. Die technische Voraussetzung (Routen und Seitenstruktur) ist erfüllt.
Tests: 5 Playwright-Tests, alle grün:
- Impressum ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Datenschutz ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Nutzungsbedingungen ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Medienrichtlinie ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Login-Seite enthält alle vier Rechtstext-Links
P-01b – Mobile/PWA-Erreichbarkeit der Rechtstexte ✅
Status: Umgesetzt (2026-05-10, Version 0.8.70) — Nacharbeit zu P-01
Betroffene Dateien:
frontend/src/pages/SettingsLegalPage.jsx(neu) — Hub-Seite/settings/legalfrontend/src/App.jsx— Route/settings/legalim ProtectedLayoutfrontend/src/pages/AccountSettingsPage.jsx— Link zu/settings/legal
Technische Änderung:
Neue Seite /settings/legal im eingeloggten Einstellungsbereich: Hub-Seite mit Links zu allen vier Rechtstextseiten, erreichbar über Einstellungen → Rechtliches. Entspricht dem bestehenden Pattern von /settings/system. Auf der AccountSettingsPage ist ein Link „Rechtliches" unterhalb der System-Info-Verlinkung ergänzt.
Tests: 3 Playwright-Tests, alle grün (17/17 Gesamt):
- Einstellungen enthält Link zu
/settings/legal /settings/legalenthält Überschrift + alle vier Rechtstext-Links- Jeder Link aus
/settings/legalführt zur korrekten öffentlichen Route
P-01c – Admin-konfigurierbare Rechtstexte ✅
Status: Umgesetzt (2026-05-10, Version 0.8.71) — Nacharbeit zu P-01
Betroffene Dateien:
backend/migrations/047_legal_documents.sql(neu) — Tabellenlegal_documents+legal_document_auditbackend/routers/legal_documents.py(neu) — Öffentliche + Superadmin-Endpointsbackend/main.py— Router-Registrierungfrontend/src/pages/LegalPage.jsx— API-Fetch mit Fallback auf statischen Platzhalterfrontend/src/pages/AdminLegalDocumentsPage.jsx(neu) — Superadmin-UIfrontend/src/App.jsx— Route/admin/legal-documentsfrontend/src/components/AdminPageNav.jsx— Link „Rechtstexte" im Admin-Navfrontend/src/utils/api.js— 8 neue API-Funktionen
Technische Änderung:
Datenbank (Migration 047):
Tabelle legal_documents: versionierte Rechtstexte mit Workflow draft → published → archived. Felder: document_type (impressum | privacy_policy | terms_of_use | media_policy), version (INT, auto-inkrementiert), title, content_sections (JSONB: [{heading, content}]), status, change_note, Timestamps, FK auf Ersteller + Publisher. Partial-Unique-Index: nur ein published-Datensatz pro document_type gleichzeitig. Tabelle legal_document_audit: unveränderlicher Änderungslog je Dokument.
Backend-Endpoints:
| Endpoint | Auth | Beschreibung |
|---|---|---|
GET /api/legal-documents/{type}/published |
Kein | Liefert veröffentlichtes Dokument oder null |
GET /api/admin/legal-documents |
Superadmin | Alle Versionen aller Typen |
POST /api/admin/legal-documents |
Superadmin | Neuen Entwurf anlegen |
GET /api/admin/legal-documents/{id} |
Superadmin | Einzeldokument mit content_sections |
PUT /api/admin/legal-documents/{id} |
Superadmin | Entwurf bearbeiten (nur status=draft) |
POST /api/admin/legal-documents/{id}/publish |
Superadmin | Veröffentlichen; bisherige Version → archived |
POST /api/admin/legal-documents/{id}/archive |
Superadmin | Archivieren |
GET /api/admin/legal-documents/{id}/audit |
Superadmin | Änderungslog |
Frontend:
LegalPage.jsx ruft beim Laden GET /api/legal-documents/{type}/published ab. Gibt die API null zurück (kein veröffentlichtes Dokument vorhanden), zeigt die Seite weiterhin den bisherigen Platzhalter mit MUSTER-Banner. Ist ein Dokument veröffentlicht, wird dessen Inhalt ohne Platzhalter-Banner angezeigt. AdminLegalDocumentsPage.jsx unter /admin/legal-documents (nur Superadmin) ermöglicht Erstellen, Bearbeiten, Veröffentlichen und Archivieren von Entwürfen mit Tabs pro Dokumententyp und Änderungslog.
Kein neues npm-Paket notwendig — JSONB-Struktur mit {heading, content} statt Markdown; keine XSS-Gefahr.
Tests: 3 Playwright-Tests:
- Rechtstextseiten laden ohne Fehler (API-fetch mit Fallback)
/admin/legal-documentserreichbar für Superadmin mit korrekter Überschrift- Admin-Nav enthält Link zu Rechtstexten
P-03 – Papierkorb-Retention-Job aktivieren ✅
Status: Umgesetzt
Betroffene Dateien:
docker-compose.yml– neuer Serviceretention-cron
Technische Änderung:
Neuer Docker-Service retention-cron nutzt dasselbe Backend-Image und führt scripts/media_retention_job.py täglich um 03:00 Uhr aus. Der Service startet beim ersten Hochfahren sofort und schläft bis zum nächsten 3 AM (Python-basierter Loop ohne externe Cron-Abhängigkeit). Zugriff auf DB und Media-Volume identisch zur Backend-Konfiguration.
Tests: Keine automatisierten Tests möglich (Runtime-Verhalten); operativ über Container-Logs (docker logs shinkan-retention-cron) überprüfbar.
P-03b – Retention-Zeiten mit Löschkonzept abgleichen ✅
Status: Umgesetzt (2026-05-10)
Betroffene Dateien:
backend/media_lifecycle.py:24– DefaultHIDDEN_TO_PURGE_DAYSdocker-compose.yml– explizite Env-Variable imretention-cron-Service
Befund des Re-Audits:
Das fachliche Löschkonzept (Audit §6.4) sieht 30 + 30 Tage vor:
- Stufe 1 → Stufe 2 (Soft → Hidden): 30 Tage ✓ (war bereits korrekt)
- Stufe 2 → Purge (Hidden → gelöscht): weitere 30 Tage → Code-Default war 90 Tage ❌
Technische Änderung:
# media_lifecycle.py Zeile 24 (vorher "90", jetzt "30"):
HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "30")))
docker-compose.yml enthält jetzt explizit MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS: "${...:-30}" mit Kommentar, der das 30+30-Konzept dokumentiert. Der Wert kann per Env-Variable in einzelnen Deployments überschrieben werden.
P-04 – Copyright-Pflicht bei Archiv-Promotion vereinheitlichen ✅
Status: Umgesetzt; Tests nachgehärtet (2026-05-10)
Betroffene Dateien:
backend/routers/media_assets.py–patch_media_asset()undbulk_media_patch()backend/tests/test_media_assets_copyright_promotion.py
Technische Änderung:
Beide Endpoints prüfen, ob copyright_notice vorhanden ist, wenn visibility auf club oder official gewechselt wird. Priorität: Wert aus dem Request-Body > bestehender Wert in der DB. Ist beides leer, wird HTTP 400 zurückgegeben.
Fehlermeldung: "Für Vereins- oder offizielle Medien ist eine Urheberrechtsangabe (copyright_notice) Pflicht." (Umlaut korrekt, 2026-05-10 korrigiert)
Tests: 7 Tests, alle grün:
- Promotion zu
clubohne Copyright → 400 (exakt) - Promotion zu
officialohne Copyright → 400 (exakt) - Promotion zu
clubmit Copyright im Body → 200 + Payload-Prüfung (gehärtet) - Promotion zu
club, Copyright bereits auf Asset → 200 + Payload-Prüfung (gehärtet) - Kein Visibility-Wechsel → keine Copyright-Prüfung → 200 (exakt)
- Bulk: ohne Copyright → in
failed,updated_count == 0(exakt) - Bulk: mit Copyright → in
updated,updated_count == 1(exakt)
P-05 – Passwort-Mindestlänge angleichen ⚠️ (Teil 1 von 2 – Re-Audit-Auflage offen)
Status: Teilweise umgesetzt
Betroffene Dateien (initialer Fix 2026-05-09):
backend/routers/auth.py:101–PUT /api/auth/pin:< 4→< 8frontend/src/pages/LoginPage.jsx:175–minLength="6"→minLength="8"frontend/src/pages/AccountSettingsPage.jsx:403–minLength={4}→minLength={8}
Verbleibende Lücke identifiziert im Re-Audit 2026-05-09: POST /api/auth/reset-password hatte kein Mindestlängen-Limit → siehe P-05b.
P-05b – reset-password Mindestlänge 8 Zeichen ✅
Status: Umgesetzt (2026-05-10)
Betroffene Dateien:
backend/models.py:29–PasswordResetConfirm.new_passwordbackend/tests/test_auth_password_reset_minlength.py– 7 neue Tests
Technische Änderung:
# models.py (vorher):
class PasswordResetConfirm(BaseModel):
token: str
new_password: str
# models.py (nachher):
class PasswordResetConfirm(BaseModel):
token: str
new_password: str = Field(min_length=8, max_length=128)
FastAPI lehnt Requests mit new_password < 8 Zeichen nun mit HTTP 422 (Pydantic Validation Error) ab, bevor der Endpoint-Handler ausgeführt wird. Kein DB-Zugriff erfolgt für unvalide Requests.
Tests: 7 Tests, alle grün:
- Leer-String → 422
- 1-Zeichen-Passwort → 422
- 7-Zeichen-Passwort (
"1234567") → 422 - Exakt 8 Zeichen (
"12345678") → 200 ✓ - Langes Passwort → 200 ✓
- Fehlendes
new_password-Feld → 422 - Gültiges Passwort, ungültiger Token → 400
P-05 vollständig geschlossen? Ja — alle passwortverarbeitenden Backend-Endpoints erzwingen jetzt Mindestlänge 8:
| Endpoint | Mindestlänge | Mechanismus |
|---|---|---|
POST /api/auth/register |
8 | if len(password) < 8 im Handler |
PUT /api/auth/pin |
8 | if len(new_pin) < 8 im Handler |
POST /api/auth/reset-password |
8 | Pydantic Field(min_length=8) |
| Management-Reset (profiles.py) | 8 | Pydantic Field(min_length=8) |
| Frontend LoginPage | 8 | minLength="8" |
| Frontend AccountSettingsPage | 8 | minLength={8} |
P-07 – ALLOW_PUBLIC_MEDIA_STATIC Release-Test ✅
Status: Umgesetzt
Betroffene Dateien:
backend/tests/test_security_release.py– 2 neue Tests
Tests: Beide grün.
P-12 – sessionStorage bei Logout bereinigen ✅
Status: Umgesetzt (2026-05-10, Version 0.8.68)
Betroffene Dateien:
frontend/src/context/AuthContext.jsx–logout()tests/dev-smoke-test.spec.js– neuer Playwright-Test
Befund (war offen):
logout() löschte nur localStorage-Einträge. TrainingCoachPage schrieb sessionStorage-Schlüssel mit Präfix sj_coach_ (sj_coach_step_{unitId}, sj_coach_deltas_{unitId}, sj_coach_debrief_{unitId}), die nach Logout im Tab erhalten blieben. Bei Nutzerwechsel im selben Tab (geteilter Rechner) konnte der neue Nutzer Trainingsfortschritt des Vorgängers sehen.
Technische Änderung:
Gezielte Präfix-Löschung aller sj_coach_*-Schlüssel beim Logout (kein sessionStorage.clear()). Fremde sessionStorage-Schlüssel (Browser-Extensions o. ä.) bleiben erhalten.
// AuthContext.jsx logout() — Ergänzung:
for (const key of Object.keys(sessionStorage)) {
if (key.startsWith('sj_coach_')) {
sessionStorage.removeItem(key)
}
}
Begründung gezielte statt vollständiger Löschung:
Alle Shinkan-spezifischen sessionStorage-Schlüssel sind eindeutig über den Präfix sj_coach_ identifizierbar (definiert in TrainingCoachPage.jsx Zeilen 15–25). Ein sessionStorage.clear() würde auch Schlüssel fremder Quellen im selben Tab löschen; die Präfix-Löschung ist spezifischer und sicherer.
Tests: Playwright E2E-Test tests/dev-smoke-test.spec.js (Test „P-12: sessionStorage wird bei Logout bereinigt"):
- Setzt drei
sj_coach_*-Schlüssel und einen fremden Schlüssel - Klickt „Abmelden"
- Prüft:
sj_coach_*→ null (entfernt), fremder Schlüssel → erhalten,authToken→ null (localStorage weiterhin korrekt bereinigt)
Hinweis Tests: Das Projekt verfügt über kein Frontend-Unit-Test-Framework (kein Vitest/Jest in package.json). Der Test ist als Playwright E2E-Test implementiert, der einen laufenden Dev-Server voraussetzt. Automatisierte Ausführung der Playwright-Tests erfordert npx playwright test mit gesetzter PLAYWRIGHT_BASE_URL.
P-23 – LoginPage: minLength + Versionsstring ✅
Status: Umgesetzt
Betroffene Dateien:
frontend/src/pages/LoginPage.jsx
Technische Änderung:
minLength="6"→minLength="8"- Hardcodierter Versionsstring
v0.1.0 • Developmententfernt
P-24 – CORS allow_methods und allow_headers einschränken ✅
Status: Umgesetzt
Betroffene Dateien:
backend/main.py:85-86
Technische Änderung:
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
Test-Zusammenfassung (Stand 0.8.69)
tests/test_auth_password_reset_minlength.py 7 passed (neu, P-05b)
tests/test_media_assets_copyright_promotion.py 7 passed (gehärtet, P-04)
tests/test_security_release.py 9 passed (inkl. 2 P-07-Tests)
Weitere bestehende Tests: 81 passed, 6 skipped
Gesamt (Backend): 104 passed, 6 skipped, 1 failed
Fehlgeschlagener Test: test_list_media_assets_invalid_lifecycle_400
→ Pre-existing: benötigt laufenden PostgreSQL-Container (Hostname "postgres")
→ Bestand bereits vor allen Compliance-Änderungen (verifiziert per git stash)
Playwright E2E (dev-smoke-test.spec.js): 14 passed (inkl. 5 neue P-01-Tests)
→ P-01: 4× Route ohne Auth + Platzhalterhinweis + Reload, 1× Login-Links
→ P-12: sessionStorage-Bereinigung (grün)
Nicht umgesetzte Pakete
Paket-IDs und -Titel gemäß kanonischem Register
docs/compliance-package-register.md. Abweichende Beschreibungen in der Ursprungsversion dieses Abschnitts wurden am 2026-05-10 korrigiert (P-06, P-08, P-09, P-10, P-11, P-18 — Details im Konsistenzbericht des Registers).
| Paket | Kanonischer Titel | Status | Begründung |
|---|---|---|---|
| P-01 | Rechtstexte | offen | Scope ausgeschlossen (juristischer Inhalt) |
| P-02 | Self-Service-Kontolöschung + Datenexport | offen | Scope ausgeschlossen |
| P-06 | Upload-Einwilligungsdialog (Recht am eigenen Bild) | offen | Scope ausgeschlossen |
| P-08 | HSTS / externe Proxy-Sicherheit dokumentieren | offen | Scope ausgeschlossen (außerhalb Repo — Reverse-Proxy) |
| P-09 | Admin-Audit-Log | offen | Scope ausgeschlossen |
| P-10 | Mindestalter-Abfrage | offen | Scope ausgeschlossen |
| P-11 | Legal-Hold Lifecycle-Status | offen | Scope ausgeschlossen |
| P-12 | sessionStorage bei Logout bereinigen | ✅ umgesetzt | Version 0.8.68 — siehe §P-12 oben |
| P-13 | Content-Melde-Backend | offen | Scope ausgeschlossen (erst juristisch klären) |
| P-14 | Moderations-UI | offen | Scope ausgeschlossen |
| P-15 | Uploader-Benachrichtigung bei Sperrung | offen | Scope ausgeschlossen |
| P-16 | Beschwerdeverfahren | offen | Scope ausgeschlossen |
| P-17 | MFA für Superadmins (TOTP) | offen | Scope ausgeschlossen |
| P-18 | HttpOnly-Cookie als Auth-Alternative | offen | Scope ausgeschlossen |
| P-19 | Anti-Virus-Scan (ClamAV) | offen | Scope ausgeschlossen |
| P-20 | VVT erstellen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
| P-21 | AV-Verträge abschließen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
| P-22 | HTML-Sanitizer für Rich-Text-Felder | offen | Scope ausgeschlossen |
Re-Audit-Empfehlung
Operativ nach Deployment 0.8.69 prüfen:
- P-03/P-03b:
docker logs shinkan-retention-cron— Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage - P-04: Manuell: PATCH privates Medium auf
officialohnecopyright_notice→ muss 400 liefern - P-05b: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden
- P-24: Browser DevTools Preflight →
Access-Control-Allow-Headers: content-type, x-auth-token, x-active-club-id
Nächster vollständiger Re-Audit nach Umsetzung der kritischen Findings (P-01: Impressum/Datenschutz, P-02: DSGVO-Löschanfragen).