128 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|