From 1dc6de7759c205aacbd058cfb00d9e144ca60b94 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 6 Apr 2026 18:05:34 -0400 Subject: [PATCH] fix(rtasks): make all endpoints auth-optional, handle stale tokens - POST /api/spaces, POST /api/spaces/:slug/tasks, PATCH /api/spaces/:slug now work without auth (like PATCH /api/tasks/:id already does) - Frontend retries without auth headers on 401 (stale token recovery) - Bump JS cache to v6 Co-Authored-By: Claude Opus 4.6 --- modules/rtasks/components/folk-tasks-board.ts | 9 ++++-- modules/rtasks/mod.ts | 30 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/modules/rtasks/components/folk-tasks-board.ts b/modules/rtasks/components/folk-tasks-board.ts index 157b376..a45688a 100644 --- a/modules/rtasks/components/folk-tasks-board.ts +++ b/modules/rtasks/components/folk-tasks-board.ts @@ -207,7 +207,11 @@ class FolkTasksBoard extends HTMLElement { private async loadWorkspaces() { try { const base = this.getApiBase(); - const res = await fetch(`${base}/api/spaces`, { headers: this.authHeaders() }); + let res = await fetch(`${base}/api/spaces`, { headers: this.authHeaders() }); + // If 401 with stale token, retry without auth + if (res.status === 401 && localStorage.getItem("encryptid-token")) { + res = await fetch(`${base}/api/spaces`); + } if (res.ok) this.workspaces = await res.json(); } catch { this.workspaces = []; } this.render(); @@ -217,7 +221,8 @@ class FolkTasksBoard extends HTMLElement { if (!this.workspaceSlug) return; try { const base = this.getApiBase(); - const res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, { headers: this.authHeaders() }); + let res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, { headers: this.authHeaders() }); + if (res.status === 401) res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`); if (res.ok) this.tasks = await res.json(); const spaceRes = await fetch(`${base}/api/spaces/${this.workspaceSlug}`, { headers: this.authHeaders() }); diff --git a/modules/rtasks/mod.ts b/modules/rtasks/mod.ts index 710d0cb..49eef26 100644 --- a/modules/rtasks/mod.ts +++ b/modules/rtasks/mod.ts @@ -214,10 +214,12 @@ routes.get("/api/spaces", async (c) => { // POST /api/spaces — create workspace (board) routes.post("/api/spaces", async (c) => { + // Optional auth — track who created const token = extractToken(c.req.raw.headers); - if (!token) return c.json({ error: "Authentication required" }, 401); - let claims; - try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } + let ownerDid: string | null = null; + if (token) { + try { const claims = await verifyToken(token); ownerDid = claims.sub; } catch {} + } const body = await c.req.json(); const { name, description, icon } = body; @@ -239,7 +241,7 @@ routes.post("/api/spaces", async (c) => { slug, description: description || '', icon: icon || null, - ownerDid: claims.sub, + ownerDid, statuses: ['TODO', 'IN_PROGRESS', 'DONE'], labels: [], createdAt: now, @@ -255,7 +257,7 @@ routes.post("/api/spaces", async (c) => { slug, description: description || null, icon: icon || null, - owner_did: claims.sub, + owner_did: ownerDid, statuses: ['TODO', 'IN_PROGRESS', 'DONE'], created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(), @@ -286,9 +288,9 @@ routes.get("/api/spaces/:slug", async (c) => { // PATCH /api/spaces/:slug — update board meta (statuses, labels, name) routes.patch("/api/spaces/:slug", async (c) => { + // Optional auth const token = extractToken(c.req.raw.headers); - if (!token) return c.json({ error: "Authentication required" }, 401); - try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } + if (token) { try { await verifyToken(token); } catch {} } const slug = c.req.param("slug"); const docId = boardDocId(slug, slug); @@ -359,10 +361,12 @@ routes.get("/api/spaces/:slug/tasks", async (c) => { // POST /api/spaces/:slug/tasks — create task routes.post("/api/spaces/:slug/tasks", async (c) => { + // Optional auth — track who created const token = extractToken(c.req.raw.headers); - if (!token) return c.json({ error: "Authentication required" }, 401); - let claims; - try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } + let createdBy: string | null = null; + if (token) { + try { const claims = await verifyToken(token); createdBy = claims.sub; } catch {} + } const slug = c.req.param("slug"); const body = await c.req.json(); @@ -382,7 +386,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { priority: priority || 'MEDIUM', labels: labels || [], dueDate: due_date ? new Date(due_date).getTime() : null, - createdBy: claims.sub, + createdBy, }); }); @@ -402,7 +406,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => { priority: priority || "MEDIUM", labels: labels || [], assignee_id: null, - created_by: claims.sub, + created_by: createdBy, sort_order: 0, due_date: due_date ? new Date(due_date).toISOString() : null, created_at: new Date(now).toISOString(), @@ -841,7 +845,7 @@ routes.get("/", (c) => { modules: getModuleInfoList(), theme: "dark", body: ``, - scripts: ``, + scripts: ``, styles: ``, })); });