From 01815759622e63987dfc5a64e3a7283838ca3669 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 5 May 2026 21:51:53 +0200 Subject: [PATCH] feat: update exercises API to fully integrate tenant context and bump version to 0.8.24 - Refactored exercises API endpoints to utilize tenant context for authentication and authorization, enhancing security and governance. - Updated access layer documentation to reflect the complete integration of tenant context for exercises. - Bumped application version to 0.8.24 in both backend and frontend files. - Enhanced changelog to document the new version and changes made in this release. --- .../working/ACCESS_LAYER_ENDPOINT_AUDIT.md | 5 +- backend/routers/exercises.py | 60 ++++++++++--------- backend/version.py | 11 +++- frontend/src/version.js | 2 +- 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md index 8cb99d8..6ed857e 100644 --- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md +++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md @@ -10,8 +10,7 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. | clubs | CRUD Organisation | ja | — | `can_manage_club_org` / member | schrittweise auf TenantContext | | club_memberships | `/clubs/{id}/members*` | ja | geplant | ja | | | club_join_requests | `/clubs/{id}/join-requests*` | ja | geplant | ja | | -| exercises | `GET /api/exercises`, `POST /api/exercises` | ja | `get_tenant_context` | ja | Liste club nach aktivem Verein; POST Governance + Default club_id | -| exercises | Detail/PATCH (Übriges) | teils | `require_auth` | Owner/Admin | später Tenant optional | +| exercises | alle geschützten `/api/exercises*` (Liste war schon) | ja | `get_tenant_context` | Detail visibility; Mutations Owner/Admin | PATCH ohne zusätzliche assert_valid bei Visibility — nächster Schritt | | 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 | optional | Admin-Rolle | | @@ -19,7 +18,7 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. **Legende:** „geplant“ = beim nächsten Umbau dieser Router `get_tenant_context` verwenden bzw. zentrale Governance-Helfer. -Letzte Änderung: 2026-05-05 — Stufe B/C partiell (Bibliothekslisten + Planung); `GET /training-units` ohne automatischen club_id-Filter (Kompatibilität). +Letzte Änderung: 2026-05-05 — u. a. Übungen komplett TenantContext; Stufe B/C partiell (Bibliothekslisten + Planung); `GET /training-units` ohne automatischen club_id-Filter (Kompatibilität). --- diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 3322f73..2a697d8 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -15,7 +15,6 @@ from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File, from pydantic import BaseModel, Field, model_validator from db import get_db, get_cursor, r2d -from auth import require_auth from club_tenancy import assert_valid_governance_visibility, exercise_visible_to_profile, is_platform_admin from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql @@ -82,8 +81,8 @@ ALLOWED_UPLOAD_MIMES = frozenset( ) -def _upload_limit_bytes(session: dict) -> int: - role = session.get("role") or "" +def _upload_limit_bytes(tenant: TenantContext) -> int: + role = tenant.global_role or "" if role in ("admin", "superadmin"): return MAX_UPLOAD_BYTES_ADMIN return MAX_UPLOAD_BYTES_USER @@ -752,12 +751,12 @@ def list_exercises( @router.get("/exercises/{exercise_id}") def get_exercise( exercise_id: int, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): """ Exercise Detail mit allen M:N Relations (vollständig enriched). """ - profile_id = session["profile_id"] + profile_id = tenant.profile_id with get_db() as conn: cur = get_cursor(conn) @@ -772,7 +771,7 @@ def get_exercise( exercise["visibility"], exercise.get("club_id"), exercise.get("created_by"), - session.get("role"), + tenant.global_role, ): raise HTTPException(status_code=403, detail="Keine Berechtigung für diese Übung") @@ -843,13 +842,13 @@ def create_exercise( def update_exercise( exercise_id: int, body: ExerciseUpdate, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): """ Aktualisiert eine Übung (Partial Update). Nur Owner darf editieren. """ - profile_id = session["profile_id"] + profile_id = tenant.profile_id with get_db() as conn: cur = get_cursor(conn) @@ -903,14 +902,14 @@ def update_exercise( @router.delete("/exercises/{exercise_id}") def delete_exercise( exercise_id: int, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): """ Löscht eine Übung. Nur Owner oder Admin darf löschen. """ - profile_id = session["profile_id"] - role = session.get("role") + profile_id = tenant.profile_id + role = tenant.global_role with get_db() as conn: cur = get_cursor(conn) @@ -922,7 +921,7 @@ def delete_exercise( raise HTTPException(status_code=404, detail="Übung nicht gefunden") # Permission Check - if _row_created_by(row) != profile_id and role not in ("admin", "superadmin"): + if _row_created_by(row) != profile_id and not is_platform_admin(role): raise HTTPException(status_code=403, detail="Nur Ersteller oder Admin darf löschen") # Prüfen ob Übung in Block-Items verwendet wird @@ -952,9 +951,9 @@ def delete_exercise( def reorder_exercise_variants( exercise_id: int, body: ExerciseVariantsReorder, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id if len(body.variant_ids) != len(set(body.variant_ids)): raise HTTPException(status_code=400, detail="variant_ids dürfen keine Duplikate enthalten") @@ -989,9 +988,9 @@ def reorder_exercise_variants( def create_exercise_variant( exercise_id: int, body: ExerciseVariantCreate, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id with get_db() as conn: cur = get_cursor(conn) @@ -1045,9 +1044,9 @@ def update_exercise_variant( exercise_id: int, variant_id: int, body: ExerciseVariantUpdate, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id data = body.dict(exclude_unset=True) if not data: @@ -1118,12 +1117,15 @@ def update_exercise_variant( conn.commit() return row + + +@router.delete("/exercises/{exercise_id}/variants/{variant_id}") def delete_exercise_variant( exercise_id: int, variant_id: int, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id with get_db() as conn: cur = get_cursor(conn) @@ -1167,7 +1169,7 @@ def _fetch_media_row(cur, exercise_id: int, media_id: int) -> Optional[dict]: @router.post("/exercises/{exercise_id}/media", status_code=201) async def upload_exercise_media( exercise_id: int, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), file: Optional[UploadFile] = File(None), embed_url: Optional[str] = Form(None), media_type: str = Form(...), @@ -1176,7 +1178,7 @@ async def upload_exercise_media( context: str = Form("ablauf"), is_primary: bool = Form(False), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id if media_type not in ("image", "video", "document", "sketch"): raise HTTPException(status_code=400, detail="Ungültiger media_type") if context not in ("ablauf", "detail", "trainer_hint"): @@ -1234,7 +1236,7 @@ async def upload_exercise_media( ) else: raw = await file.read() - max_upload = _upload_limit_bytes(session) + max_upload = _upload_limit_bytes(tenant) if len(raw) > max_upload: raise HTTPException( status_code=413, @@ -1289,9 +1291,9 @@ async def upload_exercise_media( def reorder_exercise_media( exercise_id: int, body: ExerciseMediaReorder, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id ids = body.media_ids with get_db() as conn: cur = get_cursor(conn) @@ -1320,9 +1322,9 @@ def update_exercise_media( exercise_id: int, media_id: int, body: ExerciseMediaUpdate, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id data = body.dict(exclude_unset=True) if not data: raise HTTPException(status_code=400, detail="Keine Felder zum Aktualisieren") @@ -1362,9 +1364,9 @@ def update_exercise_media( def delete_exercise_media( exercise_id: int, media_id: int, - session: dict = Depends(require_auth), + tenant: TenantContext = Depends(get_tenant_context), ): - profile_id = session["profile_id"] + profile_id = tenant.profile_id with get_db() as conn: cur = get_cursor(conn) _assert_can_edit_exercise(cur, exercise_id, profile_id) diff --git a/backend/version.py b/backend/version.py index b728bce..897cbaa 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.23" +APP_VERSION = "0.8.24" BUILD_DATE = "2026-05-05" DB_SCHEMA_VERSION = "20260505041" @@ -15,7 +15,7 @@ MODULE_VERSIONS = { "groups": "0.1.0", "skills": "0.1.0", "methods": "0.1.0", - "exercises": "2.5.0", # list/create: TenantContext + Governance; club-Liste nach aktivem Verein + "exercises": "2.6.0", # Alle geschützten Endpoints: TenantContext (Detail, Mutations, Medien, Varianten) "training_units": "0.1.0", "training_programs": "0.1.0", "planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein @@ -27,6 +27,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.24", + "date": "2026-05-05", + "changes": [ + "Übungen-Router: get/update/delete, Varianten, Medien — Depends(get_tenant_context); Upload-Limits via TenantContext; fehlenden DELETE decorator variants gefixt", + ], + }, { "version": "0.8.23", "date": "2026-05-05", diff --git a/frontend/src/version.js b/frontend/src/version.js index ce8d32a..dea8351 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.8.23" +export const APP_VERSION = "0.8.24" export const BUILD_DATE = "2026-05-05" export const PAGE_VERSIONS = {