diff --git a/VERSION b/VERSION index d1d06ad..76482c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1111 \ No newline at end of file +1112 \ No newline at end of file diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 9cc2820..8004df3 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -180,14 +180,22 @@ async def create_dog(data: DogCreate, user=Depends(get_current_user)): if dog_count == 1: # genau dieser erste Hund plausible, reason = _is_plausible_dog(data.name, data.rasse, data.geburtstag) if plausible: - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total < 100: - conn.execute( - "UPDATE users SET is_founder=1, founder_number=?, is_founder_pending=0 WHERE id=?", - (total + 1, user["id"]) - ) + # Atomare Gründer-Vergabe — Race-frei via Sub-Query im UPDATE. + # Wenn schon 100 Founder oder User schon is_founder=1 → kein Update (rowcount=0) + conn.execute( + """UPDATE users + SET is_founder = 1, + founder_number = ( + SELECT IFNULL(MAX(founder_number), 0) + 1 + FROM users WHERE is_founder = 1 + ), + is_founder_pending = 0 + WHERE id = ? + AND is_founder_pending = 1 + AND (is_founder IS NULL OR is_founder = 0) + AND (SELECT COUNT(*) FROM users WHERE is_founder = 1) < 100""", + (user["id"],) + ) return dict(dog) diff --git a/backend/routes/osm.py b/backend/routes/osm.py index d130898..4d1f36f 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -110,7 +110,7 @@ async def _fetch_overpass(query): except Exception as exc: logger.warning(f"Overpass Verbindungsfehler {url}: {exc}") break # nächste URL - raise Exception("Alle Overpass-Instanzen fehlgeschlagen") + raise HTTPException(503, "Kartendaten gerade nicht verfügbar — bitte später nochmal.") def _stale_tiles(poi_type, tiles): stale = [] diff --git a/backend/routes/partner.py b/backend/routes/partner.py index 359dc1c..b2d21f0 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -93,21 +93,34 @@ def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_adm if not target: raise HTTPException(404, "User nicht gefunden.") if updates.get("is_founder") == 1 and not target["founder_number"]: - # Neue Gründer-Nummer zuweisen - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total >= FOUNDER_MAX: + # Atomare Gründer-Vergabe — kein TOCTOU mehr zwischen COUNT und UPDATE. + # Sub-Query wird gegen Snapshot vor dem UPDATE evaluiert (SQL-Spec). + cur = conn.execute( + """UPDATE users + SET is_founder = 1, + founder_number = ( + SELECT IFNULL(MAX(founder_number), 0) + 1 + FROM users WHERE is_founder = 1 + ) + WHERE id = ? + AND (SELECT COUNT(*) FROM users WHERE is_founder = 1) < ? + AND (is_founder IS NULL OR is_founder = 0)""", + (user_id, FOUNDER_MAX) + ) + if cur.rowcount == 0: raise HTTPException(400, f"Alle {FOUNDER_MAX} Gründer-Plätze sind vergeben.") - updates["founder_number"] = total + 1 + # is_founder + founder_number sind atomar gesetzt — aus updates entfernen + updates.pop("is_founder", None) + updates.pop("founder_number", None) elif updates.get("is_founder") == 0: # Gründer-Status entfernen → founder_number ebenfalls leeren updates["founder_number"] = None - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {set_clause} WHERE id=?", - (*updates.values(), user_id) - ) + if updates: # nach atomarer Founder-Vergabe ggf. leer + set_clause = ", ".join(f"{k}=?" for k in updates) + conn.execute( + f"UPDATE users SET {set_clause} WHERE id=?", + (*updates.values(), user_id) + ) row = conn.execute( "SELECT id, name, email, is_founder, is_partner, founder_number FROM users WHERE id=?", (user_id,) diff --git a/backend/scheduler.py b/backend/scheduler.py index 11dcf62..fd04c22 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -46,6 +46,14 @@ def start(): misfire_grace_time=3600, coalesce=True, ) + _scheduler.add_job( + _job_purge_jwt_blacklist, + CronTrigger(hour=3, minute=30), # täglich 03:30 Uhr, nach poison_archive + id="purge_jwt_blacklist", + replace_existing=True, + misfire_grace_time=3600, + coalesce=True, + ) _scheduler.add_job( _job_weather_alert, CronTrigger(hour=7, minute=30), # täglich 07:30 Uhr @@ -2231,3 +2239,16 @@ async def _job_error_digest(): except Exception as e: logger.error(f"Error-Digest: Mail-Fehler: {e}") _log_job("error_digest", "error", str(e)) + + +def _job_purge_jwt_blacklist(): + """Räumt abgelaufene Einträge aus jwt_blacklist auf — sonst wächst die + Tabelle monoton mit jedem Logout. Läuft täglich 03:30.""" + try: + from auth import _purge_expired_jwt + deleted = _purge_expired_jwt() + logger.info(f"jwt_blacklist: {deleted} abgelaufene Einträge gelöscht.") + _log_job("purge_jwt_blacklist", "ok", f"{deleted} entries deleted") + except Exception as e: + logger.exception(f"jwt_blacklist purge fehlgeschlagen: {e}") + _log_job("purge_jwt_blacklist", "error", str(e)) diff --git a/backend/static/index.html b/backend/static/index.html index a06076c..02a2004 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@