Add admin endpoints to list users and export a user's private key

GET /admin/users lists id/username/address/balance_sats/created_at.
GET /admin/users/{id}/privkey derives and returns that user's raw WIF
private key, for manual intervention (e.g. sweeping funds back if
something's stuck) — this is already a custodial system, the server
holds the master key everything is derived from, so this doesn't grant
a new capability, just exposes an existing one through the API. Every
access is audit-logged (admin_privkey_accessed).

Also fixes a pre-existing test-isolation bug in test_admin.py's client
fixture: app.db.session.get_session had `from app.db.base import
AsyncSessionLocal`, a one-time reference copy at first import — later
tests reassigning db_base.AsyncSessionLocal never reached it, so any
test mixing direct DB writes with router calls silently read/wrote
against a stale, possibly-disposed engine from whichever test ran
first. Fixed by also rebinding app.db.session.AsyncSessionLocal in the
fixture on every run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:21:55 +02:00
co-authored by Claude Sonnet 5
parent f21ecbd4ee
commit 39c6ea1950
3 changed files with 141 additions and 0 deletions
+11
View File
@@ -2,6 +2,7 @@ import os
from embit import script
from embit.bip32 import HDKey
from embit.ec import PrivateKey
from app.config import settings
from app.wallet.keystore import decrypt_xprv, encrypt_xprv
@@ -39,6 +40,16 @@ def derive_user_address(derivation_index: int) -> str:
return script.p2wpkh(pub).address(network=PLM_MAINNET)
def derive_user_wif(derivation_index: int) -> str:
"""Exports a user's raw private key (WIF) for manual server-side intervention
(e.g. sweeping funds back to a user, or out, if something gets stuck). This is
a custodial system — the server already holds the master key this is derived
from — but callers must still treat the result as a live secret: log access,
never persist it, never return it over an unauthenticated channel."""
key = derive_user_key(derivation_index)
return PrivateKey(key.secret, compressed=True, network=PLM_MAINNET).wif(network=PLM_MAINNET)
def derive_pool_key() -> HDKey:
"""The "indirizzo padre" from the flowchart: all bets are sent here, and
payouts are signed with this key. Reserved on branch 1 of the account (branch 0