feat(webhooks): enrich event.* payloads with customer contact + share_token (#341)
The event.published webhook reporter wired into n8n to send WhatsApp
gallery links was missing the data needed to actually message the
customer — only event_name + share_url were in the payload, no
customer_name / customer_email / customer_phone, and no bare share
token to construct alternate URLs.
Adds a single canonical event subject helper (webhookService.buildEventSubject)
so every event.* webhook returns the same shape:
{ id, slug, event_name, event_type, event_date,
share_url, share_token,
customer_name, customer_email, customer_phone }
Fields the caller does not have in scope come back as null — keys are
always present so receivers do not have to distinguish "field missing"
from "field null". Pure addition: existing receivers continue to work,
existing templates ${data.event.event_name} keep working, and new
templates can now reference ${data.event.customer_phone} etc.
Wired into all five firing sites:
- routes/events.js — public event create (created + published)
- routes/adminEvents.js — admin create + draft→publish
- routes/v1/events.js — public v1 API (created + published)
- services/expirationChecker.js — event.expired (extra: expires_at)
- services/archiveService.js — event.archived (extra: archive_path)
PII surface area widens (customer email/phone now flow to webhook
receivers), so:
- Settings → Webhooks UI gets an amber Callout above the create form
warning admins to only point webhooks at receivers they trust.
- Docs page updated with the new payload sample, the always-present
null contract, and a Callout warning.
Verified end-to-end against the local dev webhook receiver — delivered
payload contains all 10 fields. webhookDelivery integration suite
remains 8/8 green.
This commit is contained in:
@@ -610,10 +610,26 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
|
||||
// Fire event.created webhook (#327). If the event is being published
|
||||
// immediately (not a draft), event.published also fires below.
|
||||
// Payload uses canonical event subject (#341) so receivers always see
|
||||
// the same shape (id/slug/event_name + customer contact + share_*).
|
||||
try {
|
||||
const webhookService = require('../services/webhookService');
|
||||
await webhookService.fire('event.created', {
|
||||
event: { id: eventId, slug, event_name, event_type, event_date, is_draft: parseBooleanInput(is_draft, true) },
|
||||
event: {
|
||||
...webhookService.buildEventSubject({
|
||||
id: eventId,
|
||||
slug,
|
||||
event_name,
|
||||
event_type,
|
||||
event_date,
|
||||
share_url: shareUrl,
|
||||
share_token: shareToken,
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
customer_phone: customerPhone,
|
||||
}),
|
||||
is_draft: parseBooleanInput(is_draft, true),
|
||||
},
|
||||
});
|
||||
} catch (e) { /* webhookService.fire never throws but be defensive */ }
|
||||
|
||||
@@ -661,7 +677,18 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
||||
try {
|
||||
const webhookService = require('../services/webhookService');
|
||||
await webhookService.fire('event.published', {
|
||||
event: { id: eventId, slug, event_name, share_url: shareUrl },
|
||||
event: webhookService.buildEventSubject({
|
||||
id: eventId,
|
||||
slug,
|
||||
event_name,
|
||||
event_type,
|
||||
event_date,
|
||||
share_url: shareUrl,
|
||||
share_token: shareToken,
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
customer_phone: customerPhone,
|
||||
}),
|
||||
});
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
@@ -902,11 +929,23 @@ router.post('/:id/publish', adminAuth, requirePermission('events.edit'), require
|
||||
);
|
||||
|
||||
// Fire event.published webhook (#327) — draft → live transition.
|
||||
// Canonical payload (#341): includes customer contact + share_token.
|
||||
try {
|
||||
const webhookService = require('../services/webhookService');
|
||||
const { shareUrl } = await buildShareLinkVariants({ slug: event.slug, shareToken: event.share_token });
|
||||
await webhookService.fire('event.published', {
|
||||
event: { id: parseInt(id, 10), slug: event.slug, event_name: event.event_name, share_url: shareUrl },
|
||||
event: webhookService.buildEventSubject({
|
||||
id: parseInt(id, 10),
|
||||
slug: event.slug,
|
||||
event_name: event.event_name,
|
||||
event_type: event.event_type,
|
||||
event_date: event.event_date,
|
||||
share_url: shareUrl,
|
||||
share_token: event.share_token,
|
||||
customer_name: event.customer_name || event.host_name,
|
||||
customer_email: event.customer_email || event.host_email,
|
||||
customer_phone: event.customer_phone,
|
||||
}),
|
||||
});
|
||||
} catch (e) { /* non-fatal */ }
|
||||
|
||||
|
||||
@@ -191,15 +191,24 @@ router.post('/', adminAuth, [
|
||||
});
|
||||
|
||||
// Webhook lifecycle (#327). Legacy public endpoint — events go live
|
||||
// immediately so created + published fire together.
|
||||
// immediately so created + published fire together. Payload uses the
|
||||
// canonical event subject (#341) — every event.* webhook now includes
|
||||
// customer contact + share_token.
|
||||
try {
|
||||
const webhookService = require('../services/webhookService');
|
||||
await webhookService.fire('event.created', {
|
||||
event: { id: eventId, slug, event_name, event_type, event_date, share_url: shareUrl },
|
||||
});
|
||||
await webhookService.fire('event.published', {
|
||||
event: { id: eventId, slug, event_name, share_url: shareUrl },
|
||||
const eventSubject = webhookService.buildEventSubject({
|
||||
id: eventId,
|
||||
slug,
|
||||
event_name,
|
||||
event_type,
|
||||
event_date,
|
||||
share_url: shareUrl,
|
||||
share_token: shareToken,
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
});
|
||||
await webhookService.fire('event.created', { event: eventSubject });
|
||||
await webhookService.fire('event.published', { event: eventSubject });
|
||||
} catch (e) { /* non-fatal */ }
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -183,15 +183,24 @@ router.post(
|
||||
});
|
||||
|
||||
// Webhook lifecycle (#327). v1 events are not draft-aware, so they're
|
||||
// both created AND published in the same call.
|
||||
// both created AND published in the same call. Canonical event
|
||||
// subject (#341) — customer contact + share_token always included.
|
||||
try {
|
||||
const webhookService = require('../../services/webhookService');
|
||||
await webhookService.fire('event.created', {
|
||||
event: { id, slug, event_name, event_type, event_date, share_url: shareUrl },
|
||||
});
|
||||
await webhookService.fire('event.published', {
|
||||
event: { id, slug, event_name, share_url: shareUrl },
|
||||
const eventSubject = webhookService.buildEventSubject({
|
||||
id,
|
||||
slug,
|
||||
event_name,
|
||||
event_type,
|
||||
event_date,
|
||||
share_url: shareUrl,
|
||||
share_token: shareToken,
|
||||
customer_name,
|
||||
customer_email,
|
||||
customer_phone,
|
||||
});
|
||||
await webhookService.fire('event.created', { event: eventSubject });
|
||||
await webhookService.fire('event.published', { event: eventSubject });
|
||||
} catch (e) { /* non-fatal */ }
|
||||
|
||||
res.status(201).json({ id, slug, share_url: shareUrl, share_token: shareToken });
|
||||
|
||||
Reference in New Issue
Block a user