diff --git a/.claude/docs/PROJECT_STATUS.md b/.claude/docs/PROJECT_STATUS.md index 1fcaccc..c0333bc 100644 --- a/.claude/docs/PROJECT_STATUS.md +++ b/.claude/docs/PROJECT_STATUS.md @@ -1,26 +1,33 @@ # Shinkan Jinkendo - Projekt-Status -**Stand:** 2026-05-05 -**Version (Code):** 0.8.10 (`backend/version.py`, APP_VERSION) -**DB-Schema-Version:** `20260505037` +**Stand:** 2026-05-07 +**Version (Code):** 0.8.48 (`backend/version.py`, APP_VERSION) +**DB-Schema-Version:** `20260507045` (u. a. `media_assets`, `platform_media_storage`) **Branch:** develop --- ## Executive Summary -**Aktueller Meilenstein:** **Trainingsrahmenprogramm Bibliothek + Slot‑Blueprint** (DB **036–037**): Rahmenkopf nur als Vorlage mit Kontext‑Stammdaten; pro Slot genau eine **Blueprint‑`training_unit`** mit **`framework_unit_sections`/`_items`** wie die Planung; Kalenderliste blendet Blueprints aus; **`POST /api/training-units/from-framework-slot`** materialisiert Kopien mit **`origin_framework_slot_id`**. Parallel: **Progressionsgraph** (032–034) bleibt unterstützend (**`TRAINING_FRAMEWORK_SPEC.md`** §3–§4). +**Aktueller Meilenstein (Medien):** **Medienbibliothek `/media`** ergänzt den Archiv-Picker: **Lifecycle-Filter** (aktiv / Papierkorb / ausgeblendet), **Copyright bearbeiten** (`PATCH`), **Vorschau** inkl. Papierkorb bei Verwaltungsrecht; API-Liste `copyright_notice`; zuvor **§4.2 Promotion official** (Übung + Medien). -**Letzte dokumentierte Änderungen (Mai 2026):** +**Parallel weiter relevant:** **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. -- ✅ Migration **036:** Rahmen nur Bibliothek; Fokus/Stil + M:N Trainingsarten/Zielgruppen; Entfall `plan_mode`/`group_id` am Kopf. -- ✅ Migration **037:** `training_units.framework_slot_id` / `origin_framework_slot_id`; Migration Entfall **`training_framework_slot_exercises`**. -- ✅ APIs: erweiterte Rahmen‑Hydration (`sections`, `exercises`, `blueprint_training_unit_id`); Planung siehe **`TRAINING_FRAMEWORK_SPEC.md`** §2.4. -- ✅ Frontend: `createTrainingUnitFromFrameworkSlot` in `api.js`. +**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) -**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) · Rahmen/Graph: [`technical/TRAINING_FRAMEWORK_SPEC.md`](technical/TRAINING_FRAMEWORK_SPEC.md) +**Nächste Schritte — Medien & Archiv** (neu priorisiert, Stand 2026-05-07): -**Nächste Schritte (Auszug):** +1. ~~**Übung → `official` Promotion** inkl. Medien-Anhebung + **Copyright-Pflicht** bei `official` (Spec §4.2)~~ — umgesetzt (0.8.47). +2. ~~**Eigenständige Medienmanager-Seite**~~ — **Basis umgesetzt** (`/media`): Filter, Copyright, Lifecycle-Aktionen; Ausbau: Sichtbarkeit bearbeiten, Bulk, Quotas. +3. **Tests & Observability:** gezielte pytest-Abdeckung für Archiv/Verknüpfen; optional Retention-Job-Dry-Run dokumentieren. +4. **S3 / externes Backend** hinter Speicher-Abstraktion (Spec §7) — nach stabiler Nutzung lokaler/NAS-Pfade. +5. **Inline-Medien im Fließtext** (Spec §11) — bewusst **nach** Promotion/Copyright und tragfähigem Archiv-Workflow. + +**Inline:** Leitplanken in **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**; kein Big-Bang vor stabiler Archiv-/Governance-Basis. + +--- + +**Nächste Schritte (Auszug — Planung/Rahmen):** 1. Kalender‑UI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk. 2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API). @@ -43,6 +50,7 @@ | **030** | **training_unit_exercises.exercise_variant_id** | ✅ | 🔲 | | **032–034** | **Progressionsgraph Übung→Übung** | ✅ | 🔲 | | **035–037** | **Rahmenprogramm, Bibliothek‑Kopf, Slot‑Blueprint‑Units** | ✅ | 🔲 | +| **040–045** | **u. a. Mitgliedschaften, Übungs-Governance, `media_assets`, Plattform-Speicherpfad** | ✅ | 🔲 | --- @@ -95,6 +103,10 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu ### 🔲 In Arbeit / Backlog +- [x] **Medien:** Papierkorb (§5), Retention-Job, Archiv-API, „Aus Archiv verknüpfen“, Picker/Vorschau in Übungsbearbeitung (Release 0.8.42 ff.) +- [x] **Medien:** Promotion Übung↔Medien + Copyright-Pflicht `official` (Spec §4.2) — 0.8.47 +- [x] **Medien:** Medienbibliothek `/media` (Lifecycle-Filter, Copyright PATCH, Vorschau); Ausbau Manager/Bulk/S3 — Roadmap +- [ ] **Medien:** Inline im Fließtext — nach Spec §11, nach Promotion/Archiv-Reife - [ ] Admin-UI für Skill-Kategorien (CRUD) – falls noch offen - [ ] Responsive Design / Dark Mode / PWA - [ ] KI-Suche (`ai_search`) über reine Volltextsuche hinaus @@ -125,7 +137,7 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu ### Dev -Branch `develop`; Migrations bis mindestens **037** auf dem aktuellen Entwicklungsstand; Details in `backend/version.py`. +Branch `develop`; Migrations bis mindestens **045** auf dem aktuellen Entwicklungsstand; Details in `backend/version.py`. ### Prod @@ -146,7 +158,8 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes | Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise | | Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) | | Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Limits) | -| Projektstatus | `PROJECT_STATUS.md` | 2026-05-05 | ✅ Diese Datei | +| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-07 | ✅ §11 Inline-Plan, Drift-Tab | +| Projektstatus | `PROJECT_STATUS.md` | 2026-05-07 | ✅ Medien-Meilenstein aktualisiert | --- @@ -157,4 +170,4 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes --- -**Letzte Aktualisierung:** 2026-05-05 +**Letzte Aktualisierung:** 2026-05-07 diff --git a/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md b/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md index d8d4e67..2daf6d6 100644 --- a/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md +++ b/.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md @@ -118,8 +118,9 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`). ## 7. Referenzen - `.claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md` – übergeordnetes Zielbild & Begriffe. +- `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` – verbindliche Domänenregeln für **Medien-Assets** (gleiche Sichtbarkeit wie Übungen, Promotion-Kopplung, Copyright, Papierkorb/Lebenszyklus, externer Speicher). Bei Widerspruch zur Sichtbarkeits-Tabelle in §3 dieses Dokuments: §3 für Enums/`library_content_*`-Semantik, Medien-Spez für Asset-spezifische Zusatzregeln. - `backend/club_tenancy.py` – bestehende Bausteine (`assert_club_member`, `exercise_visible_to_profile`, …); Ziel ist Deren schrittweise Zusammenführung unter die neue Zugriffsschicht ohne Big-Bang. --- -**Letzte Aktualisierung:** 2026-05-06 +**Letzte Aktualisierung:** 2026-05-07 diff --git a/.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md b/.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md new file mode 100644 index 0000000..5aa91d8 --- /dev/null +++ b/.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md @@ -0,0 +1,238 @@ +# Medien-Assets, Archiv & Lebenszyklus (Single Source of Truth) + +**Status:** verbindlich für Design, API und DB-Migrationen zum Thema Medien +**Stand:** 2026-05-07 (§11 Inline-Plan ergänzt) +**ersetzt/ergänzt:** operatives Upload-Format/Größen siehe weiterhin `MEDIA_UPLOAD_SPEC.md` +**normative Governance:** Sichtbarkeit & Mandanten wie `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`; bei Abweichung zwischen Dokumenten hat der Zugriffsplan Vorrang, **außer** dieses Dokument präzisiert explizit nur den Medien-Domain. + +--- + +## 1. Zweck und Abgrenzung + +### 1.1 Zweck + +- **Ein physisches Medium** („Datei-Asset“) wird **einmal** gespeichert und **mehrfach** mit Übungen verknüpft (Wiederverwendung, keine Dubletten auf der Platte). +- **Sichtbarkeit** von Datei-Assets folgt **derselben Semantik** wie bei Übungen (`private` \| `club` \| `official` – ggf. spätere Enum-Erweiterungen nur gemeinsam mit Migration + ACCESS_LAYER-Doku). +- **Superadmin** kann Medien **global** freigeben (fachlich: Stufe wie „offiziell“ / plattformweit lesbar gemäß `library_content_*`-Regeln). +- **Copyright:** pro Asset sind **Vermerke** vorgesehen und an `official` / Vereinsnutzung anzubinden (Validierungsstufen in Umsetzung festlegen). +- **Externer Speicher:** Speicherung darf **lokal/NAS** (Filesystem) oder **extern** (z. B. S3-kompatibler Dienst) erfolgen – die App arbeitet über eine **Speicher-Abstraktion** (Interface), nicht mit verstreuten `Path`-Annahmen. +- **Papierkorb & Retention:** mehrstufiger Lebenszyklus mit **automatischen** und **manuellen** Übergängen, siehe §5. + +### 1.2 Abgrenzung + +- **Embeds** (reine externe URL, kein Upload) bleiben pro **Verknüpfung zur Übung** möglich (`embed_url` o. Ä.); sie haben **keinen** physischen Lebenszyklus wie Datei-Assets (kein Papierkorb „Stufe 3 physische Löschung“). Trotzdem: UI kann „Link ungültig“ o. Ä. separat abbilden. +- **Übungs-Governance** (wer eine Übung bearbeiten darf) **bleibt maßgeblich** für **Anlegen/Entfernen/Umsortieren** von Medien **in dieser Übung**; Medien erzwingen **keine** schärfere Edit-Policy als die Übung selbst. + +--- + +## 2. Begriffe + +| Begriff | Bedeutung | +|--------|-----------| +| **Media Asset** | Logische Entität für eine **hochgeladene Datei** inkl. Metadaten, Sichtbarkeit, Copyright, Speicherreferenz, Lifecycle-Status. | +| **Übungs-Medium / Attachment** | Verknüpfung **Übung ↔ Asset** oder reines Embed; enthält u. a. `context` (Sektion), `sort_order`, übungsspezifischen Titel/Beschreibung, `is_primary`. | +| **Medienmanager / Archiv** | Verwaltungs-UI/API für Assets (Suche, Metadaten, Lifecycle, ggf. Superadmin-global). | + +--- + +## 3. Zieldatenmodell (Überblick) + +> Konkrete Tabellennamen/Migrationen beim Implementierungsstart festziehen; dieses Kapitel ist die **fachliche Norm**. + +### 3.1 Tabelle `media_assets` (oder gleichwertiger Name) + +**Pflichtidee:** + +- Identität, technische Metadaten: `id`, `mime_type`, `byte_size`, `sha256` (vollständig), `original_filename`, `created_at`, `updated_at`. +- Mandant & Governance: `visibility`, `club_id` (nullable je nach Semantik wie bei Übungen), `uploaded_by_profile_id`. +- Copyright: mindestens `copyright_notice` (TEXT); optional `license`, `attribution`, `source_url`. +- Speicher: `storage_backend` (Enum/String: z. B. `local`, `s3`, …), `storage_key` (relativer oder bucket-key), optional `storage_url` nur für interne Zwecke – **keine** öffentlichen Secrets in der DB. +- Lifecycle: `lifecycle_state` (siehe §5), Timestamps für Übergänge und geplante Purge-Termine. + +**Deduplizierung:** Innerhalb sinnvoller Grenze (z. B. **pro Verein** bei `club`, global nur mit Superadmin-Policy) über `sha256` + `club_id`/`visibility`; Konflikt bei Upload → bestehendes Asset verknüpfen oder expliziter Nutzerdialog (Umsetzungsdetail). + +### 3.2 Verknüpfung (heute `exercise_media`) + +- Erweiterung um **`media_asset_id`** (FK, nullable wenn reines Embed). +- Embed-Zeilen: `media_asset_id IS NULL`, `embed_url`/`embed_platform` gesetzt wie heute. +- Übungsspezifische Felder: `context` (`ablauf` \| `detail` \| `trainer_hint`), `sort_order`, `title`, `description`, `is_primary`. + +### 3.3 Referenzen & physisches Löschen + +- Vor **physischem** Löschen muss die Referenzanzahl aller aktiven/nicht-purged Links **0** sein (oder Quarantäne-Policy); siehe §5.3. + +--- + +## 4. Sichtbarkeit, Lesen und Übungs-Promotion + +### 4.1 Leseregel Datei-Asset + +- Zentrale Entscheidung analog `library_content_visible_to_profile` / TenantContext: **Profil darf Asset lesen** nur wenn Visibility+`club_id`+`created_by` zum Objekt passen (gleiche Philosophie wie Übungen). +- **GET Download / Stream:** muss **Asset-Governance** prüfen; wenn der Aufruf **im Kontext einer Übung** erfolgt, zusätzlich (oder als Schnittmenge) **Übung lesbar** – verbindlich **ohne Seitenkanal** (kein Download nur mit Übungs-ID, wenn Asset für Nutzer unsichtbar wäre). + +### 4.2 Promotion Übung → `official` (bzw. global) + +Beim Speichern/Promoten einer Übung auf **`official`** (oder vergleichbare globale Stufe): + +1. **Pflicht-Dialog:** Hinweis, dass **alle zugeordneten Datei-Assets**, die noch **nicht** diese Sichtbarkeit haben, **mit angehoben** werden (oder die Aktion schlägt fehl / erfordert Anpassung). +2. **Abbruch:** Nutzer kann abbrechen oder einzelne Assets vor Freigabe anpassen / entkoppeln. +3. **Copyright:** Für `official` sind leere oder unzureichende Copyright-Felder **nicht** akzeptabel (genaue Regel in Umsetzung + Validierung). + +Superadmin kann Medien **explizit** global/offiziell machen (Archiv-Pfad), unabhängig von einer einzelnen Übung – konsistent zur Rolle. + +### 4.3 Bearbeitung Medien in der Übung + +Wer die Übung **bearbeiten** darf (bestehende oder künftig erweiterte Regel), darf für diese Übung: + +- Medien **hinzufügen** (Upload neu oder Verknüpfung aus Archiv), +- **Reihenfolge** / **Sektion** / **Titel** ändern, +- **Verknüpfung** entfernen (Asset bleibt im Archiv, sofern nicht anderweitig gelöscht), +- Embeds pflegen. + +Das widerspricht **nicht** dem Papierkorb: wer kein Recht hat, ein Asset **global** zu Trash/ Purge zu bewegen, darf trotzdem die **Verknüpfung** in „seiner“ Übung lösen, solange die Übungs-Edit-Policy das erlaubt. + +--- + +## 5. Lebenszyklus (Papierkorb) – Norm + +### 5.1 Zustände + +| Zustand | Code (Beispiel) | Neue Zuordnung zu Übungen | Anzeige in Übung (Lesemodus) | Anzeige Bearbeiten | +|---------|-----------------|---------------------------|------------------------------|-------------------| +| Aktiv | `active` | ja | normal | normal | +| Stufe 1 – Papierkorb | `trash_soft` | **nein** | ja + **Warnhinweis** („wird abgeschafft“) | ja + Warnhinweis | +| Stufe 2 – Ausblendung | `trash_hidden` | nein | **nein** (Platzhalter/„nicht verfügbar“) | Recovery-Aktion sichtbar | +| Stufe 3 – purgiert | `purged` / Zeile entfernt | nein | nein | nur noch historischer Hinweis nach Policy | + +**Default-Retention (automatisch):** + +- Stufe 1 → Stufe 2: **ca. 30 Tage** nach Eintritt in Stufe 1 (konfigurierbar). +- Stufe 2 → Stufe 3 **physisches Löschen**: **ca. 90 Tage** (3 Monate) nach Eintritt in Stufe 2 (konfigurierbar). + +**Manuell:** Berechtigte Rollen dürfen Übergänge **vorziehen** (Stufe 1 erzwingen, Stufe 2 erzwingen, Purge erzwingen) gemäß §5.2 – ggf. **Tier-Gates** (§6). + +### 5.2 Wer darf Lifecycle-Transitionen? + +| Aktion | Vereins-Asset (`club`, Owner-Verein) | Privates Asset (`private`, Uploader) | `official` / plattformweit | +|--------|--------------------------------------|--------------------------------------|-----------------------------| +| Stufe 1 (Papierkorb) | **Vereinsadmin** + **Superadmin** | **Uploader** + **Superadmin** | **Superadmin** | +| Stufe 2 vorziehen / erzwingen | Vereinsadmin + Superadmin | Uploader + Superadmin | Superadmin | +| Recovery Stufe 2 → 1 | wie Stufe 1-Eintritt, ggf. Tier | wie links | Superadmin | +| Physisches Löschen (3) | Vereinsadmin + Superadmin; Systemjob nach Frist | Uploader + Superadmin | Superadmin | + +**Superadmin / Systemadmin** ist immer **Override** (Klarstellung zur „systemadmin“-Formulierung). + +### 5.3 Konsistenz mit bestehenden Übungen + +- In **Stufe 1** bleiben Verknüpfungen funktional für Anzeige bestehen; Nutzer sehen **Warnung**. +- In **Stufe 2** wird das Medium in der **öffentlichen Übungsansicht nicht gerendert**; im **Bearbeitungsmodus** gibt es einen **Recovery-Link**, der das Asset zurück auf **Stufe 1** setzt (Policy: nur wer nach §5.2 Stufe 1 setzen darf, oder erweiterte Regel – bei Konflikt ACCESS_LAYER + dieses Dokument anpassen). +- **Physisches Löschen:** nur nach Stufe 2-Frist oder manueller Purge-Aktion; Backend entfernt Datei über Speicher-Backend und markiert Asset endgültig. + +### 5.4 Hintergrundjobs + +- Geplanter Job (täglich o. Ä.): Transitionen 1→2 und 2→3 gemäß Timestamps. +- Alle Zeiten **konfigurierbar** (Umgebungsvariablen oder DB-Config), Defaults wie §5.1. + +--- + +## 6. Tier & Features + +- **Tier** kann erlauben oder verbieten: **manuelles** Vorziehen von Stufe 2 oder Purge, zusätzliche Speicherquoten, externes Backend, etc. +- Tier ändert **nicht** die Übungs-Sichtbarkeit an sich; es **limitiert** nur Medien-spezifische Aktionen (Löschstufen, Archiv-Größe, …). Konkrete Feature-Keys in Umsetzung in `features` / `tier_limits` ergänzen und hier im **Changelog** dieses Dokuments verlinken. + +--- + +## 7. Externe Speicherung (Server / S3) + +- Abstraktion **StorageAdapter**: `put`, `get`, `delete`, ggf. `exists`. +- Konfiguration über ENV (z. B. Endpoint, Bucket, Credentials) – nie im Frontend. +- **NAS / anderer Rechner als App-Host:** In der Praxis `MEDIA_ROOT` in Compose/ENV auf den **Mount-Punkt** des NAS (oder NFS/SMB) setzen – die App läuft z. B. auf dem Raspberry, die Bytes liegen auf dem Speichersystem. Kein »Mediaserver« mit eigener Geschäftslogik nötig. +- **Pfadkonvention** auf der Platte z. B. `clubs/{club_id}/…` für Mandantentrennung (Umsetzung schrittweise; neue Uploads können zuerst unter `exercises/{sha256}{ext}` liegen). + +### 7.1 Konfiguration: Bootstrap vs. Superadmin (Laufzeit) + +| Ebene | Inhalt | Wer / Wo | +|-------|--------|----------| +| **Bootstrap** | Basis-Verzeichnis `MEDIA_ROOT` (Container/Host), ggf. S3-Secrets | Deployment (`.env`, Docker Compose) – Container muss ohne UI starten können | +| **Laufzeit (nicht-geheim)** | Zusätzlicher **relativer Unterpfad** unter `MEDIA_ROOT` (`local_relative_root`), später: aktives Backend `local` \| `s3`, öffentlicher Endpoint/Bucket-Name | **Superadmin** über Tabelle `platform_media_storage` + API; keine Secrets für S3 im Klartext in der DB (Keys nur ENV/Vault) | +| **Effektives Wurzelverzeichnis** | `Path(MEDIA_ROOT) / local_relative_root` nach Normalisierung (kein `..`, kein absoluter Pfad im relativen Segment) | berechnet im Backend bei jedem Zugriff | + +**Hinweis Beta:** Primär ein Verein, Videos auf separatem physischen System → typischerweise **ein NAS-Mount** als `MEDIA_ROOT`; Superadmin kann bei Bedarf einen **Unterordner** setzen, ohne neues Image zu bauen. + +**Drift:** Jede Änderung an Speicher-Konfiguration oder Asset-Schema → Migration + Eintrag **§10 Changelog** + bei neuen Endpoints **ACCESS_LAYER_ENDPOINT_AUDIT.md**. Änderungen an **Inline-Platzhaltern** (§11) → ebenfalls Changelog + ggf. Frontend-Sanitize-Regeln dokumentieren. + +--- + +## 8. Embeds & Streaming-Plattformen + +- Embeds **bleiben** erste Klasse; bestehende Plattform-Erkennung erweiterbar (`embed_platform`). +- Roadmap: weitere Hosts (Player-Komponente, oEmbed, CSP) – **ohne** Änderung am Asset-Lifecycle-Modell. + +--- + +## 9. Pflichten zur Drift-Vermeidung + +| Änderung an … | Pflegepflicht | +|---------------|---------------| +| DB-Schema Medien | Neue Migration + **Abschnitt/Changelog in diesem Dokument** (Datum, Kurzbeschreibung) | +| Sichtbarkeit / Enums | Zuerst `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, dann dieses Dokument synchron halten | +| Neue Endpoints Medien/Archiv | `ACCESS_LAYER_ENDPOINT_AUDIT.md` + `get_tenant_context`/Governance | +| **Inline-Referenzen** in Übungstexten (§11) | Renderer/Sanitizer-Regeln + dieses Dokument §11; kein zweites Rechtemodell | +| Implementierungs-Meilenstein | `backend/version.py` (`MODULE_VERSIONS` / Schema-Kommentar) und bei größerem Release `PROJECT_STATUS.md` | + +--- + +## 10. Changelog + +| Datum | Änderung | +|-------|----------| +| 2026-05-07 | Erstfassung als Single Source of Truth (verbindlich); Abstimmung mit Stakeholder: Promotion Übung↔Medien, Copyright, Papierkorb 3-stufig, externe Speicher, Embeds getrennt vom Asset-Lifecycle. | +| 2026-05-07 | §11 **Inline-Medien im Fließtext**: Leitplanken (Anker `exercise_media.id`, einheitlicher Render-Pfad, keine zweite Governance); Zeitpunkt der Umsetzung; Drift-Vermeidung ohne jetzigen Vollbau. | +| 2026-05-07 | **Medienmanager (Basis):** `GET /api/media-assets?lifecycle=…`, `copyright_notice` in Response; `PATCH` Copyright; `GET …/file` für Papierkorb-Zeilen bei Verwaltungsrecht; UI-Route `/media` (Medienbibliothek). | + +--- + +## 11. Inline-Medien im Fließtext (Planung, Leitplanken) + +**Status:** nicht implementiert; verbindlich nur als **Richtschnur**, damit später **kein Big-Bang-Refactor** nötig ist. + +### 11.1 Ziel + +- Medien (Player, Bild) sollen **an definierter Stelle** in Feldern wie Ablauf / Ziel / Notizen erscheinen können – zusätzlich oder statt reiner Zuordnung zu den Sektionen `ablauf` / `detail` / `trainer_hint`. +- **Keine zweite Sichtbarkeit:** Inline verweist immer auf dieselbe **Übungs-Medium-Zeile** (`exercise_media.id`) bzw. indirekt auf das gleiche Asset wie die Medienliste; **Lesen/Ausliefern** nur nach **bestehender** Übungs- + Medien-Governance (§4.1). + +### 11.2 Platzhalter-Konvention (Vorschlag für spätere Umsetzung) + +- Beim **Speichern** im Rich-Text: markierter Verweis, z. B. + `data-shinkan-exercise-media=""` auf einem **neutralen** Element (`span`/`figure`), **oder** eine interne Kurzsyntax (`{{exerciseMedia:123}}`), die der Server beim Speichern in eine **kanonische** HTML-Form überführt. +- **Final festlegen** beim Start der Implementierung (ein Format, nicht mehrere parallele). + +### 11.3 Rendering & Sicherheit + +- **Ein zentraler Pfad** „Übungstext für Anzeige aufbereiten“: HTML sanitizen (Allowlist), erlaubte Platzhalter auflösen, **ID gehört zur aktuellen Übung** und Medium ist für den Nutzer **sichtbar** – sonst Platzhalter mit neutralem Hinweis oder ausblenden. +- **XSS/CSP:** keine rohen `iframe`/Skripte aus Nutzer-HTML ohne Kontrolle; eingebettete Player nur über kontrollierte Komponenten. + +### 11.4 Koexistenz mit Sektions-Medien + +- **Liste + Inline** dürfen dasselbe `exercise_media` referenzieren (ein Player an zwei Stellen) – Produktentscheidung: später optional „Duplikat vermeiden“-Hinweis in der UI. +- Import/Wiki: vor großen Content-Migrationen **Syntax festlegen**, damit nicht irreversibel „falsches“ HTML importiert wird. + +### 11.5 Wann umsetzen (Reihenfolge) + +1. **Vorher:** Medien-Archiv, `media_assets`, Upload/Dedupe, Speicherpfad, Basis-Papierkorb (§5) – jeweils stabil. +2. **Danach:** Inline implementieren, sobald Trainer-Feedback oder Content-Menge den Bedarf **konkret** bestätigt (typisch nach 1–2 Beta-Zyklen). +3. **Nicht nötig:** vorher kompletten Block-Editor einführen; **Platzhalter im bestehenden RTE** ist der vorgesehene **schlanke** Einstieg. + +### 11.6 Refactor-Vermeidung (jetzt schon) + +- Neue Features **nicht** so bauen, dass HTML aus Übungstexten an **vielen** Stellen „roh“ gerendert wird – **ein** wiederverwendbarer Renderer vorbereiten (schrittweise einziehen). +- Medien immer über **stabile IDs** anbinden, nicht nur über Datei-URLs im Text. + +--- + +## 12. Referenzen + +- `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` +- `.claude/docs/technical/MEDIA_UPLOAD_SPEC.md` (Limits, MIME, Embed-Typen im aktuellen Backend) +- `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` +- `backend/routers/exercises.py` – Ist-Zustand `exercise_media` bis Refactor diff --git a/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md b/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md index 15cba7d..3707e94 100644 --- a/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md +++ b/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md @@ -6,6 +6,10 @@ **Autor:** Claude Code **Änderungen v1.1:** Rollenbasierte Server-Limits (`EXERCISE_MEDIA_*_MB`) +> **Zielbild Medien-Archiv, Wiederverwendung, Papierkorb, Copyright, externe Speicherung, später Inline im Fließtext:** +> Verbindliche Single Source of Truth: **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`** (§11 Leitplanken Inline, ohne Umsetzung). +> Dieses Dokument bleibt maßgeblich für **konkrete Upload-Limits, MIME-Liste und Embed-Hosting** des aktuellen Stands bis zum Refactor. + --- ## 1. Upload-Strategie diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md index 9168685..b0a613c 100644 --- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md +++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md @@ -11,21 +11,39 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. | club_memberships | `/clubs/{id}/members*` | ja | `get_tenant_context` | ja | | | club_join_requests | `/me/club-join-requests`, `/clubs/{id}/join-requests*` | ja | `get_tenant_context` | ja | | | exercises | `PATCH /api/exercises/bulk-metadata` | ja | `get_tenant_context` | ja | Liste: UI-Mehrfachwahl; bis 500 IDs; nur Ersteller oder Plattform-Admin | -| exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | | +| exercises | `GET .../media/{mid}/file` | ja | `get_tenant_context_flexible` | ja (wie Übung lesen) | Datei oder `?ssetoken`; kein anonymes `/media/` ohne ALLOW_PUBLIC_MEDIA_STATIC | +| exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | PUT Einzelübung: bei Sichtbarkeit `official` Medien-§4.2 (422: Lifecycle/Promotion/Copyright) | | exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar | | training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id | | training_framework_programs | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Liste + POST Default club_id | | admin_users | `GET /api/admin/users` | Plattform | `require_auth` | Admin-Rolle | EXEMPT `check_access_layer_hints.py` | +| platform_media_storage | `GET/PUT /api/admin/platform-media-storage` | Plattform | `require_auth` | GET: `is_platform_admin`; PUT: nur `superadmin` | Relativer Pfad unter `MEDIA_ROOT`; keine Secrets; EXEMPT wie admin_users | +| media_assets | `POST /api/media-assets/{id}/lifecycle` | ja | `get_tenant_context` | ja | u. a. `trash_soft` mit Trainer-nur-privat-Eigentum; `purge` nur **Superadmin**; Superadmin: `superadmin_force_lifecycle`, `superadmin_hard_delete` | +| media_assets | `GET /api/media-assets` | ja | `get_tenant_context` | ja | optional `lifecycle`; Standard `active`; Liste inkl. `copyright_notice`; Papierkorb-Ansicht nur sichtbare Mandanten-Assets | +| media_assets | `POST /api/media-assets/bulk-lifecycle` | ja | `get_tenant_context` | ja | Mehrfach-Lifecycle; gleiche Regeln wie Einzel-POST | +| media_assets | `POST /api/media-assets/bulk-patch` | ja | `get_tenant_context` | ja | Copyright / Bezeichner / Sichtbarkeit für viele IDs; gemischte Fehler in `failed[]` | +| media_assets | `PATCH /api/media-assets/{id}` | ja | `get_tenant_context` | ja | Copyright, `original_filename`, optional `visibility`/`club_id`; Rechte pro Stufe | +| media_assets | `GET /api/media-assets/{id}/file` | ja | `get_tenant_context_flexible` | ja | aktiv: Bibliotheks-Sichtbarkeit; `trash_soft`/`trash_hidden`: wie Lifecycle-Verwaltung | +| exercises | `POST /api/exercises/{id}/media/from-asset` | ja | `get_tenant_context` | ja | Verknüpfung `exercise_media` → bestehendes `media_asset_id`; Bearbeitungsrecht Übung + Leserecht Archiv | | auth | `/api/auth/*` | nein | — | Login/Session | EXEMPT | | catalogs | Katalog-CRUD | nein (global) | `require_auth` | Admin/Trainer je Endpoint | EXEMPT; bei späterem `club_id` nachziehen | | skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT | -| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin | EXEMPT | +| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin für Schreiben; `GET …/{id}` nur Portal-Admin | EXEMPT | | matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT | | import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT | **Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen. -Letzte Änderung: 2026-05-06 — MULTI_TENANCY §3 Gap-Analyse aktualisiert; Audit um geschützte Profil-Endpunkte ergänzt. +**Pflege / Drift:** Änderungen an Mandanten, Governance (`visibility`/`club_id`) oder neuen inhaltsbezogenen Endpoints → eine Zeile in dieser Tabelle anpassen und `PRODUCTION_READINESS_AUDIT_2026-05.md` prüfen. + +Letzte Änderung: 2026-05-07 — Upload-Dedupe Papierkorb 409 + `reactivate`; DELETE …/media nur Verknüpfung. + +--- + +### Changelog (Fortführung) + +- **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. +- **2026-05-07 (Phase 3):** CSP SPA (nginx); API `nosniff`-Middleware — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. --- diff --git a/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md new file mode 100644 index 0000000..58ae9de --- /dev/null +++ b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md @@ -0,0 +1,95 @@ +# Produktionsreife: Audit-Ergebnis & Umsetzungsplan (Stand 2026-05-07, Phase-3-Update) + +**Zweck:** Einheitliche Referenz gegen **Drift** zwischen Sicherheits-/Betriebsanforderungen und Code. +**Bezug:** Zugriffsschicht `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, Cursor-Regel `.cursor/rules/access-layer.mdc`. + +--- + +## Teil A — Audit-Ergebnis (Kurzfassung) + +### A.1 Kritisch / Hoch (behoben oder geplant) + +| ID | Befund | Status (2026-05-07) | +|----|--------|----------------------| +| SEC-01 | `GET /api/profile` (Legacy): ohne Header wurde das **erste Profil der DB** geliefert → IDOR / Datenleck | **Behoben:** Profil immer aus Session | +| SEC-02 | OpenAPI `/docs`, `/redoc`, `/openapi.json` in Produktion exponiert | **Behoben:** bei `ENVIRONMENT=production` aus; Override `PUBLIC_OPENAPI=1` | +| SEC-03 | `/api/health/ready` mit Tabellendetails / Migrationszähler öffentlich | **Behoben:** in Prod nur kompakte Antwort; Detail via `HEALTH_READY_PUBLIC_DETAIL=1` | +| SEC-04 | Statische Auslieferung `/media` ohne Auth (Link-Leak) | **Behoben:** `GET /api/exercises/.../media/.../file` mit Governance + `ssetoken`; `/media` nur mit `ALLOW_PUBLIC_MEDIA_STATIC=1` | +| OPS-01 | PostgreSQL-Hostport in `docker-compose.yml` | **Behoben:** Bind `127.0.0.1:5434` (nur Host-Local) | + +### A.2 Mittel / Konsistenz + +| ID | Befund | Status | +|----|--------|--------| +| CON-01 | Frontend: direktes `fetch('/api/profiles')` ohne zentralen Client | **Behoben** (AdminCatalogsPage → `api.listProfiles`) | +| CON-02 | `ProfileContext.jsx` (unused): falsches Token-Feld / `session` | **Behoben** (`user` + `getCurrentProfile` / `listProfiles`) | +| MEM-01 | Vereinsmitgliederverzeichnis zeigt E-Mail an alle Mitglieder | **Behoben:** E-Mail nur Plattform-Admin / `can_manage_club_org` | +| MAT-01 | `GET /maturity-models/{id}` für jeden Auth-Nutzer | **Behoben:** nur Portal-Admin (UI nutzt ohnehin nur Admin-Panel) | + +### A.3 Niedrig / Offen + +| ID | Befund | Status | +|----|--------|--------| +| CON-03 | Sparten (`division`) vs. Zielbild ACCESS_LAYER | **Roadmap** — siehe Phase 3 unten (Stufe D); nicht in diesem Schritt implementiert | +| OPS-02 | CSP / restliche Browser-Härtung | **Behoben (Basis):** CSP auf SPA-Dokumente (nginx `location /`); API `X-Content-Type-Options: nosniff` (FastAPI-Middleware) | +| MISC-01 | `auth.py` Debug-`print` beim Import | **Behoben** (entfernt) | +| MISC-02 | `delete_profile`: DELETE auf Mitai-Tabellen schlägt fehl, wenn Tabelle fehlt | **Behoben:** nur löschen, wenn Tabelle existiert | + +--- + +## Teil B — Priorisierter Umsetzungsplan + +### Phase 1 — Sofort (Beta-Blocker) ✅ umgesetzt in Repo-Stand + +1. Legacy-Endpunkte `/api/profile` GET/PUT: **nur Session-`profile_id`**, kein „erstes Profil“-Fallback. +2. OpenAPI in Produktion deaktivieren; opt-in für Notfall-Debugging. +3. Readiness-Endpoint in Prod entschärfen; operatives Detail per Env. +4. Automatisierte **Security-Release-Tests** (`tests/test_security_release.py`) + Script `scripts/security_release_checks.py` in CI. +5. Profil-Löschung: optionale Tabellen nur bei Existenz. +6. Frontend: Admin-Kataloge Profilliste über `api.js`. + +### Phase 2 — Kurzfristig (vor breitem Go-Live) ✅ Hauptpunkte umgesetzt (2026-05-07) + +1. **Medien:** `GET /api/exercises/{id}/media/{mid}/file` — Auth (`X-Auth-Token` oder `?ssetoken=`), Zugriff wie GET Übung; statisches `/media` standardmäßig aus. +2. **Mitgliederverzeichnis:** E-Mail nur wenn Plattform-Admin oder `can_manage_club_org`. +3. **DB-Port:** Prod-Compose Postgres an `127.0.0.1` gebunden. +4. **`GET /api/maturity-models/{id}`:** nur Portal-Admin (Liste/resolve unverändert für Trainer). + +### Phase 3 — Mittelfristig + +1. **CSP & Header:** Content-Security-Policy für die SPA (nginx, `location /`); `X-Content-Type-Options: nosniff` auf allen FastAPI-Responses — ✅ umgesetzt. +2. **Audit-Pflege:** bei Tenant/Governance-Änderungen `ACCESS_LAYER_ENDPOINT_AUDIT.md` und dieses Dokument anpassen — fortlaufend (Hinweis im Endpoint-Audit). +3. **Division / Sparten (CON-03, Stufe D):** noch **nicht** code-seitig durchgesetzt. Nächste technische Schritte (für eigenes Epic): + - Ist-Zustand: `division_id` auf Objekten & `division_lead` in `can_plan_in_club` ohne strikte Objekt-Filter. + - Ziel: Lesen/Schreiben nur mit passender Sparten-Mitgliedschaft bzw. Rolle; einheitliche Filter in Listen (`training_groups`, Übungen, Planung). + - Tests: Zwei Nutzer, zwei Sparten, kein Cross-Lesen; Superadmin-Pfade getrennt. + - Spec: `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe D. + +--- + +## Teil C — CI / Qualitätssicherung + +- **ACCESS_LAYER_STRICT:** `scripts/check_access_layer_hints.py` (unverändert Pflicht). +- **Security-Release:** `python scripts/security_release_checks.py` (pytest-Modul `test_security_release`). +- **Regression Profil:** `tests/test_profiles_read_access.py` (Legacy-Route). + +--- + +## Teil D — Umgebungsvariablen (Referenz) + +| Variable | Bedeutung | +|----------|-----------| +| `ENVIRONMENT` | `production` / `prod` → OpenAPI aus, Health-Ready kompakt | +| `PUBLIC_OPENAPI` | `1` / `true` → OpenAPI trotz Prod einschalten (nur Debugging) | +| `HEALTH_READY_PUBLIC_DETAIL` | `1` / `true` → volle Ready-JSON inkl. Tabellenliste in Prod | +| `ALLOW_PUBLIC_MEDIA_STATIC` | `1` / `true` → öffentliches Mount von `/media/` (Notfall/Legacy; Standard: aus) | +| `CSP` (nginx) | Kein Env: Policy fest in `frontend/nginx.conf` (`location /`). API auf **anderer Host-URL** als SPA: `connect-src` in der Policy erweitern oder Build mit envsubst. | + +--- + +**Pflege:** Nach jeder relevanten Änderung **Status-Spalte** und ggf. **Phase** aktualisieren; bei neuem Audit Datum im Titel anpassen oder Abschnitt „Changelog“ ergänzen. + +### Changelog + +- **2026-05-07:** Phase 2 Medien, Mitgliederverzeichnis E-Mail, maturity GET admin-only, Postgres localhost bind, Tests `test_exercise_media_download.py`. +- **Phase 3:** CSP (nginx SPA), API `X-Content-Type-Options` (FastAPI), Test `test_api_attachments_x_content_type_options_nosniff`; Division-Durchsetzung nur als Roadmap (CON-03). diff --git a/.claude/rules/ARCHITECTURE.md b/.claude/rules/ARCHITECTURE.md index 066dbd3..7a7c853 100644 --- a/.claude/rules/ARCHITECTURE.md +++ b/.claude/rules/ARCHITECTURE.md @@ -58,6 +58,7 @@ return {"message": "Fehler", "success": False} ### 1.4 Mandanten & Zugriffsschicht (Shinkan / ACCESS_LAYER) **Verbindlicher Rahmen:** `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` +**Medien-Assets (Archiv, Papierkorb, Promotion, Copyright, externer Speicher):** `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` **Fortlaufendes Inventar:** `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` **Definition of Done für neue oder geänderte geschützte APIs**, sobald Daten **Verein**, **Sichtbarkeit** oder **mandantenbezogene Listen** betreffen: diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 8101e74..fba50b6 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -38,6 +38,7 @@ jobs: pip install -r /app/requirements-dev.txt && cd /app && ACCESS_LAYER_STRICT=1 python scripts/check_access_layer_hints.py && + python scripts/security_release_checks.py && ACCESS_LAYER_INTEGRATION=1 SKIP_DB_MIGRATE=1 python -m pytest tests -m 'not slow' -ra -vv --tb=short " diff --git a/CLAUDE.md b/CLAUDE.md index 85026c9..a69a599 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ > | Lessons Learned | `.claude/rules/LESSONS_LEARNED.md` | > | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` | > | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` | +> | Medien-Archiv, Lifecycle, Promotion | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | ## Projekt-Übersicht @@ -105,7 +106,7 @@ Kurz (Stand 2026-05-05): App **0.8.10**, DB‑Schema‑Version **`20260505037`** - `exercises` - Übungen (Kernobjekt) - `exercise_variants` - Übungsvarianten - `exercise_skills` - M:N Übung ↔ Fähigkeit -- `exercise_media` - Medien (Bilder, Videos) +- `exercise_media` - Medien (Bilder, Videos); Zielbild Archiv & Lifecycle: **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. **Inline im Fließtext** (Übungstexte): geplant §11 derselben Spec — Anker `exercise_media.id`, einheitlicher Render-Pfad; noch nicht implementiert. **Trainingsplanung / Rahmen:** - `training_plan_templates` + Sektionsvorlagen — Mikrovorlage pro Einheit (Migration **031**) @@ -200,6 +201,10 @@ ALLOWED_ORIGINS=https://shinkan.jinkendo.de MEDIA_DIR=/app/media ``` +## Produktions-/Sicherheitsaudit (Drift vermeiden) + +Aktuelle Befunde und Umsetzungsstände: `.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md` (Fortlaufend pflegen.) + ## Kritische Regeln für Claude Code ### Must-Do: diff --git a/backend/auth.py b/backend/auth.py index a68cf09..980e24c 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -13,8 +13,6 @@ import bcrypt from db import get_db, get_cursor -print("[AUTH.PY] Module loaded - require_auth_flexible will be defined") - def hash_pin(pin: str) -> str: """Hash password with bcrypt. Falls back gracefully from legacy SHA256.""" diff --git a/backend/club_tenancy.py b/backend/club_tenancy.py index 4da0747..10a6135 100644 --- a/backend/club_tenancy.py +++ b/backend/club_tenancy.py @@ -11,6 +11,10 @@ def is_platform_admin(role: Optional[str]) -> bool: return (role or "").lower() in ("admin", "superadmin") +def is_superadmin(role: Optional[str]) -> bool: + return (role or "").lower() == "superadmin" + + def club_ids_for_profile(cur, profile_id: int) -> Set[int]: cur.execute( """ @@ -125,6 +129,24 @@ def memberships_with_roles(cur, profile_id: int, active_only: bool = True) -> Li return out +def club_ids_for_profile_with_roles(cur, profile_id: int, *role_codes: str) -> Set[int]: + """Vereins-IDs, in denen das Profil mindestens eine der Rollen hat.""" + if not role_codes: + return set() + ph = ",".join(["%s"] * len(role_codes)) + cur.execute( + f""" + SELECT DISTINCT cm.club_id + FROM club_members cm + INNER JOIN club_member_roles r ON r.club_member_id = cm.id + WHERE cm.profile_id = %s AND cm.status = 'active' + AND r.role_code IN ({ph}) + """, + (profile_id, *role_codes), + ) + return {int(r["club_id"]) for r in cur.fetchall() if r.get("club_id") is not None} + + _GOVERNANCE_VISIBILITY = frozenset({"private", "club", "official"}) diff --git a/backend/main.py b/backend/main.py index 2699427..d3a6bfc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung from pathlib import Path from typing import Optional -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles @@ -18,6 +18,20 @@ from slowapi.errors import RateLimitExceeded from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS + +def _is_production_environment() -> bool: + return os.getenv("ENVIRONMENT", "development").strip().lower() in ("production", "prod") + + +def _public_openapi_enabled() -> bool: + return os.getenv("PUBLIC_OPENAPI", "").strip().lower() in ("1", "true", "yes") + + +def _health_ready_public_detail_enabled() -> bool: + """In Prod standardmäßig keine Tabellen-/Migrations-Details (Information Disclosure).""" + return os.getenv("HEALTH_READY_PUBLIC_DETAIL", "").strip().lower() in ("1", "true", "yes") + + # Run database migrations before API start — halbes Schema ist schlimmer als kein Start # Lokal ohne DB / nur Tests: SKIP_DB_MIGRATE=1 if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"): @@ -39,11 +53,20 @@ else: from routers.auth import limiter as auth_rate_limiter +# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1 +_expose_docs = (not _is_production_environment()) or _public_openapi_enabled() +_openapi_url = "/openapi.json" if _expose_docs else None +_docs_url = "/docs" if _expose_docs else None +_redoc_url = "/redoc" if _expose_docs else None + # Initialize FastAPI app app = FastAPI( title="Shinkan Jinkendo API", description="Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung", - version=APP_VERSION + version=APP_VERSION, + openapi_url=_openapi_url, + docs_url=_docs_url, + redoc_url=_redoc_url, ) # SlowAPI: Rate Limits auf /api/auth/* (Decorator in routers/auth.py) @@ -62,7 +85,13 @@ app.add_middleware( allow_headers=["*"], ) -# TODO: Initialize Database with migrations + +@app.middleware("http") +async def add_api_security_headers(request: Request, call_next): + """Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing).""" + response = await call_next(request) + response.headers.setdefault("X-Content-Type-Options", "nosniff") + return response # Version Endpoint (public, no auth) @app.get("/api/version") @@ -132,7 +161,7 @@ def health_ready(): migration_count = 0 complete = bool(err is None and all(tables.get(t) for t in REQUIRED)) - return { + body = { "status": "ready" if complete else "degraded", "database": err is None, "detail": err, @@ -140,21 +169,30 @@ def health_ready(): "tables": tables, "schema_migrations_count": migration_count, } + if _is_production_environment() and not _health_ready_public_detail_enabled(): + return { + "status": body["status"], + "database": body["database"], + "schema_complete": body["schema_complete"], + } + return body # Root Endpoint @app.get("/") def read_root(): """Root endpoint - API info""" - return { + out = { "app": "Shinkan Jinkendo API", "version": APP_VERSION, - "docs": "/docs", - "health": "/health" + "health": "/health", } + if _expose_docs: + out["docs"] = "/docs" + return out # Register routers -from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, admin_users, skills, training_planning, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin +from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, admin_users, platform_media_storage, media_assets, skills, training_planning, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin app.include_router(auth.router) app.include_router(profiles.router) @@ -164,6 +202,8 @@ app.include_router(clubs.router) app.include_router(club_memberships.router) app.include_router(club_join_requests.router) app.include_router(admin_users.router) +app.include_router(platform_media_storage.router) +app.include_router(media_assets.router) app.include_router(skills.router) app.include_router(training_planning.router) app.include_router(training_framework_programs.router) @@ -173,10 +213,13 @@ app.include_router(matrix_stack_bundle.router) app.include_router(import_wiki.router) app.include_router(import_wiki_admin.router) -# Lokale Medien (Übungen-Uploads) unter MEDIA_ROOT, ausliefern unter /media/... +# Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad +# GET /api/exercises/{id}/media/{mid}/file (?ssetoken für /