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>
72 lines
2.2 KiB
Python
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"})
|