보안 강화: DB 자격증명(AppKey, Secret) 및 세션토큰(Access Token) 암호화 저장 구현 (AES-GCM/CBC), .env 정리
This commit is contained in:
@@ -19,6 +19,8 @@ class SettingsSchema(BaseModel):
|
||||
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)
|
||||
@@ -26,7 +28,25 @@ async def get_settings(db: AsyncSession = Depends(get_db)):
|
||||
settings = result.scalar_one_or_none()
|
||||
if not settings:
|
||||
raise HTTPException(status_code=404, detail="Settings not initialized")
|
||||
return settings
|
||||
|
||||
# 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)):
|
||||
@@ -38,16 +58,26 @@ async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(ge
|
||||
settings = ApiSettings(id=1)
|
||||
db.add(settings)
|
||||
|
||||
# Update fields if provided
|
||||
if payload.appKey is not None: settings.appKey = payload.appKey
|
||||
if payload.appSecret is not None: settings.appSecret = payload.appSecret
|
||||
if payload.accountNumber is not None: settings.accountNumber = payload.accountNumber
|
||||
# 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)
|
||||
|
||||
if payload.kisApiDelayMs is not None: settings.kisApiDelayMs = payload.kisApiDelayMs
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(settings)
|
||||
|
||||
# Trigger Token Refresh if Creds changed (Async Background task ideally)
|
||||
# await kis_auth.get_access_token(db)
|
||||
# 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 settings
|
||||
return resp
|
||||
|
||||
|
||||
Reference in New Issue
Block a user