165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
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<string> {
|
|
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<VibnWorkspace>(
|
|
`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<VibnWorkspace>(
|
|
`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 },
|
|
);
|
|
}
|
|
}
|