This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
master-ai/vibn-frontend/app/api/workspaces/route.ts

128 lines
3.7 KiB
TypeScript

/**
* 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,
};
}