import json import os from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel from sqlmodel import select from ..wallet import ( TRUSTED_MINTS, fetch_all_balances, get_proofs_per_mint_and_unit, get_wallet, send_token, slow_filter_spend_proofs, ) from .db import ApiKey, create_session from .logging import get_logger logger = get_logger(__name__) admin_router = APIRouter(prefix="/admin", include_in_schema=False) class WithdrawRequest(BaseModel): amount: int mint_url: str | None = None unit: str = "sat" def login_form() -> str: return """

🔐 Admin Login

""" def info(content: str) -> str: return f"""

{content}

""" def admin_auth() -> str: if os.getenv("ADMIN_PASSWORD", "") == "": return info("Please set a secure ADMIN_PASSWORD= in your ENV variables.") else: return login_form() async def dashboard(request: Request) -> str: # fetch cashu / api-key data from database async with create_session() as session: result = await session.exec(select(ApiKey)) api_keys = result.all() api_keys_table_rows = [] for key in api_keys: expiry_time_utc = ( datetime.fromtimestamp(key.key_expiry_time, tz=timezone.utc) if key.key_expiry_time is not None else None ) expiry_time_human_readable = ( expiry_time_utc.strftime("%Y-%m-%d %H:%M:%S") if expiry_time_utc else "" ) api_keys_table_rows.append( f"{key.hashed_key}{key.balance}{key.total_spent}{key.total_requests}{key.refund_address}{'{} ({} UTC)'.format(key.key_expiry_time, expiry_time_human_readable) if key.key_expiry_time else key.key_expiry_time}" ) # Fetch all balances using the abstracted function ( balance_details, total_wallet_balance_sats, total_user_balance_sats, owner_balance, ) = await fetch_all_balances() return f"""

Admin Dashboard

Cashu Wallet Balance

Your Balance (Total) { owner_balance } sats
Total Wallet {total_wallet_balance_sats} sats
User Balance {total_user_balance_sats} sats

Your balance = Total wallet - User balance

Mint / Unit
Wallet
Users
Owner
{ "".join( [ f'''
{detail["mint_url"].replace("https://", "").replace("http://", "")} • {detail["unit"].upper()}
{detail["wallet_balance"] if not detail.get("error") else "error"}
{detail["user_balance"] if not detail.get("error") else "-"}
0 else ""}">{detail["owner_balance"] if not detail.get("error") else "-"}
''' for detail in balance_details if detail.get("wallet_balance", 0) > 0 or detail.get("error") ] ) }
Withdrawal Token:

Save this token! It represents your withdrawn balance.

Temporary Balances

{"".join(api_keys_table_rows)}
Hashed Key Balance (mSats) Total Spent (mSats) Total Requests Refund Address Refund Time
""" @admin_router.get("/", response_class=HTMLResponse) async def admin(request: Request) -> str: admin_cookie = request.cookies.get("admin_password") if admin_cookie and admin_cookie == os.getenv("ADMIN_PASSWORD"): return await dashboard(request) return admin_auth() @admin_router.get("/logs/{request_id}", response_class=HTMLResponse) async def view_logs(request: Request, request_id: str) -> str: admin_cookie = request.cookies.get("admin_password") if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"): return admin_auth() logger.info(f"Investigating logs for request_id: {request_id}") # Search for log entries with this request_id log_entries = [] logs_dir = Path("logs") if logs_dir.exists(): # Get all log files sorted by modification time (most recent first) log_files = sorted( logs_dir.glob("*.log"), key=lambda x: x.stat().st_mtime, reverse=True ) for log_file in log_files[:7]: # Check last 7 days of logs try: with open(log_file, "r") as f: for line in f: if request_id in line: try: # Parse JSON log entry log_data = json.loads(line.strip()) log_entries.append(log_data) except json.JSONDecodeError: # If not JSON, include raw line log_entries.append({"raw": line.strip()}) except Exception as e: logger.error(f"Error reading log file {log_file}: {e}") # Sort entries by timestamp if available log_entries.sort(key=lambda x: x.get("asctime", ""), reverse=False) # Format log entries for display formatted_logs = [] for entry in log_entries: if "raw" in entry: formatted_logs.append(f'
{entry["raw"]}
') else: # Format JSON log entry timestamp = entry.get("asctime", "Unknown time") level = entry.get("levelname", "INFO") message = entry.get("message", "") pathname = entry.get("pathname", "") lineno = entry.get("lineno", "") # Extract additional fields extra_fields = { k: v for k, v in entry.items() if k not in [ "asctime", "levelname", "message", "pathname", "lineno", "name", "version", "request_id", ] } level_class = level.lower() formatted_entry = f"""
{timestamp} [{level}] {pathname}:{lineno}
{message}
""" if extra_fields: formatted_entry += '
' for key, value in extra_fields.items(): formatted_entry += f'
{key}: {json.dumps(value) if isinstance(value, (dict, list)) else value}
' formatted_entry += "
" formatted_entry += "
" formatted_logs.append(formatted_entry) return f""" ← Back to Dashboard

Log Investigation

Request ID: {request_id}
{"".join(formatted_logs) if formatted_logs else '
No log entries found for this Request ID
'}

Found {len(log_entries)} log entries • Searched last 7 days of logs

""" @admin_router.post("/withdraw") async def withdraw( request: Request, withdraw_request: WithdrawRequest ) -> dict[str, str]: admin_cookie = request.cookies.get("admin_password") if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"): raise HTTPException(status_code=403, detail="Unauthorized") # Get wallet and check balance wallet = await get_wallet( withdraw_request.mint_url or TRUSTED_MINTS[0], withdraw_request.unit ) proofs = get_proofs_per_mint_and_unit( wallet, withdraw_request.mint_url or TRUSTED_MINTS[0], withdraw_request.unit, not_reserved=True, ) proofs = await slow_filter_spend_proofs(proofs, wallet) current_balance = sum(proof.amount for proof in proofs) if withdraw_request.amount <= 0: raise HTTPException( status_code=400, detail="Withdrawal amount must be positive" ) if withdraw_request.amount > current_balance: raise HTTPException(status_code=400, detail="Insufficient wallet balance") token = await send_token( withdraw_request.amount, withdraw_request.unit, withdraw_request.mint_url ) return {"token": token}