diff --git a/backend/auth.py b/backend/auth.py index beedb65..b3365aa 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -87,7 +87,7 @@ def get_current_user( user_id = int(payload["sub"]) with db() as conn: row = conn.execute( - "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled FROM users WHERE id=?", + "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status FROM users WHERE id=?", (user_id,) ).fetchone() diff --git a/backend/database.py b/backend/database.py index 3bea359..4db82be 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1411,3 +1411,14 @@ def _migrate(conn_factory): if col not in existing_h: conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}") logger.info(f"Migration: health.{col} hinzugefügt.") + + # Route-Suggest Rate-Limiting + conn.executescript(""" + CREATE TABLE IF NOT EXISTS route_suggest_usage ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + week TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (user_id, week) + ); + """) + logger.info("Migration: route_suggest_usage Tabelle bereit.") diff --git a/backend/routes/routen.py b/backend/routes/routen.py index 5d27e04..f812284 100644 --- a/backend/routes/routen.py +++ b/backend/routes/routen.py @@ -1,5 +1,6 @@ """BAN YARO — Gassi-Routen""" +import datetime as _dt import json, math, os, uuid import httpx import polyline as _polyline @@ -184,6 +185,8 @@ class SuggestRequest(BaseModel): distance_km: float # Zieldistanz in km (z.B. 2.0, 4.0, 6.0) seed: int = 0 # 0-4: verschiedene Routenvarianten +WEEKLY_LIMIT = 20 + @router.post("/suggest") async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)): if not (0.5 <= data.distance_km <= 15): @@ -192,6 +195,27 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)): if not ORS_API_KEY: raise HTTPException(503, "ORS nicht konfiguriert") + is_privileged = ( + user.get("rolle") in ("admin", "moderator") or + user.get("is_moderator") or + user.get("is_social_media") or + user.get("breeder_status") == "approved" + ) + + today = _dt.date.today() + week_start = (today - _dt.timedelta(days=today.weekday())).isoformat() + + if not is_privileged: + with db() as conn: + row = conn.execute( + "SELECT count FROM route_suggest_usage WHERE user_id=? AND week=?", + (user["id"], week_start) + ).fetchone() + current_count = row["count"] if row else 0 + + if current_count >= WEEKLY_LIMIT: + raise HTTPException(429, f"Wochenlimit von {WEEKLY_LIMIT} Routenvorschlägen erreicht.") + payload = { "coordinates": [[data.lon, data.lat]], "options": { @@ -251,12 +275,23 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)): else: schwierigkeit = "anspruchsvoll" + if not is_privileged: + with db() as conn: + conn.execute(""" + INSERT INTO route_suggest_usage (user_id, week, count) VALUES (?, ?, 1) + ON CONFLICT(user_id, week) DO UPDATE SET count = count + 1 + """, (user["id"], week_start)) + current_count += 1 + + weekly_remaining = None if is_privileged else max(0, WEEKLY_LIMIT - current_count) + return { - "name": f"Rundweg {distanz_km:.0f} km", - "gps_track": gps_track, - "distanz_km": distanz_km, - "dauer_min": dauer_min, - "schwierigkeit": schwierigkeit, + "name": f"Rundweg {distanz_km:.0f} km", + "gps_track": gps_track, + "distanz_km": distanz_km, + "dauer_min": dauer_min, + "schwierigkeit": schwierigkeit, + "weekly_remaining": weekly_remaining, } diff --git a/backend/static/js/app.js b/backend/static/js/app.js index ebc0c63..8c3b3a2 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '456'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '457'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/datenschutz.js b/backend/static/js/pages/datenschutz.js index 676523a..838aaf4 100644 --- a/backend/static/js/pages/datenschutz.js +++ b/backend/static/js/pages/datenschutz.js @@ -105,6 +105,29 @@ window.Page_datenschutz = (() => { findet nicht statt.
`)} + ${sec('Routenvorschläge (OpenRouteService)', ` ++ Die Funktion „Routenvorschläge" berechnet auf Wunsch einen Rundweg + ausgehend von deinem aktuellen Standort. Dazu werden deine GPS-Koordinaten einmalig + an den Dienst OpenRouteService übermittelt, der von + HeiGIT am Karlsruher Institut für Technologie (KIT), Deutschland, + betrieben wird. Es werden ausschließlich die Koordinaten übertragen — + keine Account- oder Profildaten. OpenRouteService speichert keine + personenbezogenen Daten dauerhaft. +
++ Die Funktion wird nur aktiv, wenn du deinen Standort im Browser freigibst und + bewusst einen Routenvorschlag anforderst (Einwilligung gem. Art. 6 Abs. 1 lit. a DSGVO). + Der Tagesvorschlag auf der Startseite wird nur berechnet, wenn du eingeloggt bist und + Standortzugriff erteilt hast — das Ergebnis wird lokal zwischengespeichert und + maximal einmal täglich neu abgerufen. +
++ Datenschutzerklärung von OpenRouteService: + openrouteservice.org/privacy-policy +
`)} + ${sec('Push-Benachrichtigungen', `Wenn du Push-Benachrichtigungen aktivierst, wird ein Abonnement-Token an den @@ -164,7 +187,7 @@ window.Page_datenschutz = (() => {
`)}- Stand: April 2026 + Stand: Mai 2026
diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 9fa1103..bdc48c1 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -490,11 +490,21 @@ window.Page_routes = (() => { }); _suggestResult = result; } catch (err) { + const is429 = err.status === 429 || String(err.message).includes('Wochenlimit'); if (res) res.innerHTML = ` -+ ${is429 ? 'Wochenlimit erreicht' : 'Fehler beim Berechnen'} +
++ ${is429 ? 'Du hast diese Woche alle 20 Routenvorschläge genutzt. Montag gibt es neue.' : UI.escape(err.message || 'Unbekannter Fehler')} +
+ Noch ${result.weekly_remaining} von 20 Anfragen diese Woche +
` + : ''; + res.innerHTML = ` - + ${limitHint}