fix: A43 TERMINÉ + A38 phase 1 — CSRF rendu côté serveur, helper unique (v7.33.0)
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:
+12
-12
@@ -146,7 +146,7 @@ function settingsInit() {
|
||||
var n = this.newTagName.trim();
|
||||
if (!n) return;
|
||||
var r = await fetch('/api/settings/tags', {
|
||||
method: 'POST', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
|
||||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||||
body: JSON.stringify({name: n, color: this.newTagColor})
|
||||
});
|
||||
if (r.ok) { this.newTagName = ''; this.newTagColor = '#787774'; await this.loadTags(); }
|
||||
@@ -154,7 +154,7 @@ function settingsInit() {
|
||||
|
||||
async updateTagColor(id, color) {
|
||||
await fetch('/api/settings/tags/' + id, {
|
||||
method: 'PUT', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
|
||||
method: 'PUT', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||||
body: JSON.stringify({color: color})
|
||||
});
|
||||
await this.loadTags();
|
||||
@@ -162,7 +162,7 @@ function settingsInit() {
|
||||
|
||||
async deleteTag(id) {
|
||||
if (!confirm('Delete this tag?')) return;
|
||||
await fetch('/api/settings/tags/' + id, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'DELETE' });
|
||||
await fetch('/api/settings/tags/' + id, {headers: {'X-CSRF-Token': getCsrf()}, method: 'DELETE' });
|
||||
await this.loadTags();
|
||||
},
|
||||
|
||||
@@ -183,7 +183,7 @@ function settingsInit() {
|
||||
this.renamingTag = null; return;
|
||||
}
|
||||
await fetch('/api/settings/tags/' + tag.id, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json', 'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
|
||||
method: 'PUT', headers: {'Content-Type':'application/json', 'X-CSRF-Token': getCsrf()},
|
||||
body: JSON.stringify({name: newName})
|
||||
});
|
||||
this.renamingTag = null;
|
||||
@@ -456,7 +456,7 @@ function settingsInit() {
|
||||
try {
|
||||
var r = await fetch('/api/agent/keys/' + id + '/models', {
|
||||
method: 'POST',
|
||||
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({api_key: f.api_key, api_base: f.api_base})
|
||||
});
|
||||
var d = await r.json();
|
||||
@@ -485,7 +485,7 @@ function settingsInit() {
|
||||
if (f.models && f.models.length) body.models = f.models;
|
||||
var r = await fetch('/api/agent/keys/' + id, {
|
||||
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(body)
|
||||
});
|
||||
var d = await r.json();
|
||||
@@ -521,7 +521,7 @@ function settingsInit() {
|
||||
if (f.api_key) body.api_key = f.api_key;
|
||||
var r = await fetch('/api/agent/keys/' + id + '/test', {
|
||||
method: 'POST',
|
||||
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(body)
|
||||
});
|
||||
var d = await r.json();
|
||||
@@ -547,7 +547,7 @@ function settingsInit() {
|
||||
var f = this.keyForm(id);
|
||||
f.deleting = true; f.msg = ''; f.ok = false;
|
||||
try {
|
||||
var r = await fetch('/api/agent/keys/' + id, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'DELETE' });
|
||||
var r = await fetch('/api/agent/keys/' + id, {headers: {'X-CSRF-Token': getCsrf()}, method: 'DELETE' });
|
||||
var d = await r.json();
|
||||
if (r.ok) {
|
||||
f.has_key = false; f.api_key = ''; f.models = []; f.model = ''; f.last_error = '';
|
||||
@@ -707,7 +707,7 @@ function settingsInit() {
|
||||
if (!file) return;
|
||||
var form = new FormData();
|
||||
form.append('file', file);
|
||||
var r = await fetch('/api/settings/avatar', {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'POST', body: form });
|
||||
var r = await fetch('/api/settings/avatar', {headers: {'X-CSRF-Token': getCsrf()}, method: 'POST', body: form });
|
||||
if (r.ok) {
|
||||
var d = await r.json();
|
||||
this.avatarUrl = d.avatar_url + '?t=' + Date.now();
|
||||
@@ -718,7 +718,7 @@ function settingsInit() {
|
||||
async selectAvatarColor(color) {
|
||||
this.avatarColor = color;
|
||||
var r = await fetch('/api/settings/avatar-color', {
|
||||
method: 'POST', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
|
||||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||||
body: JSON.stringify({color: color})
|
||||
});
|
||||
if (r.ok) { this.avatarUrl = ''; }
|
||||
@@ -863,7 +863,7 @@ function settingsInit() {
|
||||
// ── v5.2.0 API tokens ──
|
||||
async loadApiTokens() {
|
||||
try {
|
||||
var r = await fetch('/api/settings/tokens', {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, credentials:'same-origin'});
|
||||
var r = await fetch('/api/settings/tokens', {headers: {'X-CSRF-Token': getCsrf()}, credentials:'same-origin'});
|
||||
var d = await r.json();
|
||||
this.apiTokens = d.tokens || [];
|
||||
} catch { this.apiTokens = []; }
|
||||
@@ -873,7 +873,7 @@ function settingsInit() {
|
||||
if (!name) return;
|
||||
try {
|
||||
var r = await fetch('/api/settings/tokens', {method:'POST', credentials:'same-origin',
|
||||
headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'}, body: JSON.stringify({name: name})});
|
||||
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify({name: name})});
|
||||
var d = await r.json();
|
||||
if (!r.ok) { window.showToast && window.showToast(d.detail || 'Erreur', 'error'); return; }
|
||||
this.newToken = d;
|
||||
|
||||
Reference in New Issue
Block a user