Files
plm-lottery/app/api/routes/admin.py
T

94 lines
3.3 KiB
Python
Raw Normal View History

2026-07-21 10:26:31 +02:00
from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
2026-07-21 10:26:31 +02:00
from sqlalchemy.ext.asyncio import AsyncSession
from app.audit.log import write_audit_log
2026-07-21 10:26:31 +02:00
from app.config import settings
from app.db.models import User
2026-07-21 10:26:31 +02:00
from app.db.session import get_session
from app.rounds.config import get_round_config
from app.wallet.hd import derive_user_wif
2026-07-21 10:26:31 +02:00
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)