mitai-jinkendo/backend/routers/profiles.py
Lars b4a1856f79
All checks were successful
Deploy Development / deploy (push) Successful in 58s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
refactor: modular backend architecture with 14 router modules
Phase 2 Complete - Backend Refactoring:
- Extracted all endpoints to dedicated router modules
- main.py: 1878 → 75 lines (-96% reduction)
- Created modular structure for maintainability

Router Structure (60 endpoints total):
├── auth.py          - 7 endpoints (login, logout, password reset)
├── profiles.py      - 7 endpoints (CRUD + current user)
├── weight.py        - 5 endpoints (tracking + stats)
├── circumference.py - 4 endpoints (body measurements)
├── caliper.py       - 4 endpoints (skinfold tracking)
├── activity.py      - 6 endpoints (workouts + Apple Health import)
├── nutrition.py     - 4 endpoints (diet + FDDB import)
├── photos.py        - 3 endpoints (progress photos)
├── insights.py      - 8 endpoints (AI analysis + pipeline)
├── prompts.py       - 2 endpoints (AI prompt management)
├── admin.py         - 7 endpoints (user management)
├── stats.py         - 1 endpoint (dashboard stats)
├── exportdata.py    - 3 endpoints (CSV/JSON/ZIP export)
└── importdata.py    - 1 endpoint (ZIP import)

Core modules maintained:
- db.py: PostgreSQL connection + helpers
- auth.py: Auth functions (hash, verify, sessions)
- models.py: 11 Pydantic models

Benefits:
- Self-contained modules with clear responsibilities
- Easier to navigate and modify specific features
- Improved code organization and readability
- 100% functional compatibility maintained
- All syntax checks passed

Updated CLAUDE.md with new architecture documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00

108 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 (admin)."""
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 = 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)
@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)