"""Superadmin console FastAPI app. Independent of ``app.main``: it wires only its own routes and reuses ``app.db``'s session factory for read-only queries. No lifespan, no migrations, no scheduler. Run: uvicorn admin.main:app --host 0.0.0.0 --port 8000 """ from __future__ import annotations from datetime import datetime, timezone from pathlib import Path from fastapi import Depends, FastAPI, Form, Query, Request from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.ext.asyncio import AsyncSession from admin import auth, queries from app.db import get_session TEMPLATE_DIR = Path(__file__).resolve().parent / "templates" templates = Jinja2Templates(directory=str(TEMPLATE_DIR)) def _fmt_bytes(n: int | None) -> str: if not n: return "0 B" units = ["B", "KB", "MB", "GB", "TB"] size = float(n) for u in units: if size < 1024 or u == units[-1]: return f"{size:,.0f} {u}" if u == "B" else f"{size:,.1f} {u}" size /= 1024 return f"{n} B" def _fmt_dt(dt: datetime | None) -> str: if dt is None: return "—" if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.strftime("%Y-%m-%d %H:%M UTC") templates.env.filters["bytes"] = _fmt_bytes templates.env.filters["dt"] = _fmt_dt app = FastAPI(title="read.markets superadmin console", docs_url=None, redoc_url=None) @app.get("/healthz", response_class=PlainTextResponse, include_in_schema=False) async def healthz() -> str: return "ok" @app.get("/login", response_class=HTMLResponse, include_in_schema=False) async def login_form(request: Request): # Already signed in? Skip the form. cookie = request.cookies.get(auth.SESSION_COOKIE_NAME) if cookie and auth.verify_session(cookie): return RedirectResponse("/", status_code=303) return templates.TemplateResponse(request, "login.html", {"error": None}) @app.post("/login", response_class=HTMLResponse, include_in_schema=False) async def login_submit(request: Request, password: str = Form(...)): if not auth.check_password(password): return templates.TemplateResponse( request, "login.html", {"error": "Incorrect password."}, status_code=401, ) resp = RedirectResponse("/", status_code=303) resp.set_cookie( auth.SESSION_COOKIE_NAME, auth.sign_session(), max_age=auth.SESSION_TTL_SECONDS, httponly=True, samesite="lax", # secure=False: the console is served over http on a localhost SSH # tunnel, never public TLS. A secure cookie would never be sent. secure=False, ) return resp @app.get("/logout", include_in_schema=False) async def logout(): resp = RedirectResponse("/login", status_code=303) resp.delete_cookie(auth.SESSION_COOKIE_NAME) return resp @app.get("/", response_class=HTMLResponse, include_in_schema=False, dependencies=[Depends(auth.require_admin)]) async def overview(request: Request, session: AsyncSession = Depends(get_session)): stats = await queries.overview_stats(session) recent, _ = await queries.list_users(session, limit=10) return templates.TemplateResponse( request, "overview.html", {"stats": stats, "recent": recent}, ) @app.get("/users", response_class=HTMLResponse, include_in_schema=False, dependencies=[Depends(auth.require_admin)]) async def users_list( request: Request, session: AsyncSession = Depends(get_session), q: str | None = Query(default=None), page: int = Query(default=1, ge=1), ): per_page = 200 offset = (page - 1) * per_page rows, total = await queries.list_users(session, q=q, limit=per_page, offset=offset) return templates.TemplateResponse( request, "users.html", {"rows": rows, "total": total, "q": q or "", "page": page, "per_page": per_page}, ) @app.get("/users/{user_id}", response_class=HTMLResponse, include_in_schema=False, dependencies=[Depends(auth.require_admin)]) async def user_detail( request: Request, user_id: int, session: AsyncSession = Depends(get_session), ): detail = await queries.user_detail(session, user_id) if detail is None: return templates.TemplateResponse( request, "not_found.html", {"user_id": user_id}, status_code=404, ) return templates.TemplateResponse(request, "user_detail.html", {"d": detail}) @app.get("/db", response_class=HTMLResponse, include_in_schema=False, dependencies=[Depends(auth.require_admin)]) async def db_page(request: Request, session: AsyncSession = Depends(get_session)): stats = await queries.db_stats(session) return templates.TemplateResponse(request, "db.html", {"stats": stats})