feat(V2): IBKR key rotation admin endpoints + health probe
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@ from pathlib import Path
|
|||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query, Request
|
from fastapi import APIRouter, HTTPException, Query, Request
|
||||||
|
from pydantic import BaseModel, SecretStr
|
||||||
|
|
||||||
|
from cerbero_mcp.exchanges.ibkr.key_rotation import KeyRotationManager
|
||||||
|
|
||||||
MAX_RECORDS = 10000
|
MAX_RECORDS = 10000
|
||||||
DEFAULT_LIMIT = 1000
|
DEFAULT_LIMIT = 1000
|
||||||
@@ -155,4 +158,86 @@ def make_admin_router() -> APIRouter:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _IBKRRotateConfirmReq(BaseModel):
|
||||||
|
new_consumer_key: str
|
||||||
|
new_access_token: str
|
||||||
|
new_access_token_secret: str
|
||||||
|
|
||||||
|
@r.post("/ibkr/rotate-keys/start")
|
||||||
|
async def _ibkr_rotate_start(env: str, request: Request):
|
||||||
|
if env not in ("testnet", "mainnet"):
|
||||||
|
raise HTTPException(400, detail={"error": "invalid env"})
|
||||||
|
settings = request.app.state.settings
|
||||||
|
creds = settings.ibkr.credentials(env)
|
||||||
|
mgr = KeyRotationManager(
|
||||||
|
signature_key_path=creds["signature_key_path"],
|
||||||
|
encryption_key_path=creds["encryption_key_path"],
|
||||||
|
)
|
||||||
|
rotations = getattr(request.app.state, "ibkr_rotations", None)
|
||||||
|
if rotations is None:
|
||||||
|
rotations = {}
|
||||||
|
request.app.state.ibkr_rotations = rotations
|
||||||
|
rotations[env] = mgr
|
||||||
|
return await mgr.start()
|
||||||
|
|
||||||
|
@r.post("/ibkr/rotate-keys/confirm")
|
||||||
|
async def _ibkr_rotate_confirm(
|
||||||
|
env: str, body: _IBKRRotateConfirmReq, request: Request,
|
||||||
|
):
|
||||||
|
if env not in ("testnet", "mainnet"):
|
||||||
|
raise HTTPException(400, detail={"error": "invalid env"})
|
||||||
|
rotations = getattr(request.app.state, "ibkr_rotations", {}) or {}
|
||||||
|
mgr = rotations.get(env)
|
||||||
|
if mgr is None:
|
||||||
|
raise HTTPException(409, detail={"error": "rotation not started"})
|
||||||
|
|
||||||
|
settings = request.app.state.settings
|
||||||
|
if env == "testnet":
|
||||||
|
settings.ibkr.consumer_key_testnet = body.new_consumer_key
|
||||||
|
settings.ibkr.access_token_testnet = body.new_access_token
|
||||||
|
settings.ibkr.access_token_secret_testnet = SecretStr(body.new_access_token_secret)
|
||||||
|
else:
|
||||||
|
settings.ibkr.consumer_key_live = body.new_consumer_key
|
||||||
|
settings.ibkr.access_token_live = body.new_access_token
|
||||||
|
settings.ibkr.access_token_secret_live = SecretStr(body.new_access_token_secret)
|
||||||
|
|
||||||
|
registry = request.app.state.registry
|
||||||
|
registry._clients.pop(("ibkr", env), None)
|
||||||
|
|
||||||
|
async def _validate() -> bool:
|
||||||
|
try:
|
||||||
|
client = await registry.get("ibkr", env)
|
||||||
|
await client._request("GET", "/iserver/auth/status", skip_tickle=True)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await mgr.confirm(validate=_validate)
|
||||||
|
finally:
|
||||||
|
rotations.pop(env, None)
|
||||||
|
|
||||||
|
@r.post("/ibkr/rotate-keys/abort")
|
||||||
|
async def _ibkr_rotate_abort(env: str, request: Request):
|
||||||
|
rotations = getattr(request.app.state, "ibkr_rotations", {}) or {}
|
||||||
|
mgr = rotations.pop(env, None)
|
||||||
|
if mgr is None:
|
||||||
|
return {"aborted": False, "reason": "no rotation in progress"}
|
||||||
|
return await mgr.abort()
|
||||||
|
|
||||||
|
@r.post("/ibkr/health")
|
||||||
|
async def _ibkr_health(request: Request):
|
||||||
|
registry = request.app.state.registry
|
||||||
|
out: dict[str, Any] = {}
|
||||||
|
for env in ("testnet", "mainnet"):
|
||||||
|
try:
|
||||||
|
client = await registry.get("ibkr", env)
|
||||||
|
status = await client._request(
|
||||||
|
"GET", "/iserver/auth/status", skip_tickle=True
|
||||||
|
)
|
||||||
|
out[env] = {"healthy": True, "status": status}
|
||||||
|
except Exception as e:
|
||||||
|
out[env] = {"healthy": False, "error": str(e)[:200]}
|
||||||
|
return out
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
Reference in New Issue
Block a user