Fascicoli+Tassonomia+permessi
This commit is contained in:
@@ -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]
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user