mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
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:
parent
68dbfb707d
commit
ac923af0f7
@ -27,9 +27,12 @@ module.exports = function MembersApi({
|
|||||||
getHTML,
|
getHTML,
|
||||||
getSubject
|
getSubject
|
||||||
},
|
},
|
||||||
memberStripeCustomerModel,
|
models: {
|
||||||
stripeCustomerSubscriptionModel,
|
StripeWebhook,
|
||||||
memberModel,
|
StripeCustomer,
|
||||||
|
StripeCustomerSubscription,
|
||||||
|
Member
|
||||||
|
},
|
||||||
logger
|
logger
|
||||||
}) {
|
}) {
|
||||||
if (logger) {
|
if (logger) {
|
||||||
@ -37,10 +40,14 @@ module.exports = function MembersApi({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {encodeIdentityToken, decodeToken} = Tokens({privateKey, publicKey, issuer});
|
const {encodeIdentityToken, decodeToken} = Tokens({privateKey, publicKey, issuer});
|
||||||
const metadata = Metadata({memberStripeCustomerModel, stripeCustomerSubscriptionModel});
|
const metadata = Metadata({
|
||||||
|
StripeWebhook,
|
||||||
|
StripeCustomer,
|
||||||
|
StripeCustomerSubscription
|
||||||
|
});
|
||||||
|
|
||||||
async function hasActiveStripeSubscriptions() {
|
async function hasActiveStripeSubscriptions() {
|
||||||
const firstActiveSubscription = await stripeCustomerSubscriptionModel.findOne({
|
const firstActiveSubscription = await StripeCustomerSubscription.findOne({
|
||||||
status: 'active'
|
status: 'active'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,7 +55,7 @@ module.exports = function MembersApi({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTrialingSubscription = await stripeCustomerSubscriptionModel.findOne({
|
const firstTrialingSubscription = await StripeCustomerSubscription.findOne({
|
||||||
status: 'trialing'
|
status: 'trialing'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +99,7 @@ module.exports = function MembersApi({
|
|||||||
getSubject
|
getSubject
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}){
|
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}) {
|
||||||
if (options.forceEmailType) {
|
if (options.forceEmailType) {
|
||||||
return magicLinkService.sendMagicLink({email, payload, subject: email, type: requestedType});
|
return magicLinkService.sendMagicLink({email, payload, subject: email, type: requestedType});
|
||||||
}
|
}
|
||||||
@ -111,7 +118,7 @@ module.exports = function MembersApi({
|
|||||||
|
|
||||||
const users = Users({
|
const users = Users({
|
||||||
stripe,
|
stripe,
|
||||||
memberModel
|
Member
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getMemberDataFromMagicLinkToken(token) {
|
async function getMemberDataFromMagicLinkToken(token) {
|
||||||
@ -139,11 +146,11 @@ module.exports = function MembersApi({
|
|||||||
return getMemberIdentityData(email);
|
return getMemberIdentityData(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMemberIdentityData(email){
|
async function getMemberIdentityData(email) {
|
||||||
return users.get({email});
|
return users.get({email});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMemberIdentityToken(email){
|
async function getMemberIdentityToken(email) {
|
||||||
const member = await getMemberIdentityData(email);
|
const member = await getMemberIdentityData(email);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,54 +1,55 @@
|
|||||||
let MemberStripeCustomer;
|
|
||||||
let StripeCustomerSubscription;
|
|
||||||
|
|
||||||
async function setMetadata(module, metadata) {
|
|
||||||
if (module !== 'stripe') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.customer) {
|
|
||||||
await MemberStripeCustomer.upsert(metadata.customer, {
|
|
||||||
customer_id: metadata.customer.customer_id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.subscription) {
|
|
||||||
await StripeCustomerSubscription.upsert(metadata.subscription, {
|
|
||||||
subscription_id: metadata.subscription.subscription_id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMetadata(module, member) {
|
|
||||||
if (module !== 'stripe') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customers = (await MemberStripeCustomer.findAll({
|
|
||||||
filter: `member_id:${member.id}`
|
|
||||||
})).toJSON();
|
|
||||||
|
|
||||||
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
|
|
||||||
const customerSubscriptions = await StripeCustomerSubscription.findAll({
|
|
||||||
filter: `customer_id:${customer.customer_id}`
|
|
||||||
});
|
|
||||||
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
customers: customers,
|
|
||||||
subscriptions: subscriptions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function ({
|
module.exports = function ({
|
||||||
memberStripeCustomerModel,
|
StripeWebhook,
|
||||||
stripeCustomerSubscriptionModel
|
StripeCustomer,
|
||||||
|
StripeCustomerSubscription
|
||||||
}) {
|
}) {
|
||||||
MemberStripeCustomer = memberStripeCustomerModel;
|
async function setMetadata(module, metadata) {
|
||||||
StripeCustomerSubscription = stripeCustomerSubscriptionModel;
|
if (module !== 'stripe') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.customer) {
|
||||||
|
await StripeCustomer.upsert(metadata.customer, {
|
||||||
|
customer_id: metadata.customer.customer_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.subscription) {
|
||||||
|
await StripeCustomerSubscription.upsert(metadata.subscription, {
|
||||||
|
subscription_id: metadata.subscription.subscription_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.webhook) {
|
||||||
|
await StripeWebhook.upsert(metadata.webhook, {
|
||||||
|
webhook_id: metadata.webhook.webhook_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMetadata(module, member) {
|
||||||
|
if (module !== 'stripe') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customers = (await StripeCustomer.findAll({
|
||||||
|
filter: `member_id:${member.id}`
|
||||||
|
})).toJSON();
|
||||||
|
|
||||||
|
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
|
||||||
|
const customerSubscriptions = await StripeCustomerSubscription.findAll({
|
||||||
|
filter: `customer_id:${customer.customer_id}`
|
||||||
|
});
|
||||||
|
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customers: customers,
|
||||||
|
subscriptions: subscriptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setMetadata,
|
setMetadata,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const debug = require('ghost-ignition').debug('stripe');
|
const debug = require('ghost-ignition').debug('stripe');
|
||||||
const _ = require('lodash');
|
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 api = require('./api');
|
||||||
|
|
||||||
const STRIPE_API_VERSION = '2019-09-09';
|
const STRIPE_API_VERSION = '2019-09-09';
|
||||||
@ -39,49 +39,109 @@ module.exports = class StripePaymentProcessor {
|
|||||||
this._checkoutCancelUrl = config.checkoutCancelUrl;
|
this._checkoutCancelUrl = config.checkoutCancelUrl;
|
||||||
this._billingSuccessUrl = config.billingSuccessUrl;
|
this._billingSuccessUrl = config.billingSuccessUrl;
|
||||||
this._billingCancelUrl = config.billingCancelUrl;
|
this._billingCancelUrl = config.billingCancelUrl;
|
||||||
this._webhookHandlerUrl = config.webhookHandlerUrl;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._product = await api.products.ensure(this._stripe, config.product);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
this._plans = [];
|
/**
|
||||||
for (const planSpec of config.plans) {
|
* @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);
|
const plan = await api.plans.ensure(this._stripe, planSpec, this._product);
|
||||||
this._plans.push(plan);
|
this._plans.push(plan);
|
||||||
}
|
|
||||||
|
|
||||||
const webhooks = await list(this._stripe, 'webhookEndpoints', {
|
|
||||||
limit: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
enabled_events: [
|
|
||||||
'checkout.session.completed',
|
|
||||||
'customer.subscription.deleted',
|
|
||||||
'customer.subscription.updated',
|
|
||||||
'invoice.payment_succeeded',
|
|
||||||
'invoice.payment_failed'
|
|
||||||
]
|
|
||||||
});
|
|
||||||
this._webhookSecret = process.env.WEBHOOK_SECRET || webhook.secret;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._webhookSecret = process.env.WEBHOOK_SECRET;
|
this.logging.error('There was an error creating the Stripe Plan');
|
||||||
this.logging.warn(err);
|
this.logging.error(err);
|
||||||
|
return this._rejectReady(err);
|
||||||
}
|
}
|
||||||
debug(`Webhook secret set to ${this._webhookSecret}`);
|
}
|
||||||
|
|
||||||
|
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 webhookConfig = {
|
||||||
|
url: config.webhookHandlerUrl,
|
||||||
|
enabled_events: [
|
||||||
|
'checkout.session.completed',
|
||||||
|
'customer.subscription.deleted',
|
||||||
|
'customer.subscription.updated',
|
||||||
|
'invoice.payment_succeeded',
|
||||||
|
'invoice.payment_failed'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
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.logging.error(`Unable to delete Stripe webhook with id: ${id}`);
|
||||||
|
this.logging.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
} catch (err) {
|
||||||
debug(`Error configuring ${err.message}`);
|
|
||||||
return this._rejectReady(err);
|
return this._rejectReady(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,60 +2,56 @@ const _ = require('lodash');
|
|||||||
const debug = require('ghost-ignition').debug('users');
|
const debug = require('ghost-ignition').debug('users');
|
||||||
const common = require('./common');
|
const common = require('./common');
|
||||||
|
|
||||||
let Member;
|
|
||||||
|
|
||||||
async function createMember({email, name, note, labels, geolocation}) {
|
|
||||||
const model = await Member.add({
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
note,
|
|
||||||
labels,
|
|
||||||
geolocation
|
|
||||||
});
|
|
||||||
const member = model.toJSON();
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMember(data, options = {}) {
|
|
||||||
if (!data.email && !data.id && !data.uuid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const model = await Member.findOne(data, options);
|
|
||||||
if (!model) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const member = model.toJSON(options);
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMember(data, options = {}) {
|
|
||||||
const attrs = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation']);
|
|
||||||
|
|
||||||
const model = await Member.edit(attrs, options);
|
|
||||||
|
|
||||||
const member = model.toJSON(options);
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteMember(options) {
|
|
||||||
options = options || {};
|
|
||||||
return Member.destroy(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function listMembers(options) {
|
|
||||||
return Member.findPage(options).then((models) => {
|
|
||||||
return {
|
|
||||||
members: models.data.map(model => model.toJSON(options)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function ({
|
module.exports = function ({
|
||||||
stripe,
|
stripe,
|
||||||
memberModel
|
Member
|
||||||
}) {
|
}) {
|
||||||
Member = memberModel;
|
async function createMember({email, name, note, labels, geolocation}) {
|
||||||
|
const model = await Member.add({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
note,
|
||||||
|
labels,
|
||||||
|
geolocation
|
||||||
|
});
|
||||||
|
const member = model.toJSON();
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMember(data, options = {}) {
|
||||||
|
if (!data.email && !data.id && !data.uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const model = await Member.findOne(data, options);
|
||||||
|
if (!model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const member = model.toJSON(options);
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMember(data, options = {}) {
|
||||||
|
const attrs = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation']);
|
||||||
|
|
||||||
|
const model = await Member.edit(attrs, options);
|
||||||
|
|
||||||
|
const member = model.toJSON(options);
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMember(options) {
|
||||||
|
options = options || {};
|
||||||
|
return Member.destroy(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMembers(options) {
|
||||||
|
return Member.findPage(options).then((models) => {
|
||||||
|
return {
|
||||||
|
members: models.data.map(model => model.toJSON(options)),
|
||||||
|
meta: models.meta
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getStripeSubscriptions(member) {
|
async function getStripeSubscriptions(member) {
|
||||||
if (!stripe) {
|
if (!stripe) {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"gateway"
|
"gateway"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/stripe": "^7.13.24",
|
||||||
"jsdom": "15.2.1",
|
"jsdom": "15.2.1",
|
||||||
"mocha": "6.2.2",
|
"mocha": "6.2.2",
|
||||||
"nock": "12.0.3",
|
"nock": "12.0.3",
|
||||||
|
Loading…
Reference in New Issue
Block a user