A43 (les 4 items sont clos) :
- `__CSRF_PLACEHOLDER__` supprimé : ContextVar CSRF_TOKEN posée par
CSRFMiddleware avant call_next (même mécanique que le nonce CSP),
global `{{ csrf_token() }}` dans templating, base.html rend
`{"X-CSRF-Token":{{ csrf_token()|tojson }}}` — vide si cookie absent,
`htmx:configRequest` re-lit le cookie à chaque appel → plus jamais de
jeton factice servi.
- Palette : `fetch('/api/search…')` SANS header (GET ∈ SAFE_METHODS →
CSRF inapplicable) — le JSON.parse du body-attr par frappe disparaît.
- utcnow déprécié = 0 dans app/**.py ; health loggé (A25) + booléens db/
gitea = raison ; probe réseau = voulu (test de connectivité).
A38 phase 1 (CSRF unifié) :
- `window.getCsrf()` unique dans le <head> de base.html.
- 76 lectures brutes du cookie → getCsrf() dans 13 fichiers (47 formes
`(…||[])[1]||''`, 25 déclarations `const X = match(…)` avec conversion
de leurs usages `X?X[1]:''` → `X` (noms collectés PAR FICHIER, jamais
de règle globale), 4 formes espacées).
- Définitions dupliquées supprimées : card_detail.html (multi-lignes),
database_table.js (1 ligne dans IIFE → chute sur le global).
- Les 3 variantes de base.html (IIFE getCsrf + 2 getCsrfToken) →
`return getCsrf()`.
- welcome.html garde sa lecture locale (page autonome sans base) ;
reste exactement 1 raw dans base.html (la déf head) + 2 dans welcome.
Test : test_csrf_server_rendered_no_placeholder (pas de placeholder +
token du hx-headers == cookie csrf_token).
suite **1092/1092** · ruff OK · node --check vert · docs à jour
43 lines
1.6 KiB
Python
43 lines
1.6 KiB
Python
"""Environment Jinja2 partagé (A10 : autoescape activé partout).
|
|
|
|
Une seule instance au lieu de 29 `Environment(loader=FileSystemLoader(...))`
|
|
sans autoescape — 326 interpolations `{{ … }}` étaient servies crues et tous
|
|
les `|safe` du codebase étaient des no-op.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
from contextvars import ContextVar
|
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
|
# A20 : nonce de script par requête, posé par le middleware CSP, lu par les
|
|
# templates via `{{ csp_nonce() }}` (vide hors requête — pas de header CSP
|
|
# dans ce cas, donc rien n'est bloqué).
|
|
CSP_NONCE: ContextVar[str] = ContextVar("csp_nonce", default="")
|
|
|
|
# A43 : jeton CSRF rendu côté serveur dans `hx-headers` (base.html) — posé
|
|
# par le middleware CSRF AVANT call_next, lu via `{{ csrf_token() }}`
|
|
# (vide = cookie absent sur cette requête, htmx:configRequest re-lit le
|
|
# cookie au moment de l'appel → jamais de « __CSRF_PLACEHOLDER__ » servi).
|
|
CSRF_TOKEN: ContextVar[str] = ContextVar("csrf_token", default="")
|
|
|
|
ENV = Environment(
|
|
loader=FileSystemLoader("app/templates"),
|
|
autoescape=select_autoescape(["html"]),
|
|
)
|
|
|
|
|
|
# A40 : version des assets statiques première main, lue une seule fois (le
|
|
# fichier VERSION fait foi ; repli "dev" si l'image ne le contient pas).
|
|
try:
|
|
ASSET_VERSION = (pathlib.Path(__file__).resolve().parent.parent / "VERSION").read_text(
|
|
encoding="utf-8"
|
|
).strip()
|
|
except OSError: # pragma: no cover
|
|
ASSET_VERSION = "dev"
|
|
|
|
ENV.globals["asset_version"] = ASSET_VERSION
|
|
ENV.globals["csp_nonce"] = lambda: CSP_NONCE.get()
|
|
ENV.globals["csrf_token"] = lambda: CSRF_TOKEN.get()
|