fix: A43 TERMINÉ + A38 phase 1 — CSRF rendu côté serveur, helper unique (v7.33.0)
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m20s
FlowDeck CI / docker (push) Canceled after 0s

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
This commit is contained in:
2026-10-02 08:45:27 -04:00
parent 0bc74ad728
commit 770fdc2b68
25 changed files with 190 additions and 119 deletions
+4 -4
View File
@@ -98,7 +98,7 @@ document.addEventListener('alpine:init', () => {
var newPath = oldPath.replace(/[^/]+$/, newName.trim());
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file', {
method: 'PUT',
headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type': 'application/json' },
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json' },
body: JSON.stringify({ path: oldPath, new_path: newPath, message: 'Rename ' + oldPath + ' to ' + newPath })
}).then(function(r){ return r.json(); })
.then(function(d){
@@ -113,7 +113,7 @@ document.addEventListener('alpine:init', () => {
if (!item) return;
if (!confirm('Delete ' + item.name + '?')) return;
var self = this;
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(item.path) + '&sha=' + (item.sha || '') + '&message=Delete ' + item.path, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(item.path) + '&sha=' + (item.sha || '') + '&message=Delete ' + item.path, {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE'
}).then(function(r){
if (r.ok) { self.refreshTree(); }
@@ -208,7 +208,7 @@ document.addEventListener('alpine:init', () => {
if (!path) return;
if (!confirm('Delete ' + path + ' from ' + self.owner + '/' + self.repo + '?')) return;
var sha = el.getAttribute('data-gitea-sha') || '';
fetch('/api/gitea/projects/' + self.owner + '/' + self.repo + '/file?path=' + encodeURIComponent(path) + '&sha=' + encodeURIComponent(sha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
fetch('/api/gitea/projects/' + self.owner + '/' + self.repo + '/file?path=' + encodeURIComponent(path) + '&sha=' + encodeURIComponent(sha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE',
}).then(function(r) {
if (r.ok) self.refreshTree();
@@ -350,7 +350,7 @@ document.addEventListener('alpine:init', () => {
if (!this.filePath) return;
if (!confirm('Delete ' + this.filePath + ' from ' + this.owner + '/' + this.repo + '?')) return;
try {
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(this.filePath) + '&sha=' + encodeURIComponent(this.fileSha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
var r = await fetch('/api/gitea/projects/' + this.owner + '/' + this.repo + '/file?path=' + encodeURIComponent(this.filePath) + '&sha=' + encodeURIComponent(this.fileSha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE',
});
if (r.ok) {