Files
walkup/backend/app/routes/teamsnap.py
2026-04-22 06:46:23 -05:00

82 lines
3.1 KiB
Python

from __future__ import annotations
from collections.abc import Mapping, Sequence
import json
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from ..auth import require_session
from ..models import UserSession
router = APIRouter(prefix="/teamsnap", tags=["teamsnap"])
def build_proxy_api_root(request: Request) -> str:
scheme = request.headers.get("x-forwarded-proto", request.url.scheme)
host = request.headers.get("x-forwarded-host", request.headers.get("host", request.url.netloc))
return f"{scheme}://{host}/api/teamsnap"
def rewrite_teamsnap_urls(value: object, upstream_root: str, proxy_root: str) -> object:
if isinstance(value, str):
return value.replace(upstream_root, proxy_root)
if isinstance(value, Mapping):
return {key: rewrite_teamsnap_urls(item, upstream_root, proxy_root) for key, item in value.items()}
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return [rewrite_teamsnap_urls(item, upstream_root, proxy_root) for item in value]
return value
def build_upstream_url(request: Request, proxy_path: str) -> str:
base = request.app.state.settings.teamsnap_api_root.rstrip("/")
if not proxy_path:
return base
return f"{base}/{proxy_path.lstrip('/')}"
@router.api_route("/{proxy_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@router.api_route("", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def teamsnap_proxy(
request: Request,
session: UserSession = Depends(require_session),
proxy_path: str = "",
) -> Response:
if session.provider != "teamsnap":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Session is not TeamSnap-backed")
if not session.access_token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing TeamSnap access token")
upstream_url = build_upstream_url(request, proxy_path)
outgoing_headers = {
"Accept": request.headers.get("accept", "application/vnd.collection+json"),
"Authorization": f"Bearer {session.access_token}",
}
if request.headers.get("content-type"):
outgoing_headers["Content-Type"] = request.headers["content-type"]
body = await request.body()
async with httpx.AsyncClient(timeout=30.0) as client:
upstream = await client.request(
request.method,
upstream_url,
params=request.query_params,
content=body or None,
headers=outgoing_headers,
)
content_type = upstream.headers.get("content-type", "")
if "json" not in content_type.lower():
response = Response(content=upstream.content, status_code=upstream.status_code)
if content_type:
response.headers["Content-Type"] = content_type
return response
proxy_root = build_proxy_api_root(request)
rewritten = rewrite_teamsnap_urls(upstream.json(), request.app.state.settings.teamsnap_api_root, proxy_root)
return Response(
content=json.dumps(rewritten),
status_code=upstream.status_code,
media_type=content_type or "application/json",
)