mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Fase 2
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Router caselle PEC – CRUD + test connessione.
|
||||
|
||||
Permessi:
|
||||
- admin: CRUD completo su tutte le caselle del tenant
|
||||
- altri ruoli: solo lettura (caselle accessibili tramite PermissionService)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import ForbiddenError
|
||||
from app.database import get_db
|
||||
from app.dependencies import AdminUser, CurrentUser, DB
|
||||
from app.schemas.mailbox import (
|
||||
ConnectionTestRequest,
|
||||
ConnectionTestResult,
|
||||
MailboxCreateRequest,
|
||||
MailboxListResponse,
|
||||
MailboxResponse,
|
||||
MailboxUpdateRequest,
|
||||
)
|
||||
from app.services.mailbox_service import MailboxService
|
||||
|
||||
router = APIRouter(prefix="/mailboxes", tags=["Mailboxes"])
|
||||
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _svc(db: AsyncSession) -> MailboxService:
|
||||
return MailboxService(db)
|
||||
|
||||
|
||||
def _build_response(mailbox, svc: MailboxService) -> MailboxResponse:
|
||||
return MailboxResponse(**MailboxService.to_response_dict(mailbox))
|
||||
|
||||
|
||||
# ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("", response_model=MailboxResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_mailbox(
|
||||
data: MailboxCreateRequest,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> MailboxResponse:
|
||||
"""
|
||||
Crea una nuova casella PEC.
|
||||
Richiede ruolo **admin** o **super_admin**.
|
||||
Le credenziali vengono cifrate con AES-256-GCM prima della persistenza.
|
||||
"""
|
||||
svc = _svc(db)
|
||||
mailbox = await svc.create_mailbox(
|
||||
tenant_id=current_user.tenant_id,
|
||||
data=data,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
await db.commit()
|
||||
return _build_response(mailbox, svc)
|
||||
|
||||
|
||||
@router.get("", response_model=MailboxListResponse)
|
||||
async def list_mailboxes(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
) -> MailboxListResponse:
|
||||
"""
|
||||
Elenca le caselle PEC.
|
||||
- Admin vede tutte le caselle del tenant.
|
||||
- Operatori vedono solo le caselle su cui hanno permesso can_read.
|
||||
"""
|
||||
svc = _svc(db)
|
||||
|
||||
if current_user.is_admin:
|
||||
# Admin: 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
|
||||
from app.services.permission_service import PermissionService
|
||||
perm_svc = PermissionService(db)
|
||||
visible_ids = await perm_svc.get_visible_mailboxes(current_user)
|
||||
|
||||
if not visible_ids:
|
||||
return MailboxListResponse(items=[], total=0, page=page, page_size=page_size)
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.mailbox import Mailbox
|
||||
from sqlalchemy import func
|
||||
|
||||
q = (
|
||||
select(Mailbox)
|
||||
.where(
|
||||
Mailbox.id.in_(visible_ids),
|
||||
Mailbox.status != "deleted",
|
||||
)
|
||||
.order_by(Mailbox.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
count_q = select(func.count()).select_from(
|
||||
select(Mailbox.id).where(
|
||||
Mailbox.id.in_(visible_ids),
|
||||
Mailbox.status != "deleted",
|
||||
).subquery()
|
||||
)
|
||||
result = await db.execute(q)
|
||||
items = list(result.scalars().all())
|
||||
total = (await db.execute(count_q)).scalar_one()
|
||||
|
||||
return MailboxListResponse(
|
||||
items=[_build_response(m, svc) for m in items],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{mailbox_id}", response_model=MailboxResponse)
|
||||
async def get_mailbox(
|
||||
mailbox_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> MailboxResponse:
|
||||
"""Carica una casella PEC per ID."""
|
||||
svc = _svc(db)
|
||||
|
||||
if not current_user.is_admin:
|
||||
from app.services.permission_service import PermissionService
|
||||
perm_svc = PermissionService(db)
|
||||
if not await perm_svc.check_can_read(current_user, mailbox_id):
|
||||
raise ForbiddenError("Accesso alla casella non autorizzato")
|
||||
|
||||
mailbox = await svc.get_mailbox(mailbox_id, current_user.tenant_id)
|
||||
return _build_response(mailbox, svc)
|
||||
|
||||
|
||||
@router.put("/{mailbox_id}", response_model=MailboxResponse)
|
||||
async def update_mailbox(
|
||||
mailbox_id: uuid.UUID,
|
||||
data: MailboxUpdateRequest,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> MailboxResponse:
|
||||
"""
|
||||
Aggiorna una casella PEC.
|
||||
Richiede ruolo **admin** o **super_admin**.
|
||||
Se vengono fornite nuove credenziali, vengono ri-cifrate.
|
||||
"""
|
||||
svc = _svc(db)
|
||||
mailbox = await svc.update_mailbox(
|
||||
mailbox_id=mailbox_id,
|
||||
tenant_id=current_user.tenant_id,
|
||||
data=data,
|
||||
)
|
||||
await db.commit()
|
||||
return _build_response(mailbox, svc)
|
||||
|
||||
|
||||
@router.delete("/{mailbox_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_mailbox(
|
||||
mailbox_id: uuid.UUID,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> None:
|
||||
"""
|
||||
Soft-delete di una casella PEC (status=deleted).
|
||||
Richiede ruolo **admin**.
|
||||
"""
|
||||
svc = _svc(db)
|
||||
await svc.delete_mailbox(mailbox_id, current_user.tenant_id)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{mailbox_id}/test-connection",
|
||||
response_model=ConnectionTestResult,
|
||||
)
|
||||
async def test_mailbox_connection(
|
||||
mailbox_id: uuid.UUID,
|
||||
data: ConnectionTestRequest,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> ConnectionTestResult:
|
||||
"""
|
||||
Testa la connessione IMAP o SMTP della casella.
|
||||
Non invia messaggi – solo verifica la connessione.
|
||||
Richiede ruolo **admin**.
|
||||
"""
|
||||
svc = _svc(db)
|
||||
return await svc.test_connection(
|
||||
mailbox_id=mailbox_id,
|
||||
tenant_id=current_user.tenant_id,
|
||||
data=data,
|
||||
)
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
WebSocket endpoint – connessione real-time per aggiornamenti inbox.
|
||||
|
||||
URL: ws://<host>/api/v1/ws?token=<jwt_access_token>
|
||||
|
||||
Protocollo messaggi (dal server al client):
|
||||
{
|
||||
"type": "mailbox:new_message",
|
||||
"mailbox_id": "<uuid>",
|
||||
"message_id": "<uuid>",
|
||||
"subject": "...",
|
||||
"from_address": "...",
|
||||
"received_at": "2026-03-18T14:00:00Z"
|
||||
}
|
||||
|
||||
{
|
||||
"type": "mailbox:sync_error",
|
||||
"mailbox_id": "<uuid>",
|
||||
"error": "...",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
{ "type": "ping" } ← heartbeat ogni 30s per mantenere connessione viva
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
|
||||
from jose import JWTError
|
||||
|
||||
from app.core.security import decode_token
|
||||
from app.core.logging import get_logger
|
||||
from app.websocket.manager import manager
|
||||
|
||||
router = APIRouter(tags=["WebSocket"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
token: str = Query(..., description="JWT access token per autenticazione"),
|
||||
) -> None:
|
||||
"""
|
||||
WebSocket endpoint autenticato via JWT query param.
|
||||
Invia eventi real-time per il tenant dell'utente connesso.
|
||||
"""
|
||||
# Autenticazione: valida JWT
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
if payload.get("type") != "access":
|
||||
await websocket.close(code=4001, reason="Token non valido")
|
||||
return
|
||||
|
||||
tenant_id_str = payload.get("tid")
|
||||
user_id_str = payload.get("sub")
|
||||
if not tenant_id_str or not user_id_str:
|
||||
await websocket.close(code=4001, reason="Token malformato")
|
||||
return
|
||||
|
||||
tenant_id = uuid.UUID(tenant_id_str)
|
||||
user_id = uuid.UUID(user_id_str)
|
||||
|
||||
except (JWTError, ValueError):
|
||||
await websocket.close(code=4001, reason="Token non valido")
|
||||
return
|
||||
|
||||
# Connessione accettata
|
||||
await manager.connect(websocket, tenant_id)
|
||||
logger.info(
|
||||
"WS autenticato",
|
||||
extra={"user_id": str(user_id), "tenant_id": str(tenant_id)},
|
||||
)
|
||||
|
||||
# Invia ack di connessione
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"type": "connected",
|
||||
"tenant_id": str(tenant_id),
|
||||
"user_id": str(user_id),
|
||||
})
|
||||
except Exception:
|
||||
await manager.disconnect(websocket, tenant_id)
|
||||
return
|
||||
|
||||
# Heartbeat task
|
||||
async def send_pings() -> None:
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(30)
|
||||
await websocket.send_json({"type": "ping"})
|
||||
except Exception:
|
||||
break
|
||||
|
||||
ping_task = asyncio.create_task(send_pings())
|
||||
|
||||
try:
|
||||
# Mantieni la connessione aperta, gestisci messaggi client (pong, ecc.)
|
||||
while True:
|
||||
try:
|
||||
data = await asyncio.wait_for(websocket.receive_text(), timeout=35.0)
|
||||
# Gestisci pong dal client (opzionale)
|
||||
if data == "pong":
|
||||
continue
|
||||
except asyncio.TimeoutError:
|
||||
# Nessun messaggio dal client in 35s: connessione morta
|
||||
break
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
finally:
|
||||
ping_task.cancel()
|
||||
await manager.disconnect(websocket, tenant_id)
|
||||
Reference in New Issue
Block a user