mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
286 lines
9.6 KiB
Python
286 lines
9.6 KiB
Python
"""
|
||
Router API – Invio PEC.
|
||
|
||
Endpoint:
|
||
POST /send – invia una PEC via JSON (body_text, senza allegati)
|
||
POST /send/multipart – invia una PEC con allegati (multipart/form-data)
|
||
GET /send/jobs – lista job di invio del tenant (paginata)
|
||
GET /send/jobs/{id} – dettaglio di un singolo job
|
||
DELETE /send/jobs/{id} – annulla job se ancora pending/retrying
|
||
"""
|
||
|
||
import json
|
||
import uuid
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, File, Form, HTTPException, Query, Request, UploadFile, status
|
||
|
||
from app.core.exceptions import ForbiddenError
|
||
from app.dependencies import CurrentUser, DB
|
||
from app.schemas.send import SendJobListResponse, SendJobResponse, SendPecRequest
|
||
from app.services.audit_service import get_real_ip
|
||
from app.services.permission_service import PermissionService
|
||
from app.services.send_service import SendService
|
||
|
||
router = APIRouter(prefix="/send", tags=["Invio PEC"])
|
||
|
||
|
||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
def _svc(db) -> SendService:
|
||
return SendService(db)
|
||
|
||
|
||
def _job_response(job) -> SendJobResponse:
|
||
return SendJobResponse.model_validate(job)
|
||
|
||
|
||
# ─── POST /send (JSON – retrocompatibile) ────────────────────────────────────
|
||
|
||
@router.post(
|
||
"",
|
||
response_model=SendJobResponse,
|
||
status_code=status.HTTP_201_CREATED,
|
||
summary="Invia una PEC (JSON, senza allegati)",
|
||
description=(
|
||
"Crea un messaggio PEC in uscita e accoda il job di invio SMTP. "
|
||
"Per inviare allegati utilizzare `POST /send/multipart`."
|
||
),
|
||
)
|
||
async def create_send_job(
|
||
request: Request,
|
||
data: SendPecRequest,
|
||
current_user: CurrentUser,
|
||
db: DB,
|
||
) -> SendJobResponse:
|
||
from app.services.audit_service import log_audit
|
||
|
||
svc = _svc(db)
|
||
job = await svc.create_send_job(current_user=current_user, data=data)
|
||
|
||
await log_audit(
|
||
db,
|
||
"pec.sent",
|
||
tenant_id=current_user.tenant_id,
|
||
user_id=current_user.id,
|
||
resource_type="send_job",
|
||
resource_id=job.id,
|
||
ip_address=get_real_ip(request),
|
||
user_agent=request.headers.get("user-agent"),
|
||
payload={
|
||
"mailbox_id": str(job.mailbox_id),
|
||
"to_addresses": data.to_addresses,
|
||
"subject": data.subject,
|
||
"has_attachments": False,
|
||
},
|
||
)
|
||
|
||
await db.commit()
|
||
await db.refresh(job)
|
||
return _job_response(job)
|
||
|
||
|
||
# ─── POST /send/multipart (Form + files) ─────────────────────────────────────
|
||
|
||
@router.post(
|
||
"/multipart",
|
||
response_model=SendJobResponse,
|
||
status_code=status.HTTP_201_CREATED,
|
||
summary="Invia una PEC con allegati (multipart/form-data)",
|
||
description=(
|
||
"Accetta un form multipart con:\n"
|
||
"- **data**: JSON string con i campi della PEC (stessa struttura di `POST /send`, "
|
||
" più il campo opzionale `body_html`)\n"
|
||
"- **attachments**: zero o più file allegati (max 20 MB ciascuno)\n\n"
|
||
"Carica gli allegati su MinIO e crea i record `Attachment` nel DB prima di accodare il job."
|
||
),
|
||
)
|
||
async def create_send_job_multipart(
|
||
request: Request,
|
||
current_user: CurrentUser,
|
||
db: DB,
|
||
data: str = Form(
|
||
...,
|
||
description="JSON con i dati della PEC (campi SendPecRequest + body_html opzionale)",
|
||
),
|
||
attachments: list[UploadFile] = File(
|
||
default=[],
|
||
description="File allegati (0 o più, max 20 MB ciascuno)",
|
||
),
|
||
) -> SendJobResponse:
|
||
from app.services.audit_service import log_audit
|
||
|
||
# ── Parse del JSON ────────────────────────────────────────────────────────
|
||
try:
|
||
raw = json.loads(data)
|
||
pec_data = SendPecRequest.model_validate(raw)
|
||
# Estrai body_html (non presente nel modello base)
|
||
body_html: str | None = raw.get("body_html") or None
|
||
except Exception as exc:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||
detail=f"Dati PEC non validi: {exc}",
|
||
)
|
||
|
||
# ── Leggi file allegati ───────────────────────────────────────────────────
|
||
MAX_FILE_BYTES = 20 * 1024 * 1024 # 20 MB
|
||
files_data: list[dict] = []
|
||
|
||
for upload in attachments:
|
||
content = await upload.read()
|
||
if not content:
|
||
continue # Salta file vuoti
|
||
if len(content) > MAX_FILE_BYTES:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||
detail=f"Il file '{upload.filename}' supera il limite di 20 MB",
|
||
)
|
||
files_data.append(
|
||
{
|
||
"filename": upload.filename or "allegato",
|
||
"content": content,
|
||
"content_type": upload.content_type or "application/octet-stream",
|
||
}
|
||
)
|
||
|
||
# ── Sovrascrive body_html se fornito ──────────────────────────────────────
|
||
if body_html:
|
||
pec_data.body_html = body_html # type: ignore[attr-defined]
|
||
|
||
svc = _svc(db)
|
||
job = await svc.create_send_job(
|
||
current_user=current_user,
|
||
data=pec_data,
|
||
attachments=files_data if files_data else None,
|
||
)
|
||
|
||
await log_audit(
|
||
db,
|
||
"pec.sent",
|
||
tenant_id=current_user.tenant_id,
|
||
user_id=current_user.id,
|
||
resource_type="send_job",
|
||
resource_id=job.id,
|
||
ip_address=get_real_ip(request),
|
||
user_agent=request.headers.get("user-agent"),
|
||
payload={
|
||
"mailbox_id": str(job.mailbox_id),
|
||
"to_addresses": pec_data.to_addresses,
|
||
"subject": pec_data.subject,
|
||
"has_attachments": bool(files_data),
|
||
"attachment_count": len(files_data),
|
||
},
|
||
)
|
||
|
||
await db.commit()
|
||
await db.refresh(job)
|
||
return _job_response(job)
|
||
|
||
|
||
# ─── GET /send/jobs ────────────────────────────────────────────────────────────
|
||
|
||
@router.get(
|
||
"/jobs",
|
||
response_model=SendJobListResponse,
|
||
summary="Lista job di invio",
|
||
)
|
||
async def list_send_jobs(
|
||
current_user: CurrentUser,
|
||
db: DB,
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(50, ge=1, le=200),
|
||
mailbox_id: uuid.UUID | None = Query(None),
|
||
status_filter: str | None = Query(
|
||
None,
|
||
alias="status",
|
||
description="Filtra per stato: pending | sending | sent | failed | retrying",
|
||
),
|
||
) -> SendJobListResponse:
|
||
svc = _svc(db)
|
||
|
||
if mailbox_id and not current_user.is_admin:
|
||
perm_svc = PermissionService(db)
|
||
if not await perm_svc.check_can_read(current_user, mailbox_id):
|
||
raise ForbiddenError("Accesso alla casella non autorizzato")
|
||
|
||
items, total = await svc.list_send_jobs(
|
||
tenant_id=current_user.tenant_id,
|
||
page=page,
|
||
page_size=page_size,
|
||
mailbox_id=mailbox_id,
|
||
status_filter=status_filter,
|
||
)
|
||
return SendJobListResponse(
|
||
items=[_job_response(j) for j in items],
|
||
total=total,
|
||
page=page,
|
||
page_size=page_size,
|
||
)
|
||
|
||
|
||
# ─── GET /send/jobs/{id} ───────────────────────────────────────────────────────
|
||
|
||
@router.get(
|
||
"/jobs/{job_id}",
|
||
response_model=SendJobResponse,
|
||
summary="Dettaglio job di invio",
|
||
)
|
||
async def get_send_job(
|
||
job_id: uuid.UUID,
|
||
current_user: CurrentUser,
|
||
db: DB,
|
||
) -> SendJobResponse:
|
||
svc = _svc(db)
|
||
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
||
|
||
if not current_user.is_admin:
|
||
perm_svc = PermissionService(db)
|
||
if not await perm_svc.check_can_read(current_user, job.mailbox_id):
|
||
raise ForbiddenError("Accesso non autorizzato")
|
||
|
||
return _job_response(job)
|
||
|
||
|
||
# ─── DELETE /send/jobs/{id} ────────────────────────────────────────────────────
|
||
|
||
@router.delete(
|
||
"/jobs/{job_id}",
|
||
status_code=status.HTTP_204_NO_CONTENT,
|
||
summary="Annulla job di invio",
|
||
)
|
||
async def cancel_send_job(
|
||
request: Request,
|
||
job_id: uuid.UUID,
|
||
current_user: CurrentUser,
|
||
db: DB,
|
||
) -> None:
|
||
from app.services.audit_service import log_audit
|
||
|
||
svc = _svc(db)
|
||
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
||
|
||
if not current_user.is_admin:
|
||
perm_svc = PermissionService(db)
|
||
if not await perm_svc.check_can_send(current_user, job.mailbox_id):
|
||
raise ForbiddenError("Autorizzazione insufficiente per annullare questo invio")
|
||
|
||
await svc.cancel_send_job(job_id, current_user.tenant_id)
|
||
|
||
await log_audit(
|
||
db,
|
||
"pec.send_cancelled",
|
||
tenant_id=current_user.tenant_id,
|
||
user_id=current_user.id,
|
||
resource_type="send_job",
|
||
resource_id=job_id,
|
||
ip_address=get_real_ip(request),
|
||
user_agent=request.headers.get("user-agent"),
|
||
payload={
|
||
"job_id": str(job_id),
|
||
"mailbox_id": str(job.mailbox_id),
|
||
"previous_status": job.status,
|
||
},
|
||
)
|
||
|
||
await db.commit()
|