from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel from app.db.database import get_db from app.db.models import ApiSettings from app.services.kis_auth import kis_auth from app.workers.scheduler import service_watchdog_job import logging router = APIRouter() logger = logging.getLogger("SettingsAPI") class SettingsSchema(BaseModel): # Partial schema for updates appKey: str | None = None appSecret: str | None = None accountNumber: str | None = None # Integrations useTelegram: bool | None = None telegramToken: str | None = None telegramChatId: str | None = None useNaverNews: bool | None = None naverClientId: str | None = None naverClientSecret: str | None = None # Configs kisApiDelayMs: int | None = None newsScrapIntervalMin: int | None = None class Config: from_attributes = True from app.core.crypto import encrypt_str, decrypt_str @router.get("/", response_model=SettingsSchema) async def get_settings(db: AsyncSession = Depends(get_db)): stmt = select(ApiSettings).where(ApiSettings.id == 1) result = await db.execute(stmt) settings = result.scalar_one_or_none() if not settings: raise HTTPException(status_code=404, detail="Settings not initialized") # Clone logic to mask secrets for display # We can't easily clone SQLA model to Pydantic and modify without validation error if strict. # So we construct dict. resp_data = SettingsSchema.model_validate(settings) if settings.appKey: resp_data.appKey = "********" # Masked if settings.appSecret: resp_data.appSecret = "********" # Masked if settings.accountNumber: # Decrypt first if we want to show last 4 digits? # Or just show encrypted? Users prefer masking. # Let's decrypt and mask everything except last 2. pass # Keep simple for now: Mask all or partial? # User just sees ***** # Actually proper UX is to leave field blank or show placeholder. # Let's mask entirely for keys. return resp_data @router.put("/", response_model=SettingsSchema) async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(get_db)): logger.info("Updating API Settings...") stmt = select(ApiSettings).where(ApiSettings.id == 1) result = await db.execute(stmt) settings = result.scalar_one_or_none() if not settings: settings = ApiSettings(id=1) db.add(settings) # Update fields if provided - ENCRYPT SENSITIVE DATA if payload.appKey is not None: settings.appKey = encrypt_str(payload.appKey) if payload.appSecret is not None: settings.appSecret = encrypt_str(payload.appSecret) if payload.accountNumber is not None: settings.accountNumber = encrypt_str(payload.accountNumber) # Integrations if payload.useTelegram is not None: settings.useTelegram = payload.useTelegram if payload.telegramToken is not None: settings.telegramToken = payload.telegramToken if payload.telegramChatId is not None: settings.telegramChatId = payload.telegramChatId if payload.useNaverNews is not None: settings.useNaverNews = payload.useNaverNews if payload.naverClientId is not None: settings.naverClientId = payload.naverClientId if payload.naverClientSecret is not None: settings.naverClientSecret = payload.naverClientSecret # Configs if payload.kisApiDelayMs is not None: settings.kisApiDelayMs = payload.kisApiDelayMs if payload.newsScrapIntervalMin is not None: settings.newsScrapIntervalMin = payload.newsScrapIntervalMin await db.commit() await db.refresh(settings) # Trigger Startup-like sequence immediately await service_watchdog_job() # Return masked object check # We return what was saved, but masked? # Usually convention is to return updated state. resp = SettingsSchema.model_validate(settings) resp.appKey = "********" resp.appSecret = "********" resp.accountNumber = "********" return resp