Recherche partout (#68) : routes+middleware

This commit is contained in:
2026-10-28 12:17:43 -04:00
parent 1d6ea4728f
commit 67a486313f
+76
View File
@@ -521,6 +521,16 @@ app.add_middleware(
allow_headers=["*"],
)
# Recherche partout (#68) : CORS bornée pour GET /api/search — ajoutée
# après le CORSMiddleware global donc exécutée avant lui (dernier
# ajouté = le plus externe) : les preflights OPTIONS sont répondus
# ici, et les réponses de /api/search reçoivent les en-têtes sœurs.
# La politique same-origin explicite du middleware global reste la
# règle par défaut pour tout le reste.
from backend.recherche_cors import RechercheCorsMiddleware
app.add_middleware(RechercheCorsMiddleware)
# Auth router
# Multi-format export (HTML / MD bundle / ePub) — voir backend.routers.files_media (#85 T6c).
from backend.ai_routes import router as ai_router
@@ -940,6 +950,71 @@ if FRONTEND_DIR.exists():
return HTMLResponse(content=_html_with_nonce(request, "admin.html"), headers={"Cache-Control": "no-cache"})
raise HTTPException(status_code=404, detail="Admin page not found")
@app.get("/recherche", response_class=HTMLResponse)
async def serve_recherche_page(request: Request):
"""Page « Rechercher partout » (fusion #68) — déclarée AVANT le
catch-all SPA comme /admin.html, sinon index.html serait servi.
frontend/recherche.html est servi avec les jetons {{URL_*}}
remplacés par urls_publiques() (même contrat que le fragment
de barre) et le nonce CSP frais ; la réponse pose sa propre
CSP (préservée par SecurityHeadersMiddleware, qui ne la pose
que si elle est absente) dont le connect-src autorise les
origines sœurs — sinon le navigateur bloquerait les appels
sœurs avant même CORS. La CSP par défaut (same-origin) reste
inchangée pour toutes les autres pages.
"""
import json as _json
from backend.csp import inject_csp_nonce
from backend.draco_config import urls_publiques
from backend.recherche_cors import origin_of
recherche_file = FRONTEND_DIR / "recherche.html"
if not recherche_file.exists():
raise HTTPException(status_code=404, detail="Recherche page not found")
html = recherche_file.read_text(encoding="utf-8")
urls = urls_publiques()
for jeton, cle in (
("{{URL_OBSIGATE}}", "obsigate"),
("{{URL_FLOWDECK}}", "flowdeck"),
("{{URL_TIKTAK}}", "tiktak"),
("{{URL_KANBANTE}}", "kanbante"),
):
html = html.replace(jeton, urls.get(cle, ""))
script = (
"<script>window.DRACO_URLS = "
+ _json.dumps(urls).replace("<", "\\u003c")
+ ";</script>"
)
if "</head>" in html:
html = html.replace("</head>", script + "</head>", 1)
else:
html = script + html
html = inject_csp_nonce(html, request.state.csp_nonce)
soeurs = " ".join(sorted({
origin_of(urls.get(cle, "")) or ""
for cle in ("obsigate", "flowdeck", "tiktak", "kanbante")
} - {""}))
csp = (
"default-src 'self'; "
f"script-src 'self' 'nonce-{request.state.csp_nonce}'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
f"connect-src 'self' {soeurs}; "
"font-src 'self' data:; "
"worker-src 'self' blob:; "
"frame-src 'self'; "
"object-src 'none'; "
"base-uri 'self'; "
"form-action 'self'; "
"frame-ancestors 'self';"
)
return HTMLResponse(
content=html,
headers={"Cache-Control": "no-cache", "Content-Security-Policy": csp},
)
@app.get("/{full_path:path}")
async def serve_spa(request: Request, full_path: str):
"""Serve the SPA index.html for all non-API routes."""
@@ -947,3 +1022,4 @@ if FRONTEND_DIR.exists():
if index_file.exists():
return HTMLResponse(content=_index_html(request), headers={"Cache-Control": "no-cache"})
raise HTTPException(status_code=404, detail="Frontend not found")