feat: enhance governance visibility checks and update login rate limit
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 34s

- Updated governance visibility logic in `assert_valid_governance_visibility` to enforce club membership checks for platform admins and ensure proper club existence validation.
- Increased login request limit from 5 to 30 per minute to improve user experience.
- Refactored exercise update logic to better handle visibility and club ID requirements, ensuring compliance with governance rules.
This commit is contained in:
Lars 2026-05-05 21:57:55 +02:00
parent 0181575962
commit 870a7611dc
5 changed files with 64 additions and 18 deletions

View File

@ -108,7 +108,7 @@ def assert_valid_governance_visibility(
visibility: str,
club_id: Optional[int],
) -> 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:
raise HTTPException(status_code=400, detail="Ungültige visibility")
if visibility == "official" and not is_platform_admin(role):
@ -119,7 +119,12 @@ def assert_valid_governance_visibility(
if visibility == "club":
if club_id is None:
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(

View File

@ -26,7 +26,7 @@ limiter = Limiter(key_func=get_remote_address)
@router.post("/login")
@limiter.limit("5/minute")
@limiter.limit("30/minute")
async def login(req: LoginRequest, request: Request):
"""Login with email + password."""
with get_db() as conn:

View File

@ -853,23 +853,59 @@ def update_exercise(
with get_db() as conn:
cur = get_cursor(conn)
# Existiert die Übung?
cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,))
cur.execute(
"SELECT created_by, visibility, club_id FROM exercises WHERE id = %s",
(exercise_id,),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
# Permission Check
if _row_created_by(row) != profile_id:
raise HTTPException(status_code=403, detail="Nur der Ersteller darf editieren")
# UPDATE (nur gesetzte Felder)
fields = []
params = []
ex_vis = (row.get("visibility") or "private").strip().lower()
ex_cid = row.get("club_id")
if ex_cid is not None:
ex_cid = int(ex_cid)
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",
"duration_min", "duration_max", "group_size_min", "group_size_max",
"visibility", "status", "club_id"]:
@ -877,12 +913,10 @@ def update_exercise(
fields.append(f"{field} = %s")
params.append(data[field])
# Equipment (JSONB)
if "equipment" in data:
fields.append("equipment = %s")
params.append(json.dumps(data["equipment"]) if data["equipment"] else None)
# UPDATE ausführen (wenn Basis-Felder geändert wurden)
if fields:
fields.append("updated_at = NOW()")
params.append(exercise_id)
@ -890,10 +924,8 @@ def update_exercise(
cur.execute(query, params)
conn.commit()
# M:N Relations aktualisieren (wenn angegeben)
assign_exercise_relations(cur, conn, exercise_id, data)
# Vollständiges Objekt zurückgeben
exercise = enrich_exercise_detail(exercise_id, cur)
return exercise

View File

@ -1,11 +1,11 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.24"
APP_VERSION = "0.8.25"
BUILD_DATE = "2026-05-05"
DB_SCHEMA_VERSION = "20260505041"
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
"tenant_context": "1.0.0", # resolve/get_depends; library_content_visibility_sql
"clubs": "0.4.0", # public-directory, members/directory; Vereins-GUI verwendet Endpoints
@ -15,7 +15,7 @@ MODULE_VERSIONS = {
"groups": "0.1.0",
"skills": "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_programs": "0.1.0",
"planning": "0.8.0", # TenantContext auf allen Planungs-Endpunkten; Vorlagen-Liste wie Übungen nach aktivem Verein
@ -27,6 +27,15 @@ MODULE_VERSIONS = {
}
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",
"date": "2026-05-05",

View File

@ -1,6 +1,6 @@
// 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 PAGE_VERSIONS = {