All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 55s
567 lines
32 KiB
Markdown
567 lines
32 KiB
Markdown
# 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` → `<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/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.
|
||
|
||
---
|
||
|
||
## 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.
|