From dd4fe2f8682910694993772711c19becb41f7eff Mon Sep 17 00:00:00 2001 From: Saffron Normal Worker Date: Mon, 8 Jun 2026 00:57:30 -0600 Subject: [PATCH 1/2] Normalize browser API auth helpers to use authedFetch Replace all plain fetch() calls in browser UI components with authedFetch() to ensure Basic Auth credentials are included for protected mutating routes. - src/app/automation/page.tsx: Replace 8 fetch() calls with authedFetch() - src/app/automation/repos/[...repo]/page.tsx: Replace 3 fetch() calls with authedFetch() - src/components/issue-card.tsx: Replace 5 fetch() calls with authedFetch() - Add vitest regression tests for client-auth Basic Auth behavior Fixes #311 --- src/app/automation/page.tsx | 17 ++- src/app/automation/repos/[...repo]/page.tsx | 7 +- src/components/issue-card.tsx | 11 +- src/lib/client-auth.test.ts | 161 ++++++++++++++++++++ 4 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 src/lib/client-auth.test.ts diff --git a/src/app/automation/page.tsx b/src/app/automation/page.tsx index cb670d6..f0f62cb 100644 --- a/src/app/automation/page.tsx +++ b/src/app/automation/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { authedFetch } from "@/lib/client-auth"; import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -141,7 +142,7 @@ function RepoCard({ repo, onDelete }: { repo: RepoOverview; onDelete?: () => voi variant="ghost" size="sm" onClick={() => { - fetch(`/api/automation/sync`, { + authedFetch(`/api/automation/sync`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ repo: repo.fullName }), @@ -182,10 +183,10 @@ export default function AutomationOverview() { const [addLoading, setAddLoading] = useState(false); useEffect(() => { - fetch("/api/automation/sync") + authedFetch("/api/automation/sync") .then((res) => res.json()) .catch(() => ({})); - fetch("/api/automation/repos") + authedFetch("/api/automation/repos") .then((res) => res.json()) .then((data) => setRepos(data)) .catch(() => setRepos([])) @@ -195,8 +196,8 @@ export default function AutomationOverview() { async function syncAll() { setSyncing(true); try { - await fetch("/api/automation/sync", { method: "POST" }); - const res = await fetch("/api/automation/repos"); + await authedFetch("/api/automation/sync", { method: "POST" }); + const res = await authedFetch("/api/automation/repos"); const data = await res.json(); setRepos(data); } finally { @@ -209,7 +210,7 @@ export default function AutomationOverview() { setAddLoading(true); setAddError(""); try { - const res = await fetch("/api/automation/repos", { + const res = await authedFetch("/api/automation/repos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fullName: newRepo.trim() }), @@ -221,7 +222,7 @@ export default function AutomationOverview() { } setNewRepo(""); setShowAddForm(false); - const res2 = await fetch("/api/automation/repos"); + const res2 = await authedFetch("/api/automation/repos"); const data2 = await res2.json(); setRepos(data2); } catch { @@ -236,7 +237,7 @@ export default function AutomationOverview() { return; } try { - const res = await fetch(`/api/automation/repos/${encodeURIComponent(fullName)}`, { + const res = await authedFetch(`/api/automation/repos/${encodeURIComponent(fullName)}`, { method: "DELETE", }); if (!res.ok) return; diff --git a/src/app/automation/repos/[...repo]/page.tsx b/src/app/automation/repos/[...repo]/page.tsx index 8d0bebe..e326156 100644 --- a/src/app/automation/repos/[...repo]/page.tsx +++ b/src/app/automation/repos/[...repo]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState, use } from "react"; +import { authedFetch } from "@/lib/client-auth"; import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -107,7 +108,7 @@ export default function RepoDetailPage({ params }: { params: Promise<{ repo: str useEffect(() => { if (!repoFullName) return; const decoded = decodeURIComponent(repoFullName); - fetch(`/api/automation/repos/${decoded}`) + authedFetch(`/api/automation/repos/${decoded}`) .then((res) => { if (!res.ok) throw new Error("Repo not found"); return res.json(); @@ -160,7 +161,7 @@ export default function RepoDetailPage({ params }: { params: Promise<{ repo: str