Refactored webhook creation (#175)

no-issue

* Refactored model dependencies
  This groups all of the model depenencies into a single models object,
  and renames the models with more concise identifiers

* Fixed spacing
* Added webhook support to metadata
* Refactored stripe configure to have better logging
* Refactored webhook creation to reuse existing webhook
* Installed @types/stripe
This commit is contained in:
Fabien 'egg' O'Carroll 2020-07-09 16:40:48 +02:00 committed by GitHub
parent 68dbfb707d
commit ac923af0f7
5 changed files with 210 additions and 145 deletions

View File

@ -27,9 +27,12 @@ module.exports = function MembersApi({
getHTML,
getSubject
},
memberStripeCustomerModel,
stripeCustomerSubscriptionModel,
memberModel,
models: {
StripeWebhook,
StripeCustomer,
StripeCustomerSubscription,
Member
},
logger
}) {
if (logger) {
@ -37,10 +40,14 @@ module.exports = function MembersApi({
}
const {encodeIdentityToken, decodeToken} = Tokens({privateKey, publicKey, issuer});
const metadata = Metadata({memberStripeCustomerModel, stripeCustomerSubscriptionModel});
const metadata = Metadata({
StripeWebhook,
StripeCustomer,
StripeCustomerSubscription
});
async function hasActiveStripeSubscriptions() {
const firstActiveSubscription = await stripeCustomerSubscriptionModel.findOne({
const firstActiveSubscription = await StripeCustomerSubscription.findOne({
status: 'active'
});
@ -48,7 +55,7 @@ module.exports = function MembersApi({
return true;
}
const firstTrialingSubscription = await stripeCustomerSubscriptionModel.findOne({
const firstTrialingSubscription = await StripeCustomerSubscription.findOne({
status: 'trialing'
});
@ -111,7 +118,7 @@ module.exports = function MembersApi({
const users = Users({
stripe,
memberModel
Member
});
async function getMemberDataFromMagicLinkToken(token) {

View File

@ -1,13 +1,15 @@
let MemberStripeCustomer;
let StripeCustomerSubscription;
module.exports = function ({
StripeWebhook,
StripeCustomer,
StripeCustomerSubscription
}) {
async function setMetadata(module, metadata) {
if (module !== 'stripe') {
return;
}
if (metadata.customer) {
await MemberStripeCustomer.upsert(metadata.customer, {
await StripeCustomer.upsert(metadata.customer, {
customer_id: metadata.customer.customer_id
});
}
@ -18,6 +20,12 @@ async function setMetadata(module, metadata) {
});
}
if (metadata.webhook) {
await StripeWebhook.upsert(metadata.webhook, {
webhook_id: metadata.webhook.webhook_id
});
}
return;
}
@ -26,7 +34,7 @@ async function getMetadata(module, member) {
return;
}
const customers = (await MemberStripeCustomer.findAll({
const customers = (await StripeCustomer.findAll({
filter: `member_id:${member.id}`
})).toJSON();
@ -43,13 +51,6 @@ async function getMetadata(module, member) {
};
}
module.exports = function ({
memberStripeCustomerModel,
stripeCustomerSubscriptionModel
}) {
MemberStripeCustomer = memberStripeCustomerModel;
StripeCustomerSubscription = stripeCustomerSubscriptionModel;
return {
setMetadata,
getMetadata

View File

@ -1,6 +1,6 @@
const debug = require('ghost-ignition').debug('stripe');
const _ = require('lodash');
const {retrieve, list, create, update, del} = require('./api/stripeRequests');
const {retrieve, create, update, del} = require('./api/stripeRequests');
const api = require('./api');
const STRIPE_API_VERSION = '2019-09-09';
@ -39,33 +39,41 @@ module.exports = class StripePaymentProcessor {
this._checkoutCancelUrl = config.checkoutCancelUrl;
this._billingSuccessUrl = config.billingSuccessUrl;
this._billingCancelUrl = config.billingCancelUrl;
this._webhookHandlerUrl = config.webhookHandlerUrl;
try {
this._product = await api.products.ensure(this._stripe, config.product);
} catch (err) {
this.logging.error('There was an error creating the Stripe Product');
this.logging.error(err);
return this._rejectReady(err);
}
/**
* @type Array<import('stripe').plans.IPlan>
*/
this._plans = [];
for (const planSpec of config.plans) {
try {
const plan = await api.plans.ensure(this._stripe, planSpec, this._product);
this._plans.push(plan);
} catch (err) {
this.logging.error('There was an error creating the Stripe Plan');
this.logging.error(err);
return this._rejectReady(err);
}
}
const webhooks = await list(this._stripe, 'webhookEndpoints', {
limit: 100
if (process.env.WEBHOOK_SECRET) {
this.logging.warn(`Skipping Stripe webhook creation and validation, using WEBHOOK_SECRET environment variable`);
this._webhookSecret = process.env.WEBHOOK_SECRET;
return this._resolveReady({
product: this._product,
plans: this._plans
});
const webhookToDelete = webhooks.data.find((webhook) => {
return webhook.url === this._webhookHandlerUrl;
});
if (webhookToDelete) {
await del(this._stripe, 'webhookEndpoints', webhookToDelete.id);
}
try {
const webhook = await create(this._stripe, 'webhookEndpoints', {
url: this._webhookHandlerUrl,
api_version: STRIPE_API_VERSION,
const webhookConfig = {
url: config.webhookHandlerUrl,
enabled_events: [
'checkout.session.completed',
'customer.subscription.deleted',
@ -73,15 +81,67 @@ module.exports = class StripePaymentProcessor {
'invoice.payment_succeeded',
'invoice.payment_failed'
]
});
this._webhookSecret = process.env.WEBHOOK_SECRET || webhook.secret;
};
const setupWebhook = async (id, secret, opts = {}) => {
if (!id || !secret || opts.forceCreate) {
if (id && !opts.skipDelete) {
try {
this.logging.info(`Deleting Stripe webhook ${id}`);
await del(this._stripe, 'webhookEndpoints', id);
} catch (err) {
this._webhookSecret = process.env.WEBHOOK_SECRET;
this.logging.warn(err);
this.logging.error(`Unable to delete Stripe webhook with id: ${id}`);
this.logging.error(err);
}
debug(`Webhook secret set to ${this._webhookSecret}`);
}
try {
this.logging.info(`Creating Stripe webhook with url: ${webhookConfig.url}, version: ${STRIPE_API_VERSION}, events: ${webhookConfig.enabled_events.join(', ')}`);
const webhook = await create(this._stripe, 'webhookEndpoints', Object.assign({}, webhookConfig, {
api_version: STRIPE_API_VERSION
}));
return {
id: webhook.id,
secret: webhook.secret
};
} catch (err) {
this.logging.error('Failed to create Stripe webhook. For local development please see https://ghost.org/docs/members/webhooks/#stripe-webhooks');
this.logging.error(err);
throw err;
}
} else {
try {
this.logging.info(`Updating Stripe webhook ${id} with url: ${webhookConfig.url}, events: ${webhookConfig.enabled_events.join(', ')}`);
const updatedWebhook = await update(this._stripe, 'webhookEndpoints', id, webhookConfig);
if (updatedWebhook.api_version !== STRIPE_API_VERSION) {
throw new Error(`Webhook ${id} has api_version ${updatedWebhook.api_version}, expected ${STRIPE_API_VERSION}`);
}
return {
id,
secret
};
} catch (err) {
this.logging.error(`Unable to update Stripe webhook ${id}`);
this.logging.error(err);
if (err.code === 'resource_missing') {
return setupWebhook(id, secret, {skipDelete: true, forceCreate: true});
}
return setupWebhook(id, secret, {skipDelete: false, forceCreate: true});
}
}
};
try {
const webhook = await setupWebhook(config.webhook.id, config.webhook.secret);
await this.storage.set({
webhook: {
webhook_id: webhook.id,
secret: webhook.secret
}
});
this._webhookSecret = webhook.secret;
} catch (err) {
debug(`Error configuring ${err.message}`);
return this._rejectReady(err);
}

View File

@ -2,8 +2,10 @@ const _ = require('lodash');
const debug = require('ghost-ignition').debug('users');
const common = require('./common');
let Member;
module.exports = function ({
stripe,
Member
}) {
async function createMember({email, name, note, labels, geolocation}) {
const model = await Member.add({
email,
@ -51,12 +53,6 @@ function listMembers(options) {
});
}
module.exports = function ({
stripe,
memberModel
}) {
Member = memberModel;
async function getStripeSubscriptions(member) {
if (!stripe) {
return [];

View File

@ -17,6 +17,7 @@
"gateway"
],
"devDependencies": {
"@types/stripe": "^7.13.24",
"jsdom": "15.2.1",
"mocha": "6.2.2",
"nock": "12.0.3",