feat: enhance governance visibility checks and update login rate limit
- 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:
parent
0181575962
commit
870a7611dc
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user