From 02804535705761c54806cbf36559f3d098dce62c Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 10 Jan 2026 10:23:20 -0500 Subject: [PATCH] fix: block merging personal workspaces with company organizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Personal workspaces represent individual users and should never be merged with company organizations. This adds defense-in-depth: - Block the merge in mergeOrganizations() with a clear error message - Show prominent warnings in previewMerge() if personal workspaces are incorrectly included in merge candidates - Return 400 (not 500) for validation errors in the merge endpoint This prevents accidental merges even if upstream duplicate detection fails to filter personal workspaces. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/block-personal-workspace-merge.md | 2 ++ server/src/db/org-merge-db.ts | 19 ++++++++++++++++--- server/src/routes/admin/cleanup.ts | 9 +++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .changeset/block-personal-workspace-merge.md diff --git a/.changeset/block-personal-workspace-merge.md b/.changeset/block-personal-workspace-merge.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/block-personal-workspace-merge.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/src/db/org-merge-db.ts b/server/src/db/org-merge-db.ts index 93d9737385..4d891d8377 100644 --- a/server/src/db/org-merge-db.ts +++ b/server/src/db/org-merge-db.ts @@ -68,7 +68,7 @@ export async function mergeOrganizations( // Validate both organizations exist and fetch all needed fields const orgsResult = await client.query( - `SELECT workos_organization_id, name, prospect_notes, + `SELECT workos_organization_id, name, is_personal, prospect_notes, enrichment_at, enrichment_industry, enrichment_sub_industry, enrichment_employee_count, enrichment_revenue, enrichment_revenue_range, enrichment_country, enrichment_city, enrichment_description @@ -88,6 +88,11 @@ export async function mergeOrganizations( throw new Error('Could not load organization details'); } + // Block merging personal workspaces + if (primaryOrg.is_personal || secondaryOrg.is_personal) { + throw new Error('Cannot merge personal workspaces. Personal workspaces represent individual users and should not be merged with company organizations.'); + } + logger.info( { primary: primaryOrg.name, secondary: secondaryOrg.name }, 'Merging organizations' @@ -662,9 +667,9 @@ export async function previewMerge( }> { const pool = getPool(); - // Get organization names + // Get organization names and personal workspace status const orgsResult = await pool.query( - `SELECT workos_organization_id, name FROM organizations + `SELECT workos_organization_id, name, is_personal FROM organizations WHERE workos_organization_id = ANY($1)`, [[primaryOrgId, secondaryOrgId]] ); @@ -681,6 +686,14 @@ export async function previewMerge( } const warnings: string[] = []; + + // Warn if either organization is a personal workspace + if (primaryOrg.is_personal) { + warnings.unshift(`🔴 PRIMARY IS PERSONAL WORKSPACE: "${primaryOrg.name}" is a personal workspace and should not be merged with company organizations.`); + } + if (secondaryOrg.is_personal) { + warnings.unshift(`🔴 SECONDARY IS PERSONAL WORKSPACE: "${secondaryOrg.name}" is a personal workspace and should not be merged with company organizations.`); + } const estimatedChanges: { table_name: string; rows_to_move: number }[] = []; // Count rows in each table diff --git a/server/src/routes/admin/cleanup.ts b/server/src/routes/admin/cleanup.ts index f81028d453..24afee2c36 100644 --- a/server/src/routes/admin/cleanup.ts +++ b/server/src/routes/admin/cleanup.ts @@ -307,6 +307,15 @@ export function setupCleanupRoutes(apiRouter: Router): void { res.json(result); } catch (error) { logger.error({ err: error }, "Error executing merge"); + + // Return 400 for validation errors (e.g., personal workspace merge attempts) + if (error instanceof Error && error.message.startsWith('Cannot merge personal workspaces')) { + return res.status(400).json({ + error: "Validation error", + message: error.message, + }); + } + res.status(500).json({ error: "Internal server error", message: