"""Settings endpoints.""" import json from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app import crud, schemas from app.dependencies import get_db, get_current_superuser, get_current_user from app.models.user import User from app.core.settings_registry import ( THEME_KEYS, MODULE_KEYS, UI_KEYS, SETTINGS_REGISTRY, SettingStorage, SettingType, get_default_value, ) router = APIRouter() def _require_database_setting(key: str): setting_def = SETTINGS_REGISTRY.get(key) if not setting_def: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unknown setting key '{key}'" ) if setting_def.storage != SettingStorage.DATABASE: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Setting '{key}' is not stored in database" ) return setting_def def _coerce_setting_value(key: str, value: Any) -> Any: setting_def = _require_database_setting(key) if setting_def.type == SettingType.BOOLEAN: if isinstance(value, bool): coerced = value elif isinstance(value, int) and not isinstance(value, bool): if value in (0, 1): coerced = bool(value) else: raise HTTPException(status_code=400, detail=f"Invalid boolean value for '{key}'") elif isinstance(value, str): lowered = value.strip().lower() if lowered in ("true", "1"): coerced = True elif lowered in ("false", "0"): coerced = False else: raise HTTPException(status_code=400, detail=f"Invalid boolean value for '{key}'") else: raise HTTPException(status_code=400, detail=f"Invalid boolean value for '{key}'") elif setting_def.type == SettingType.INTEGER: if isinstance(value, bool): raise HTTPException(status_code=400, detail=f"Invalid integer value for '{key}'") if isinstance(value, int): coerced = value elif isinstance(value, str): try: coerced = int(value.strip()) except ValueError as exc: raise HTTPException(status_code=400, detail=f"Invalid integer value for '{key}'") from exc else: raise HTTPException(status_code=400, detail=f"Invalid integer value for '{key}'") elif setting_def.type == SettingType.STRING: if not isinstance(value, str): raise HTTPException(status_code=400, detail=f"Invalid string value for '{key}'") coerced = value elif setting_def.type == SettingType.JSON: if isinstance(value, (dict, list)): coerced = value elif isinstance(value, str): try: coerced = json.loads(value) except json.JSONDecodeError as exc: raise HTTPException(status_code=400, detail=f"Invalid JSON value for '{key}'") from exc else: raise HTTPException(status_code=400, detail=f"Invalid JSON value for '{key}'") elif setting_def.type == SettingType.LIST: if isinstance(value, list): coerced = value elif isinstance(value, str): try: parsed = json.loads(value) except json.JSONDecodeError as exc: raise HTTPException(status_code=400, detail=f"Invalid list value for '{key}'") from exc if not isinstance(parsed, list): raise HTTPException(status_code=400, detail=f"Invalid list value for '{key}'") coerced = parsed else: raise HTTPException(status_code=400, detail=f"Invalid list value for '{key}'") else: coerced = value if setting_def.choices is not None and coerced not in setting_def.choices: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid value for '{key}'. Allowed: {setting_def.choices}" ) return coerced def _validate_bulk_keys(payload: dict[str, Any], allowed_keys: set[str]) -> None: unknown = [key for key in payload.keys() if key not in allowed_keys] if unknown: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unknown/unsupported keys: {sorted(unknown)}" ) @router.get("/theme", response_model=dict[str, Any]) def get_theme_settings( *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ) -> Any: """ Get theme settings (accessible to all authenticated users). Returns theme-related settings that apply to all users. """ result = {} for key in THEME_KEYS: setting = crud.settings.get_setting(db, key=key) if setting: result[key] = setting.get_value() else: # Use default from registry result[key] = get_default_value(key) return result @router.get("/theme/public", response_model=dict[str, Any]) def get_theme_settings_public( *, db: Session = Depends(get_db), ) -> Any: """ Get theme settings without authentication. Intended for login/landing pages to keep UI consistent before auth. """ result = {} for key in THEME_KEYS: setting = crud.settings.get_setting(db, key=key) if setting: result[key] = setting.get_value() else: result[key] = get_default_value(key) return result @router.put("/theme", response_model=dict[str, Any]) def update_theme_settings( *, db: Session = Depends(get_db), theme_data: dict[str, Any], current_user: User = Depends(get_current_superuser) ) -> Any: """ Update theme settings (admin only). Updates multiple theme settings at once. """ _validate_bulk_keys(theme_data, set(THEME_KEYS)) result = {} for key, value in theme_data.items(): coerced = _coerce_setting_value(key, value) setting = crud.settings.update_setting(db, key=key, value=coerced) result[key] = setting.get_value() return result @router.get("/modules", response_model=dict[str, Any]) def get_module_settings( *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ) -> Any: """ Get module settings (accessible to all authenticated users). Returns module enabled/disabled states. """ result = {} for key in MODULE_KEYS: setting = crud.settings.get_setting(db, key=key) if setting: result[key] = setting.get_value() else: # Use default from registry result[key] = get_default_value(key) return result @router.get("/ui", response_model=dict[str, Any]) def get_ui_settings( *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ) -> Any: """ Get UI settings (accessible to all authenticated users). Returns UI/layout settings that apply to all users. """ result = {} for key in UI_KEYS: setting = crud.settings.get_setting(db, key=key) if setting: result[key] = setting.get_value() else: result[key] = get_default_value(key) return result @router.put("/ui", response_model=dict[str, Any]) def update_ui_settings( *, db: Session = Depends(get_db), ui_data: dict[str, Any], current_user: User = Depends(get_current_superuser) ) -> Any: """ Update UI settings (admin only). Updates multiple UI/layout settings at once. """ _validate_bulk_keys(ui_data, set(UI_KEYS)) result = {} for key, value in ui_data.items(): coerced = _coerce_setting_value(key, value) setting = crud.settings.update_setting(db, key=key, value=coerced) result[key] = setting.get_value() return result @router.put("/modules", response_model=dict[str, Any]) def update_module_settings( *, db: Session = Depends(get_db), module_data: dict[str, Any], current_user: User = Depends(get_current_superuser) ) -> Any: """ Update module settings (admin only). Enable or disable modules for all users. """ _validate_bulk_keys(module_data, set(MODULE_KEYS)) result = {} for key, value in module_data.items(): coerced = _coerce_setting_value(key, value) setting = crud.settings.update_setting(db, key=key, value=coerced) result[key] = setting.get_value() return result @router.get("/user_mode_enabled", response_model=dict[str, Any]) def get_user_mode_enabled( *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ) -> Any: """ Get user mode enabled setting (accessible to all authenticated users). """ setting = crud.settings.get_setting(db, key="user_mode_enabled") if setting: return {"key": "user_mode_enabled", "value": setting.get_value()} # Use default from registry return {"key": "user_mode_enabled", "value": get_default_value("user_mode_enabled")} @router.put("/user_mode_enabled", response_model=dict[str, Any]) def update_user_mode_enabled( *, db: Session = Depends(get_db), data: dict[str, Any], current_user: User = Depends(get_current_superuser) ) -> Any: """ Update user mode enabled setting (admin only). """ value = data.get("value", True) coerced = _coerce_setting_value("user_mode_enabled", value) setting = crud.settings.update_setting(db, key="user_mode_enabled", value=coerced) return {"key": "user_mode_enabled", "value": setting.get_value()} @router.get("", response_model=dict[str, Any]) def get_all_settings( *, db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser) ) -> Any: """ Get all settings (admin only). Returns all application settings as a dictionary. """ settings_list = crud.settings.get_all_settings(db) return {s.key: s.get_value() for s in settings_list} @router.get("/{key}", response_model=schemas.Setting) def get_setting( *, db: Session = Depends(get_db), key: str, current_user: User = Depends(get_current_superuser) ) -> Any: """ Get a specific setting by key (admin only). """ setting = crud.settings.get_setting(db, key=key) if not setting: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Setting '{key}' not found" ) return schemas.Setting.from_orm(setting) @router.put("/{key}", response_model=schemas.Setting) def update_setting( *, db: Session = Depends(get_db), key: str, setting_in: schemas.SettingUpdate, current_user: User = Depends(get_current_superuser) ) -> Any: """ Update a setting (admin only). Creates the setting if it doesn't exist. """ coerced = _coerce_setting_value(key, setting_in.value) setting = crud.settings.update_setting(db, key=key, value=coerced) return schemas.Setting.from_orm(setting)