""" Servizio Audit Log – registrazione e consultazione degli eventi di sistema. Uso tipico nei router/servizi: from app.services.audit_service import log_audit await log_audit( db=db, tenant_id=current_user.tenant_id, user_id=current_user.id, action="user.created", resource_type="user", resource_id=new_user.id, outcome="success", ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), payload={"email": new_user.email}, ) """ import math import uuid from datetime import datetime from typing import Optional from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.core.pagination import PaginatedResponse, PaginationParams from app.models.audit_log import AuditLog from app.schemas.audit_log import AuditLogResponse # ─── Helper standalone (da chiamare ovunque senza istanziare la classe) ─────── async def log_audit( db: AsyncSession, action: str, *, tenant_id: Optional[uuid.UUID] = None, user_id: Optional[uuid.UUID] = None, resource_type: Optional[str] = None, resource_id: Optional[uuid.UUID] = None, outcome: str = "success", ip_address: Optional[str] = None, user_agent: Optional[str] = None, payload: Optional[dict] = None, ) -> None: """ Inserisce un record di audit log nella sessione corrente. Non fa commit: il commit avviene con la transazione del chiamante. Non solleva eccezioni: gli errori sono loggati ma non propagati per evitare di bloccare l'operazione principale. """ try: entry = AuditLog( tenant_id=tenant_id, user_id=user_id, action=action, resource_type=resource_type, resource_id=resource_id, ip_address=ip_address, user_agent=user_agent, payload=payload or {}, outcome=outcome, ) db.add(entry) except Exception: # Mai bloccare l'operazione principale per un errore di audit import logging logging.getLogger(__name__).warning( "Impossibile registrare evento audit: action=%s", action, exc_info=True ) # ─── Servizio per query (usato dal router) ──────────────────────────────────── class AuditService: def __init__(self, db: AsyncSession) -> None: self.db = db async def list( self, *, tenant_id: Optional[uuid.UUID], page: int = 1, page_size: int = 25, action: Optional[str] = None, user_id: Optional[uuid.UUID] = None, outcome: Optional[str] = None, date_from: Optional[datetime] = None, date_to: Optional[datetime] = None, resource_type: Optional[str] = None, ) -> PaginatedResponse[AuditLogResponse]: """ Restituisce la lista paginata degli eventi audit. Se tenant_id e' None (super_admin), restituisce eventi di tutti i tenant. """ filters = [] if tenant_id is not None: filters.append(AuditLog.tenant_id == tenant_id) if action: # Supporta prefisso: "auth." corrisponde a tutti gli eventi auth.* if action.endswith("*"): filters.append(AuditLog.action.like(action[:-1] + "%")) else: filters.append(AuditLog.action == action) if user_id: filters.append(AuditLog.user_id == user_id) if outcome: filters.append(AuditLog.outcome == outcome) if date_from: filters.append(AuditLog.occurred_at >= date_from) if date_to: filters.append(AuditLog.occurred_at <= date_to) if resource_type: filters.append(AuditLog.resource_type == resource_type) where_clause = and_(*filters) if filters else True # type: ignore[arg-type] # Count totale count_q = select(func.count()).select_from(AuditLog).where(where_clause) total = (await self.db.execute(count_q)).scalar_one() # Dati paginati offset = (page - 1) * page_size items_q = ( select(AuditLog) .where(where_clause) .order_by(AuditLog.occurred_at.desc()) .offset(offset) .limit(page_size) ) result = await self.db.execute(items_q) items = list(result.scalars().all()) pages = math.ceil(total / page_size) if page_size > 0 else 0 return PaginatedResponse[AuditLogResponse]( items=[AuditLogResponse.model_validate(item) for item in items], total=total, page=page, page_size=page_size, pages=pages, )