From 6504598752e8ae2a6ac8460001cf6d860f93534b Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Thu, 16 Apr 2026 13:08:23 -0600 Subject: [PATCH] ccp: surface slug-collision as 409, not raw Prisma error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agents.routes.ts approve handler: wrap prisma.instance.create in try/catch. When a PrismaClientKnownRequestError P2002 with target including 'slug' is thrown, convert to a 409 AppError with a clear message directing the admin at DELETE /api/instances/:id or the new scripts/ccp-deregister.sh on the target host. Before this, re-registering a previously-registered host (after the operator tore down the underlying stack without cleaning CCP's DB row) returned 500 with a raw Prisma error string — the operator had to read a stack trace to understand the cause. Now the error is self-describing and points at the fix. Matches the pattern other CCP error paths use. Bunker Admin --- .../api/src/modules/agents/agents.routes.ts | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/changemaker-control-panel/api/src/modules/agents/agents.routes.ts b/changemaker-control-panel/api/src/modules/agents/agents.routes.ts index cbbb640d..e33a0910 100644 --- a/changemaker-control-panel/api/src/modules/agents/agents.routes.ts +++ b/changemaker-control-panel/api/src/modules/agents/agents.routes.ts @@ -150,23 +150,41 @@ router.post('/registrations/:id/approve', authenticate, requireRole('SUPER_ADMIN throw new AppError(400, `Registration is ${registration.status}, not PENDING`); } - // Create the Instance record - const instance = await prisma.instance.create({ - data: { - slug: registration.slug, - name: registration.name, - domain: registration.domain, - status: InstanceStatus.STOPPED, - statusMessage: 'Remote instance registered — agent connecting', - basePath: registration.basePath, - composeProject: registration.composeProject, - portConfig: (registration.metadata as Record)?.portConfig || { api: 4000, admin: 3000, postgres: 5432, nginx: 80 }, - isRegistered: true, - isRemote: true, - agentUrl: registration.agentUrl, - adminEmail: (registration.metadata as Record)?.adminEmail as string || 'admin@example.com', - }, - }); + // Create the Instance record. + // The slug is unique. If a stale Instance with the same slug exists (e.g. the + // operator tore down the underlying stack without running ccp-deregister.sh) + // surface a clean 409 instead of leaking the raw Prisma error. + let instance; + try { + instance = await prisma.instance.create({ + data: { + slug: registration.slug, + name: registration.name, + domain: registration.domain, + status: InstanceStatus.STOPPED, + statusMessage: 'Remote instance registered — agent connecting', + basePath: registration.basePath, + composeProject: registration.composeProject, + portConfig: (registration.metadata as Record)?.portConfig || { api: 4000, admin: 3000, postgres: 5432, nginx: 80 }, + isRegistered: true, + isRemote: true, + agentUrl: registration.agentUrl, + adminEmail: (registration.metadata as Record)?.adminEmail as string || 'admin@example.com', + }, + }); + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') { + const target = (err.meta?.target as string[] | undefined) ?? []; + if (target.includes('slug')) { + throw new AppError( + 409, + `Slug '${registration.slug}' is already in use by another Instance. Delete the stale instance first (DELETE /api/instances/:id) or run scripts/ccp-deregister.sh from the target host.`, + 'SLUG_CONFLICT' + ); + } + } + throw err; + } // Issue mTLS certificates const certMaterials = await issueAgentCert(instance.id, registration.slug, registration.agentUrl);