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", )