chart/main.py
Chris Amow 0bafe9de01 Add the OAuth callback endpoint at /api/qt
Schwab requires an HTTPS callback. The usual answer is https://127.0.0.1:8182
behind a self-signed certificate, which means clicking through a browser warning
on every re-authentication — and the refresh token expires weekly. There are
also reports of Schwab refusing to register apps whose callback is a loopback
address. This app already terminates real HTTPS, so it can take the redirect
itself.

Unauthenticated by necessity: the provider redirects a browser here and cannot
attach the chart token, so it sits alongside /health and /version. It is inert —
nothing is stored, and the page echoes only the query string of the request that
produced it, which the caller already has in their address bar. Retaining the
code would let a later anonymous visitor read it.

The path and the page are both deliberately unrevealing. That is not a security
control; it just avoids advertising which brokerage this host talks to. Treat
the path as fixed — changing a registered callback means editing the app, which
can send it back through approval.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 04:45:31 -05:00

72 lines
2.2 KiB
Python

"""chart.amow.com FastAPI backend."""
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
import re
from hashlib import sha256
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.api.meta import router as meta_router
from app.api.routes import router as api_router
from app.api.schwab_auth import router as schwab_auth_router
from app.api.ws import router as ws_router
from app.config import Settings
from app.runtime import Runtime
BASE_DIR = Path(__file__).parent
STATIC_DIR = BASE_DIR / "static"
@asynccontextmanager
async def lifespan(app: FastAPI):
runtime = Runtime(Settings())
app.state.runtime = runtime
task = await runtime.start()
yield
runtime.stream.stop()
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
app = FastAPI(title="chart", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.include_router(meta_router)
app.include_router(schwab_auth_router)
app.include_router(api_router)
app.include_router(ws_router)
ASSET_REF = re.compile(r'((?:src|href)="/static/[^"?]+)"')
def asset_version() -> str:
"""A digest of the served assets, so the URL changes iff the content does.
StaticFiles sends an ETag but no Cache-Control, so a browser is free to keep
using the copy it already has — and a tab left open simply never fetches
again. That turned a fixed bug into a bug that still reproduced, because the
page was running the JavaScript it had loaded hours earlier.
Hashing rather than stamping mtimes: a deploy checks every file out fresh,
which would otherwise invalidate assets that never changed.
"""
digest = sha256()
for path in sorted(STATIC_DIR.glob("*.*")):
digest.update(path.read_bytes())
return digest.hexdigest()[:12]
@app.get("/")
def index():
html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
html = ASSET_REF.sub(rf'\1?v={asset_version()}"', html)
# The document itself must never be cached, or the versioned URLs inside it
# are the stale thing instead.
return HTMLResponse(html, headers={"Cache-Control": "no-store"})