Files
PecHub/backend/app/services/tenant_settings_service.py
2026-03-19 14:43:36 +01:00

153 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Servizio TenantSettings lettura e aggiornamento configurazione per-tenant.
Responsabilità:
- Upsert della riga settings (crea se non esiste, aggiorna altrimenti)
- Cifratura/decifratura AES-256-GCM delle credenziali conservatore
- Validazione di consistenza (produzione richiede endpoint + credenziali)
"""
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import BadRequestError
from app.core.security import decrypt_credential, encrypt_credential
from app.models.tenant_settings import TenantSettings
from app.schemas.tenant_settings import TenantSettingsResponse, TenantSettingsUpdate
class TenantSettingsService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
# ─── Lettura (o creazione defaults) ──────────────────────────────────────
async def get_or_create(self, tenant_id: uuid.UUID) -> TenantSettings:
"""
Restituisce le impostazioni del tenant.
Se non esistono, crea una riga con i valori di default (mock).
"""
result = await self.db.execute(
select(TenantSettings).where(TenantSettings.tenant_id == tenant_id)
)
settings = result.scalar_one_or_none()
if settings is None:
settings = TenantSettings(
tenant_id=tenant_id,
archival_mode="mock",
conservatore_id="mock",
)
self.db.add(settings)
await self.db.flush()
return settings
# ─── Aggiornamento ────────────────────────────────────────────────────────
async def update(
self,
tenant_id: uuid.UUID,
data: TenantSettingsUpdate,
) -> TenantSettings:
"""
Aggiornamento parziale delle impostazioni.
Regole:
- Il passaggio a 'production' è consentito solo se conservatore_endpoint
è già configurato oppure viene fornito nel body corrente.
- Le credenziali vengono cifrate prima del salvataggio.
- Una stringa vuota "" su username/password cancella la credenziale.
"""
settings = await self.get_or_create(tenant_id)
# Applica aggiornamenti campi non-credenziali
if data.archival_mode is not None:
settings.archival_mode = data.archival_mode
if data.conservatore_id is not None:
settings.conservatore_id = data.conservatore_id
if data.conservatore_endpoint is not None:
settings.conservatore_endpoint = data.conservatore_endpoint or None
if data.archival_notes is not None:
settings.archival_notes = data.archival_notes or None
# Aggiorna credenziali (cifra se fornite, cancella se stringa vuota)
if data.conservatore_username is not None:
if data.conservatore_username == "":
settings.conservatore_username_enc = None
else:
settings.conservatore_username_enc = encrypt_credential(
data.conservatore_username
)
if data.conservatore_password is not None:
if data.conservatore_password == "":
settings.conservatore_password_enc = None
else:
settings.conservatore_password_enc = encrypt_credential(
data.conservatore_password
)
# Validazione: produzione richiede endpoint configurato
if settings.archival_mode == "production":
if not settings.conservatore_endpoint:
raise BadRequestError(
"La modalità produzione richiede un endpoint conservatore valido. "
"Inserisci l'URL prima di attivare la modalità produzione."
)
await self.db.flush()
return settings
# ─── Costruzione response ─────────────────────────────────────────────────
@staticmethod
def to_response(settings: TenantSettings) -> TenantSettingsResponse:
"""
Converte il modello in DTO di risposta.
Le credenziali NON vengono mai esposte in chiaro.
"""
return TenantSettingsResponse(
id=settings.id,
tenant_id=settings.tenant_id,
archival_mode=settings.archival_mode, # type: ignore[arg-type]
conservatore_id=settings.conservatore_id,
conservatore_endpoint=settings.conservatore_endpoint,
conservatore_username_configured=settings.conservatore_username_enc is not None,
conservatore_password_configured=settings.conservatore_password_enc is not None,
archival_notes=settings.archival_notes,
created_at=settings.created_at,
updated_at=settings.updated_at,
)
# ─── Helper per il worker (recupera credenziali decifrate) ───────────────
async def get_conservatore_credentials(
self, tenant_id: uuid.UUID
) -> dict:
"""
Restituisce le credenziali del conservatore decifrate.
Usato dal worker per effettuare versamenti.
"""
settings = await self.get_or_create(tenant_id)
return {
"mode": settings.archival_mode,
"conservatore_id": settings.conservatore_id,
"endpoint": settings.conservatore_endpoint,
"username": (
decrypt_credential(settings.conservatore_username_enc)
if settings.conservatore_username_enc
else None
),
"password": (
decrypt_credential(settings.conservatore_password_enc)
if settings.conservatore_password_enc
else None
),
}