ProdLaunch
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user