|
| 1 | +import glob |
1 | 2 | import time |
2 | 3 | from collections.abc import AsyncIterator |
3 | 4 | from contextlib import asynccontextmanager |
| 5 | +from pathlib import Path |
4 | 6 |
|
5 | 7 | from fastapi import FastAPI, Request |
| 8 | +from fastapi.responses import FileResponse |
6 | 9 | from starlette.exceptions import HTTPException |
7 | 10 | from starlette.middleware.base import RequestResponseEndpoint |
8 | 11 | from starlette.middleware.cors import CORSMiddleware |
@@ -153,3 +156,23 @@ async def generic_exception_handler(request: Request, exc: Exception) -> JSONRes |
153 | 156 |
|
154 | 157 | for tag, router in routers.items(): |
155 | 158 | app.include_router(router, tags=[tag]) |
| 159 | + |
| 160 | +if config.serve_frontend: |
| 161 | + frontend_root = Path("frontend-dist").resolve() |
| 162 | + allowed_paths = list(glob.iglob("frontend-dist/**/*", recursive=True)) |
| 163 | + |
| 164 | + @app.get("/{full_path:path}") |
| 165 | + async def frontend(full_path: str) -> FileResponse: |
| 166 | + path = (frontend_root / Path(full_path)).resolve() |
| 167 | + |
| 168 | + # Checking `str(path) in allowed_paths` should be enough here but we check for more cases |
| 169 | + # to be sure and avoid AI tools raising false positives. |
| 170 | + if ( |
| 171 | + path.exists() |
| 172 | + and path.is_file() |
| 173 | + and str(path) in allowed_paths |
| 174 | + and frontend_root in path.parents |
| 175 | + ): |
| 176 | + return FileResponse(path) |
| 177 | + |
| 178 | + return FileResponse(frontend_root / Path("index.html")) |
0 commit comments