ProdLaunch

This commit is contained in:
2026-06-18 15:14:10 +02:00
parent d8f58640e5
commit 4c90a7c1a3
12 changed files with 1412 additions and 5 deletions
+11
View File
@@ -47,6 +47,7 @@ limiter = Limiter(key_func=get_remote_address)
summary="Login con email e password",
description="Autentica l'utente. Se 2FA è attivo, richiede anche il codice TOTP.",
)
@limiter.limit(settings.rate_limit_auth)
async def login(
request: Request,
body: LoginRequest,
@@ -64,6 +65,11 @@ async def login(
user_agent=ua,
)
# Commit esplicito prima di restituire la risposta: garantisce che il
# RefreshToken sia gia' in DB quando il client chiama /auth/refresh
# in sequenza rapida, evitando race condition.
await db.commit()
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
@@ -83,6 +89,11 @@ async def refresh_tokens(
service = AuthService(db)
access_token, refresh_token = await service.refresh_tokens(body.refresh_token)
# Commit esplicito prima di restituire la risposta: garantisce che la
# revoca del vecchio token (rotation) sia persistita in DB prima che
# il client possa usare il nuovo token, evitando race condition.
await db.commit()
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
+3 -1
View File
@@ -205,12 +205,14 @@ async def update_mailbox(
Se vengono fornite nuove credenziali, vengono ri-cifrate.
"""
svc = _svc(db)
mailbox = await svc.update_mailbox(
await svc.update_mailbox(
mailbox_id=mailbox_id,
tenant_id=current_user.tenant_id,
data=data,
)
await db.commit()
# Ricarica dal DB dopo il commit per evitare lazy-load su oggetto stale
mailbox = await svc.get_mailbox(mailbox_id, current_user.tenant_id)
return _build_response(mailbox, svc)
+6 -2
View File
@@ -923,9 +923,13 @@ async def get_thread(
# Carica ricorsivamente tutti i messaggi del thread dalla radice
# Limita a posta_certificata (esclude accettazioni, consegne, ecc.)
# Depth limit per evitare stack overflow su thread eccezionalmente profondi
_THREAD_MAX_DEPTH = 100
thread_messages: list[Message] = []
async def _collect(msg_id: uuid.UUID) -> None:
async def _collect(msg_id: uuid.UUID, depth: int = 0) -> None:
if depth >= _THREAD_MAX_DEPTH:
return
result = await db.execute(
select(Message)
.where(
@@ -951,7 +955,7 @@ async def get_thread(
)
children = list(children_result.scalars().all())
for child in children:
await _collect(child.id)
await _collect(child.id, depth + 1)
await _collect(root_id)
+5
View File
@@ -80,13 +80,18 @@ def create_refresh_token(subject: str | UUID, tenant_id: str | UUID) -> str:
"""
Crea un JWT refresh token con scadenza lunga (30 giorni default).
Non contiene il ruolo viene rivalutato a ogni refresh.
Include un jti (JWT ID) UUID per garantire unicita' anche se due token
vengono creati nello stesso secondo per lo stesso utente.
"""
import uuid as _uuid
now = datetime.now(UTC)
expire = now + timedelta(days=settings.refresh_token_expire_days)
payload: dict[str, Any] = {
"sub": str(subject),
"tid": str(tenant_id),
"jti": str(_uuid.uuid4()), # JWT ID unico per evitare hash duplicati in DB
"iat": now,
"exp": expire,
"type": "refresh",
+32 -1
View File
@@ -24,14 +24,45 @@ settings = get_settings()
logger = get_logger(__name__)
def _validate_production_config() -> None:
"""
Verifica che le variabili critiche di sicurezza siano state
sostituite rispetto ai valori di default insicuri.
Blocca il boot se APP_ENV=production e i valori non sono stati cambiati.
"""
insecure_defaults = {
"SECRET_KEY": (settings.secret_key, "change-me-in-production"),
"ENCRYPTION_KEY": (settings.encryption_key, "0" * 64),
}
errors: list[str] = []
for var, (current, default) in insecure_defaults.items():
if current == default:
errors.append(f"{var} non e' stato impostato (usa ancora il valore di default)")
if not settings.admin_secret_key:
errors.append(
"ADMIN_SECRET_KEY e' vuota: gli endpoint /api/v1/tenants non sono protetti"
)
if errors:
for err in errors:
logger.warning(f"[CONFIG] {err}")
if settings.is_production:
raise RuntimeError(
"Configurazione insicura rilevata in ambiente production. "
"Correggere le variabili e riavviare: " + "; ".join(errors)
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Gestione ciclo di vita dell'applicazione."""
import asyncio
setup_logging()
_validate_production_config()
logger.info(
"🚀 PEChub Backend avviato",
"PEChub Backend avviato",
extra={"env": settings.app_env, "debug": settings.app_debug},
)
+3 -1
View File
@@ -53,7 +53,9 @@ class MailboxService:
"Aggiorna il piano per aggiungerne altre."
)
# Verifica unicità email nel tenant
# Verifica unicità email nel tenant (solo caselle non-deleted).
# L'indice parziale DB esclude le caselle soft-deleted, permettendo
# la ri-creazione di una casella con lo stesso indirizzo.
existing = await self.db.execute(
select(Mailbox.id).where(
Mailbox.tenant_id == tenant_id,