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

@ -5,7 +5,7 @@ import os
import tempfile
import shutil
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, UploadFile, File
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, UploadFile, File, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@ -282,15 +282,21 @@ async def delete_backup(
@router.get("/{backup_id}/download")
async def download_backup(
backup_id: int,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db)
):
"""Download a backup file. Auth via token query param for direct browser downloads."""
from auth import get_current_user, require_cap_from_token
current_user = await get_current_user_from_token(token, db)
"""Download a backup file. Cookie auth preferred; ?token= accepted as fallback."""
current_user = None
if token:
current_user = await get_current_user_from_token(token, db)
if not current_user:
raise HTTPException(status_code=401, detail="Invalid token")
try:
current_user = await get_current_user(request)
except HTTPException:
pass
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin only")