From d7ae840ac66e6b963a774e8f6d66b5d9aaf02236 Mon Sep 17 00:00:00 2001 From: idrainformatica Date: Fri, 27 Mar 2026 14:43:42 +0100 Subject: [PATCH] Ruolo supervisor --- GapAnalysis.md | 10 -- backend/app/api/v1/mailboxes.py | 10 +- backend/app/api/v1/messages.py | 11 +- backend/app/dependencies.py | 15 +++ backend/app/models/user.py | 10 ++ backend/app/services/permission_service.py | 33 ++++-- frontend/src/components/Layout/Sidebar.tsx | 36 +++++- frontend/src/hooks/useAuth.ts | 12 ++ .../src/pages/Mailboxes/MailboxesPage.tsx | 110 ++++++++++-------- 9 files changed, 166 insertions(+), 81 deletions(-) diff --git a/GapAnalysis.md b/GapAnalysis.md index cad5ced..ec4f1a6 100644 --- a/GapAnalysis.md +++ b/GapAnalysis.md @@ -64,12 +64,7 @@ backend/app/api/v1/archival.py (endpoint GET /archival/batches, POST /archival/d frontend/src/pages/Archival/ (pagina log versamenti, download RdV, richiesta DIP) Il modello archival.py esiste ma la tabella archival_batches non e' nella migrazione corrente La configurazione conservatore nelle impostazioni tenant e' pronta, ma il "pulsante" che avvia il versamento non esiste -4. Dashboard e Reportistica (Fase 7 – completamente mancante) -Non esistono endpoint /reports/summary, /reports/export -Non esiste pagina Reports/Dashboard nel frontend (nessuna rotta in App.tsx) -Non c'e' generazione PDF (WeasyPrint) ne' export CSV -Non c'e' nessun grafico o KPI visibile (PEC ricevute/inviate oggi, anomalie, tasso consegna) 5. Audit Log – modello esistente, tutto il resto mancante Il modello audit_log.py e la tabella esistono @@ -89,12 +84,7 @@ La cifratura dei segreti notifiche usa base64.b64encode() senza encryption reale Il CI/CD GitHub Actions e' disabilitato (ci.yml.bak): non c'e' lint automatico, test o build su PR Non c'e' docker-compose.prod.yml (override produzione con configurazioni rafforzate) Docs /docs, /redoc sono disabilitate in produzione ma non c'e' un meccanismo di secret scan -8. Invio PEC – funzionalita' mancanti -Non c'e' Forward messaggio (la risposta e' parzialmente implementata in ComposePage ma non e' chiaro se funziona end-to-end) -Non c'e' endpoint per forzare un re-sync manuale di una casella (utile dopo un errore di connessione) -Non c'e' indicazione visiva del numero di messaggi non letti nella sidebar per casella -La barra ricerca nell'Inbox non ha filtri per data (da/a), stato PEC, tipo PEC 9. Ruolo Supervisor Il ruolo supervisor e' definito nell'enum DB e nella documentazione ma non ha logica differenziata dal operator nel codice: is_admin controlla solo admin/super_admin, tutto il resto e' trattato uguale diff --git a/backend/app/api/v1/mailboxes.py b/backend/app/api/v1/mailboxes.py index 5bbf1e5..a219b45 100644 --- a/backend/app/api/v1/mailboxes.py +++ b/backend/app/api/v1/mailboxes.py @@ -77,15 +77,15 @@ async def list_mailboxes( """ svc = _svc(db) - if current_user.is_admin: - # Admin: tutte le caselle del tenant + if current_user.is_supervisor_or_admin: + # Admin e supervisor: tutte le caselle del tenant items, total = await svc.list_mailboxes( tenant_id=current_user.tenant_id, page=page, page_size=page_size, ) else: - # Operatori: caselle con permesso + # Operator e readonly: caselle con permesso esplicito from app.services.permission_service import PermissionService perm_svc = PermissionService(db) visible_ids = await perm_svc.get_visible_mailboxes(current_user) @@ -140,7 +140,9 @@ async def get_unread_counts( from app.models.message import Message # Determina le caselle visibili - if current_user.is_admin: + # Admin e supervisor: nessun filtro (accesso a tutto il tenant) + # Operator e readonly: solo caselle con permesso esplicito can_read + if current_user.is_supervisor_or_admin: visible_ids = None # nessun filtro else: from app.services.permission_service import PermissionService diff --git a/backend/app/api/v1/messages.py b/backend/app/api/v1/messages.py index f003ec7..027f9aa 100644 --- a/backend/app/api/v1/messages.py +++ b/backend/app/api/v1/messages.py @@ -96,11 +96,14 @@ async def _get_visible_mailbox_ids( user, db: AsyncSession ) -> Optional[list[uuid.UUID]]: """ - Per utenti non-admin restituisce la lista di mailbox_id accessibili. - Restituisce None se l'utente e admin (accesso illimitato al tenant). + Per utenti non-admin/supervisor restituisce la lista di mailbox_id accessibili. + Restituisce None se l'utente e' admin o supervisor (accesso illimitato al tenant). + + Admin e supervisor: None (nessun filtro, query diretta sull'intero tenant). + Operator e readonly: lista esplicita di caselle con can_read=True. """ - if user.is_admin: - return None # nessun filtro per admin + if user.is_supervisor_or_admin: + return None # nessun filtro per admin e supervisor from app.services.permission_service import PermissionService perm_svc = PermissionService(db) diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 771960f..f51268b 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -153,6 +153,20 @@ async def require_super_admin( return current_user +async def require_supervisor_or_admin( + current_user: Annotated[User, Depends(get_current_user)], +) -> User: + """ + Richiede ruolo supervisor, admin o super_admin. + + Il supervisor ha accesso in lettura implicito a tutte le caselle del tenant + ma non puo' gestire la configurazione (caselle, utenti, permessi, impostazioni). + """ + if not current_user.is_supervisor_or_admin: + raise ForbiddenError("Richiesto ruolo supervisore o amministratore") + return current_user + + # ─── Protezione endpoint admin con X-Admin-Key header ───────────────────────── async def verify_admin_key( @@ -176,4 +190,5 @@ CurrentUser = Annotated[User, Depends(get_current_user)] CurrentTenant = Annotated[Tenant, Depends(get_current_tenant)] AdminUser = Annotated[User, Depends(require_admin)] SuperAdminUser = Annotated[User, Depends(require_super_admin)] +SupervisorOrAdminUser = Annotated[User, Depends(require_supervisor_or_admin)] DB = Annotated[AsyncSession, Depends(get_db)] diff --git a/backend/app/models/user.py b/backend/app/models/user.py index a208ff8..f3104fe 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -86,6 +86,16 @@ class User(Base): def is_super_admin(self) -> bool: return self.role == "super_admin" + @property + def is_supervisor(self) -> bool: + """Ruolo supervisor: lettura implicita su tutte le caselle, senza poteri di gestione.""" + return self.role == "supervisor" + + @property + def is_supervisor_or_admin(self) -> bool: + """True per super_admin, admin e supervisor (accesso in lettura a tutto il tenant).""" + return self.role in ("super_admin", "admin", "supervisor") + def __repr__(self) -> str: return f"" diff --git a/backend/app/services/permission_service.py b/backend/app/services/permission_service.py index e35ab1d..697f69b 100644 --- a/backend/app/services/permission_service.py +++ b/backend/app/services/permission_service.py @@ -27,9 +27,13 @@ class PermissionService: async def get_visible_mailboxes( self, user: User ) -> list[uuid.UUID]: - """Restituisce gli UUID delle caselle visibili all'utente.""" - if user.role in ("super_admin", "admin"): - # Admin vede tutte le caselle del tenant + """Restituisce gli UUID delle caselle visibili all'utente. + + Admin e supervisor vedono tutte le caselle del tenant. + Operator e readonly vedono solo le caselle con can_read=True esplicito. + """ + if user.role in ("super_admin", "admin", "supervisor"): + # Admin e supervisor vedono tutte le caselle del tenant result = await self.db.execute( select(Mailbox.id).where( Mailbox.tenant_id == user.tenant_id, @@ -38,7 +42,7 @@ class PermissionService: ) return [row[0] for row in result.all()] - # Operatori: solo caselle con can_read=True + # Operator e readonly: solo caselle con can_read=True esplicito result = await self.db.execute( select(MailboxPermission.mailbox_id).where( MailboxPermission.user_id == user.id, @@ -50,9 +54,13 @@ class PermissionService: async def check_can_read( self, user: User, mailbox_id: uuid.UUID ) -> bool: - """Verifica se l'utente può leggere i messaggi della casella.""" - if user.role in ("super_admin", "admin"): - # Verifica solo che la casella appartenga al tenant + """Verifica se l'utente puo' leggere i messaggi della casella. + + Admin e supervisor hanno accesso implicito a tutte le caselle del tenant. + Operator e readonly richiedono permesso esplicito can_read. + """ + if user.role in ("super_admin", "admin", "supervisor"): + # Admin e supervisor: verifica solo che la casella appartenga al tenant return await self._mailbox_belongs_to_tenant(mailbox_id, user.tenant_id) perm = await self._get_permission(user.id, mailbox_id) @@ -62,12 +70,15 @@ class PermissionService: self, user: User, mailbox_id: uuid.UUID ) -> bool: """ - Verifica se l'utente può inviare dalla casella. + Verifica se l'utente puo' inviare dalla casella. - L'accesso in invio è concesso se: - 1. L'utente è admin del tenant, oppure + L'accesso in invio e' concesso se: + 1. L'utente e' admin del tenant, oppure 2. L'utente ha un permesso diretto can_send sulla casella, oppure - 3. L'utente è assegnato a una Virtual Box attiva che include la casella. + 3. L'utente e' assegnato a una Virtual Box attiva che include la casella. + + Nota: il supervisor NON ha invio implicito – richiede can_send esplicito + come operator, ma diversamente da operator vede tutte le caselle. """ if user.role in ("super_admin", "admin"): return await self._mailbox_belongs_to_tenant(mailbox_id, user.tenant_id) diff --git a/frontend/src/components/Layout/Sidebar.tsx b/frontend/src/components/Layout/Sidebar.tsx index 968b617..4a726e5 100644 --- a/frontend/src/components/Layout/Sidebar.tsx +++ b/frontend/src/components/Layout/Sidebar.tsx @@ -75,7 +75,7 @@ export function Sidebar() { /** Set degli ID virtual box che l'utente ha esplicitamente chiuso. */ const [collapsedVboxes, setCollapsedVboxes] = useState>(new Set()) - const { user, isAdmin, isSuperAdmin, logout } = useAuth() + const { user, isAdmin, isSuperAdmin, isSupervisor, logout } = useAuth() const unreadCount = useInboxStore((s) => s.unreadCount) // Le caselle PEC vengono caricate qui e condivise via React Query cache @@ -416,6 +416,40 @@ export function Sidebar() { + {/* ── Sezione Supervisione – visibile solo ai supervisor ── */} + {isSupervisor && ( +
+ {!collapsed && ( + <> +
+

+ Supervisione +

+ + )} + {collapsed &&
} + +
+ + cn( + 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors', + isActive + ? 'bg-cyan-700 text-white' + : 'text-cyan-300 hover:bg-cyan-900/40 hover:text-white', + collapsed && 'justify-center px-2', + ) + } + title={collapsed ? 'Caselle PEC' : undefined} + > + + {!collapsed && Caselle PEC} + +
+
+ )} + {/* ── Sezione Amministrazione ── */} {isAdmin && (
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 621fc32..d658db7 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -2,6 +2,14 @@ import { useAuthStore } from '@/store/auth.store' /** * Hook helper per accedere all'utente corrente e ai permessi. + * + * Gerarchia ruoli: + * super_admin > admin > supervisor > operator > readonly + * + * Supervisor: + * - Lettura implicita su tutte le caselle del tenant + * - Invio solo se ha permesso esplicito can_send sulla casella + * - Non puo' gestire caselle, utenti, permessi o impostazioni */ export function useAuth() { const user = useAuthStore((s) => s.user) @@ -11,6 +19,8 @@ export function useAuth() { const isAdmin = user?.role === 'admin' || user?.role === 'super_admin' const isSuperAdmin = user?.role === 'super_admin' + const isSupervisor = user?.role === 'supervisor' + const isSupervisorOrAdmin = isAdmin || isSupervisor const canSend = user?.role !== 'readonly' const canManage = isAdmin @@ -20,6 +30,8 @@ export function useAuth() { isLoading, isAdmin, isSuperAdmin, + isSupervisor, + isSupervisorOrAdmin, canSend, canManage, logout, diff --git a/frontend/src/pages/Mailboxes/MailboxesPage.tsx b/frontend/src/pages/Mailboxes/MailboxesPage.tsx index 74d64d8..955ff4a 100644 --- a/frontend/src/pages/Mailboxes/MailboxesPage.tsx +++ b/frontend/src/pages/Mailboxes/MailboxesPage.tsx @@ -27,6 +27,7 @@ import { mailboxesApi } from '@/api/mailboxes.api' import { getErrorMessage } from '@/api/client' import { formatDate, MAILBOX_STATUS_LABELS } from '@/lib/utils' import { cn } from '@/lib/utils' +import { useAuth } from '@/hooks/useAuth' import type { MailboxCreateRequest, MailboxResponse } from '@/types/api.types' const STATUS_COLORS = { @@ -45,6 +46,7 @@ const STATUS_ICONS = { export function MailboxesPage() { const queryClient = useQueryClient() + const { isAdmin } = useAuth() const [showCreateDialog, setShowCreateDialog] = useState(false) const [editingMailbox, setEditingMailbox] = useState(null) const [testingId, setTestingId] = useState(null) @@ -112,10 +114,12 @@ export function MailboxesPage() {

Caselle PEC

({mailboxes.length})
- + {isAdmin && ( + + )}
{/* Contenuto */} @@ -129,12 +133,14 @@ export function MailboxesPage() {

Nessuna casella PEC configurata

- Aggiungi una casella per iniziare a gestire le PEC + {isAdmin ? 'Aggiungi una casella per iniziare a gestire le PEC' : 'Nessuna casella disponibile'}

- + {isAdmin && ( + + )}
) : (
@@ -186,51 +192,53 @@ export function MailboxesPage() {
-
- {/* Test connessione */} - + {isAdmin && ( +
+ {/* Test connessione */} + - {/* Forza sincronizzazione */} - + {/* Forza sincronizzazione */} + - {/* Modifica */} - + {/* Modifica */} + - {/* Elimina */} - -
+ {/* Elimina */} + +
+ )} ) })}