from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from starlette.types import Scope from config import UPLOAD_DIR, ENVIRONMENT from database.schema import create_tables from routers import documents, review, search, upload, auth from services.auth_service import get_session, cleanup_expired_sessions Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) Path("data").mkdir(exist_ok=True) class CachedStaticFiles(StaticFiles): """StaticFiles that sets a long Cache-Control header so browsers don't re-download the same image on every refresh.""" def __init__(self, *args, max_age: int = 86400, **kwargs): super().__init__(*args, **kwargs) self._max_age = max_age async def get_response(self, path: str, scope: Scope): response = await super().get_response(path, scope) if response.status_code == 200: response.headers["Cache-Control"] = f"public, max-age={self._max_age}" return response @asynccontextmanager async def lifespan(app: FastAPI): create_tables() try: cleanup_expired_sessions() except Exception: pass # Resume any documents that were left in 'pending' state from a previous run # (e.g. server restart killed the background extraction task). try: import asyncio from database.connection import get_db from services.extractor import get_default_provider from routers.upload import _extract_and_save default_provider = get_default_provider() with get_db() as conn: stuck = conn.execute( "SELECT id, image_path, provider FROM documents WHERE status='pending'" ).fetchall() for row in stuck: provider = row["provider"] or default_provider asyncio.create_task(_extract_and_save(row["id"], row["image_path"], provider)) except Exception: pass yield # Disable docs/openapi in production if ENVIRONMENT == "production": app = FastAPI(title="Lebanese Real Estate Registry", docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan) else: app = FastAPI(title="Lebanese Real Estate Registry", lifespan=lifespan) @app.middleware("http") async def check_authentication(request: Request, call_next): path = request.url.path allowed_paths = ["/auth/login", "/static"] is_allowed = any(path.startswith(p) for p in allowed_paths) request.state.user = None if not is_allowed: session_id = request.cookies.get("session_id") user = get_session(session_id) if session_id else None if not user: return RedirectResponse(url="/auth/login", status_code=303) request.state.user = user response = await call_next(request) return response app.mount("/uploads", CachedStaticFiles(directory=UPLOAD_DIR, max_age=604800), name="uploads") app.mount("/static", CachedStaticFiles(directory="static", max_age=86400), name="static") app.include_router(upload.router) app.include_router(review.router) app.include_router(search.router) app.include_router(documents.router) app.include_router(auth.router, prefix="/auth")