# Compliance-Implementierung – Umsetzungsbericht **Erstellt:** 2026-05-09 **Zuletzt aktualisiert:** 2026-05-11 **Audit-Basis:** `docs/compliance-audit.md` **App-Version nach Umsetzung:** 0.8.86 --- ## 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 Kernum­setzung (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 — Kernum­setzung (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. --- ## 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 | 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 prüfen (Stand 0.8.86): 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 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.