Mandantenfähigkeit V1 #10
|
|
@ -108,7 +108,7 @@ def assert_valid_governance_visibility(
|
||||||
visibility: str,
|
visibility: str,
|
||||||
club_id: Optional[int],
|
club_id: Optional[int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Pflicht club_id bei visibility=club + Mitgliedschaft; official nur Plattform-Admin."""
|
"""Pflicht club_id bei visibility=club; Mitgliedschaft außer Plattform-Admin; official nur Plattform-Admin."""
|
||||||
if visibility not in _GOVERNANCE_VISIBILITY:
|
if visibility not in _GOVERNANCE_VISIBILITY:
|
||||||
raise HTTPException(status_code=400, detail="Ungültige visibility")
|
raise HTTPException(status_code=400, detail="Ungültige visibility")
|
||||||
if visibility == "official" and not is_platform_admin(role):
|
if visibility == "official" and not is_platform_admin(role):
|
||||||
|
|
@ -119,7 +119,12 @@ def assert_valid_governance_visibility(
|
||||||
if visibility == "club":
|
if visibility == "club":
|
||||||
if club_id is None:
|
if club_id is None:
|
||||||
raise HTTPException(status_code=400, detail="club_id ist bei visibility=club erforderlich")
|
raise HTTPException(status_code=400, detail="club_id ist bei visibility=club erforderlich")
|
||||||
assert_club_member(cur, profile_id, club_id)
|
if is_platform_admin(role):
|
||||||
|
cur.execute("SELECT 1 FROM clubs WHERE id = %s", (club_id,))
|
||||||
|
if not cur.fetchone():
|
||||||
|
raise HTTPException(status_code=400, detail="Verein nicht gefunden")
|
||||||
|
else:
|
||||||
|
assert_club_member(cur, profile_id, club_id)
|
||||||
|
|
||||||
|
|
||||||
def exercise_visible_to_profile(
|
def exercise_visible_to_profile(
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
@limiter.limit("5/minute")
|
@limiter.limit("30/minute")
|
||||||
async def login(req: LoginRequest, request: Request):
|
async def login(req: LoginRequest, request: Request):
|
||||||
"""Login with email + password."""
|
"""Login with email + password."""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
|
|
||||||
|
|
@ -853,23 +853,59 @@ def update_exercise(
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
# Existiert die Übung?
|
cur.execute(
|
||||||
cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,))
|
"SELECT created_by, visibility, club_id FROM exercises WHERE id = %s",
|
||||||
|
(exercise_id,),
|
||||||
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
|
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
|
||||||
|
|
||||||
# Permission Check
|
|
||||||
if _row_created_by(row) != profile_id:
|
if _row_created_by(row) != profile_id:
|
||||||
raise HTTPException(status_code=403, detail="Nur der Ersteller darf editieren")
|
raise HTTPException(status_code=403, detail="Nur der Ersteller darf editieren")
|
||||||
|
|
||||||
# UPDATE (nur gesetzte Felder)
|
ex_vis = (row.get("visibility") or "private").strip().lower()
|
||||||
fields = []
|
ex_cid = row.get("club_id")
|
||||||
params = []
|
if ex_cid is not None:
|
||||||
|
ex_cid = int(ex_cid)
|
||||||
|
|
||||||
data = body.dict(exclude_unset=True)
|
data = body.dict(exclude_unset=True)
|
||||||
|
|
||||||
# Basis-Felder
|
next_vis = ex_vis
|
||||||
|
if "visibility" in data and data["visibility"] is not None:
|
||||||
|
v_raw = str(data["visibility"]).strip().lower()
|
||||||
|
if v_raw:
|
||||||
|
next_vis = v_raw
|
||||||
|
|
||||||
|
next_club = ex_cid
|
||||||
|
if "club_id" in data:
|
||||||
|
raw_c = data["club_id"]
|
||||||
|
if raw_c in (None, "", []):
|
||||||
|
next_club = None
|
||||||
|
else:
|
||||||
|
next_club = int(raw_c)
|
||||||
|
|
||||||
|
if next_vis == "club":
|
||||||
|
if next_club is None:
|
||||||
|
next_club = tenant.effective_club_id
|
||||||
|
if next_club is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Vereins-Übung: club_id angeben oder aktiven Verein wählen (X-Active-Club-Id).",
|
||||||
|
)
|
||||||
|
data["club_id"] = next_club
|
||||||
|
|
||||||
|
if next_vis != ex_vis:
|
||||||
|
data["visibility"] = next_vis
|
||||||
|
|
||||||
|
gov_club = next_club if next_vis == "club" else None
|
||||||
|
assert_valid_governance_visibility(
|
||||||
|
cur, profile_id, tenant.global_role, next_vis, gov_club
|
||||||
|
)
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
params = []
|
||||||
|
|
||||||
for field in ["title", "summary", "goal", "execution", "preparation", "trainer_notes",
|
for field in ["title", "summary", "goal", "execution", "preparation", "trainer_notes",
|
||||||
"duration_min", "duration_max", "group_size_min", "group_size_max",
|
"duration_min", "duration_max", "group_size_min", "group_size_max",
|
||||||
"visibility", "status", "club_id"]:
|
"visibility", "status", "club_id"]:
|
||||||
|
|
@ -877,12 +913,10 @@ def update_exercise(
|
||||||
fields.append(f"{field} = %s")
|
fields.append(f"{field} = %s")
|
||||||
params.append(data[field])
|
params.append(data[field])
|
||||||
|
|
||||||
# Equipment (JSONB)
|
|
||||||
if "equipment" in data:
|
if "equipment" in data:
|
||||||
fields.append("equipment = %s")
|
fields.append("equipment = %s")
|
||||||
params.append(json.dumps(data["equipment"]) if data["equipment"] else None)
|
params.append(json.dumps(data["equipment"]) if data["equipment"] else None)
|
||||||
|
|
||||||
# UPDATE ausführen (wenn Basis-Felder geändert wurden)
|
|
||||||
if fields:
|
if fields:
|
||||||
fields.append("updated_at = NOW()")
|
fields.append("updated_at = NOW()")
|
||||||
params.append(exercise_id)
|
params.append(exercise_id)
|
||||||
|
|
@ -890,10 +924,8 @@ def update_exercise(
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# M:N Relations aktualisieren (wenn angegeben)
|
|
||||||
assign_exercise_relations(cur, conn, exercise_id, data)
|
assign_exercise_relations(cur, conn, exercise_id, data)
|
||||||
|
|
||||||
# Vollständiges Objekt zurückgeben
|
|
||||||
exercise = enrich_exercise_detail(exercise_id, cur)
|
exercise = enrich_exercise_detail(exercise_id, cur)
|
||||||
|
|
||||||
return exercise
|
return exercise
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.24"
|
APP_VERSION = "0.8.25"
|
||||||
BUILD_DATE = "2026-05-05"
|
BUILD_DATE = "2026-05-05"
|
||||||
DB_SCHEMA_VERSION = "20260505041"
|
DB_SCHEMA_VERSION = "20260505041"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"auth": "1.2.0", # Erster/bootstrap Nutzer und ADMIN_BOOTSTRAP_EMAILS → superadmin (nicht mehr admin)
|
"auth": "1.2.1", # Login-Rate-Limit 30/minute pro IP (vorher 5/minute)
|
||||||
"profiles": "1.4.0", # GET /profiles/me: effective_club_id (TenantContext-Auflösung); TenantContext-Modul
|
"profiles": "1.4.0", # GET /profiles/me: effective_club_id (TenantContext-Auflösung); TenantContext-Modul
|
||||||
"tenant_context": "1.0.0", # resolve/get_depends; library_content_visibility_sql
|
"tenant_context": "1.0.0", # resolve/get_depends; library_content_visibility_sql
|
||||||
"clubs": "0.4.0", # public-directory, members/directory; Vereins-GUI verwendet Endpoints
|
"clubs": "0.4.0", # public-directory, members/directory; Vereins-GUI verwendet Endpoints
|
||||||
|
|
@ -15,7 +15,7 @@ MODULE_VERSIONS = {
|
||||||
"groups": "0.1.0",
|
"groups": "0.1.0",
|
||||||
"skills": "0.1.0",
|
"skills": "0.1.0",
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.6.0", # Alle geschützten Endpoints: TenantContext (Detail, Mutations, Medien, Varianten)
|
"exercises": "2.6.1", # PUT Übung: visibility club ohne club_id → effective_club_id / Body club_id; Governance für Plattform-Admins ohne Vereinsmitgliedschaft bei club
|
||||||
"training_units": "0.1.0",
|
"training_units": "0.1.0",
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein
|
"planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein
|
||||||
|
|
@ -27,6 +27,15 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.25",
|
||||||
|
"date": "2026-05-05",
|
||||||
|
"changes": [
|
||||||
|
"Übungen PUT: bei visibility club wird club_id aus aktivem Verein oder Body gesetzt (verhindert club ohne club_id für Vereinsnutzer)",
|
||||||
|
"club_tenancy: governance visibility club für Plattform-Admins ohne Vereinsmitgliedschaft (nur Existenz clubs.id)",
|
||||||
|
"Login POST /api/auth/login: Rate-Limit 30/minute pro IP (vorher 5/minute)",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.24",
|
"version": "0.8.24",
|
||||||
"date": "2026-05-05",
|
"date": "2026-05-05",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Shinkan Jinkendo Frontend Version
|
// Shinkan Jinkendo Frontend Version
|
||||||
|
|
||||||
export const APP_VERSION = "0.8.24"
|
export const APP_VERSION = "0.8.25"
|
||||||
export const BUILD_DATE = "2026-05-05"
|
export const BUILD_DATE = "2026-05-05"
|
||||||
|
|
||||||
export const PAGE_VERSIONS = {
|
export const PAGE_VERSIONS = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user