Files
flowdeck/static/js/workspaces.js
T
bruno 770fdc2b68
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m20s
FlowDeck CI / docker (push) Canceled after 0s
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
2026-10-02 08:45:27 -04:00

155 lines
5.0 KiB
JavaScript

/* exported workspacesPage -- appeles depuis les attributs HTML des templates */
function workspacesPage() {
return {
workspaces: [],
activeId: null,
showCreate: false,
showRename: false,
wsName: '',
renameTarget: null,
// Gitea
giteaStatus: 'loading', // loading|ok|not_linked|error
giteaGroups: [],
giteaTotal: 0,
activeOrg: 'all',
giteaSearch: '',
get filteredGroups() {
var self = this;
var q = (this.giteaSearch || '').toLowerCase();
var groups = this.activeOrg === 'all' ? this.giteaGroups : this.giteaGroups.filter(function(g) { return g.org === self.activeOrg; });
if (!q) return groups;
return groups.map(function(g) {
return {org: g.org, avatar: g.avatar, projects: g.projects.filter(function(p) {
return p.name.toLowerCase().includes(q) || (p.description||'').toLowerCase().includes(q);
})};
}).filter(function(g) { return g.projects.length > 0; });
},
async init() {
await this.load();
await this.loadGitea();
},
async load() {
const r = await fetch('/api/workspaces');
const d = await r.json();
this.workspaces = d.workspaces || [];
this.activeId = d.active_id;
},
async selectLocal(ws) {
await fetch(`/api/workspaces/${ws.id}/select`, {headers: {'X-CSRF-Token': getCsrf()}, method:'POST'});
window.location = '/local-workspace';
},
async doCreate() {
if (!this.wsName.trim()) return;
await fetch('/api/workspaces', {
method:'POST',
headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
body:JSON.stringify({name:this.wsName.trim()})
});
this.wsName = '';
this.showCreate = false;
await this.load();
},
renameWs(ws) {
this.renameTarget = ws;
this.wsName = ws.name;
this.showRename = true;
},
async doRename() {
if (!this.wsName.trim()||!this.renameTarget) return;
await fetch(`/api/workspaces/${this.renameTarget.id}`, {
method:'PUT',
headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
body:JSON.stringify({name:this.wsName.trim()})
});
this.wsName = '';
this.showRename = false;
this.renameTarget = null;
await this.load();
},
async deleteWs(ws) {
if (!confirm(`Delete workspace "${ws.name}" and all its pages?`)) return;
await fetch(`/api/workspaces/${ws.id}`, {headers: {'X-CSRF-Token': getCsrf()}, method:'DELETE'});
await this.load();
},
// ── Gitea ──
async loadGitea() {
this.giteaStatus = 'loading';
try {
// Get orgs
const orgsRes = await fetch('/api/gitea/orgs');
if (orgsRes.status === 401) { this.giteaStatus = 'not_linked'; return; }
if (!orgsRes.ok) { this.giteaStatus = 'error'; return; }
const orgsData = await orgsRes.json();
const orgs = orgsData.orgs || [];
// Fetch repos: user repos + org repos
const groups = [];
// Get username for the "personal" tab
let myLogin = 'Me';
try {
const meRes = await fetch('/auth/user');
if (meRes.ok) {
const meData = await meRes.json();
myLogin = meData.user?.login || 'Me';
if (myLogin.includes('_')) myLogin = myLogin.split('_').pop(); // strip gitea_ prefix if present
}
} catch { /* volontaire */ }
// User repos (no org filter)
try {
const userRes = await fetch('/api/gitea/projects');
if (userRes.ok) {
const d = await userRes.json();
const repos = d.projects || [];
if (repos.length) {
groups.push({org: myLogin, avatar: '', projects: repos.map(r => ({...r, owner: r.owner || {login: myLogin}}))});
}
}
} catch { /* volontaire */ }
// Org repos
for (const org of orgs) {
try {
const res = await fetch('/api/gitea/projects?org=' + encodeURIComponent(org.username || org.login || org.name));
if (res.ok) {
const d = await res.json();
if (d.projects && d.projects.length) {
groups.push({org: org.username || org.login || org.name, avatar: org.avatar_url || '', projects: d.projects});
}
}
} catch { /* volontaire */ }
}
this.giteaGroups = groups;
this.giteaTotal = groups.reduce((sum, g) => sum + g.projects.length, 0);
this.giteaStatus = 'ok';
} catch {
this.giteaStatus = 'error';
}
},
openGitea(proj) {
const owner = proj.owner?.login || proj.owner?.username || proj.full_name?.split('/')[0] || '';
const repo = proj.name;
if (owner && repo) {
// Set workspace cookie so sidebar shows tree
document.cookie = 'flowdeck_workspace=gitea:' + owner + ':' + repo + ';path=/;SameSite=Lax';
window.location = `/gitea-workspace?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
}
}
};
}