From 704849962482dee44e0787f0ba231e8ca2bd07c9 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 29 Apr 2026 08:23:55 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20ORS-Wochenlimit=20(20/Woche),=20Tage?= =?UTF-8?q?s-Cache,=20Privilegien-Bypass,=20Datenschutz-Update=20=E2=80=94?= =?UTF-8?q?=20SW=20by-v480,=20APP=5FVER=20457?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/auth.py | 2 +- backend/database.py | 11 +++++++ backend/routes/routen.py | 45 +++++++++++++++++++++++--- backend/static/js/app.js | 2 +- backend/static/js/pages/datenschutz.js | 25 +++++++++++++- backend/static/js/pages/routes.js | 26 ++++++++++++--- backend/static/js/pages/welcome.js | 22 +++++++++++++ backend/static/sw.js | 2 +- 8 files changed, 121 insertions(+), 14 deletions(-) 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 = ` -
- ${UI.icon('warning')} ${UI.escape(err.message || 'Fehler beim Berechnen des Rundwegs.')} +
+ +

+ ${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')} +

`; if (calcBtn) calcBtn.disabled = false; return; @@ -515,8 +525,14 @@ window.Page_routes = (() => { : '–'; const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || ''; + const limitHint = (result.weekly_remaining != null) + ? `

+ Noch ${result.weekly_remaining} von 20 Anfragen diese Woche +

` + : ''; + res.innerHTML = ` -
+ ${limitHint}
${UI.icon('map-trifold')} ${UI.escape(distStr)} diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index 9484601..1300d55 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -375,12 +375,34 @@ window.Page_welcome = (() => { const km = [2, 4, 6][dayIdx % 3]; const seed = dayIdx % 5; + // Tages-Cache prüfen — ORS nur einmal pro Tag anfragen + const today = new Date().toISOString().slice(0, 10); // 'YYYY-MM-DD' + const cacheKey = 'by_daily_route_' + today; + const cached = localStorage.getItem(cacheKey); + if (cached) { + try { + const result = JSON.parse(cached); + _applyRouteChip(result, km, seed); + return; + } catch {} + } + let result; try { result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed }); } catch { return; } if (!result?.gps_track?.length) return; + // Ergebnis cachen und alte Einträge aufräumen + localStorage.setItem(cacheKey, JSON.stringify(result)); + Object.keys(localStorage) + .filter(k => k.startsWith('by_daily_route_') && k !== cacheKey) + .forEach(k => localStorage.removeItem(k)); + + _applyRouteChip(result, km, seed); + } + + function _applyRouteChip(result, km, seed) { const chipsRow = _container.querySelector('#wc-chips-row'); if (!chipsRow) return; diff --git a/backend/static/sw.js b/backend/static/sw.js index f762bef..d2486a9 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v479'; +const CACHE_VERSION = 'by-v480'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten