Files
KisStock/backend/app/api/endpoints/settings.py

114 lines
4.1 KiB
Python

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