diff --git a/app/api/workspaces/route.ts b/app/api/workspaces/route.ts index 323eb0f..62c37c5 100644 --- a/app/api/workspaces/route.ts +++ b/app/api/workspaces/route.ts @@ -9,11 +9,10 @@ import { NextResponse } from 'next/server'; import { authSession } from '@/lib/auth/session-server'; import { queryOne } from '@/lib/db-postgres'; -import { listWorkspacesForUser } from '@/lib/workspaces'; +import { ensureWorkspaceForUser, listWorkspacesForUser } from '@/lib/workspaces'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; export async function GET(request: Request) { - // API-key clients are pinned to one workspace if (request.headers.get('authorization')?.toLowerCase().startsWith('bearer vibn_sk_')) { const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; @@ -33,7 +32,23 @@ export async function GET(request: Request) { return NextResponse.json({ workspaces: [] }); } - const list = await listWorkspacesForUser(userRow.id); + // Migration path: users who signed in before the signIn hook was + // added (or before vibn_workspaces existed) have no row yet. Create + // one on first list so the UI never shows an empty state for them. + let list = await listWorkspacesForUser(userRow.id); + if (list.length === 0) { + try { + await ensureWorkspaceForUser({ + userId: userRow.id, + email: session.user.email, + displayName: session.user.name ?? null, + }); + list = await listWorkspacesForUser(userRow.id); + } catch (err) { + console.error('[api/workspaces] lazy ensure failed', err); + } + } + return NextResponse.json({ workspaces: list.map(serializeWorkspace) }); } diff --git a/components/workspace/WorkspaceKeysPanel.tsx b/components/workspace/WorkspaceKeysPanel.tsx index dfd6c9b..b8a7855 100644 --- a/components/workspace/WorkspaceKeysPanel.tsx +++ b/components/workspace/WorkspaceKeysPanel.tsx @@ -68,10 +68,18 @@ interface MintedKey { const APP_BASE = typeof window !== "undefined" ? window.location.origin : "https://vibnai.com"; -export function WorkspaceKeysPanel({ workspaceSlug }: { workspaceSlug: string }) { +/** + * The URL param (e.g. `/mark-account/settings`) is treated as a hint — + * the actual workspace is resolved via `GET /api/workspaces`, which is + * keyed off the signed-in user. That endpoint also lazy-creates the + * workspace row for users who signed in before the workspace hook + * landed, so this panel never gets stuck on a missing row. + */ +export function WorkspaceKeysPanel({ workspaceSlug: _urlHint }: { workspaceSlug?: string }) { const [workspace, setWorkspace] = useState(null); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); + const [errorMsg, setErrorMsg] = useState(null); const [provisioning, setProvisioning] = useState(false); const [showCreate, setShowCreate] = useState(false); @@ -84,31 +92,50 @@ export function WorkspaceKeysPanel({ workspaceSlug }: { workspaceSlug: string }) const refresh = useCallback(async () => { setLoading(true); + setErrorMsg(null); try { - const [wsRes, keysRes] = await Promise.all([ - fetch(`/api/workspaces/${workspaceSlug}`), - fetch(`/api/workspaces/${workspaceSlug}/keys`), - ]); - if (wsRes.ok) setWorkspace(await wsRes.json()); + const listRes = await fetch(`/api/workspaces`, { credentials: "include" }); + if (!listRes.ok) { + const body = listRes.status === 401 + ? "Sign in to view your workspace." + : `Couldn't load workspace (HTTP ${listRes.status}).`; + setErrorMsg(body); + setWorkspace(null); + setKeys([]); + return; + } + const list = (await listRes.json()) as { workspaces: WorkspaceSummary[] }; + const ws = list.workspaces?.[0] ?? null; + setWorkspace(ws); + if (!ws) { + setKeys([]); + return; + } + const keysRes = await fetch(`/api/workspaces/${ws.slug}/keys`, { credentials: "include" }); if (keysRes.ok) { const j = (await keysRes.json()) as { keys: ApiKey[] }; setKeys(j.keys ?? []); } } catch (err) { console.error("[workspace-keys] refresh failed", err); + setErrorMsg(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } - }, [workspaceSlug]); + }, []); useEffect(() => { refresh(); }, [refresh]); const provision = useCallback(async () => { + if (!workspace) return; setProvisioning(true); try { - const res = await fetch(`/api/workspaces/${workspaceSlug}/provision`, { method: "POST" }); + const res = await fetch(`/api/workspaces/${workspace.slug}/provision`, { + method: "POST", + credentials: "include", + }); if (!res.ok) throw new Error(await res.text()); toast.success("Provisioning re-run"); await refresh(); @@ -117,17 +144,19 @@ export function WorkspaceKeysPanel({ workspaceSlug }: { workspaceSlug: string }) } finally { setProvisioning(false); } - }, [workspaceSlug, refresh]); + }, [workspace, refresh]); const createKey = useCallback(async () => { + if (!workspace) return; if (!newName.trim()) { toast.error("Give the key a name"); return; } setCreating(true); try { - const res = await fetch(`/api/workspaces/${workspaceSlug}/keys`, { + const res = await fetch(`/api/workspaces/${workspace.slug}/keys`, { method: "POST", + credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newName.trim() }), }); @@ -142,15 +171,15 @@ export function WorkspaceKeysPanel({ workspaceSlug }: { workspaceSlug: string }) } finally { setCreating(false); } - }, [workspaceSlug, newName, refresh]); + }, [workspace, newName, refresh]); const revokeKey = useCallback(async () => { - if (!keyToRevoke) return; + if (!workspace || !keyToRevoke) return; setRevoking(true); try { const res = await fetch( - `/api/workspaces/${workspaceSlug}/keys/${keyToRevoke.id}`, - { method: "DELETE" } + `/api/workspaces/${workspace.slug}/keys/${keyToRevoke.id}`, + { method: "DELETE", credentials: "include" } ); if (!res.ok) throw new Error(await res.text()); toast.success("Key revoked"); @@ -161,21 +190,39 @@ export function WorkspaceKeysPanel({ workspaceSlug }: { workspaceSlug: string }) } finally { setRevoking(false); } - }, [workspaceSlug, keyToRevoke, refresh]); + }, [workspace, keyToRevoke, refresh]); if (loading && !workspace) { return ( -
- Loading workspace… -
+
+
+ Loading workspace… +
+
); } if (!workspace) { return ( -
- Workspace not found. -
+
+
+
+

Workspace

+

+ {errorMsg ?? "No workspace yet — this usually means you signed in before AI access was rolled out."} +

+
+ +
+

+ Try signing out and back in, then refresh this page. If the problem + persists, the API may be down or your account may need to be + provisioned manually. +

+
); }