Ruolo supervisor

This commit is contained in:
2026-03-27 14:43:42 +01:00
parent ab6db28449
commit d7ae840ac6
9 changed files with 166 additions and 81 deletions
-10
View File
@@ -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
+6 -4
View File
@@ -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
+7 -4
View File
@@ -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)
+15
View File
@@ -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)]
+10
View File
@@ -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"<User {self.email!r} role={self.role!r}>"
+22 -11
View File
@@ -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)
+35 -1
View File
@@ -75,7 +75,7 @@ export function Sidebar() {
/** Set degli ID virtual box che l'utente ha esplicitamente chiuso. */
const [collapsedVboxes, setCollapsedVboxes] = useState<Set<string>>(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() {
</div>
</div>
{/* ── Sezione Supervisione visibile solo ai supervisor ── */}
{isSupervisor && (
<div>
{!collapsed && (
<>
<div className="border-t border-gray-700 mx-4 mb-3" />
<p className="px-4 mb-1.5 text-xs font-semibold text-cyan-400 uppercase tracking-wider">
Supervisione
</p>
</>
)}
{collapsed && <div className="border-t border-gray-700 mx-3 mb-2" />}
<div className="space-y-0.5 px-2">
<NavLink
to="/mailboxes"
className={({ isActive }) =>
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}
>
<MailCheck className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>Caselle PEC</span>}
</NavLink>
</div>
</div>
)}
{/* ── Sezione Amministrazione ── */}
{isAdmin && (
<div>
+12
View File
@@ -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,
@@ -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<MailboxResponse | null>(null)
const [testingId, setTestingId] = useState<string | null>(null)
@@ -112,10 +114,12 @@ export function MailboxesPage() {
<h1 className="text-xl font-semibold">Caselle PEC</h1>
<span className="text-sm text-muted-foreground">({mailboxes.length})</span>
</div>
{isAdmin && (
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Aggiungi casella
</Button>
)}
</div>
{/* Contenuto */}
@@ -129,12 +133,14 @@ export function MailboxesPage() {
<MailCheck className="h-12 w-12 text-muted-foreground/30 mb-3" />
<p className="text-muted-foreground font-medium">Nessuna casella PEC configurata</p>
<p className="text-sm text-muted-foreground/70 mt-1">
Aggiungi una casella per iniziare a gestire le PEC
{isAdmin ? 'Aggiungi una casella per iniziare a gestire le PEC' : 'Nessuna casella disponibile'}
</p>
{isAdmin && (
<Button className="mt-4" onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Aggiungi casella
</Button>
)}
</div>
) : (
<div className="grid gap-4">
@@ -186,6 +192,7 @@ export function MailboxesPage() {
</div>
</div>
{isAdmin && (
<div className="flex items-center gap-2 flex-shrink-0">
{/* Test connessione */}
<Button
@@ -231,6 +238,7 @@ export function MailboxesPage() {
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
)
})}