mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Ruolo supervisor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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}>"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user