From 9fbedb6c4b614174682bb01193f7ca5ffee7069d Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 18 Mar 2026 12:42:46 +0100 Subject: [PATCH] fix: use RealDictCursor for PostgreSQL row access All conn.cursor() calls replaced with get_cursor(conn) to enable dict-like row access (prof['pin_hash'] instead of prof[column_index]). This fixes KeyError when accessing PostgreSQL query results. Fixes: 'tuple' object has no attribute '__getitem__' with string keys Co-Authored-By: Claude Opus 4.6 --- backend/main.py | 114 ++++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/backend/main.py b/backend/main.py index 103ada8..7556af1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,7 +14,7 @@ from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded from starlette.requests import Request -from db import get_db, r2d +from db import get_db, get_cursor, r2d DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) PHOTOS_DIR = Path(os.getenv("PHOTOS_DIR", "./photos")) @@ -51,7 +51,7 @@ def get_pid(x_profile_id: Optional[str] = Header(default=None)) -> str: if x_profile_id: return x_profile_id with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id FROM profiles ORDER BY created LIMIT 1") row = cur.fetchone() if row: return row['id'] @@ -134,7 +134,7 @@ def make_token() -> str: def get_session(token: str): if not token: return None with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT s.*, p.role, p.name, p.ai_enabled, p.ai_limit_day, p.export_enabled " "FROM sessions s JOIN profiles p ON s.profile_id=p.id " @@ -157,7 +157,7 @@ def require_admin(x_auth_token: Optional[str]=Header(default=None)): @app.get("/api/profiles") def list_profiles(session=Depends(require_auth)): with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles ORDER BY created") rows = cur.fetchall() return [r2d(r) for r in rows] @@ -166,19 +166,19 @@ def list_profiles(session=Depends(require_auth)): def create_profile(p: ProfileCreate, session=Depends(require_auth)): pid = str(uuid.uuid4()) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("""INSERT INTO profiles (id,name,avatar_color,sex,dob,height,goal_weight,goal_bf_pct,created,updated) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)""", (pid,p.name,p.avatar_color,p.sex,p.dob,p.height,p.goal_weight,p.goal_bf_pct)) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) return r2d(cur.fetchone()) @app.get("/api/profiles/{pid}") def get_profile(pid: str, session=Depends(require_auth)): with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) row = cur.fetchone() if not row: raise HTTPException(404, "Profil nicht gefunden") @@ -189,7 +189,7 @@ def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)): with get_db() as conn: data = {k:v for k,v in p.model_dump().items() if v is not None} data['updated'] = datetime.now().isoformat() - cur = conn.cursor() + cur = get_cursor(conn) cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in data)} WHERE id=%s", list(data.values())+[pid]) return get_profile(pid, session) @@ -197,7 +197,7 @@ def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)): @app.delete("/api/profiles/{pid}") def delete_profile(pid: str, session=Depends(require_auth)): with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT COUNT(*) FROM profiles") count = cur.fetchone()[0] if count <= 1: raise HTTPException(400, "Letztes Profil kann nicht gelöscht werden") @@ -222,7 +222,7 @@ def update_active_profile(p: ProfileUpdate, x_profile_id: Optional[str] = Header def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @@ -231,7 +231,7 @@ def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None) def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id FROM weight_log WHERE profile_id=%s AND date=%s", (pid,e.date)) ex = cur.fetchone() if ex: @@ -247,7 +247,7 @@ def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=Non def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("UPDATE weight_log SET date=%s,weight=%s,note=%s WHERE id=%s AND profile_id=%s", (e.date,e.weight,e.note,wid,pid)) return {"id":wid} @@ -256,7 +256,7 @@ def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(d def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM weight_log WHERE id=%s AND profile_id=%s", (wid,pid)) return {"ok":True} @@ -264,7 +264,7 @@ def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), se def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) rows = cur.fetchall() if not rows: return {"count":0,"latest":None,"prev":None,"min":None,"max":None,"avg_7d":None} @@ -278,7 +278,7 @@ def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict def list_circs(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM circumference_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @@ -287,7 +287,7 @@ def list_circs(limit: int=100, x_profile_id: Optional[str]=Header(default=None), def upsert_circ(e: CircumferenceEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id FROM circumference_log WHERE profile_id=%s AND date=%s", (pid,e.date)) ex = cur.fetchone() d = e.model_dump() @@ -310,7 +310,7 @@ def update_circ(eid: str, e: CircumferenceEntry, x_profile_id: Optional[str]=Hea pid = get_pid(x_profile_id) with get_db() as conn: d = e.model_dump() - cur = conn.cursor() + cur = get_cursor(conn) cur.execute(f"UPDATE circumference_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", list(d.values())+[eid,pid]) return {"id":eid} @@ -319,7 +319,7 @@ def update_circ(eid: str, e: CircumferenceEntry, x_profile_id: Optional[str]=Hea def delete_circ(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM circumference_log WHERE id=%s AND profile_id=%s", (eid,pid)) return {"ok":True} @@ -328,7 +328,7 @@ def delete_circ(eid: str, x_profile_id: Optional[str]=Header(default=None), sess def list_caliper(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM caliper_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @@ -337,7 +337,7 @@ def list_caliper(limit: int=100, x_profile_id: Optional[str]=Header(default=None def upsert_caliper(e: CaliperEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id FROM caliper_log WHERE profile_id=%s AND date=%s", (pid,e.date)) ex = cur.fetchone() d = e.model_dump() @@ -362,7 +362,7 @@ def update_caliper(eid: str, e: CaliperEntry, x_profile_id: Optional[str]=Header pid = get_pid(x_profile_id) with get_db() as conn: d = e.model_dump() - cur = conn.cursor() + cur = get_cursor(conn) cur.execute(f"UPDATE caliper_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", list(d.values())+[eid,pid]) return {"id":eid} @@ -371,7 +371,7 @@ def update_caliper(eid: str, e: CaliperEntry, x_profile_id: Optional[str]=Header def delete_caliper(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM caliper_log WHERE id=%s AND profile_id=%s", (eid,pid)) return {"ok":True} @@ -380,7 +380,7 @@ def delete_caliper(eid: str, x_profile_id: Optional[str]=Header(default=None), s def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC, start_time DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @@ -391,7 +391,7 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default eid = str(uuid.uuid4()) d = e.model_dump() with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("""INSERT INTO activity_log (id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, hr_avg,hr_max,distance_km,rpe,source,notes,created) @@ -406,7 +406,7 @@ def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Head pid = get_pid(x_profile_id) with get_db() as conn: d = e.model_dump() - cur = conn.cursor() + cur = get_cursor(conn) cur.execute(f"UPDATE activity_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", list(d.values())+[eid,pid]) return {"id":eid} @@ -415,7 +415,7 @@ def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Head def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM activity_log WHERE id=%s AND profile_id=%s", (eid,pid)) return {"ok":True} @@ -423,7 +423,7 @@ def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None), def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) rows = [r2d(r) for r in cur.fetchall()] @@ -448,7 +448,7 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional reader = csv.DictReader(io.StringIO(text)) inserted = skipped = 0 with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) for row in reader: wtype = row.get('Workout Type','').strip() start = row.get('Start','').strip() @@ -492,7 +492,7 @@ async def upload_photo(file: UploadFile=File(...), date: str="", path = PHOTOS_DIR / f"{fid}{ext}" async with aiofiles.open(path,'wb') as f: await f.write(await file.read()) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("INSERT INTO photos (id,profile_id,date,path,created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)", (fid,pid,date,str(path))) return {"id":fid,"date":date} @@ -500,7 +500,7 @@ async def upload_photo(file: UploadFile=File(...), date: str="", @app.get("/api/photos/{fid}") def get_photo(fid: str, session: dict=Depends(require_auth)): with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT path FROM photos WHERE id=%s", (fid,)) row = cur.fetchone() if not row: raise HTTPException(404) @@ -510,7 +510,7 @@ def get_photo(fid: str, session: dict=Depends(require_auth)): def list_photos(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM photos WHERE profile_id=%s ORDER BY created DESC LIMIT 100", (pid,)) return [r2d(r) for r in cur.fetchall()] @@ -546,7 +546,7 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona count+=1 inserted=0 with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) for iso,vals in days.items(): kcal=round(vals['kcal'],1); fat=round(vals['fat_g'],1) carbs=round(vals['carbs_g'],1); prot=round(vals['protein_g'],1) @@ -565,7 +565,7 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute( "SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @@ -574,7 +574,7 @@ def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=No def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date",(pid,)) nutr={r['date']:r2d(r) for r in cur.fetchall()} cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date",(pid,)) @@ -602,7 +602,7 @@ def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), ses def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s",(pid,weeks*7)) rows=[r2d(r) for r in cur.fetchall()] if not rows: return [] @@ -622,7 +622,7 @@ def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=N def get_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT COUNT(*) FROM weight_log WHERE profile_id=%s",(pid,)) weight_count = cur.fetchone()[0] cur.execute("SELECT COUNT(*) FROM circumference_log WHERE profile_id=%s",(pid,)) @@ -648,7 +648,7 @@ import httpx, json def get_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s AND scope=%s ORDER BY created DESC LIMIT 1", (pid,scope)) row = cur.fetchone() if not row: return None @@ -658,14 +658,14 @@ def get_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), def delete_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid,scope)) return {"ok":True} def check_ai_limit(pid: str): """Check if profile has reached daily AI limit. Returns (allowed, limit, used).""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT ai_enabled, ai_limit_day FROM profiles WHERE id=%s", (pid,)) prof = cur.fetchone() if not prof or not prof['ai_enabled']: @@ -685,7 +685,7 @@ def inc_ai_usage(pid: str): """Increment AI usage counter for today.""" today = datetime.now().date().isoformat() with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id, call_count FROM ai_usage WHERE profile_id=%s AND date=%s", (pid, today)) row = cur.fetchone() if row: @@ -697,7 +697,7 @@ def inc_ai_usage(pid: str): def _get_profile_data(pid: str): """Fetch all relevant data for AI analysis.""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) prof = r2d(cur.fetchone()) cur.execute("SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) @@ -831,7 +831,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa # Get prompt template with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM ai_prompts WHERE slug=%s AND active=1", (slug,)) prompt_row = cur.fetchone() if not prompt_row: @@ -872,7 +872,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa # Save insight with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid, slug)) cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)", (str(uuid.uuid4()), pid, slug, content)) @@ -891,7 +891,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses # Stage 1: Parallel JSON analyses with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT slug, template FROM ai_prompts WHERE slug LIKE 'pipeline_%' AND slug NOT IN ('pipeline_synthesis','pipeline_goals') AND active=1") stage1_prompts = [r2d(r) for r in cur.fetchall()] @@ -936,7 +936,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses vars['stage1_activity'] = json.dumps(stage1_results.get('pipeline_activity', {}), ensure_ascii=False) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_synthesis' AND active=1") synth_row = cur.fetchone() if not synth_row: @@ -973,7 +973,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses prof = data['profile'] if prof.get('goal_weight') or prof.get('goal_bf_pct'): with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_goals' AND active=1") goals_row = cur.fetchone() if goals_row: @@ -1008,7 +1008,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses # Save as 'gesamt' scope with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope='gesamt'", (pid,)) cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,'gesamt',%s,CURRENT_TIMESTAMP)", (str(uuid.uuid4()), pid, final_content)) @@ -1020,7 +1020,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses def list_prompts(session: dict=Depends(require_auth)): """List all available AI prompts.""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM ai_prompts WHERE active=1 AND slug NOT LIKE 'pipeline_%' ORDER BY sort_order") return [r2d(r) for r in cur.fetchall()] @@ -1029,7 +1029,7 @@ def get_ai_usage(x_profile_id: Optional[str]=Header(default=None), session: dict """Get AI usage stats for current profile.""" pid = get_pid(x_profile_id) with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT ai_limit_day FROM profiles WHERE id=%s", (pid,)) prof = cur.fetchone() limit = prof['ai_limit_day'] if prof else None @@ -1066,7 +1066,7 @@ class PasswordResetConfirm(BaseModel): async def login(req: LoginRequest, request: Request): """Login with email + password.""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE email=%s", (req.email.lower().strip(),)) prof = cur.fetchone() if not prof: @@ -1101,7 +1101,7 @@ def logout(x_auth_token: Optional[str]=Header(default=None)): """Logout (delete session).""" if x_auth_token: with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("DELETE FROM sessions WHERE token=%s", (x_auth_token,)) return {"ok": True} @@ -1117,7 +1117,7 @@ async def password_reset_request(req: PasswordResetRequest, request: Request): """Request password reset email.""" email = req.email.lower().strip() with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT id, name FROM profiles WHERE email=%s", (email,)) prof = cur.fetchone() if not prof: @@ -1173,7 +1173,7 @@ Dein Mitai Jinkendo Team def password_reset_confirm(req: PasswordResetConfirm): """Confirm password reset with token.""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT profile_id FROM sessions WHERE token=%s AND expires_at > CURRENT_TIMESTAMP", (f"reset_{req.token}",)) sess = cur.fetchone() @@ -1198,7 +1198,7 @@ class AdminProfileUpdate(BaseModel): def admin_list_profiles(session: dict=Depends(require_admin)): """Admin: List all profiles with stats.""" with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT * FROM profiles ORDER BY created") profs = [r2d(r) for r in cur.fetchall()] @@ -1224,7 +1224,7 @@ def admin_update_profile(pid: str, data: AdminProfileUpdate, session: dict=Depen if not updates: return {"ok": True} - cur = conn.cursor() + cur = get_cursor(conn) cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in updates)} WHERE id=%s", list(updates.values()) + [pid]) @@ -1267,7 +1267,7 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D # Check export permission with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,)) prof = cur.fetchone() if not prof or not prof['export_enabled']: @@ -1282,7 +1282,7 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D # Weight with get_db() as conn: - cur = conn.cursor() + cur = get_cursor(conn) cur.execute("SELECT date, weight, note FROM weight_log WHERE profile_id=%s ORDER BY date", (pid,)) for r in cur.fetchall(): writer.writerow(["Gewicht", r['date'], f"{r['weight']}kg", r['note'] or ""])