diff --git a/app/routers/internal.py b/app/routers/internal.py index baf5757..76f54e2 100644 --- a/app/routers/internal.py +++ b/app/routers/internal.py @@ -16,9 +16,12 @@ from uuid import UUID from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Header, Query +from fastapi.responses import FileResponse from sqlalchemy.orm import Session from sqlalchemy import and_, or_, text +from ..storage import BASE_PATH, StorageError + from ..db import get_db from ..config import get_settings, Settings from ..models import RemissionAmendmentRequest, RemissionPractice @@ -185,3 +188,80 @@ def mark_pec_failed( db.commit() return ApiResponse(message="PEC marcata come fallita", data={"id": str(amendment_id), "reason": body.reason[:200]}) + + +def _resolve_amendment_file(amendment_id: UUID, db: Session, + kind: str) -> tuple: + """Risolve il path fisico di un allegato amendment. + + kind: 'instructor' (amendment_document_path) | 'response' (response_document_path). + Ritorna tuple (abs_path, mime, safe_filename_hint). + Solleva HTTPException(404) se amendment non esiste o non ha il file. + Hardening: path deve restare dentro BASE_PATH (no traversal). + """ + ar = db.query(RemissionAmendmentRequest).filter( + RemissionAmendmentRequest.id == amendment_id + ).first() + if not ar: + raise HTTPException(status_code=404, detail="Amendment non trovata") + + if kind == "instructor": + rel_path, mime = ar.amendment_document_path, ar.amendment_document_type + elif kind == "response": + rel_path, mime = ar.response_document_path, ar.response_document_type + else: + raise HTTPException(status_code=422, detail=f"kind invalido: {kind}") + + if not rel_path: + raise HTTPException(status_code=404, + detail=f"Nessun documento {kind} per amendment {amendment_id}") + + abs_path = BASE_PATH / rel_path + try: + abs_path.resolve().relative_to(BASE_PATH.resolve()) + except ValueError: + raise HTTPException(status_code=500, detail="Path non valido (hardening)") + + if not abs_path.is_file(): + raise HTTPException(status_code=404, + detail=f"File non presente su storage: {rel_path}") + + # filename hint = tail dopo l'ultimo "-" (struttura {sha}-{nome.ext}) + base = abs_path.name + if "-" in base: + safe_name = base.split("-", 1)[1] + else: + safe_name = base + return abs_path, (mime or "application/pdf"), safe_name + + +@router.get("/{amendment_id}/document") +def get_amendment_document( + amendment_id: UUID, + db: Session = Depends(get_db), + _: bool = Depends(_check_internal_auth), +): + """Scarica il PDF allegato dall'istruttore al soccorso (binary stream). + Usato dal poller BE per archiviare su S3 nel folder pratica. + """ + abs_path, mime, safe_name = _resolve_amendment_file(amendment_id, db, "instructor") + return FileResponse( + path=str(abs_path), media_type=mime, + headers={"Content-Disposition": f'attachment; filename="{safe_name}"'}, + ) + + +@router.get("/{amendment_id}/response-document") +def get_response_document( + amendment_id: UUID, + db: Session = Depends(get_db), + _: bool = Depends(_check_internal_auth), +): + """Scarica il PDF allegato dal beneficiario come risposta (binary stream). + Usato dal poller BE per archiviare su S3 nel folder pratica. + """ + abs_path, mime, safe_name = _resolve_amendment_file(amendment_id, db, "response") + return FileResponse( + path=str(abs_path), media_type=mime, + headers={"Content-Disposition": f'attachment; filename="{safe_name}"'}, + )