Problem: - Photo upload with empty date parameter (date='') - PostgreSQL rejects empty string for DATE field - Error: "invalid input syntax for type date: ''" - Occurred when saving circumference entry with only photo Fix: - Convert empty string to NULL before INSERT - Check: date if date and date.strip() else None - NULL is valid for optional date field Test case: - Circumference entry with only photo → should work now - Photo without date → stored with date=NULL ✓ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
3.2 KiB
Python
91 lines
3.2 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 4: Check feature access and ENFORCE
|
|
access = check_feature_access(pid, 'photos')
|
|
log_feature_usage(pid, 'photos', access, 'upload')
|
|
|
|
if not access['allowed']:
|
|
logger.warning(
|
|
f"[FEATURE-LIMIT] User {pid} blocked: "
|
|
f"photos {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
|
)
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=f"Limit erreicht: Du hast das Kontingent für Fotos überschritten ({access['used']}/{access['limit']}). "
|
|
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
)
|
|
|
|
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())
|
|
|
|
# Convert empty string to NULL for date field
|
|
photo_date = date if date and date.strip() else None
|
|
|
|
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,photo_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()]
|