mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Modifiche varie
This commit is contained in:
@@ -12,25 +12,54 @@ Uso tipico nei router/servizi:
|
||||
resource_type="user",
|
||||
resource_id=new_user.id,
|
||||
outcome="success",
|
||||
ip_address=request.client.host if request.client else None,
|
||||
ip_address=get_real_ip(request),
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
payload={"email": new_user.email},
|
||||
)
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import math
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from app.core.pagination import PaginatedResponse, PaginationParams
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.user import User
|
||||
from app.schemas.audit_log import AuditLogResponse
|
||||
|
||||
|
||||
# ─── Helper IP reale (legge X-Real-IP o X-Forwarded-For da reverse proxy) ────
|
||||
|
||||
def get_real_ip(request: Request) -> Optional[str]:
|
||||
"""
|
||||
Restituisce l'IP reale del client, leggendo gli header del reverse proxy.
|
||||
Priorita': X-Real-IP > primo IP di X-Forwarded-For > request.client.host
|
||||
"""
|
||||
x_real_ip = request.headers.get("x-real-ip")
|
||||
if x_real_ip:
|
||||
return x_real_ip.strip()
|
||||
|
||||
x_forwarded_for = request.headers.get("x-forwarded-for", "")
|
||||
if x_forwarded_for:
|
||||
# Prende il primo IP della catena (quello del client originale)
|
||||
first_ip = x_forwarded_for.split(",")[0].strip()
|
||||
if first_ip:
|
||||
return first_ip
|
||||
|
||||
if request.client:
|
||||
return request.client.host
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ─── Helper standalone (da chiamare ovunque senza istanziare la classe) ───────
|
||||
|
||||
async def log_audit(
|
||||
@@ -79,31 +108,35 @@ class AuditService:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
async def list(
|
||||
def _build_query(
|
||||
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.
|
||||
):
|
||||
"""Costruisce la query base con JOIN utente e filtri."""
|
||||
UserAlias = aliased(User)
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
AuditLog,
|
||||
UserAlias.email.label("user_email"),
|
||||
UserAlias.full_name.label("user_full_name"),
|
||||
)
|
||||
.outerjoin(UserAlias, UserAlias.id == AuditLog.user_id)
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -124,30 +157,221 @@ class AuditService:
|
||||
if resource_type:
|
||||
filters.append(AuditLog.resource_type == resource_type)
|
||||
|
||||
where_clause = and_(*filters) if filters else True # type: ignore[arg-type]
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
|
||||
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 con dati utente.
|
||||
Se tenant_id e' None (super_admin), restituisce eventi di tutti i tenant.
|
||||
"""
|
||||
base_stmt = self._build_query(
|
||||
tenant_id=tenant_id,
|
||||
action=action,
|
||||
user_id=user_id,
|
||||
outcome=outcome,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
resource_type=resource_type,
|
||||
)
|
||||
|
||||
# Count totale
|
||||
count_q = select(func.count()).select_from(AuditLog).where(where_clause)
|
||||
count_q = select(func.count()).select_from(base_stmt.subquery())
|
||||
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())
|
||||
items_q = base_stmt.order_by(AuditLog.occurred_at.desc()).offset(offset).limit(page_size)
|
||||
rows = (await self.db.execute(items_q)).all()
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
entry: AuditLog = row[0]
|
||||
email: Optional[str] = row[1]
|
||||
full_name: Optional[str] = row[2]
|
||||
resp = AuditLogResponse.model_validate(entry)
|
||||
resp.user_email = email
|
||||
resp.user_full_name = full_name
|
||||
items.append(resp)
|
||||
|
||||
pages = math.ceil(total / page_size) if page_size > 0 else 0
|
||||
|
||||
return PaginatedResponse[AuditLogResponse](
|
||||
items=[AuditLogResponse.model_validate(item) for item in items],
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
pages=pages,
|
||||
)
|
||||
|
||||
async def export_csv(
|
||||
self,
|
||||
*,
|
||||
tenant_id: Optional[uuid.UUID],
|
||||
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,
|
||||
limit: int = 10000,
|
||||
) -> str:
|
||||
"""
|
||||
Restituisce i log in formato CSV (stringa).
|
||||
Massimo `limit` righe per prevenire export eccessivi.
|
||||
"""
|
||||
base_stmt = self._build_query(
|
||||
tenant_id=tenant_id,
|
||||
action=action,
|
||||
user_id=user_id,
|
||||
outcome=outcome,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
resource_type=resource_type,
|
||||
)
|
||||
items_q = base_stmt.order_by(AuditLog.occurred_at.desc()).limit(limit)
|
||||
rows = (await self.db.execute(items_q)).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"ID", "Data/Ora", "Azione", "Esito",
|
||||
"Utente", "Email utente", "IP",
|
||||
"Tipo risorsa", "ID risorsa",
|
||||
])
|
||||
|
||||
for row in rows:
|
||||
entry: AuditLog = row[0]
|
||||
email: Optional[str] = row[1]
|
||||
full_name: Optional[str] = row[2]
|
||||
writer.writerow([
|
||||
entry.id,
|
||||
entry.occurred_at.strftime("%d/%m/%Y %H:%M:%S"),
|
||||
entry.action,
|
||||
entry.outcome,
|
||||
full_name or "",
|
||||
email or "",
|
||||
str(entry.ip_address) if entry.ip_address else "",
|
||||
entry.resource_type or "",
|
||||
str(entry.resource_id) if entry.resource_id else "",
|
||||
])
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
async def export_pdf_bytes(
|
||||
self,
|
||||
*,
|
||||
tenant_id: Optional[uuid.UUID],
|
||||
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,
|
||||
limit: int = 5000,
|
||||
) -> bytes:
|
||||
"""
|
||||
Genera un PDF con i log di audit usando reportlab.
|
||||
Restituisce i byte del PDF.
|
||||
"""
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4, landscape
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
|
||||
)
|
||||
|
||||
base_stmt = self._build_query(
|
||||
tenant_id=tenant_id,
|
||||
action=action,
|
||||
user_id=user_id,
|
||||
outcome=outcome,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
resource_type=resource_type,
|
||||
)
|
||||
items_q = base_stmt.order_by(AuditLog.occurred_at.desc()).limit(limit)
|
||||
rows = (await self.db.execute(items_q)).all()
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buf,
|
||||
pagesize=landscape(A4),
|
||||
rightMargin=1 * cm,
|
||||
leftMargin=1 * cm,
|
||||
topMargin=1.5 * cm,
|
||||
bottomMargin=1.5 * cm,
|
||||
)
|
||||
styles = getSampleStyleSheet()
|
||||
story = []
|
||||
|
||||
# Titolo
|
||||
title_text = "Audit Log"
|
||||
if date_from or date_to:
|
||||
parts = []
|
||||
if date_from:
|
||||
parts.append(f"dal {date_from.strftime('%d/%m/%Y')}")
|
||||
if date_to:
|
||||
parts.append(f"al {date_to.strftime('%d/%m/%Y')}")
|
||||
title_text += " – " + " ".join(parts)
|
||||
|
||||
story.append(Paragraph(title_text, styles["Heading1"]))
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
story.append(Paragraph(
|
||||
f"Esportato il {datetime.now().strftime('%d/%m/%Y %H:%M')} — {len(rows)} record",
|
||||
styles["Normal"],
|
||||
))
|
||||
story.append(Spacer(1, 0.5 * cm))
|
||||
|
||||
# Tabella
|
||||
headers = ["Data/Ora", "Azione", "Esito", "Utente", "IP", "Risorsa"]
|
||||
table_data = [headers]
|
||||
|
||||
for row in rows:
|
||||
entry: AuditLog = row[0]
|
||||
email: Optional[str] = row[1]
|
||||
full_name: Optional[str] = row[2]
|
||||
utente = full_name or email or (str(entry.user_id)[:8] if entry.user_id else "—")
|
||||
risorsa = entry.resource_type or "—"
|
||||
table_data.append([
|
||||
entry.occurred_at.strftime("%d/%m/%Y %H:%M:%S"),
|
||||
entry.action,
|
||||
"OK" if entry.outcome == "success" else "FAIL",
|
||||
utente,
|
||||
str(entry.ip_address) if entry.ip_address else "—",
|
||||
risorsa,
|
||||
])
|
||||
|
||||
col_widths = [3.8 * cm, 5.0 * cm, 1.8 * cm, 4.5 * cm, 3.5 * cm, 3.0 * cm]
|
||||
table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
||||
table.setStyle(TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e3a5f")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 8),
|
||||
("FONTSIZE", (0, 1), (-1, -1), 7),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f8ff")]),
|
||||
("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#cccccc")),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||||
]))
|
||||
story.append(table)
|
||||
|
||||
doc.build(story)
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Service layer per la gestione delle firme automatiche.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import NotFoundError, ValidationError
|
||||
from app.models.signature import Signature, SignatureAssignment
|
||||
from app.schemas.signature import SignatureCreate, SignatureUpdate, SignatureAssignmentCreate
|
||||
|
||||
|
||||
class SignatureService:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
# ─── Firme ────────────────────────────────────────────────────────────────
|
||||
|
||||
async def list_signatures(
|
||||
self, tenant_id: uuid.UUID, q: str | None = None
|
||||
) -> tuple[Sequence[Signature], int]:
|
||||
stmt = select(Signature).where(Signature.tenant_id == tenant_id)
|
||||
if q:
|
||||
stmt = stmt.where(Signature.name.ilike(f"%{q}%"))
|
||||
stmt = stmt.order_by(Signature.name)
|
||||
|
||||
count_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total = (await self.db.execute(count_stmt)).scalar_one()
|
||||
items = (await self.db.execute(stmt)).scalars().all()
|
||||
return items, total
|
||||
|
||||
async def get_signature(
|
||||
self, tenant_id: uuid.UUID, signature_id: uuid.UUID
|
||||
) -> Signature:
|
||||
stmt = select(Signature).where(
|
||||
Signature.id == signature_id,
|
||||
Signature.tenant_id == tenant_id,
|
||||
)
|
||||
sig = (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
if sig is None:
|
||||
raise NotFoundError("Firma non trovata")
|
||||
return sig
|
||||
|
||||
async def create_signature(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
data: SignatureCreate,
|
||||
created_by: uuid.UUID | None = None,
|
||||
) -> Signature:
|
||||
sig = Signature(
|
||||
tenant_id=tenant_id,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
body_html=data.body_html,
|
||||
body_text=data.body_text,
|
||||
created_by=created_by,
|
||||
)
|
||||
self.db.add(sig)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(sig)
|
||||
return sig
|
||||
|
||||
async def update_signature(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
signature_id: uuid.UUID,
|
||||
data: SignatureUpdate,
|
||||
) -> Signature:
|
||||
sig = await self.get_signature(tenant_id, signature_id)
|
||||
if data.name is not None:
|
||||
sig.name = data.name
|
||||
if data.description is not None:
|
||||
sig.description = data.description
|
||||
if data.body_html is not None:
|
||||
sig.body_html = data.body_html
|
||||
if data.body_text is not None:
|
||||
sig.body_text = data.body_text
|
||||
await self.db.commit()
|
||||
await self.db.refresh(sig)
|
||||
return sig
|
||||
|
||||
async def delete_signature(
|
||||
self, tenant_id: uuid.UUID, signature_id: uuid.UUID
|
||||
) -> None:
|
||||
sig = await self.get_signature(tenant_id, signature_id)
|
||||
await self.db.delete(sig)
|
||||
await self.db.commit()
|
||||
|
||||
# ─── Assegnazioni ─────────────────────────────────────────────────────────
|
||||
|
||||
async def list_assignments(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
mailbox_id: uuid.UUID | None = None,
|
||||
virtual_box_id: uuid.UUID | None = None,
|
||||
) -> tuple[list[dict], int]:
|
||||
"""
|
||||
Restituisce le assegnazioni con il nome della firma incluso.
|
||||
Filtro opzionale per casella o virtual box.
|
||||
"""
|
||||
stmt = (
|
||||
select(SignatureAssignment, Signature.name.label("signature_name"))
|
||||
.join(Signature, Signature.id == SignatureAssignment.signature_id)
|
||||
.where(SignatureAssignment.tenant_id == tenant_id)
|
||||
)
|
||||
if mailbox_id:
|
||||
stmt = stmt.where(SignatureAssignment.mailbox_id == mailbox_id)
|
||||
if virtual_box_id:
|
||||
stmt = stmt.where(SignatureAssignment.virtual_box_id == virtual_box_id)
|
||||
stmt = stmt.order_by(SignatureAssignment.created_at)
|
||||
|
||||
rows = (await self.db.execute(stmt)).all()
|
||||
items = []
|
||||
for row in rows:
|
||||
assignment: SignatureAssignment = row[0]
|
||||
sig_name: str = row[1]
|
||||
items.append({
|
||||
"id": assignment.id,
|
||||
"tenant_id": assignment.tenant_id,
|
||||
"signature_id": assignment.signature_id,
|
||||
"mailbox_id": assignment.mailbox_id,
|
||||
"virtual_box_id": assignment.virtual_box_id,
|
||||
"context": assignment.context,
|
||||
"created_at": assignment.created_at,
|
||||
"signature_name": sig_name,
|
||||
})
|
||||
return items, len(items)
|
||||
|
||||
async def create_assignment(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
data: SignatureAssignmentCreate,
|
||||
) -> dict:
|
||||
# Validazione: esattamente uno tra mailbox_id e virtual_box_id
|
||||
if bool(data.mailbox_id) == bool(data.virtual_box_id):
|
||||
raise ValidationError(
|
||||
"Specificare esattamente uno tra mailbox_id e virtual_box_id"
|
||||
)
|
||||
|
||||
# Verifica che la firma esista nel tenant
|
||||
await self.get_signature(tenant_id, data.signature_id)
|
||||
|
||||
# Rimuovi eventuale assegnazione precedente per la stessa coppia (target, context)
|
||||
existing_stmt = select(SignatureAssignment).where(
|
||||
SignatureAssignment.tenant_id == tenant_id,
|
||||
SignatureAssignment.context == data.context,
|
||||
)
|
||||
if data.mailbox_id:
|
||||
existing_stmt = existing_stmt.where(
|
||||
SignatureAssignment.mailbox_id == data.mailbox_id
|
||||
)
|
||||
else:
|
||||
existing_stmt = existing_stmt.where(
|
||||
SignatureAssignment.virtual_box_id == data.virtual_box_id
|
||||
)
|
||||
existing = (await self.db.execute(existing_stmt)).scalar_one_or_none()
|
||||
if existing:
|
||||
await self.db.delete(existing)
|
||||
await self.db.flush() # Flush il DELETE prima dell'INSERT per evitare UniqueViolationError
|
||||
|
||||
assignment = SignatureAssignment(
|
||||
tenant_id=tenant_id,
|
||||
signature_id=data.signature_id,
|
||||
mailbox_id=data.mailbox_id,
|
||||
virtual_box_id=data.virtual_box_id,
|
||||
context=data.context,
|
||||
)
|
||||
self.db.add(assignment)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(assignment)
|
||||
|
||||
# Carica il nome della firma per la risposta
|
||||
sig = await self.get_signature(tenant_id, data.signature_id)
|
||||
return {
|
||||
"id": assignment.id,
|
||||
"tenant_id": assignment.tenant_id,
|
||||
"signature_id": assignment.signature_id,
|
||||
"mailbox_id": assignment.mailbox_id,
|
||||
"virtual_box_id": assignment.virtual_box_id,
|
||||
"context": assignment.context,
|
||||
"created_at": assignment.created_at,
|
||||
"signature_name": sig.name,
|
||||
}
|
||||
|
||||
async def delete_assignment(
|
||||
self, tenant_id: uuid.UUID, assignment_id: uuid.UUID
|
||||
) -> None:
|
||||
stmt = select(SignatureAssignment).where(
|
||||
SignatureAssignment.id == assignment_id,
|
||||
SignatureAssignment.tenant_id == tenant_id,
|
||||
)
|
||||
assignment = (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
if assignment is None:
|
||||
raise NotFoundError("Assegnazione firma non trovata")
|
||||
await self.db.delete(assignment)
|
||||
await self.db.commit()
|
||||
|
||||
async def resolve_signature(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
context: str,
|
||||
mailbox_id: uuid.UUID | None = None,
|
||||
virtual_box_id: uuid.UUID | None = None,
|
||||
) -> Signature | None:
|
||||
"""
|
||||
Restituisce la firma assegnata per casella/vbox nel contesto specificato.
|
||||
Cerca prima un'assegnazione con context == context, poi context == 'both'.
|
||||
"""
|
||||
if not mailbox_id and not virtual_box_id:
|
||||
return None
|
||||
|
||||
stmt = (
|
||||
select(SignatureAssignment)
|
||||
.where(
|
||||
SignatureAssignment.tenant_id == tenant_id,
|
||||
SignatureAssignment.context.in_([context, "both"]),
|
||||
)
|
||||
)
|
||||
if mailbox_id:
|
||||
stmt = stmt.where(SignatureAssignment.mailbox_id == mailbox_id)
|
||||
else:
|
||||
stmt = stmt.where(SignatureAssignment.virtual_box_id == virtual_box_id)
|
||||
|
||||
assignments = (await self.db.execute(stmt)).scalars().all()
|
||||
if not assignments:
|
||||
return None
|
||||
|
||||
# Preferisce il match esatto sul contesto rispetto a 'both'
|
||||
exact = next((a for a in assignments if a.context == context), None)
|
||||
assignment = exact or assignments[0]
|
||||
|
||||
return await self.get_signature(tenant_id, assignment.signature_id)
|
||||
|
||||
async def delete_assignment_by_target(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
context: str,
|
||||
mailbox_id: uuid.UUID | None = None,
|
||||
virtual_box_id: uuid.UUID | None = None,
|
||||
) -> None:
|
||||
"""Rimuove l'assegnazione per una specifica casella/vbox+contesto (se presente)."""
|
||||
stmt = select(SignatureAssignment).where(
|
||||
SignatureAssignment.tenant_id == tenant_id,
|
||||
SignatureAssignment.context == context,
|
||||
)
|
||||
if mailbox_id:
|
||||
stmt = stmt.where(SignatureAssignment.mailbox_id == mailbox_id)
|
||||
elif virtual_box_id:
|
||||
stmt = stmt.where(SignatureAssignment.virtual_box_id == virtual_box_id)
|
||||
else:
|
||||
return
|
||||
assignment = (await self.db.execute(stmt)).scalar_one_or_none()
|
||||
if assignment:
|
||||
await self.db.delete(assignment)
|
||||
await self.db.commit()
|
||||
Reference in New Issue
Block a user