132 lines
4.1 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|