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.
This commit is contained in:
parent
c919e02441
commit
0181575962
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user