# Compliance-Implementierung – Umsetzungsbericht
**Erstellt:** 2026-05-09
**Zuletzt aktualisiert:** 2026-05-11
**Audit-Basis:** `docs/compliance-audit.md`
**App-Version nach Umsetzung:** 0.8.87
---
## 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 Seiten
- `frontend/src/App.jsx` — 4 neue öffentliche Routen
- `frontend/src/pages/LoginPage.jsx` — Rechtstext-Links im Card-Footer
- `frontend/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` → ``
- `/datenschutz` → ``
- `/nutzungsbedingungen` → ``
- `/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/legal`
- `frontend/src/App.jsx` — Route `/settings/legal` im ProtectedLayout
- `frontend/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/legal` enthält Überschrift + alle vier Rechtstext-Links
- Jeder Link aus `/settings/legal` fü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) — Tabellen `legal_documents` + `legal_document_audit`
- `backend/routers/legal_documents.py` (neu) — Öffentliche + Superadmin-Endpoints
- `backend/main.py` — Router-Registrierung
- `frontend/src/pages/LegalPage.jsx` — API-Fetch mit Fallback auf statischen Platzhalter
- `frontend/src/pages/AdminLegalDocumentsPage.jsx` (neu) — Superadmin-UI
- `frontend/src/App.jsx` — Route `/admin/legal-documents`
- `frontend/src/components/AdminPageNav.jsx` — Link „Rechtstexte" im Admin-Nav
- `frontend/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-documents` erreichbar für Superadmin mit korrekter Überschrift
- Admin-Nav enthält Link zu Rechtstexten
---
### P-01c Erweiterung — Als-Entwurf-kopieren ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.72) — Ergänzung zu P-01c
**Betroffene Dateien:**
- `backend/routers/legal_documents.py` — Neuer Endpoint `POST /api/admin/legal-documents/{id}/copy-as-draft`
- `frontend/src/utils/api.js` — `copyLegalDocumentAsDraft(id)`
- `frontend/src/pages/AdminLegalDocumentsPage.jsx` — „Als Entwurf kopieren"-Button in der Dokumentliste
**Technische Änderung:**
Neuer Superadmin-Endpoint kopiert `title` und `content_sections` eines bestehenden Dokuments in einen neuen Entwurf. Die Versionsnummer wird dabei automatisch inkrementiert (letztes Dokument dieses Typs + 1). Status ist immer `draft`. Ermöglicht inkrementelle Überarbeitung ohne vollständige Neueingabe.
**Motivation:** Bei jeder fälligen Textanpassung mussten alle Abschnitte neu erfasst werden. Die Kopierfunktion ermöglicht, den letzten Stand zu übernehmen und nur die geänderten Abschnitte zu bearbeiten.
---
### P-01c Erweiterung — Echter PDF-Download + Abschnitts-Sortierung ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.74) — Ergänzung zu P-01c
**Betroffene Dateien:**
- `frontend/src/pages/AdminLegalDocumentsPage.jsx` — `generateLegalPdf()` via jsPDF, `SectionEditor` mit Sortierung
- `frontend/src/pages/LegalPage.jsx` — `generateLegalPdf()` via jsPDF, Button „PDF herunterladen"
- `frontend/package.json` — neues npm-Paket `jspdf`
**Technische Änderung — PDF:**
Ersetzt die bisherige `window.open()` + `window.print()`-Lösung (Browser-Druckdialog) durch `jsPDF`. Die Funktion `generateLegalPdf(doc)` erzeugt ein A4-PDF client-seitig mit:
- Titel (bold, 20 pt), Metazeile (Version + Gültigkeitsdatum), Trennlinie
- Abschnitte mit Heading (bold, 11 pt) und Fließtext (10 pt, `splitTextToSize` für automatischen Zeilenumbruch)
- Automatischer Seitenumbruch bei `y > 277 mm`
- Footer auf jeder Seite: „Shinkan Jinkendo | Exportiert am DD.MM.YYYY Seite X von Y"
- Direkter Dateidownload via `pdf.save('{document_type}_v{version}.pdf')`
Gilt sowohl für die Admin-Seite (Download aus der Dokumentliste, ruft `getLegalDocument(id)` für Volldokument ab) als auch für `LegalPage.jsx` (öffentlich, nur bei veröffentlichten Dokumenten).
**Technische Änderung — Abschnitts-Sortierung und Einfügen:**
`SectionEditor` in `AdminLegalDocumentsPage.jsx` erhält:
- ▲/▼-Buttons pro Abschnitt (deaktiviert an den Rändern) — Reihenfolge per Array-Swap
- `InsertButton` zwischen jedem Abschnitt (inkl. vor dem ersten) — fügt leeren Abschnitt an beliebiger Stelle per `splice` ein
- Kein Drag-and-Drop-Framework — reine React-State-Manipulation
---
### P-03 – Papierkorb-Retention-Job aktivieren ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `docker-compose.yml` – neuer Service `retention-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` – Default `HIDDEN_TO_PURGE_DAYS`
- `docker-compose.yml` – explizite Env-Variable im `retention-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:**
```python
# 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()` und `bulk_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 `club` ohne Copyright → **400** (exakt)
- Promotion zu `official` ohne Copyright → **400** (exakt)
- Promotion zu `club` mit 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` → `< 8`
- `frontend/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_password`
- `backend/tests/test_auth_password_reset_minlength.py` – 7 neue Tests
**Technische Änderung:**
```python
# 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-11 – Legal-Hold Lifecycle-Status ✅
**Status:** Vollständig umgesetzt (2026-05-11, Version 0.8.84 + Nachfixe 0.8.85–0.8.86)
#### P-11 Kernumsetzung (Version 0.8.84)
**Betroffene Dateien:**
- `backend/migrations/051_legal_hold.sql` – neue Spalten `legal_hold_active`, `legal_hold_reason_code`, `legal_hold_reason_note`, `legal_hold_set_by_profile_id`, `legal_hold_set_at`, `legal_hold_released_by_profile_id`, `legal_hold_released_at`, `legal_hold_release_note` in `media_assets`; Audit-Log-CHECK um `legal_hold_set`/`legal_hold_released` erweitert
- `backend/media_legal_hold.py` – Neu: zentrale Services `set_legal_hold`, `release_legal_hold`, `assert_not_under_legal_hold`, `is_media_available_for_normal_use`, `assert_superadmin_for_legal_hold`
- `backend/media_lifecycle.py` – `run_retention_pass()` filtert `AND (legal_hold_active = FALSE OR legal_hold_active IS NULL)` in beiden Retention-Queries
- `backend/routers/media_assets.py` – `admin_legal_hold_router` mit drei Endpoints; `_list_active_visibility_clause()` berücksichtigt `include_legal_hold`-Parameter; `download_media_asset_file()` prüft Legal-Hold für Nicht-Superadmins
- `backend/routers/exercises.py` – `attach_exercise_media_from_asset()` ruft `assert_not_under_legal_hold()` vor Verknüpfung auf
- `backend/main.py` – `app.include_router(media_assets.admin_legal_hold_router)`
- `frontend/src/utils/api.js` – `setMediaAssetLegalHold`, `releaseMediaAssetLegalHold`, `listMediaAssetsWithLegalHold`
- `frontend/src/pages/MediaLibraryPage.jsx` – Legal-Hold-Badge auf Kacheln; Superadmin-Aktionen „Sofort sperren"/„Sperre aufheben" im Edit-Modal; Bestätigungs-Dialog mit Pflichtfeldern; Journal-Renderpfad für `legal_hold_set`/`legal_hold_released`
- `frontend/src/app.css` – CSS-Klassen für Legal-Hold-Badge, -Dialog, -Button
**Neue API-Endpoints (Superadmin):**
- `POST /api/admin/media-assets/{asset_id}/legal-hold` — Sofortsperre setzen (reason_code + reason_note Pflicht)
- `POST /api/admin/media-assets/{asset_id}/legal-hold/release` — Sofortsperre aufheben (release_note Pflicht)
- `GET /api/admin/media-assets/legal-hold` — Liste aller aktuell gesperrten Assets
**Tests:** 15 Backend-Unit-Tests in `backend/tests/test_p11_legal_hold.py` — alle grün.
#### P-11 Nachfixe – UI-Bugs und Sichtbarkeitskorrektur (Version 0.8.85–0.8.86)
Nach Erstimplementierung wurden beim Testen weitere Mängel festgestellt und behoben:
**Version 0.8.85 – UI-Bugfixes:**
- Fix: `submitLegalHold` rief `loadMedia()` statt `loadItems()` auf → "loadMedia is not a function" beim Sperren
- Fix: Listabfrage in `list_media_assets` enthielt `legal_hold_active`, `reason_code`, `reason_note`, `set_at` nicht → Badge und „Sperre aufheben"-Button im Modal waren nie sichtbar
- Fix: Journal-Renderpfad verwendete Keys `nw.legal_hold_reason_code/note` statt `nw.reason_code/note` (Audit-Log-Format) → Begründung und Kommentar nicht angezeigt
- Fix: Archiv-Picker (ExerciseFormPage, ExerciseInlineFileMediaModal) filterte Legal-Hold-Assets bereits client-seitig heraus
**Version 0.8.86 – Vollständige Absicherung der Auslieferung:**
Betroffene Dateien:
- `backend/routers/exercises.py` – `download_exercise_media_file()` gibt HTTP 451 zurück wenn `asset_legal_hold_active=TRUE` (Datei wird nicht ausgeliefert); `enrich_exercise_detail()` SELECT erweitert um `ma.legal_hold_active AS asset_legal_hold_active`
- `backend/routers/media_assets.py` – `list_media_assets` übergibt `include_legal_hold=is_sup` statt `include_legal_hold=(is_plat or is_sup)` — Legal-Hold-Assets nur noch für Superadmin sichtbar, nicht für alle Plattform-Admins
- `frontend/src/components/ExerciseMediaEmbed.jsx` – Zeigt „Medium nicht verfügbar (gesperrt)" statt Datei wenn `asset_legal_hold_active`
- `frontend/src/components/ExerciseMediaThumbTile.jsx` – Zeigt rot-markierte „Gesperrt"-Kachel statt Dateivorschau; kein Preview-Trigger
- `frontend/src/pages/ExerciseFormPage.jsx` – Vorschau-Modal zeigt Hinweis statt Datei wenn `asset_legal_hold_active`
#### Sicherheitsarchitektur (vollständig)
- Legal-Hold ist orthogonal zum normalen Papierkorb-Lifecycle (P-03) — kein 30-Tage-Warten
- `rights_status='blocked'` wird als Schnell-Spiegel gesetzt und bei Aufhebung basierend auf vorhandenen Deklarationen wiederhergestellt (`declared` wenn Deklaration vorhanden, sonst `legacy_unreviewed`)
- Nur Superadmin darf setzen/aufheben; nur Superadmin sieht Legal-Hold-Assets in der Medienliste; normale Nutzer sehen gesperrte Assets nicht
- Retention-Job überspringt Legal-Hold-Assets (verhindert versehentliche Löschung unter laufender Sperrmaßnahme)
- `assert_not_under_legal_hold()` blockiert das Verknüpfen von Legal-Hold-Assets mit Übungen
- Dateiauslieferung (`download_exercise_media_file`) gibt HTTP 451 zurück — keine Umgehung via direkten File-Endpoint
- Frontend-Komponenten zeigen Placeholder statt Datei, auch wenn das Asset bereits in einer Übung verknüpft ist
---
### 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.
```javascript
// 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 • Development` entfernt
---
### P-24 – CORS allow_methods und allow_headers einschränken ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `backend/main.py:85-86`
**Technische Änderung:**
```python
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
```
---
## Test-Zusammenfassung (Stand 0.8.86)
```
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_media_rights_declaration.py 25 passed (neu, P-06a–P-06d)
tests/test_security_release.py 9 passed (inkl. 2 P-07-Tests)
tests/test_p11_legal_hold.py 15 passed (neu, P-11)
Weitere bestehende Tests: 81 passed, 6 skipped
Gesamt (Backend): 144 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): 22+ passed (inkl. P-06-Tests)
→ P-01: 4× Route ohne Auth + Platzhalterhinweis + Reload, 1× Login-Links
→ P-01b: 3× /settings/legal (Link in Einstellungen, Überschrift, Rechtstext-Links)
→ P-01c: 3× Admin-Rechtstexte (Seitenladung, Route /admin/legal-documents, Admin-Nav)
→ P-12: sessionStorage-Bereinigung (grün)
→ P-06: 5× RightsDeclarationDialog (Anzeige, Pflichtfeld-Validierung, Upload-Flow,
Promotions-Dialog, Legacy-Altbestand-Indikator)
Anmerkung jsPDF (0.8.74):
→ PDF-Download via pdf.save() ist ein Browser-Download, kein Server-Request.
→ Kein Backend-Test möglich; funktional im Browser verifizierbar.
→ Playwright-E2E-Test erfordert laufenden Dev-Server (npx playwright test).
Anmerkung P-06+ Journal/Korrektur (0.8.82–0.8.83):
→ Journal-Endpoint und Korrektur-Endpoint durch manuellen API-Test (curl) auf Dev-System verifiziert.
→ Bugfix club_admin-Prüfung (has_club_role) verifiziert: 500 → 200 nach Fix.
→ Keine dedizierte Playwright-Testsuite für Journal-Modal und Korrektur-Formular (UI-Verifikation ausstehend).
Anmerkung P-11 Frontend-Absicherung (0.8.85–0.8.86):
→ UI-Fixes (loadItems, Badge-Sichtbarkeit, Journal-Keys) manuell auf Dev-System verifiziert.
→ 15 Backend-Unit-Tests decken Services und Retention-Schutz ab.
→ Keine Playwright-Tests für Legal-Hold-Aktionen im Modal — manuelle UI-Verifikation.
```
---
### P-06 – Upload-Einwilligungsdialog ⚠️ (technisch vollständig umgesetzt inkl. P-06+; juristische Validierung offen)
**Status:** Technisch vollständig umgesetzt (inkl. P-06+ Volljournal + Korrektur, Version 0.8.83) — KRIT-04 bleibt offen.
**Deklarationsversion:** `p06-v1-conservative`
---
#### P-06a–P-06d — Kernumsetzung (Version 0.8.75)
**Betroffene Dateien:**
- `backend/migrations/048_media_rights_declarations.sql` (neu) — Append-only Deklarations-Log + 3 Schnellfelder in `media_assets` (`rights_status`, `rights_declared_for_visibility`, `rights_declared_at`)
- `backend/migrations/049_media_rights_consent_context.sql` (neu) — Kontext-Freitextfelder in `media_asset_rights_declarations` (`person_consent_context`, `parental_consent_context`, `music_rights_context`, `third_party_rights_context`)
- `backend/media_rights.py` (neu) — Zentrales Policy-Modul: `validate_rights_declaration`, `check_rights_coverage`, `assert_rights_for_promotion`, `assert_rights_for_exercise_link`, `write_rights_declaration`, `update_rights_quick_fields`
- `backend/routers/media_assets.py` — P-06-Enforcement in Bulk-Upload, PATCH, Bulk-PATCH; 3 neue Endpoints
- `backend/routers/exercises.py` — P-06 bei `upload_exercise_media` (neue Assets) und `attach_exercise_media_from_asset`
- `frontend/src/components/RightsDeclarationDialog.jsx` (neu) — Einwilligungsdialog (9 Pflichtfelder + Kontext-Freitexte)
- `frontend/src/pages/MediaLibraryPage.jsx` — Dialog-Integration vor Bulk-Upload; Altbestand-Indikator
- `frontend/src/pages/ExerciseInlineFileMediaModal.jsx` + `ExerciseInlineEmbedModal.jsx` — RightsDeclarationDialog vor Upload
- `frontend/src/utils/api.js` — `bulkUploadMediaAssets` erweitert um P-06-Felder
- `backend/tests/test_media_rights_declaration.py` (neu) — 25 Unit/HTTP-Tests
**Neue Endpoints (P-06b):**
| Endpoint | Beschreibung |
|----------|-------------|
| `POST /api/media-assets/{id}/rights-declarations` | Explizite Re-/Nachdeklaration |
| `GET /api/admin/media-rights/legacy-summary` | Zusammenfassung Altbestand nach Sichtbarkeit (Plattform-Admin) |
| `GET /api/admin/media-rights/legacy-assets` | Paginierte Liste Altbestand club/official (Plattform-Admin) |
**Abweichung von Spec §3 (konservative Erstannahme):**
Person-Fragen sind auch bei Sichtbarkeit `private` Pflicht (§10.1 in `docs/p06-upload-rights-spec.md`).
**Altbestand (Legacy):**
Alle vor Migration 048 hochgeladenen Medien erhalten `rights_status = 'legacy_unreviewed'`.
Promotion blockiert bis Nachdeklaration. In Bibliotheks-UI als „Altbestand ⚠" markiert.
---
#### P-06+ — Volljournal + Korrektur (Version 0.8.82–0.8.83)
**Motivation:** Vollständige Nachvollziehbarkeit aller Medien-Ereignisse, nicht nur der Deklarationen. Plus: Möglichkeit, fehlerhafte Deklarationen mit Begründung nachträglich zu korrigieren (append-only — neueste gilt).
**Betroffene Dateien:**
- `backend/migrations/050_media_audit_log.sql` (neu) — Neue Tabelle `media_asset_audit_log` + `correction_note TEXT` in Deklarations-Tabelle + erweiterter CHECK (action_type += 'correction')
- `backend/media_rights.py` — Neue Funktionen: `write_audit_log_entry()`, `write_rights_correction_declaration()`
- `backend/routers/media_assets.py` — Neues Pydantic-Model `RightsCorrectionBody`; PATCH-Endpoint schreibt Audit-Log-Einträge; Lifecycle-Aktionen schreiben `lifecycle_change`-Einträge; 2 neue Endpoints; Import `has_club_role` ergänzt
- `frontend/src/pages/MediaLibraryPage.jsx` — Journal-Modal komplett überarbeitet: unified `events[]`-Ansicht; Korrektur-Formular inline; Helper-Funktionen `actionTypeLabel`, `eventTypeLabel`, `visLabel`
- `frontend/src/utils/api.js` — Neue Funktion `addMediaAssetDeclarationCorrection(assetId, body)`
- `frontend/src/app.css` — CSS für Audit-Einträge (`--audit`), Korrektur-Einträge (`--correction`), Korrektur-Formular
**Neue Endpoints (P-06+):**
| Endpoint | Auth | Beschreibung |
|----------|------|-------------|
| `GET /api/admin/media-rights/assets/{id}/journal` | Superadmin / Uploader / Vereins-Admin | Volljournal: `events[]` aus Deklarationen + Audit chronologisch gemischt. Gibt `can_correct` zurück. |
| `POST /api/admin/media-rights/assets/{id}/correction` | Superadmin / Uploader / Vereins-Admin | Korrektur-Deklaration (append-only, neueste gilt). Felder = P-06-Dialog + `correction_note`. |
**Automatische Audit-Log-Einträge:**
| event_type | Auslöser |
|-----------|----------|
| `visibility_change` | PATCH wenn `visibility` oder `club_id` sich ändert |
| `copyright_change` | PATCH wenn `copyright_notice` sich ändert |
| `metadata_change` | PATCH wenn `original_filename` etc. sich ändert |
| `lifecycle_change` | Lifecycle-Aktionen: trash_soft, trash_hidden, recover, reactivate (nicht bei Hard-Delete/Purge) |
**Bugfixes in P-06+:**
| Bug | Fix |
|-----|-----|
| Journal + Korrektur gaben 500 (falsches Schema club_admin) | `has_club_role(cur, profile_id, club_id, "club_admin")` statt `AND role = 'admin'` in `club_members` |
| Frontend-Build-Fehler: doppelte `lcLabel`-Deklaration | Duplikat in Zeile 264 von `MediaLibraryPage.jsx` entfernt |
**KRIT-04 Status:**
Offen. Juristische Validierung der Feldtexte (§10.3 p06-v1-conservative, T1–T10), KUG/DSGVO-Anforderungen (§7.1–§7.12 der Spec) und Korrekturfähigkeit (Spec §11.9) steht aus.
Referenz: `docs/p06-upload-rights-spec.md` §10.5, §11.9.
---
### P-13 – Content-Melde-Backend ✅
**Status:** Vollständig umgesetzt (2026-05-11, Version 0.8.87)
**Finding:** KRIT-03
**Architekturentscheidung:** Anstelle einer separaten Moderations-Queue wurde die bestehende Admin-Inbox (`InboxPage.jsx`) um einen zweiten Abschnitt erweitert. Keine generische `inbox_items`-Tabelle, keine separate `/api/admin/reports`-Queue.
**Betroffene Dateien:**
- `backend/migrations/052_content_reports.sql` (neu) — Tabelle `content_reports` mit Status-Workflow, Priorisierung, 3 Indizes
- `backend/routers/content_reports.py` (neu) — alle Endpoints
- `backend/tests/test_p13_content_reports.py` (neu) — 15 Unit-Tests
- `backend/main.py` — Router-Registrierung
- `backend/version.py` — Version 0.8.87, content_reports 1.0.0
- `frontend/src/utils/api.js` — 5 neue API-Funktionen (submitContentReport, getInboxContentReports, getContentReport, patchContentReport, setLegalHoldFromReport)
- `frontend/src/context/OrgInboxContext.jsx` — contentReports-State, contentReportCount, canAccessContentReports, isSuperadmin
- `frontend/src/pages/InboxPage.jsx` — zweiter Abschnitt „Inhaltsmeldungen", ReportDetailModal
**API-Endpoints:**
| Endpoint | Auth | Beschreibung |
|----------|------|--------------|
| `POST /api/content-reports` | Optional | Meldung einreichen (official-Medien ohne Login; eingeloggt: alle sichtbaren Medien) |
| `GET /api/me/inbox/content-reports` | Plattform-Admin | Liste aller Meldungen, JOIN auf Zieltabellen |
| `GET /api/content-reports/{id}` | Plattform-Admin | Einzel-Detail |
| `PATCH /api/content-reports/{id}` | Plattform-Admin | Status/Notiz/Zuweisung; resolution_note Pflicht für Abschluss-Status |
| `POST /api/content-reports/{id}/legal-hold` | Superadmin | Legal Hold via P-11 `set_legal_hold()`; setzt Status auf `resolved_legal_hold` |
**Reason-Code-Mapping (P-13 → P-11):**
| Meldegrund | Legal-Hold reason_code |
|------------|------------------------|
| copyright | copyright_complaint |
| image_rights | rights_dispute |
| privacy | privacy_complaint |
| minors | youth_protection |
| illegal_content | illegal_content |
| youth_protection | youth_protection |
| offensive_content | illegal_content |
| other | other |
**Nicht in P-13-Scope:** P-14 (Moderations-UI), P-15 (Uploader-Benachrichtigung per E-Mail), P-16 (Beschwerdeverfahren). SMTP-Infrastruktur vorhanden; E-Mail-Benachrichtigungen folgen in P-15.
---
## 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) | **teilweise umgesetzt** | Technisch umgesetzt (2026-05-11, v0.8.75) unter vorläufigen Erstannahmen `p06-v1-conservative` — siehe §P-06 unten. KRIT-04 bleibt offen bis juristische Validierung. |
| 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 | ✅ umgesetzt | Version 0.8.84–0.8.86 — siehe §P-11 oben |
| P-12 | sessionStorage bei Logout bereinigen | ✅ umgesetzt | Version 0.8.68 — siehe §P-12 oben |
| P-13 | Content-Melde-Backend | ✅ umgesetzt | Version 0.8.87 — siehe §P-13 unten |
| 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 prüfen (Stand 0.8.87):
1. **P-03/P-03b**: `docker logs shinkan-retention-cron` — Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage
2. **P-04**: Manuell: PATCH privates Medium auf `official` ohne `copyright_notice` → muss 400 liefern
3. **P-05b**: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden
4. **P-24**: Browser DevTools Preflight → `Access-Control-Allow-Headers: content-type, x-auth-token, x-active-club-id`
5. **P-06**: Manuell: Upload ohne `rights_holder_confirmed` → muss 400 liefern; Journal-Endpoint für vorhandene Assets → muss 200 + `events[]` liefern; Korrektur-Endpoint → muss neue Deklaration mit `action_type='correction'` schreiben
6. **P-06 Audit-Log**: PATCH Sichtbarkeit eines Assets → `media_asset_audit_log` muss Eintrag `visibility_change` enthalten
7. **P-11**: Superadmin → Medium sperren → in Übung öffnen → Kachel zeigt „Gesperrt"; direkter Dateiaufruf `/exercises/{id}/media/{mid}/file` → muss HTTP 451 liefern; Plattform-Admin (kein Superadmin) → gesperrtes Medium darf in Medienliste nicht erscheinen
8. **P-13**: Anonym → `POST /api/content-reports` für ein official-Medium ohne Auth → muss 200 liefern; gültige Meldung ohne `good_faith_confirmed=true` → muss 400 liefern; Plattform-Admin → `GET /api/me/inbox/content-reports` → Liste; Superadmin → `POST /api/content-reports/{id}/legal-hold` → setzt Legal Hold + Report-Status `resolved_legal_hold`
Nächster vollständiger Re-Audit empfohlen nach juristischer Klärung P-06/KRIT-04 (Textfreigabe T1–T10) und nach Einpflegen der Rechtstexte P-01 durch Betreiber.