"""Router per la gestione della configurazione applicativa. Endpoint: - GET /api/settings — ritorna la configurazione corrente (api_key mascherata) - PUT /api/settings — salva la configurazione aggiornata - GET /api/settings/status — ritorna se l'api_key è configurata (per abilitare/disabilitare UI) """ from __future__ import annotations import json import logging from typing import Optional from fastapi import APIRouter from pydantic import BaseModel from backend.config import CONFIG_PATH from backend.schemas.settings import Settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/settings", tags=["settings"]) # Percorso al file di configurazione _SETTINGS_FILE = CONFIG_PATH / "settings.json" # --------------------------------------------------------------------------- # Response schemas # --------------------------------------------------------------------------- class SettingsStatusResponse(BaseModel): """Risposta per GET /status — usata dal frontend per abilitare/disabilitare il pulsante genera.""" api_key_configured: bool llm_model: str unsplash_api_key_configured: bool class SettingsResponse(BaseModel): """Risposta per GET / — api_key mascherata per sicurezza.""" api_key_masked: Optional[str] # Solo ultimi 4 caratteri o None llm_model: str nicchie_attive: list[str] lingua: str frequenza_post: int brand_name: Optional[str] tono: Optional[str] unsplash_api_key_masked: Optional[str] # Solo ultimi 4 caratteri o None # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _load_settings() -> Settings: """Carica le impostazioni da disco. Ritorna i default se il file non esiste.""" if not _SETTINGS_FILE.exists(): return Settings() try: data = json.loads(_SETTINGS_FILE.read_text(encoding="utf-8")) return Settings.model_validate(data) except Exception as e: logger.warning("Errore caricamento settings: %s — uso default", str(e)) return Settings() def _save_settings(settings: Settings) -> None: """Salva le impostazioni su disco.""" _SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) _SETTINGS_FILE.write_text( settings.model_dump_json(indent=2), encoding="utf-8", ) def _mask_api_key(api_key: Optional[str]) -> Optional[str]: """Maschera la chiave API mostrando solo gli ultimi 4 caratteri. Es: "sk-ant-api03-abc...xyz1234" -> "...1234" """ if not api_key: return None if len(api_key) <= 4: return "****" return f"...{api_key[-4:]}" # --------------------------------------------------------------------------- # Endpoint # --------------------------------------------------------------------------- @router.get("/status", response_model=SettingsStatusResponse) async def get_settings_status() -> SettingsStatusResponse: """Ritorna lo stato della configurazione essenziale. Usato dal frontend per abilitare/disabilitare il pulsante "Genera". Returns: SettingsStatusResponse con api_key_configured e llm_model. """ settings = _load_settings() return SettingsStatusResponse( api_key_configured=bool(settings.api_key), llm_model=settings.llm_model, unsplash_api_key_configured=bool(settings.unsplash_api_key), ) @router.get("/", response_model=SettingsResponse) async def get_settings() -> SettingsResponse: """Ritorna la configurazione corrente con api_key mascherata. La chiave API non viene mai inviata al frontend in chiaro — solo gli ultimi 4 caratteri vengono mostrati per conferma. Returns: SettingsResponse con tutti i parametri configurabili. """ settings = _load_settings() return SettingsResponse( api_key_masked=_mask_api_key(settings.api_key), llm_model=settings.llm_model, nicchie_attive=settings.nicchie_attive, lingua=settings.lingua, frequenza_post=settings.frequenza_post, brand_name=settings.brand_name, tono=settings.tono, unsplash_api_key_masked=_mask_api_key(settings.unsplash_api_key), ) @router.put("/", response_model=SettingsResponse) async def update_settings(new_settings: Settings) -> SettingsResponse: """Aggiorna e salva la configurazione. Nota: se api_key è None nel body, la chiave esistente viene mantenuta (evita di sovrascrivere accidentalmente la chiave quando si aggiornano altri parametri senza inviare la chiave). Args: new_settings: Nuova configurazione da salvare. Returns: SettingsResponse aggiornata con api_key mascherata. """ # Carica settings esistenti per merger (non sovrascrive api_key con None) existing = _load_settings() # Se la nuova api_key è None, mantieni quella esistente if new_settings.api_key is None: new_settings = new_settings.model_copy(update={"api_key": existing.api_key}) # Se la nuova unsplash_api_key è None, mantieni quella esistente (stessa logica) if new_settings.unsplash_api_key is None: new_settings = new_settings.model_copy(update={"unsplash_api_key": existing.unsplash_api_key}) _save_settings(new_settings) logger.info("Settings aggiornate | model=%s | brand=%s | unsplash=%s", new_settings.llm_model, new_settings.brand_name, bool(new_settings.unsplash_api_key)) return SettingsResponse( api_key_masked=_mask_api_key(new_settings.api_key), llm_model=new_settings.llm_model, nicchie_attive=new_settings.nicchie_attive, lingua=new_settings.lingua, frequenza_post=new_settings.frequenza_post, brand_name=new_settings.brand_name, tono=new_settings.tono, unsplash_api_key_masked=_mask_api_key(new_settings.unsplash_api_key), )