from __future__ import annotations from typing import Any from fastapi import FastAPI def extend_with_problem_and_cookie_auth(app: FastAPI) -> None: """Augment FastAPI's OpenAPI spec with RFC7807 responses and cookie auth. This mutates the app's OpenAPI generation in-place while delegating to the original generator for the base schema. """ original_openapi = app.openapi def custom_openapi() -> dict[str, Any]: spec = original_openapi() components = spec.setdefault("components", {}) responses = components.setdefault("responses", {}) security_schemes = components.setdefault("securitySchemes", {}) # Standard ProblemDetails responses responses.setdefault( "Problem400", { "description": "Bad Request", "content": { "application/problem+json": {}, "application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}, }, }, ) responses.setdefault( "Problem404", { "description": "Not Found", "content": { "application/problem+json": {}, "application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}, }, }, ) responses.setdefault( "Problem422", { "description": "Validation Error", "content": { "application/problem+json": { "schema": {"$ref": "#/components/schemas/ProblemDetails"} }, "application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}, }, }, ) # Cookie-based auth for documentation (does not enforce at runtime) security_schemes.setdefault( "cookieAuth", { "type": "apiKey", "in": "cookie", "name": "user_id", "description": "Authentication via user_id cookie (session-style).", }, ) # Normalize v1 responses and mark cookie security for known endpoints paths = spec.get("paths", {}) protected_ops: set[str] = { "parseRecipe", "createRecipe", "deleteRecipe", "markMealConsumed", "deleteMeal", "purchaseIngredients", "getMyShoppingList", "syncMyShoppingList", "requestMeal", "unrequestMeal", "refresh", } for path, ops in paths.items(): if not isinstance(path, str) or not path.startswith("/api/v1/"): continue if not isinstance(ops, dict): continue for _method, op in ops.items(): if not isinstance(op, dict): continue resp = op.get("responses") if not isinstance(resp, dict): continue if "400" in resp: resp["400"] = {"$ref": "#/components/responses/Problem400"} if "404" in resp: resp["404"] = {"$ref": "#/components/responses/Problem404"} if "422" not in resp: resp["422"] = {"$ref": "#/components/responses/Problem422"} op_id = op.get("operationId") if isinstance(op_id, str) and op_id in protected_ops: security = op.setdefault("security", []) if not any(isinstance(s, dict) and "cookieAuth" in s for s in security): security.append({"cookieAuth": []}) # Keep endpoint-specific schemas driven by route declarations only (no forced overrides) return spec # Rebind app.openapi to our generator (FastAPI supports this pattern). Mypy needs a narrow ignore here. app.openapi = custom_openapi # type: ignore[method-assign]