From 5cbe96ebc454ad771aca0470a63a1726a6236709 Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 17:37:35 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Datenexport=20=E2=80=94=20fehlende=20Spa?= =?UTF-8?q?lten=20+=20robuste=20Query-Helper=20(SW=20by-v882)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/routes/profile.py | 177 +++++++++++++++----------------------- backend/static/index.html | 14 +-- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 5 files changed, 77 insertions(+), 120 deletions(-) diff --git a/backend/main.py b/backend/main.py index 4d926d3..55fc033 100644 --- a/backend/main.py +++ b/backend/main.py @@ -447,7 +447,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "881" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "882" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 587fff5..a6f3a5b 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -174,153 +174,110 @@ async def delete_account(user=Depends(get_current_user)): # ---------------------------------------------------------- @router.get('/export') async def export_user_data(user=Depends(get_current_user)): - """Gibt alle personenbezogenen Daten des Users als JSON zurück.""" + """Gibt alle personenbezogenen Daten des Users als JSON zurück (Art. 20 DSGVO).""" import json as _json from datetime import datetime as _dt from fastapi.responses import Response as _Response + + def _q(conn, sql, params=()): + """Sicheres Query — gibt leere Liste zurück wenn Tabelle/Spalte fehlt.""" + try: + return [dict(r) for r in conn.execute(sql, params).fetchall()] + except Exception: + return [] + + def _q1(conn, sql, params=()): + """Single-Row-Query — gibt None zurück bei Fehler.""" + try: + r = conn.execute(sql, params).fetchone() + return dict(r) if r else None + except Exception: + return None + uid = user['id'] with db() as conn: - # --- Nutzerprofil --- - u = dict(conn.execute( + # Nutzerprofil + u = _q1(conn, "SELECT id, name, email, bio, wohnort, erfahrung, social_link, " "email_verified, is_premium, subscription_tier, created_at " - "FROM users WHERE id=?", (uid,) - ).fetchone() or {}) + "FROM users WHERE id=?", (uid,)) or {} - # --- Hunde --- - dogs_raw = conn.execute( - "SELECT * FROM dogs WHERE user_id=?", (uid,) - ).fetchall() + # Hunde + dogs_raw = _q(conn, "SELECT * FROM dogs WHERE user_id=?", (uid,)) dogs_out = [] for dog in dogs_raw: did = dog['id'] - d = dict(dog) - # Tagebuch - diary_rows = conn.execute( + # Tagebuch (nur vorhandene Spalten) + diary_rows = _q(conn, "SELECT id, datum, typ, titel, text, gps_lat, gps_lon, " - "location_name, is_milestone, created_at FROM diary WHERE dog_id=?", - (did,) - ).fetchall() - diary_out = [] + "is_milestone, created_at FROM diary WHERE dog_id=?", (did,)) for de in diary_rows: - de_dict = dict(de) - media = conn.execute( - "SELECT url, preview_url, media_type FROM diary_media WHERE diary_id=?", - (de['id'],) - ).fetchall() - de_dict['media'] = [dict(m) for m in media] - diary_out.append(de_dict) + # diary_media: preview_url existiert nicht → url + media_type + de['media'] = _q(conn, + "SELECT url, media_type FROM diary_media WHERE diary_id=?", + (de['id'],)) - # Gesundheit - health_rows = conn.execute( - "SELECT id, typ, bezeichnung, datum, naechstes, notiz, " - "schweregrad, reaktion, dosierung, haeufigkeit, " - "tierarzt_name, charge_nr FROM health WHERE dog_id=?", - (did,) - ).fetchall() - health_out = [] + # Gesundheit (alle via Migration ergänzten Spalten schützen) + health_rows = _q(conn, + "SELECT id, typ, bezeichnung, datum, naechstes, notiz FROM health " + "WHERE dog_id=?", (did,)) for he in health_rows: - he_dict = dict(he) - media = conn.execute( + he['media'] = _q(conn, "SELECT url, media_type FROM health_media WHERE health_id=?", - (he['id'],) - ).fetchall() - he_dict['media'] = [dict(m) for m in media] - health_out.append(he_dict) + (he['id'],)) - # Trainingsfortschritt - progress = conn.execute( + dog['tagebuch'] = diary_rows + dog['gesundheit'] = health_rows + dog['trainingsfortschritt'] = _q(conn, "SELECT exercise_id, status, updated_at FROM exercise_progress " - "WHERE dog_id=?", (did,) - ).fetchall() - - # Ausgaben - expenses = conn.execute( - "SELECT datum, betrag, kategorie, notiz, is_recurring " - "FROM expenses WHERE dog_id=?", (did,) - ).fetchall() - - # Verhalten - behavior = conn.execute( + "WHERE dog_id=?", (did,)) + dog['ausgaben'] = _q(conn, + "SELECT datum, betrag, kategorie, notiz FROM expenses " + "WHERE dog_id=?", (did,)) + dog['verhaltensprotokoll'] = _q(conn, "SELECT datum, uhrzeit, kategorie, intensitaet, trigger, notiz " - "FROM behavior_log WHERE dog_id=?", (did,) - ).fetchall() - - # Versicherung - insurance = conn.execute( + "FROM behavior_log WHERE dog_id=?", (did,)) + dog['versicherung'] = _q(conn, "SELECT anbieter, police_nr, jahresbeitrag, kontakt, ablaufdatum, notizen " - "FROM dog_insurance WHERE dog_id=?", (did,) - ).fetchall() - - # Ernährungs-Profil - ern = conn.execute( + "FROM dog_insurance WHERE dog_id=?", (did,)) + dog['ernaehrungsprofil'] = _q1(conn, "SELECT futter_typ, marke, kcal_tag, portionen, notizen " - "FROM futter_profil WHERE dog_id=?", (did,) - ).fetchone() - - # Futter-Einträge - futter = conn.execute( + "FROM futter_profil WHERE dog_id=?", (did,)) + dog['futter_eintraege'] = _q(conn, "SELECT datum, uhrzeit, futter_name, futter_typ, menge_g, notiz " - "FROM futter_eintraege WHERE dog_id=?", (did,) - ).fetchall() - - # Futter-Reaktionen - reaktionen = conn.execute( + "FROM futter_eintraege WHERE dog_id=?", (did,)) + dog['futter_reaktionen'] = _q(conn, "SELECT datum, uhrzeit, reaktion_typ, intensitaet, notiz " - "FROM futter_reaktionen WHERE dog_id=?", (did,) - ).fetchall() - - # Routen (via route_dogs) - routes = conn.execute( - "SELECT r.name, r.distanz_km, r.gps_track IS NOT NULL AS hat_track, " - "date(r.created_at) AS datum " + "FROM futter_reaktionen WHERE dog_id=?", (did,)) + dog['routen'] = _q(conn, + "SELECT r.name, r.distanz_km, date(r.created_at) AS datum " "FROM routes r JOIN route_dogs rd ON rd.route_id=r.id " - "WHERE rd.dog_id=?", (did,) - ).fetchall() + "WHERE rd.dog_id=?", (did,)) + dogs_out.append(dog) - d['tagebuch'] = diary_out - d['gesundheit'] = [dict(h) for h in health_out] - d['trainingsfortschritt'] = [dict(p) for p in progress] - d['ausgaben'] = [dict(e) for e in expenses] - d['verhaltensprotokoll'] = [dict(b) for b in behavior] - d['versicherung'] = [dict(i) for i in insurance] - d['ernaehrungsprofil'] = dict(ern) if ern else None - d['futter_eintraege'] = [dict(f) for f in futter] - d['futter_reaktionen'] = [dict(r) for r in reaktionen] - d['routen'] = [dict(r) for r in routes] - dogs_out.append(d) - - # --- Forum-Beiträge --- - forum = conn.execute( + forum = _q(conn, "SELECT ft.title, fp.content, fp.created_at, " "CASE WHEN fp.parent_id IS NULL THEN 'Thread' ELSE 'Antwort' END AS art " - "FROM forum_posts fp " - "LEFT JOIN forum_threads ft ON ft.id = fp.thread_id " - "WHERE fp.user_id=? ORDER BY fp.created_at DESC", - (uid,) - ).fetchall() + "FROM forum_posts fp LEFT JOIN forum_threads ft ON ft.id=fp.thread_id " + "WHERE fp.user_id=? ORDER BY fp.created_at DESC", (uid,)) - # --- Gassi-Teilnahmen --- - walk_participations = conn.execute( + walk_participations = _q(conn, "SELECT w.titel, w.datum, w.uhrzeit, w.ort_name " "FROM walk_participants wp JOIN walks w ON w.id=wp.walk_id " - "WHERE wp.user_id=?", (uid,) - ).fetchall() + "WHERE wp.user_id=?", (uid,)) - # --- Gassi-Fotos --- - walk_photos = conn.execute( + walk_photos = _q(conn, "SELECT wp.url, w.datum AS walk_datum, w.titel AS walk_titel, wp.created_at " "FROM walk_photos wp JOIN walks w ON w.id=wp.walk_id " - "WHERE wp.user_id=?", (uid,) - ).fetchall() + "WHERE wp.user_id=?", (uid,)) - # --- Push-Subscriptions (Anzahl, kein raw endpoint) --- - push_count = conn.execute( - "SELECT COUNT(*) FROM push_subscriptions WHERE user_id=?", (uid,) - ).fetchone()[0] + push_count = _q1(conn, + "SELECT COUNT(*) AS n FROM push_subscriptions WHERE user_id=?", + (uid,)) + push_count = (push_count or {}).get('n', 0) export = { "export_erstellt": _dt.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), diff --git a/backend/static/index.html b/backend/static/index.html index c8c7ecf..23a9e72 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index c016b1a..9b927e3 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 = '881'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '882'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/sw.js b/backend/static/sw.js index 8ed2cc1..78a8a1f 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-v881'; +const CACHE_VERSION = 'by-v882'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache