fix(hub/campaigns): move /templates routes above the /:id wildcard
The /campaigns/:id GET handler uses a wildcard regex /^\/campaigns\/([^/]+)$/ which captures "templates" as a fake campaign id and returns 404 before the fixed /campaigns/templates routes get a chance to match. Reorder the handle() chain so the fixed paths (/templates, /webhook) come first, then the wildcard :id routes. Add a comment block calling out the ordering requirement so future endpoints don't reintroduce the bug. Verified live: GET /campaigns/templates returns the editable list, GET /campaigns/templates/gift-email-fr still returns the HTML. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
611f4ed5a6
commit
0f78fbe27e
|
|
@ -637,38 +637,10 @@ async function handle (req, res, method, path) {
|
||||||
return json(res, 200, { campaigns: listCampaigns() })
|
return json(res, 200, { campaigns: listCampaigns() })
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /campaigns/:id — full detail
|
|
||||||
const detailMatch = path.match(/^\/campaigns\/([^/]+)$/)
|
|
||||||
if (detailMatch && method === 'GET') {
|
|
||||||
const c = loadCampaign(detailMatch[1])
|
|
||||||
if (!c) return json(res, 404, { error: 'not found' })
|
|
||||||
return json(res, 200, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PATCH /campaigns/:id — update recipients (e.g. exclude rows, edit email)
|
|
||||||
if (detailMatch && method === 'PATCH') {
|
|
||||||
const body = await parseBody(req)
|
|
||||||
const c = loadCampaign(detailMatch[1])
|
|
||||||
if (!c) return json(res, 404, { error: 'not found' })
|
|
||||||
if (body.name) c.name = body.name
|
|
||||||
if (body.params) c.params = { ...c.params, ...body.params }
|
|
||||||
if (Array.isArray(body.recipients)) c.recipients = body.recipients
|
|
||||||
return json(res, 200, saveCampaign(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /campaigns/:id/send — fire background worker
|
|
||||||
const sendMatch = path.match(/^\/campaigns\/([^/]+)\/send$/)
|
|
||||||
if (sendMatch && method === 'POST') {
|
|
||||||
const id = sendMatch[1]
|
|
||||||
const c = loadCampaign(id)
|
|
||||||
if (!c) return json(res, 404, { error: 'not found' })
|
|
||||||
if (activeWorkers.has(id)) return json(res, 409, { error: 'already sending' })
|
|
||||||
// Fire and forget
|
|
||||||
setImmediate(() => sendCampaignAsync(id))
|
|
||||||
return json(res, 202, { id, status: 'sending' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Template CRUD (for the GrapesJS editor in the ops UI) ─────────────────
|
// ── Template CRUD (for the GrapesJS editor in the ops UI) ─────────────────
|
||||||
|
// ORDER MATTERS: these template routes must be BEFORE the /campaigns/:id
|
||||||
|
// wildcard below, otherwise paths like /campaigns/templates get matched
|
||||||
|
// by the wildcard as if "templates" were a campaign ID.
|
||||||
|
|
||||||
// GET /campaigns/templates — list editable templates with metadata
|
// GET /campaigns/templates — list editable templates with metadata
|
||||||
if (path === '/campaigns/templates' && method === 'GET') {
|
if (path === '/campaigns/templates' && method === 'GET') {
|
||||||
|
|
@ -739,6 +711,40 @@ async function handle (req, res, method, path) {
|
||||||
return json(res, 200, { received: events.length, applied })
|
return json(res, 200, { received: events.length, applied })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Per-campaign wildcard routes (MUST stay below the /templates and
|
||||||
|
// /webhook fixed paths above, otherwise the wildcard captures them) ─────
|
||||||
|
|
||||||
|
// GET /campaigns/:id — full detail
|
||||||
|
const detailMatch = path.match(/^\/campaigns\/([^/]+)$/)
|
||||||
|
if (detailMatch && method === 'GET') {
|
||||||
|
const c = loadCampaign(detailMatch[1])
|
||||||
|
if (!c) return json(res, 404, { error: 'not found' })
|
||||||
|
return json(res, 200, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /campaigns/:id — update recipients (e.g. exclude rows, edit email)
|
||||||
|
if (detailMatch && method === 'PATCH') {
|
||||||
|
const body = await parseBody(req)
|
||||||
|
const c = loadCampaign(detailMatch[1])
|
||||||
|
if (!c) return json(res, 404, { error: 'not found' })
|
||||||
|
if (body.name) c.name = body.name
|
||||||
|
if (body.params) c.params = { ...c.params, ...body.params }
|
||||||
|
if (Array.isArray(body.recipients)) c.recipients = body.recipients
|
||||||
|
return json(res, 200, saveCampaign(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /campaigns/:id/send — fire background worker
|
||||||
|
const sendMatch = path.match(/^\/campaigns\/([^/]+)\/send$/)
|
||||||
|
if (sendMatch && method === 'POST') {
|
||||||
|
const id = sendMatch[1]
|
||||||
|
const c = loadCampaign(id)
|
||||||
|
if (!c) return json(res, 404, { error: 'not found' })
|
||||||
|
if (activeWorkers.has(id)) return json(res, 409, { error: 'already sending' })
|
||||||
|
// Fire and forget
|
||||||
|
setImmediate(() => sendCampaignAsync(id))
|
||||||
|
return json(res, 202, { id, status: 'sending' })
|
||||||
|
}
|
||||||
|
|
||||||
return json(res, 404, { error: 'campaigns endpoint not found' })
|
return json(res, 404, { error: 'campaigns endpoint not found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user