mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-18 21:08:07 +00:00
60 lines
2.4 KiB
Python
60 lines
2.4 KiB
Python
"""Secure credential storage routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from server.core.secrets import MissingMasterKey, SecretsError
|
|
from server.db.credentials_repo import delete_credential, get_credential_status, log_credential_access
|
|
from server.services.credentials_service import store_credential
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CredentialUpsertRequest(BaseModel):
|
|
secret: str = Field(min_length=1, description="Plaintext API secret. It is envelope-encrypted before storage.")
|
|
user_id: str = Field(default="local", min_length=1)
|
|
|
|
|
|
def _request_meta(request: Request) -> tuple[str | None, str | None]:
|
|
ip = request.client.host if request.client else None
|
|
ua = request.headers.get("user-agent")
|
|
return ip, ua
|
|
|
|
|
|
@router.put("/{provider}")
|
|
async def upsert_provider_credential(provider: str, body: CredentialUpsertRequest, request: Request) -> dict[str, object]:
|
|
ip, ua = _request_meta(request)
|
|
try:
|
|
await store_credential(body.user_id, provider, body.secret, ip, ua)
|
|
except MissingMasterKey as exc:
|
|
raise HTTPException(status_code=503, detail="ATLAS_MASTER_KEY is not configured") from exc
|
|
except SecretsError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {"user_id": body.user_id, "provider": provider.lower(), "stored": True}
|
|
|
|
|
|
@router.get("/{provider}/status")
|
|
async def provider_credential_status(provider: str, request: Request, user_id: str = "local") -> dict[str, object]:
|
|
ip, ua = _request_meta(request)
|
|
row = await get_credential_status(user_id, provider)
|
|
await log_credential_access(user_id, provider, "status", ip, ua)
|
|
if not row:
|
|
return {"user_id": user_id, "provider": provider.lower(), "configured": False}
|
|
return {
|
|
"user_id": user_id,
|
|
"provider": provider.lower(),
|
|
"configured": True,
|
|
"created_at": row.get("created_at"),
|
|
"last_used_at": row.get("last_used_at"),
|
|
}
|
|
|
|
|
|
@router.delete("/{provider}")
|
|
async def delete_provider_credential(provider: str, request: Request, user_id: str = "local") -> dict[str, object]:
|
|
ip, ua = _request_meta(request)
|
|
deleted = await delete_credential(user_id, provider)
|
|
await log_credential_access(user_id, provider, "delete", ip, ua)
|
|
return {"user_id": user_id, "provider": provider.lower(), "deleted": deleted}
|