Audit pass: cookie auth migration, route guards, GP% clamp, CSV export, OCR transaction safety, N+1 fix, and cleanup

- Migrated all 435 frontend fetch calls from Authorization Bearer header to credentials: 'include' (cookie auth)
- Removed ?token= from all file/image URLs (browser history exposure)
- Added ProtectedRoute wrapper to all capability-gated routes in App.tsx
- OCR background task: added transaction boundary, improved error handling and status rollback
- DuplicateDetector: wrapped in non-fatal try/except so crashes don't abort invoice processing
- File upload: commit DB row before writing to disk to prevent orphaned files
- GP% clamped to 100% in GPReport (credit notes can inflate above 100%)
- Added CSV export to GPReport (suppliers, daily data, allowances breakdown)
- Backend file-serving endpoints: cookie auth with ?token= fallback for backward compatibility
- DATA_DIR: moved from hardcoded /app/data to os.getenv in invoices.py and recipes.py
- N+1 fix in list_recipes: batch-loads latest cost snapshot in 1 query (was N)
- Zero-yield sub-recipe: logs warning instead of silently zeroing cost contribution
- Budget spend rate input: rejects negative values
- GPReport allowances toggle: persisted to localStorage across page loads
- DB pool_size/max_overflow: configurable via DB_POOL_SIZE/DB_MAX_OVERFLOW env vars
- Fixed SyntaxWarning from \\d in invoices.py docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-13 10:04:20 +00:00
parent 6f6e16c88f
commit ba075276b1
57 changed files with 15427 additions and 15274 deletions

View file

@ -6,7 +6,7 @@ from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, and_, or_, delete, update
@ -783,14 +783,20 @@ async def get_ingredient(
@router.get("/{ingredient_id}/label-image")
async def get_label_image(
ingredient_id: int,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Serve the stored label image for a prepackaged ingredient.
Uses token query param for auth (allows window.open / img src usage)."""
"""Serve the stored label image for a prepackaged ingredient. Cookie auth preferred; ?token= accepted as fallback."""
import os
from auth import get_current_user, require_cap_from_token
user = await get_current_user_from_token(token, db)
user = None
if token:
user = await get_current_user_from_token(token, db)
if not user:
try:
user = await get_current_user(request)
except HTTPException:
pass
if not user:
raise HTTPException(401, "Not authenticated")
result = await db.execute(