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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-06 18:05:34 -04:00
parent c00221c1e5
commit 1dc6de7759
2 changed files with 24 additions and 15 deletions

View File

@ -207,7 +207,11 @@ class FolkTasksBoard extends HTMLElement {
private async loadWorkspaces() { private async loadWorkspaces() {
try { try {
const base = this.getApiBase(); 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(); if (res.ok) this.workspaces = await res.json();
} catch { this.workspaces = []; } } catch { this.workspaces = []; }
this.render(); this.render();
@ -217,7 +221,8 @@ class FolkTasksBoard extends HTMLElement {
if (!this.workspaceSlug) return; if (!this.workspaceSlug) return;
try { try {
const base = this.getApiBase(); 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(); if (res.ok) this.tasks = await res.json();
const spaceRes = await fetch(`${base}/api/spaces/${this.workspaceSlug}`, { headers: this.authHeaders() }); const spaceRes = await fetch(`${base}/api/spaces/${this.workspaceSlug}`, { headers: this.authHeaders() });

View File

@ -214,10 +214,12 @@ routes.get("/api/spaces", async (c) => {
// POST /api/spaces — create workspace (board) // POST /api/spaces — create workspace (board)
routes.post("/api/spaces", async (c) => { routes.post("/api/spaces", async (c) => {
// Optional auth — track who created
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); let ownerDid: string | null = null;
let claims; if (token) {
try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { const claims = await verifyToken(token); ownerDid = claims.sub; } catch {}
}
const body = await c.req.json(); const body = await c.req.json();
const { name, description, icon } = body; const { name, description, icon } = body;
@ -239,7 +241,7 @@ routes.post("/api/spaces", async (c) => {
slug, slug,
description: description || '', description: description || '',
icon: icon || null, icon: icon || null,
ownerDid: claims.sub, ownerDid,
statuses: ['TODO', 'IN_PROGRESS', 'DONE'], statuses: ['TODO', 'IN_PROGRESS', 'DONE'],
labels: [], labels: [],
createdAt: now, createdAt: now,
@ -255,7 +257,7 @@ routes.post("/api/spaces", async (c) => {
slug, slug,
description: description || null, description: description || null,
icon: icon || null, icon: icon || null,
owner_did: claims.sub, owner_did: ownerDid,
statuses: ['TODO', 'IN_PROGRESS', 'DONE'], statuses: ['TODO', 'IN_PROGRESS', 'DONE'],
created_at: new Date(now).toISOString(), created_at: new Date(now).toISOString(),
updated_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) // PATCH /api/spaces/:slug — update board meta (statuses, labels, name)
routes.patch("/api/spaces/:slug", async (c) => { routes.patch("/api/spaces/:slug", async (c) => {
// Optional auth
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (token) { try { await verifyToken(token); } catch {} }
try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const slug = c.req.param("slug"); const slug = c.req.param("slug");
const docId = boardDocId(slug, 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 // POST /api/spaces/:slug/tasks — create task
routes.post("/api/spaces/:slug/tasks", async (c) => { routes.post("/api/spaces/:slug/tasks", async (c) => {
// Optional auth — track who created
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); let createdBy: string | null = null;
let claims; if (token) {
try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { const claims = await verifyToken(token); createdBy = claims.sub; } catch {}
}
const slug = c.req.param("slug"); const slug = c.req.param("slug");
const body = await c.req.json(); const body = await c.req.json();
@ -382,7 +386,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => {
priority: priority || 'MEDIUM', priority: priority || 'MEDIUM',
labels: labels || [], labels: labels || [],
dueDate: due_date ? new Date(due_date).getTime() : null, 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", priority: priority || "MEDIUM",
labels: labels || [], labels: labels || [],
assignee_id: null, assignee_id: null,
created_by: claims.sub, created_by: createdBy,
sort_order: 0, sort_order: 0,
due_date: due_date ? new Date(due_date).toISOString() : null, due_date: due_date ? new Date(due_date).toISOString() : null,
created_at: new Date(now).toISOString(), created_at: new Date(now).toISOString(),
@ -841,7 +845,7 @@ routes.get("/", (c) => {
modules: getModuleInfoList(), modules: getModuleInfoList(),
theme: "dark", theme: "dark",
body: `<folk-tasks-board space="${space}"></folk-tasks-board>`, body: `<folk-tasks-board space="${space}"></folk-tasks-board>`,
scripts: `<script type="module" src="/modules/rtasks/folk-tasks-board.js?v=5"></script>`, scripts: `<script type="module" src="/modules/rtasks/folk-tasks-board.js?v=6"></script>`,
styles: `<link rel="stylesheet" href="/modules/rtasks/tasks.css">`, styles: `<link rel="stylesheet" href="/modules/rtasks/tasks.css">`,
})); }));
}); });