Fascicoli+Tassonomia+permessi

This commit is contained in:
2026-06-17 21:47:46 +02:00
parent e31676d22e
commit 3fd3c72f06
42 changed files with 4554 additions and 99 deletions
+225
View File
@@ -0,0 +1,225 @@
"""
Router Fascicoli — fascicolazione pratiche.
Endpoint:
- GET /fascicoli lista fascicoli (con filtri)
- POST /fascicoli crea un fascicolo
- GET /fascicoli/{id} dettaglio fascicolo
- PATCH /fascicoli/{id} modifica fascicolo
- DELETE /fascicoli/{id} elimina fascicolo (solo admin)
- GET /fascicoli/{id}/messages messaggi del fascicolo
- POST /fascicoli/{id}/messages aggiungi messaggi al fascicolo
- DELETE /fascicoli/{id}/messages rimuovi messaggi dal fascicolo
- GET /messages/{message_id}/fascicoli fascicoli di un messaggio
Permessi:
- Tutti gli utenti autenticati possono creare fascicoli.
- PATCH: creatore o admin.
- DELETE fascicolo: solo admin.
- Operazioni su messaggi: utente con accesso al tenant.
"""
import uuid
from typing import Optional
from fastapi import APIRouter, Query, status
from sqlalchemy import select
from app.core.exceptions import ForbiddenError, NotFoundError
from app.dependencies import AdminUser, CurrentUser, DB
from app.models.message import Message
from app.schemas.fascicolo import (
FascicoloAddMessagesRequest,
FascicoloCreate,
FascicoloMessageItem,
FascicoloRemoveMessagesRequest,
FascicoloResponse,
FascicoloUpdate,
MessageFascicoloSummary,
)
from app.services.fascicolo_service import FascicoloService
router = APIRouter(tags=["Fascicoli"])
# ─── Helpers ──────────────────────────────────────────────────────────────────
def _to_response(fascicolo, message_count: int) -> FascicoloResponse:
resp = FascicoloResponse.model_validate(fascicolo)
resp.message_count = int(message_count)
return resp
# ─── CRUD Fascicolo ───────────────────────────────────────────────────────────
@router.get("/fascicoli", response_model=list[FascicoloResponse])
async def list_fascicoli(
current_user: CurrentUser,
db: DB,
stato: Optional[str] = Query(None, pattern=r"^(aperto|chiuso|archiviato)$"),
responsabile_id: Optional[uuid.UUID] = Query(None),
search: Optional[str] = Query(None, max_length=200),
) -> list[FascicoloResponse]:
"""Elenca i fascicoli del tenant con filtri opzionali."""
svc = FascicoloService(db)
rows = await svc.list_fascicoli(
current_user.tenant_id,
stato=stato,
responsabile_id=responsabile_id,
search=search,
)
return [_to_response(f, cnt) for f, cnt in rows]
@router.post("/fascicoli", response_model=FascicoloResponse, status_code=status.HTTP_201_CREATED)
async def create_fascicolo(
data: FascicoloCreate,
current_user: CurrentUser,
db: DB,
) -> FascicoloResponse:
"""Crea un nuovo fascicolo."""
svc = FascicoloService(db)
fascicolo, cnt = await svc.create_fascicolo(
current_user.tenant_id, data, created_by=current_user.id
)
return _to_response(fascicolo, cnt)
@router.get("/fascicoli/{fascicolo_id}", response_model=FascicoloResponse)
async def get_fascicolo(
fascicolo_id: uuid.UUID,
current_user: CurrentUser,
db: DB,
) -> FascicoloResponse:
"""Restituisce il dettaglio di un fascicolo."""
svc = FascicoloService(db)
fascicolo, cnt = await svc.get_fascicolo(current_user.tenant_id, fascicolo_id)
return _to_response(fascicolo, cnt)
@router.patch("/fascicoli/{fascicolo_id}", response_model=FascicoloResponse)
async def update_fascicolo(
fascicolo_id: uuid.UUID,
data: FascicoloUpdate,
current_user: CurrentUser,
db: DB,
) -> FascicoloResponse:
"""
Modifica un fascicolo.
Solo il creatore o un amministratore possono modificarlo.
"""
svc = FascicoloService(db)
fascicolo, cnt = await svc.update_fascicolo(
current_user.tenant_id,
fascicolo_id,
data,
current_user_id=current_user.id,
is_admin=current_user.is_admin,
)
return _to_response(fascicolo, cnt)
@router.delete("/fascicoli/{fascicolo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_fascicolo(
fascicolo_id: uuid.UUID,
current_user: AdminUser,
db: DB,
) -> None:
"""Elimina un fascicolo (solo admin). I messaggi non vengono eliminati."""
svc = FascicoloService(db)
await svc.delete_fascicolo(current_user.tenant_id, fascicolo_id)
# ─── Messaggi del fascicolo ───────────────────────────────────────────────────
@router.get("/fascicoli/{fascicolo_id}/messages", response_model=list[FascicoloMessageItem])
async def get_fascicolo_messages(
fascicolo_id: uuid.UUID,
current_user: CurrentUser,
db: DB,
) -> list[FascicoloMessageItem]:
"""Restituisce i messaggi collegati al fascicolo."""
svc = FascicoloService(db)
rows = await svc.get_fascicolo_messages(current_user.tenant_id, fascicolo_id)
items = []
for msg, added_at in rows:
item = FascicoloMessageItem(
id=msg.id,
subject=msg.subject,
from_address=msg.from_address,
to_addresses=msg.to_addresses,
direction=msg.direction,
pec_type=msg.pec_type,
state=msg.state,
mailbox_id=msg.mailbox_id,
received_at=msg.received_at,
sent_at=msg.sent_at,
created_at=msg.created_at,
added_at=added_at,
)
items.append(item)
return items
@router.post("/fascicoli/{fascicolo_id}/messages", response_model=dict)
async def add_messages_to_fascicolo(
fascicolo_id: uuid.UUID,
data: FascicoloAddMessagesRequest,
current_user: CurrentUser,
db: DB,
) -> dict:
"""
Aggiunge messaggi al fascicolo.
Ignora i messaggi non del tenant o gia' presenti.
"""
svc = FascicoloService(db)
added = await svc.add_messages(
current_user.tenant_id,
fascicolo_id,
data.message_ids,
added_by=current_user.id,
)
return {"added": added}
@router.delete("/fascicoli/{fascicolo_id}/messages", response_model=dict)
async def remove_messages_from_fascicolo(
fascicolo_id: uuid.UUID,
data: FascicoloRemoveMessagesRequest,
current_user: CurrentUser,
db: DB,
) -> dict:
"""Rimuove messaggi dal fascicolo (non li elimina dalla posta)."""
svc = FascicoloService(db)
removed = await svc.remove_messages(
current_user.tenant_id,
fascicolo_id,
data.message_ids,
)
return {"removed": removed}
# ─── Fascicoli di un messaggio ────────────────────────────────────────────────
@router.get("/messages/{message_id}/fascicoli", response_model=list[MessageFascicoloSummary])
async def get_message_fascicoli(
message_id: uuid.UUID,
current_user: CurrentUser,
db: DB,
) -> list[MessageFascicoloSummary]:
"""Restituisce i fascicoli a cui appartiene un messaggio."""
# Verifica che il messaggio esista nel tenant
result = await db.execute(
select(Message).where(
Message.id == message_id,
Message.tenant_id == current_user.tenant_id,
)
)
if not result.scalar_one_or_none():
raise NotFoundError(f"Messaggio {message_id} non trovato")
svc = FascicoloService(db)
fascicoli = await svc.get_message_fascicoli(current_user.tenant_id, message_id)
return [MessageFascicoloSummary.model_validate(f) for f in fascicoli]
+22 -4
View File
@@ -1,9 +1,10 @@
"""
Router tag (Label).
Router tag (Label) con supporto tassonomia gerarchica (Feature N2).
Endpoint:
- GET /labels elenca i tag del tenant
- POST /labels crea un nuovo tag (admin)
- GET /labels elenca i tag del tenant (flat)
- GET /labels/tree albero tassonomico (Ambito > Processo > Classificazione)
- POST /labels crea un nuovo tag / nodo tassonomico (admin)
- PATCH /labels/{id} modifica un tag (admin)
- DELETE /labels/{id} elimina un tag (admin)
@@ -31,6 +32,7 @@ from app.models.message import Message
from app.schemas.label import (
LabelCreate,
LabelResponse,
LabelTreeResponse,
LabelUpdate,
MessageBulkLabelRequest,
MessageBulkLabelResponse,
@@ -77,12 +79,28 @@ async def list_labels(
current_user: CurrentUser,
db: DB,
) -> list[LabelResponse]:
"""Elenca tutti i tag del tenant corrente."""
"""Elenca tutti i tag del tenant corrente (lista flat, include nodi tassonomici)."""
svc = LabelService(db)
labels = await svc.list_labels(current_user.tenant_id)
return [LabelResponse.model_validate(l) for l in labels]
@router.get("/labels/tree", response_model=list[LabelTreeResponse])
async def get_label_tree(
current_user: CurrentUser,
db: DB,
) -> list[LabelTreeResponse]:
"""
Restituisce la tassonomia come albero annidato.
Struttura: [ Ambito { children: [ Processo { children: [ Classificazione ] } ] } ]
I nodi radice (parent_id=NULL) possono essere sia Ambiti tassonomici che label piatte.
"""
svc = LabelService(db)
return await svc.get_label_tree(current_user.tenant_id)
@router.post("/labels", response_model=LabelResponse, status_code=status.HTTP_201_CREATED)
async def create_label(
data: LabelCreate,
+8
View File
@@ -595,6 +595,14 @@ async def update_message(
elif not data.is_conserved:
message.conserved_at = None
# Rischio e Riservatezza (Feature N3) — stringa vuota resetta a NULL
_VALID_RISK = {"low", "medium", "high", "critical"}
_VALID_CONF = {"public", "internal", "confidential", "secret"}
if data.risk_level is not None:
message.risk_level = data.risk_level if data.risk_level in _VALID_RISK else None
if data.confidentiality is not None:
message.confidentiality = data.confidentiality if data.confidentiality in _VALID_CONF else None
# Registra un evento di audit per ogni flag modificato
for field, (action_true, action_false) in _FLAG_ACTIONS.items():
value = getattr(data, field, None)
+114
View File
@@ -0,0 +1,114 @@
"""
Router preset permessi (sottoruoli nominati).
Endpoint:
GET /api/v1/permission-presets → lista preset del tenant
POST /api/v1/permission-presets → crea preset
GET /api/v1/permission-presets/{id} → dettaglio preset
PUT /api/v1/permission-presets/{id} → aggiorna preset
DELETE /api/v1/permission-presets/{id} → elimina preset
Accesso: admin e supervisor.
"""
import uuid
from fastapi import APIRouter
from app.dependencies import DB, SupervisorOrAdminUser
from app.schemas.permission_preset import (
PermissionPresetCreate,
PermissionPresetResponse,
PermissionPresetUpdate,
)
from app.services.permission_preset_service import PermissionPresetService
router = APIRouter(prefix="/permission-presets", tags=["Preset permessi"])
@router.get(
"",
response_model=list[PermissionPresetResponse],
summary="Lista preset permessi del tenant",
)
async def list_presets(
current_user: SupervisorOrAdminUser,
db: DB,
) -> list[PermissionPresetResponse]:
service = PermissionPresetService(db)
presets = await service.list_presets(current_user.tenant_id)
return [PermissionPresetResponse.model_validate(p) for p in presets]
@router.post(
"",
response_model=PermissionPresetResponse,
status_code=201,
summary="Crea un nuovo preset di permessi",
)
async def create_preset(
body: PermissionPresetCreate,
current_user: SupervisorOrAdminUser,
db: DB,
) -> PermissionPresetResponse:
service = PermissionPresetService(db)
preset = await service.create_preset(
tenant_id=current_user.tenant_id,
data=body,
created_by=current_user,
)
return PermissionPresetResponse.model_validate(preset)
@router.get(
"/{preset_id}",
response_model=PermissionPresetResponse,
summary="Dettaglio preset",
)
async def get_preset(
preset_id: uuid.UUID,
current_user: SupervisorOrAdminUser,
db: DB,
) -> PermissionPresetResponse:
service = PermissionPresetService(db)
preset = await service.get_preset(preset_id, current_user.tenant_id)
return PermissionPresetResponse.model_validate(preset)
@router.put(
"/{preset_id}",
response_model=PermissionPresetResponse,
summary="Aggiorna un preset",
)
async def update_preset(
preset_id: uuid.UUID,
body: PermissionPresetUpdate,
current_user: SupervisorOrAdminUser,
db: DB,
) -> PermissionPresetResponse:
service = PermissionPresetService(db)
preset = await service.update_preset(
preset_id=preset_id,
tenant_id=current_user.tenant_id,
data=body,
updated_by=current_user,
)
return PermissionPresetResponse.model_validate(preset)
@router.delete(
"/{preset_id}",
status_code=204,
summary="Elimina un preset",
)
async def delete_preset(
preset_id: uuid.UUID,
current_user: SupervisorOrAdminUser,
db: DB,
) -> None:
service = PermissionPresetService(db)
await service.delete_preset(
preset_id=preset_id,
tenant_id=current_user.tenant_id,
deleted_by=current_user,
)
+5 -5
View File
@@ -12,7 +12,7 @@ import uuid
from fastapi import APIRouter
from app.dependencies import AdminUser, CurrentUser, DB
from app.dependencies import AdminUser, CurrentUser, DB, SupervisorOrAdminUser
from app.schemas.permission import (
MailboxUserPermissionResponse,
PermissionGrantRequest,
@@ -35,7 +35,7 @@ async def grant_permission(
mailbox_id: uuid.UUID,
user_id: uuid.UUID,
body: PermissionGrantRequest,
current_user: AdminUser,
current_user: SupervisorOrAdminUser,
db: DB,
) -> PermissionResponse:
service = PermissionService(db)
@@ -57,7 +57,7 @@ async def grant_permission(
async def revoke_permission(
mailbox_id: uuid.UUID,
user_id: uuid.UUID,
current_user: AdminUser,
current_user: SupervisorOrAdminUser,
db: DB,
) -> None:
service = PermissionService(db)
@@ -75,7 +75,7 @@ async def revoke_permission(
)
async def list_mailbox_users(
mailbox_id: uuid.UUID,
current_user: AdminUser,
current_user: SupervisorOrAdminUser,
db: DB,
) -> list[MailboxUserPermissionResponse]:
service = PermissionService(db)
@@ -90,7 +90,7 @@ async def list_mailbox_users(
)
async def list_user_mailboxes(
user_id: uuid.UUID,
current_user: AdminUser,
current_user: SupervisorOrAdminUser,
db: DB,
) -> list[UserMailboxPermissionResponse]:
service = PermissionService(db)