Skip to content

Commit

Permalink
SendGrid Events
Browse files Browse the repository at this point in the history
* Generalise the event source for both Delivery and Engagement events
* Add webhook signature verification
  • Loading branch information
jverce authored Dec 12, 2020
1 parent a5ff739 commit 3b9ab93
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 109 deletions.
21 changes: 21 additions & 0 deletions components/sendgrid/sendgrid.app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ module.exports = {
const baseUrl = this._apiUrl();
return `${baseUrl}/user/webhooks/event/settings`;
},
_setSignedWebhookUrl() {
const baseUrl = this._webhookSettingsUrl();
return `${baseUrl}/signed`;
},
_makeRequestConfig() {
const authToken = this._authToken();
const headers = {
Expand Down Expand Up @@ -87,5 +91,22 @@ module.exports = {
() => axios.patch(url, webhookSettings, requestConfig),
);
},
async _setSignedWebhook(enabled) {
const url = this._setSignedWebhookUrl();
const requestData = {
enabled,
};
const requestConfig = this._makeRequestConfig();
return this._withRetries(
() => axios.patch(url, requestData, requestConfig),
);
},
async enableSignedWebhook() {
const { data } = await this._setSignedWebhook(true);
return data.public_key;
},
async disableSignedWebhook() {
return this._setSignedWebhook(false);
},
},
};
72 changes: 36 additions & 36 deletions components/sendgrid/sources/common/http-based.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
const {
EventWebhook,
EventWebhookHeader,
} = require('@sendgrid/eventwebhook');
const base = require("./base");

module.exports = {
...base,
props: {
...base.props,
http: "$.interface.http",
http: {
type: "$.interface.http",
customResponse: true,
},
},
hooks: {
...base.hooks,
Expand All @@ -19,60 +26,53 @@ module.exports = {
}

const newWebhookSettings = {
...this._baseWebhookSettings(),
...this.baseWebhookSettings(),
...this.webhookEventFlags(),
enabled: true,
url: endpointUrl,
};
await this.sendgrid.setWebhookSettings(newWebhookSettings);

const webhookPublicKey = await this.sendgrid.enableSignedWebhook();
this.db.set("webhookPublicKey", webhookPublicKey);
},
async deactivate() {
const webhookSettings = {
...this._baseWebhookSettings(),
...this.baseWebhookSettings(),
enabled: false,
url: null,
};
await this.sendgrid.setWebhookSettings(webhookSettings);
await this.sendgrid.disableSignedWebhook();
},
},
methods: {
...base.methods,
_baseWebhookSettings() {
// The list of events that a webhook can listen to. This method returns an
// exhaustive list of all such flags disabled, and each event source can
// then override the flags that are relevant to the event they handle.
//
// See the docs for more information:
// https://sendgrid.com/docs/api-reference/
return {
group_resubscribe: false,
delivered: false,
group_unsubscribe: false,
spam_report: false,
bounce: false,
deferred: false,
unsubscribe: false,
processed: false,
open: false,
click: false,
dropped: false,
};
},
webhookEventFlags() {
throw new Error('webhookEventFlags is not implemented');
},
generateMeta(data) {
_isValidSource(event) {
const {
sg_event_id: id,
timestamp: ts,
} = data;
return {
id,
summary: "New event",
ts,
};
[EventWebhookHeader.SIGNATURE().toLowerCase()]: signature,
[EventWebhookHeader.TIMESTAMP().toLowerCase()]: timestamp,
} = event.headers;
const { bodyRaw: payload } = event;
const webhookPublicKey = this.db.get("webhookPublicKey");

const webhookHelper = new EventWebhook();
const ecdsaPublicKey = webhookHelper.convertPublicKeyToECDSA(webhookPublicKey);
return webhookHelper.verifySignature(ecdsaPublicKey, payload, signature, timestamp);
},
processEvent(event) {
if (!this._isValidSource(event)) {
this.http.respond({
status: 400,
body: "Signature check failed",
});
return;
}

this.http.respond({
status: 200,
});

const { body: events } = event;
events.forEach(e => {
const meta = this.generateMeta(e);
Expand Down

This file was deleted.

51 changes: 0 additions & 51 deletions components/sendgrid/sources/delivery-events/delivery-events.js

This file was deleted.

22 changes: 22 additions & 0 deletions components/sendgrid/sources/events/delivery-event-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = [
{
label: "Processed (Delivery Event)",
value: "processed"
},
{
label: "Dropped (Delivery Event)",
value: "dropped"
},
{
label: "Delivered (Delivery Event)",
value: "delivered"
},
{
label: "Deferred (Delivery Event)",
value: "deferred"
},
{
label: "Bounce (Delivery Event)",
value: "bounce"
}
];
26 changes: 26 additions & 0 deletions components/sendgrid/sources/events/engagement-event-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = [
{
label: "Open (Engagement Event)",
value: "open"
},
{
label: "Click (Engagement Event)",
value: "click"
},
{
label: "Spam Report (Engagement Event)",
value: "spam_report"
},
{
label: "Unsubscribe (Engagement Event)",
value: "unsubscribe"
},
{
label: "Group Unsubscribe (Engagement Event)",
value: "group_unsubscribe"
},
{
label: "Group Resubscribe (Engagement Event)",
value: "group_resubscribe"
}
];
72 changes: 72 additions & 0 deletions components/sendgrid/sources/events/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const common = require("../common/http-based");

module.exports = {
...common,
key: "sendgrid-events",
name: "Events (Instant)",
description: "Emit an event when any of the specified SendGrid events is received",
version: "0.0.1",
dedupe: "unique",
props: {
...common.props,
eventTypes: {
type: "string[]",
label: "Event Types",
description: "The type of events to listen to",
options(context) {
const { page } = context;
if (page !== 0) {
return {
options: [],
};
}

const options = [
...require('./delivery-event-types'),
...require('./engagement-event-types'),
];
return {
options,
};
}
},
},
methods: {
...common.methods,
baseWebhookSettings() {
// The list of events that a webhook can listen to. This method returns an
// exhaustive list of all such flags disabled, and each event source can
// then override the flags that are relevant to the event they handle.
//
// See the docs for more information:
// https://sendgrid.com/docs/api-reference/
const eventTypesData = [
...require('./delivery-event-types'),
...require('./engagement-event-types'),
];
return eventTypesData.reduce((accum, eventTypeData) => ({
...accum,
[eventTypeData.value]: false,
}), {});
},
webhookEventFlags() {
return this.eventTypes.reduce((accum, eventType) => ({
...accum,
[eventType]: true,
}), {});
},
generateMeta(data) {
const {
event: eventType,
sg_event_id: id,
timestamp: ts,
} = data;
const summary = `New event: ${eventType}`;
return {
id,
summary,
ts,
};
},
},
};

0 comments on commit 3b9ab93

Please sign in to comment.