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);