"""FastAPI entrypoint for the DMS backend service.""" from typing import Awaitable, Callable from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api.router import api_router from app.core.config import get_settings from app.db.base import init_db from app.services.app_settings import ensure_app_settings from app.services.authentication import ensure_bootstrap_users from app.services.handwriting_style import ensure_handwriting_style_collection from app.services.storage import ensure_storage from app.services.typesense_index import ensure_typesense_collection settings = get_settings() UPLOAD_ENDPOINT_PATH = "/api/v1/documents/upload" UPLOAD_ENDPOINT_METHOD = "POST" def _is_upload_size_guard_target(request: Request) -> bool: """Returns whether upload request-size enforcement applies to this request. Upload-size validation is intentionally scoped to the upload POST endpoint so CORS preflight OPTIONS requests can pass through CORSMiddleware. """ return request.method.upper() == UPLOAD_ENDPOINT_METHOD and request.url.path == UPLOAD_ENDPOINT_PATH def create_app() -> FastAPI: """Builds and configures the FastAPI application instance.""" app = FastAPI(title="DCM DMS API", version="0.1.0") allowed_origins = [origin.strip() for origin in settings.cors_origins if isinstance(origin, str) and origin.strip()] app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, allow_credentials=bool(getattr(settings, "cors_allow_credentials", False)), allow_methods=["*"], allow_headers=["*"], ) app.include_router(api_router, prefix="/api/v1") @app.middleware("http") async def enforce_upload_request_size( request: Request, call_next: Callable[[Request], Awaitable[Response]], ) -> Response: """Rejects only POST upload bodies without deterministic length or with oversized request totals.""" if _is_upload_size_guard_target(request): content_length = request.headers.get("content-length", "").strip() if not content_length: return JSONResponse( status_code=411, content={"detail": "Content-Length header is required for document uploads"}, ) try: content_length_value = int(content_length) except ValueError: return JSONResponse(status_code=400, content={"detail": "Invalid Content-Length header"}) if content_length_value <= 0: return JSONResponse(status_code=400, content={"detail": "Content-Length must be a positive integer"}) if content_length_value > settings.max_upload_request_size_bytes: return JSONResponse( status_code=413, content={ "detail": ( "Upload request exceeds total size limit " f"({content_length_value} > {settings.max_upload_request_size_bytes} bytes)" ) }, ) return await call_next(request) @app.on_event("startup") def startup_event() -> None: """Initializes storage directories and database schema on service startup.""" ensure_storage() init_db() ensure_bootstrap_users() ensure_app_settings() try: ensure_typesense_collection() except Exception: pass try: ensure_handwriting_style_collection() except Exception: pass return app app = create_app()