fix: use RealDictCursor for PostgreSQL row access
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 12s

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 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-18 12:42:46 +01:00
parent 124df01983
commit 9fbedb6c4b

View File

@ -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 ""])