diff --git a/services/targo-hub/lib/campaigns.js b/services/targo-hub/lib/campaigns.js index fe16e09..ef7d3fb 100644 --- a/services/targo-hub/lib/campaigns.js +++ b/services/targo-hub/lib/campaigns.js @@ -637,38 +637,10 @@ async function handle (req, res, method, path) { 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) ───────────────── + // 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 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 }) } + // ── 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' }) }