Deler 11b op i tre dele under afklaring (runder/vinliste og
mail-skabeloner/annoncering er egne skærme). Denne del bygger den
delte infrastruktur alle senere admin-skærme skal bruge:
- api.ts: autentificeret fetch-wrapper + elevate()/decodeJwtExpiry().
- AuthContext.tsx: session overlever nu en sideopdatering (GET
/auth/me ved appstart). Holder en eksplicit "eleveret
session"-tilstand — react-router-dom + Layout (navbar) +
ParticipantsPage (liste/opret/redigér/hård-sletning).
- Ny GET /routes (minimal, kun læsning, org-scoped) — UI'et skal
kende rutens id for at oprette deltagere; ikke en rute-
indstillings-skærm (den er bevidst udskudt).
To fund fra brugertest, begge rettet:
1. "Slet permanent" krævede kun et klik + en browser-confirm(), og
elevered sig selv usynligt i baggrunden ved hvert klik — det
underminerede hele pointen med en bevidst elevations-handling.
Elevation er nu en synlig, separat handling ("Forhøj
rettigheder"-knap i navbaren); destruktive knapper vises kun mens
en eleveret session er aktiv (udløber automatisk efter ~5 min).
2. GET /participants uden et eksplicit limit ramte API'ets default på
100 — med 308 rigtige deltagere (sorteret efter stigende id)
kunne hverken alle deltagere eller nyoprettede deltagere ses.
ParticipantsPage sender nu limit=500 (API'ets maksimum).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
from pathlib import Path
|
|
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
from starlette.responses import Response
|
|
from starlette.staticfiles import StaticFiles
|
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
|
|
from app.core.config import get_settings
|
|
|
|
ADMIN_DIST_DIR = Path(__file__).parent / "admin_dist"
|
|
|
|
# Præfikser der altid rammer det almindelige API, uanset domæne — skal
|
|
# holdes i sync med routerne registreret i app/main.py
|
|
_API_PREFIXES = (
|
|
"/auth",
|
|
"/participants",
|
|
"/purchase-rounds",
|
|
"/wine-offerings",
|
|
"/wine-categories",
|
|
"/mail-templates",
|
|
"/mail-logs",
|
|
"/orders",
|
|
"/routes",
|
|
"/webhooks",
|
|
"/public",
|
|
"/health",
|
|
"/docs",
|
|
"/redoc",
|
|
"/openapi.json",
|
|
)
|
|
|
|
|
|
class SPAStaticFiles(StaticFiles):
|
|
"""Standard SPA-fallback: ukendte stier (client-side routing)
|
|
falder tilbage til index.html i stedet for 404."""
|
|
|
|
async def get_response(self, path: str, scope: Scope) -> Response:
|
|
try:
|
|
return await super().get_response(path, scope)
|
|
except StarletteHTTPException as exc:
|
|
if exc.status_code == 404:
|
|
return await super().get_response("index.html", scope)
|
|
raise
|
|
|
|
|
|
class AdminDomainDispatch:
|
|
"""Sender requests mod admin-domænet til den byggede React-SPA i
|
|
stedet for den domæne-opløste offentlige side (public_site.py) —
|
|
men lader API-kald (/auth, /participants, ...) passere uændret,
|
|
så admin-UI'et kan kalde API'et uden CORS (samme origin)."""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
self.app = app
|
|
self.admin_domain = get_settings().admin_domain
|
|
self.spa_app = SPAStaticFiles(directory=str(ADMIN_DIST_DIR), html=True) if ADMIN_DIST_DIR.is_dir() else None
|
|
|
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
if scope["type"] != "http" or self.spa_app is None:
|
|
await self.app(scope, receive, send)
|
|
return
|
|
headers = dict(scope["headers"])
|
|
host = headers.get(b"host", b"").decode().split(":")[0]
|
|
if host == self.admin_domain and not scope["path"].startswith(_API_PREFIXES):
|
|
await self.spa_app(scope, receive, send)
|
|
return
|
|
await self.app(scope, receive, send)
|