/** * GET /api/workspaces — list workspaces the caller can access * * Auth: * - NextAuth session: returns the user's owned + member workspaces * - vibn_sk_... API key: returns just the one workspace the key is bound to */ import { NextResponse } from "next/server"; import { authSession } from "@/lib/auth/session-server"; import { queryOne } from "@/lib/db-postgres"; import { ensureWorkspaceForUser, listWorkspacesForUser, } from "@/lib/workspaces"; import { requireWorkspacePrincipal, listWorkspaceApiKeys, mintWorkspaceApiKey, revealWorkspaceApiKey, } from "@/lib/auth/workspace-auth"; export async function GET(request: Request) { if ( request.headers .get("authorization") ?.toLowerCase() .startsWith("bearer vibn_sk_") ) { const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; return NextResponse.json({ workspaces: [serializeWorkspace(principal.workspace)], }); } const session = await authSession(); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const userRow = await queryOne<{ id: string }>( `SELECT id FROM fs_users WHERE data->>'email' = $1 LIMIT 1`, [session.user.email], ); if (!userRow) { return NextResponse.json({ workspaces: [] }); } // 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); } } const url = new URL(request.url); const includeDefaultToken = url.searchParams.get("include_default_token") === "true"; if (includeDefaultToken && list.length > 0) { const ws = list[0]; let defaultToken: string | null = null; try { const keys = await listWorkspaceApiKeys(ws.id); let defaultKey = keys.find( (k: any) => k.name === "default" && !k.revoked_at, ); if (!defaultKey) { const minted = await mintWorkspaceApiKey({ workspaceId: ws.id, name: "default", createdBy: userRow!.id, scopes: ["workspace:*"], }); defaultToken = minted.token; } else { const revealed = await revealWorkspaceApiKey(ws.id, defaultKey.id); if (revealed) { defaultToken = revealed.token; } else { const minted = await mintWorkspaceApiKey({ workspaceId: ws.id, name: "default", createdBy: userRow!.id, scopes: ["workspace:*"], }); defaultToken = minted.token; } } } catch { /* non-fatal */ } return NextResponse.json({ workspaces: list.map(serializeWorkspace), defaultToken, }); } return NextResponse.json({ workspaces: list.map(serializeWorkspace) }); } function serializeWorkspace(w: import("@/lib/workspaces").VibnWorkspace) { return { id: w.id, slug: w.slug, name: w.name, coolifyProjectUuid: w.coolify_project_uuid, giteaOrg: w.gitea_org, giteaBotUsername: w.gitea_bot_username, giteaBotReady: !!(w.gitea_bot_username && w.gitea_bot_token_encrypted), provisionStatus: w.provision_status, provisionError: w.provision_error, createdAt: w.created_at, updatedAt: w.updated_at, }; }