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/onboarding/route.ts

132 lines
4.1 KiB
TypeScript

import { NextResponse } from "next/server";
import { authSession } from "../../../lib/auth/session-server";
import { query, queryOne } from "../../../lib/db-postgres";
import {
ensureWorkspaceProvisioned,
type VibnWorkspace,
} from "../../../lib/workspaces";
// Generates a URL-safe slug from a business name, ensuring uniqueness in the database.
async function generateUniqueSlug(name: string): Promise<string> {
const base =
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") || "workspace";
let slug = base;
let count = 0;
while (true) {
const existing = await queryOne(
"SELECT id FROM vibn_workspaces WHERE slug = $1 LIMIT 1",
[slug],
);
if (!existing) return slug;
count++;
slug = `${base}-${count}`;
}
}
// POST /api/onboarding
// Saves ALL onboarding choices (Agency or Personal) to the PostgreSQL database,
// creates the workspace/tenant, links the user as owner, and triggers async provisioning.
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 } = payload;
// 1. Resolve User ID from email
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 business name & create unique slug
const businessName = isAgency
? profile?.name || "My Agency"
: data?.bizName || "My Workspace";
const slug = await generateUniqueSlug(businessName);
// 3. Assemble GTM Metadata block to store in JSONB
const onboardingMetadata = isAgency
? {
isAgency: true,
city: profile?.city,
hasWebsite: profile?.hasWebsite,
websiteUrl: profile?.websiteUrl,
hasSocials: profile?.hasSocials,
hasBlog: profile?.hasBlog,
hasCustomDomain: profile?.hasCustomDomain,
hasExistingClients: profile?.hasExistingClients,
expertise,
tools,
}
: {
isAgency: false,
city: data?.bizCity,
websiteUrl: data?.bizWebsite,
bizType: data?.biz,
tools: data?.tools,
theme: data?.theme || "minimal",
template: data?.template || "crm",
buildDesc: data?.buildDesc,
};
// 4. Insert Workspace Row (logical multi-tenancy)
const insertedWorkspaces = await query<VibnWorkspace>(
`INSERT INTO vibn_workspaces (slug, name, owner_user_id, data, provision_status)
VALUES ($1, $2, $3, $4, 'pending')
RETURNING *`,
[
slug,
businessName,
userId,
JSON.stringify({ onboarding: onboardingMetadata }),
],
);
const workspace = insertedWorkspaces[0];
if (!workspace) {
return NextResponse.json(
{ error: "Failed to create workspace" },
{ status: 500 },
);
}
// 5. Insert Workspace Member Row (link user as Owner)
await query(
`INSERT INTO vibn_workspace_members (workspace_id, user_id, role)
VALUES ($1, $2, 'owner')`,
[workspace.id, userId],
);
// 6. Trigger Async Tenant Provisioning (Coolify Project boundaries + Gitea org)
// Runs in the background so the user's isolated fleet stands up instantly.
try {
ensureWorkspaceProvisioned(workspace).catch((err: unknown) => {
console.error("Background workspace provisioning failed:", err);
});
} catch (e) {
console.error("Failed to kick off provisioning:", e);
}
// Return the workspace slug so the frontend can redirect they immediately!
return NextResponse.json({ success: true, slug });
} catch (err) {
console.error("Onboarding GTM save exception:", err);
return NextResponse.json(
{ error: "Internal GTM server error" },
{ status: 500 },
);
}
}