Files
vibn-frontend/app/api/workspaces/route.ts
Mark Henderson b9511601bc feat(ai-access): per-workspace Gitea bot + tenant-safe Coolify proxy + MCP
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.

Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
  columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
  createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
  a Writers team (write perms only) on the workspace's org, and stores
  its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
  bot PAT + clone URL template; session or vibn_sk_ bearer auth.

Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
  getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
  GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
  GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
  keys, exposing workspace.describe, gitea.credentials, projects.*,
  apps.* (list/get/deploy/deployments, envs.list/upsert/delete).

Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
  .cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
  Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
  one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
  fallback. Minted-key modal also shows the bootstrap one-liner.

Ops prerequisites:
  - Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
  - Run /api/admin/migrate to add the three bot columns.
  - GITEA_API_TOKEN must be a site-admin token (needed for admin/users
    + Sudo PAT mint); otherwise provision_status lands on 'partial'.

Made-with: Cursor
2026-04-21 10:49:17 -07:00

70 lines
2.4 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 } 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);
}
}
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,
};
}