""" Proxy all /api/documents/* requests to doc-service:8001/documents/*. Uses a module-level AsyncClient for connection pooling. Strips hop-by-hop headers that must not be forwarded. Injects X-User-Id and X-User-Groups headers so the doc-service can enforce ownership and group-sharing access without querying the backend database directly. """ import os import httpx from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.deps import get_current_user from app.models.group import GroupMembership from app.models.user import User DOC_SERVICE_URL = os.environ.get("DOC_SERVICE_URL", "http://doc-service:8001") _client = httpx.AsyncClient(base_url=DOC_SERVICE_URL, timeout=120.0) router = APIRouter() # Headers that must not be forwarded in either direction. # Also strip accept-encoding so doc-service never compresses responses — # httpx decompresses transparently but the content-encoding header would # then mismatch the already-decompressed body we forward to the browser. _HOP_BY_HOP = frozenset([ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host", "accept-encoding", ]) # Additional response headers we let FastAPI recalculate rather than forward. # content-length is set automatically from the response body size. # content-type is set via the media_type argument, so strip it from headers # to avoid duplicates. _STRIP_RESPONSE = frozenset([*_HOP_BY_HOP, "content-length", "content-type"]) async def _forward_headers(request: Request, user_id: str, db: AsyncSession) -> dict: headers = { k: v for k, v in request.headers.items() if k.lower() not in _HOP_BY_HOP } headers["x-user-id"] = user_id # Inject the user's group memberships so the doc-service can evaluate # group-shared document access without querying the backend DB. result = await db.execute( select(GroupMembership.group_id).where(GroupMembership.user_id == user_id) ) group_ids = [row[0] for row in result.all()] headers["x-user-groups"] = ",".join(group_ids) return headers @router.api_route("", methods=["GET", "POST"]) @router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def proxy_documents( request: Request, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), path: str = "", ) -> Response: url = f"/documents/{path}" if path else "/documents" headers = await _forward_headers(request, str(current_user.id), db) body = await request.body() try: response = await _client.request( method=request.method, url=url, headers=headers, content=body, params=dict(request.query_params), ) except httpx.RequestError as exc: raise HTTPException(status_code=502, detail=f"doc-service unreachable: {exc}") resp_headers = { k: v for k, v in response.headers.items() if k.lower() not in _STRIP_RESPONSE } return Response( content=response.content, status_code=response.status_code, headers=resp_headers, media_type=response.headers.get("content-type"), )