from fastapi import APIRouter, Depends, Header, HTTPException, status from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.audit.log import write_audit_log from app.config import settings from app.db.models import User from app.db.session import get_session from app.rounds.config import get_round_config from app.wallet.hd import derive_user_wif router = APIRouter(prefix="/admin", tags=["admin"]) async def require_admin(x_admin_token: str = Header(default="")) -> None: if not settings.admin_token or x_admin_token != settings.admin_token: raise HTTPException(status.HTTP_403_FORBIDDEN, "invalid admin token") class RoundConfigResponse(BaseModel): fee_address: str bet_amount_sats: int class RoundConfigUpdate(BaseModel): fee_address: str | None = None bet_amount_sats: int | None = None @router.get("/config", response_model=RoundConfigResponse, dependencies=[Depends(require_admin)]) async def read_config(session: AsyncSession = Depends(get_session)) -> RoundConfigResponse: config = await get_round_config(session) await session.commit() return RoundConfigResponse(fee_address=config.fee_address, bet_amount_sats=config.bet_amount_sats) @router.put("/config", response_model=RoundConfigResponse, dependencies=[Depends(require_admin)]) async def update_config( body: RoundConfigUpdate, session: AsyncSession = Depends(get_session) ) -> RoundConfigResponse: config = await get_round_config(session) if body.fee_address is not None: config.fee_address = body.fee_address if body.bet_amount_sats is not None: config.bet_amount_sats = body.bet_amount_sats await session.commit() return RoundConfigResponse(fee_address=config.fee_address, bet_amount_sats=config.bet_amount_sats) class AdminUserResponse(BaseModel): id: int username: str address: str balance_sats: int created_at: str @router.get("/users", response_model=list[AdminUserResponse], dependencies=[Depends(require_admin)]) async def list_users(session: AsyncSession = Depends(get_session)) -> list[AdminUserResponse]: users = (await session.scalars(select(User).order_by(User.id))).all() return [ AdminUserResponse( id=u.id, username=u.username, address=u.address, balance_sats=u.cached_balance_sats, created_at=u.created_at.isoformat(), ) for u in users ] class AdminPrivkeyResponse(BaseModel): address: str wif: str @router.get( "/users/{user_id}/privkey", response_model=AdminPrivkeyResponse, dependencies=[Depends(require_admin)] ) async def user_privkey(user_id: int, session: AsyncSession = Depends(get_session)) -> AdminPrivkeyResponse: """Exports a user's raw private key for manual intervention (e.g. sweeping funds back if something's stuck). Every access is audit-logged since this is the most sensitive data the platform holds.""" user = await session.get(User, user_id) if user is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "user not found") wif = derive_user_wif(user.derivation_index) await write_audit_log(session, "admin_privkey_accessed", {"user_id": user_id}, user_id=user_id) await session.commit() return AdminPrivkeyResponse(address=user.address, wif=wif)