import { NextResponse } from "next/server"; import { authSession } from "../../../lib/auth/session-server"; import { query, queryOne } from "../../../lib/db-postgres"; import { ensureWorkspaceProvisioned, getWorkspaceByOwner, type VibnWorkspace, } from "../../../lib/workspaces"; // URL-safe, unique slug from a name. Optionally exclude a workspace id so a // workspace can keep/rename to a slug it already owns. async function generateUniqueSlug( name: string, excludeWorkspaceId?: string, ): Promise { const base = name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "workspace"; let slug = base; let count = 0; while (true) { const existing = excludeWorkspaceId ? await queryOne( "SELECT id FROM vibn_workspaces WHERE slug = $1 AND id <> $2 LIMIT 1", [slug, excludeWorkspaceId], ) : await queryOne( "SELECT id FROM vibn_workspaces WHERE slug = $1 LIMIT 1", [slug], ); if (!existing) return slug; count++; slug = `${base}-${count}`; } } // POST /api/onboarding // Finalises onboarding. Every signed-in user already has a workspace (created // lazily at sign-in by ensureWorkspaceForUser), so this RENAMES that workspace // to the name chosen in onboarding rather than creating a second one. Onboarding // answers are stashed on the user's fs_users row. Returns the workspace slug so // the client can redirect straight into it. export async function POST(request: Request) { const session = await authSession(); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { const payload = await request.json(); const { isAgency, profile, expertise, tools, data, workspaceName } = payload; // 1. Resolve the user (fs_users.id is the workspace owner id). 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({ error: "User not found" }, { status: 404 }); } const userId = userRow.id; // 2. Determine the workspace name. An explicit name from the // "Name your workspace" step wins; otherwise fall back to what the flow // already collected. const explicitName = typeof workspaceName === "string" ? workspaceName.trim() : ""; const businessName = explicitName || (isAgency ? profile?.name : data?.bizName) || "My Workspace"; // 3. Stash onboarding answers on the user (vibn_workspaces has no `data` // column; fs_users does). Non-fatal. // Persist EVERYTHING the flow collected (the full raw payload), not a // curated subset, so nothing the user chose is lost. const onboardingMetadata = { isAgency: !!isAgency, workspaceName: businessName, completedAt: new Date().toISOString(), ...(isAgency ? { profile: profile ?? null, expertise: expertise ?? null, tools: tools ?? null, } : { data: data ?? null }), }; try { await query( "UPDATE fs_users SET data = data || $2::jsonb, updated_at = NOW() WHERE id = $1", [ userId, JSON.stringify({ onboardingComplete: true, onboarding: onboardingMetadata, }), ], ); } catch (metaErr) { console.error("Onboarding metadata save failed (non-fatal):", metaErr); } // 4. Rename the user's existing workspace, or create one if (unexpectedly) // none exists yet. let workspace: VibnWorkspace | undefined; const existing = await getWorkspaceByOwner(userId); if (existing) { const slug = await generateUniqueSlug(businessName, existing.id); const rows = await query( `UPDATE vibn_workspaces SET name = $2, slug = $3, updated_at = NOW() WHERE id = $1 RETURNING *`, [existing.id, businessName, slug], ); workspace = rows[0]; } else { const slug = await generateUniqueSlug(businessName); const rows = await query( `INSERT INTO vibn_workspaces (slug, name, owner_user_id) VALUES ($1, $2, $3) RETURNING *`, [slug, businessName, userId], ); workspace = rows[0]; await query( `INSERT INTO vibn_workspace_members (workspace_id, user_id, role) VALUES ($1, $2, 'owner') ON CONFLICT (workspace_id, user_id) DO NOTHING`, [workspace.id, userId], ); } if (!workspace) { return NextResponse.json( { error: "Failed to create workspace" }, { status: 500 }, ); } // 5. Kick off provisioning in the background (Coolify project + Gitea org). try { ensureWorkspaceProvisioned(workspace).catch((err: unknown) => { console.error("Background workspace provisioning failed:", err); }); } catch (e) { console.error("Failed to kick off provisioning:", e); } return NextResponse.json({ success: true, slug: workspace.slug }); } catch (err) { console.error("Onboarding GTM save exception:", err); return NextResponse.json( { error: "Internal GTM server error" }, { status: 500 }, ); } }