""" 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 ), }