mitai-jinkendo/backend/routers/photos.py
Lars 1298bd235f
All checks were successful
Deploy Development / deploy (push) Successful in 35s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 12s
feat: add structured JSON logging for all feature usage (Phase 2)
- Create feature_logger.py with JSON logging infrastructure
- Add log_feature_usage() calls to all 9 routers after check_feature_access()
- Logs written to /app/logs/feature-usage.log
- Tracks all usage (not just violations) for future analysis
- Phase 2: Non-blocking monitoring complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 22:18:12 +01:00

82 lines
2.9 KiB
Python

"""
Photo Management Endpoints for Mitai Jinkendo
Handles progress photo uploads and retrieval.
"""
import os
import uuid
import logging
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, UploadFile, File, Header, HTTPException, Depends
from fastapi.responses import FileResponse
import aiofiles
from db import get_db, get_cursor, r2d
from auth import require_auth, require_auth_flexible, check_feature_access, increment_feature_usage
from routers.profiles import get_pid
from feature_logger import log_feature_usage
router = APIRouter(prefix="/api/photos", tags=["photos"])
logger = logging.getLogger(__name__)
PHOTOS_DIR = Path(os.getenv("PHOTOS_DIR", "./photos"))
PHOTOS_DIR.mkdir(parents=True, exist_ok=True)
@router.post("")
async def upload_photo(file: UploadFile=File(...), date: str="",
x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Upload progress photo."""
pid = get_pid(x_profile_id)
# Phase 2: Check feature access (non-blocking, log only)
access = check_feature_access(pid, 'photos')
log_feature_usage(pid, 'photos', access, 'upload')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {pid} would be blocked: "
f"photos {access['reason']} (used: {access['used']}, limit: {access['limit']})"
)
fid = str(uuid.uuid4())
ext = Path(file.filename).suffix or '.jpg'
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 = 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)))
# Phase 2: Increment usage counter
increment_feature_usage(pid, 'photos')
return {"id":fid,"date":date}
@router.get("/{fid}")
def get_photo(fid: str, session: dict=Depends(require_auth_flexible)):
"""Get photo by ID. Auth via header or query param (for <img> tags)."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT path FROM photos WHERE id=%s", (fid,))
row = cur.fetchone()
if not row: raise HTTPException(404, "Photo not found")
photo_path = Path(PHOTOS_DIR) / row['path']
if not photo_path.exists():
raise HTTPException(404, "Photo file not found")
return FileResponse(photo_path)
@router.get("")
def list_photos(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get all photos for current profile."""
pid = get_pid(x_profile_id)
with get_db() as conn:
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()]