All checks were successful
Deploy Development / deploy (push) Successful in 39s
Backend: - Auth router (login, register, logout) - Profiles router (get current profile) - Registered in main.py Frontend: - LoginPage with login/register tabs - Dashboard with welcome screen - Simplified AuthContext for Shinkan - Protected routes in App.jsx - Public routes redirect when logged in Ready for testing! version: 0.1.0
157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
"""
|
||
Profile Management Endpoints for Mitai Jinkendo
|
||
|
||
Handles profile CRUD operations for both admin and current user.
|
||
"""
|
||
import uuid
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
|
||
from fastapi import APIRouter, HTTPException, Header, Depends
|
||
|
||
from db import get_db, get_cursor, r2d
|
||
from auth import require_auth
|
||
from models import ProfileCreate, ProfileUpdate
|
||
|
||
router = APIRouter(prefix="/api", tags=["profiles"])
|
||
|
||
|
||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||
def get_pid(x_profile_id: Optional[str] = Header(default=None)) -> str:
|
||
"""Get profile_id - from header for legacy endpoints."""
|
||
if x_profile_id:
|
||
return x_profile_id
|
||
with get_db() as conn:
|
||
cur = get_cursor(conn)
|
||
cur.execute("SELECT id FROM profiles ORDER BY created LIMIT 1")
|
||
row = cur.fetchone()
|
||
if row: return row['id']
|
||
raise HTTPException(400, "Kein Profil gefunden")
|
||
|
||
|
||
# ── Admin Profile Management ──────────────────────────────────────────────────
|
||
@router.get("/profiles")
|
||
def list_profiles(session=Depends(require_auth)):
|
||
"""List all profiles (admin)."""
|
||
with get_db() as conn:
|
||
cur = get_cursor(conn)
|
||
cur.execute("SELECT * FROM profiles ORDER BY created")
|
||
rows = cur.fetchall()
|
||
return [r2d(r) for r in rows]
|
||
|
||
|
||
@router.post("/profiles")
|
||
def create_profile(p: ProfileCreate, session=Depends(require_auth)):
|
||
"""Create new profile (admin)."""
|
||
pid = str(uuid.uuid4())
|
||
with get_db() as conn:
|
||
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 = get_cursor(conn)
|
||
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
|
||
return r2d(cur.fetchone())
|
||
|
||
|
||
@router.get("/profiles/{pid}")
|
||
def get_profile(pid: str, session=Depends(require_auth)):
|
||
"""Get profile by ID."""
|
||
with get_db() as conn:
|
||
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")
|
||
return r2d(row)
|
||
|
||
|
||
@router.put("/profiles/{pid}")
|
||
def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)):
|
||
"""Update profile by ID."""
|
||
with get_db() as conn:
|
||
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")
|
||
rowd = r2d(row)
|
||
cur_email_norm = (rowd.get("email") or "").strip().lower()
|
||
|
||
patch = p.model_dump(exclude_unset=True)
|
||
data = {}
|
||
|
||
if "email" in patch:
|
||
ev = patch["email"]
|
||
if ev is None or (isinstance(ev, str) and ev.strip() == ""):
|
||
if rowd.get("email") is not None:
|
||
data["email"] = None
|
||
data["email_verified"] = False
|
||
data["verification_token"] = None
|
||
data["verification_expires"] = None
|
||
else:
|
||
email_norm = ev.strip().lower()
|
||
if "@" not in email_norm or len(email_norm) < 5:
|
||
raise HTTPException(400, "Ungültige E-Mail-Adresse")
|
||
cur.execute(
|
||
"""
|
||
SELECT id FROM profiles
|
||
WHERE email IS NOT NULL AND lower(trim(email)) = %s AND id <> %s
|
||
""",
|
||
(email_norm, pid),
|
||
)
|
||
if cur.fetchone():
|
||
raise HTTPException(409, "E-Mail wird bereits verwendet")
|
||
data["email"] = email_norm
|
||
if email_norm != cur_email_norm:
|
||
data["email_verified"] = False
|
||
data["verification_token"] = None
|
||
data["verification_expires"] = None
|
||
|
||
nullable_keys = {"goal_weight", "goal_bf_pct", "dob"}
|
||
for k, v in patch.items():
|
||
if k == "email":
|
||
continue
|
||
if v is None and k in nullable_keys:
|
||
data[k] = None
|
||
elif v is not None:
|
||
data[k] = v
|
||
|
||
if not data:
|
||
return get_profile(pid, session)
|
||
|
||
data["updated"] = datetime.now().isoformat()
|
||
cols = ", ".join(f"{k}=%s" for k in data)
|
||
vals = list(data.values()) + [pid]
|
||
cur.execute(f"UPDATE profiles SET {cols} WHERE id=%s", vals)
|
||
return get_profile(pid, session)
|
||
|
||
|
||
@router.delete("/profiles/{pid}")
|
||
def delete_profile(pid: str, session=Depends(require_auth)):
|
||
"""Delete profile (admin)."""
|
||
with get_db() as conn:
|
||
cur = get_cursor(conn)
|
||
cur.execute("SELECT COUNT(*) as count FROM profiles")
|
||
count = cur.fetchone()['count']
|
||
if count <= 1: raise HTTPException(400, "Letztes Profil kann nicht gelöscht werden")
|
||
for table in ['weight_log','circumference_log','caliper_log','nutrition_log','activity_log','ai_insights']:
|
||
cur.execute(f"DELETE FROM {table} WHERE profile_id=%s", (pid,))
|
||
cur.execute("DELETE FROM profiles WHERE id=%s", (pid,))
|
||
return {"ok": True}
|
||
|
||
|
||
# ── Current User Profile ──────────────────────────────────────────────────────
|
||
@router.get("/profile")
|
||
def get_active_profile(x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth)):
|
||
"""Legacy endpoint – returns active profile."""
|
||
pid = get_pid(x_profile_id)
|
||
return get_profile(pid, session)
|
||
|
||
|
||
@router.put("/profile")
|
||
def update_active_profile(p: ProfileUpdate, x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth)):
|
||
"""Update current user's profile."""
|
||
pid = get_pid(x_profile_id)
|
||
return update_profile(pid, p, session)
|