This commit is contained in:
2026-03-18 17:30:13 +01:00
parent 58a233236c
commit d80d912fb3
36 changed files with 3502 additions and 4 deletions
+114
View File
@@ -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)