From a1f883edbc145c1e086285768fdcff0855936996 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 28 May 2020 17:55:23 +0200 Subject: [PATCH] Refactored members config to use DI no-issue This makes testing it much easier --- core/server/services/members/api.js | 3 +- core/server/services/members/config.js | 301 +++++++++++++------------ core/server/services/members/index.js | 17 +- 3 files changed, 168 insertions(+), 153 deletions(-) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 3074b5449f..b59d5d31c6 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -6,13 +6,12 @@ const models = require('../../models'); const signinEmail = require('./emails/signin'); const signupEmail = require('./emails/signup'); const subscribeEmail = require('./emails/subscribe'); -const config = require('./config'); const ghostMailer = new mail.GhostMailer(); module.exports = createApiInstance; -function createApiInstance() { +function createApiInstance(config) { const membersApiInstance = MembersApi({ tokenConfig: config.getTokenConfig(), auth: { diff --git a/core/server/services/members/config.js b/core/server/services/members/config.js index e4e5c58da3..570e9a0085 100644 --- a/core/server/services/members/config.js +++ b/core/server/services/members/config.js @@ -1,10 +1,6 @@ const {URL} = require('url'); -const settingsCache = require('../settings/cache'); -const ghostVersion = require('../../lib/ghost-version'); const crypto = require('crypto'); const path = require('path'); -const logging = require('../../../shared/logging'); -const urlUtils = require('../../../shared/url-utils'); const COMPLIMENTARY_PLAN = { name: 'Complimentary', @@ -13,161 +9,170 @@ const COMPLIMENTARY_PLAN = { amount: '0' }; -// NOTE: the function is an exact duplicate of one in GhostMailer should be extracted -// into a common lib once it needs to be reused anywhere else again -function getDomain() { - const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i')); - return domain && domain[1]; -} +class MembersConfigProvider { + /** + * @param {object} options + * @param {{get: (key: string) => any}} options.settingsCache + * @param {{get: (key: string) => any}} options.config + * @param {any} options.urlUtils + * @param {any} options.logging + * @param {{original: string}} options.ghostVersion + */ + constructor(options) { + this._settingsCache = options.settingsCache; + this._config = options.config; + this._urlUtils = options.urlUtils; + this._logging = options.logging; + this._ghostVersion = options.ghostVersion; + } -function getEmailFromAddress() { - const subscriptionSettings = settingsCache.get('members_subscription_settings') || {}; + /** + * @private + */ + _getDomain() { + const domain = this._urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i')); + return domain && domain[1]; + } - return `${subscriptionSettings.fromAddress || 'noreply'}@${getDomain()}`; -} + /** + */ + getEmailFromAddress() { + const subscriptionSettings = this._settingsCache.get('members_subscription_settings') || {}; -/** Copied from theme middleware, remove it there after cleanup to keep this in single place */ -function getPublicPlans() { - const CURRENCY_SYMBOLS = { - USD: '$', - AUD: '$', - CAD: '$', - GBP: '£', - EUR: '€' - }; - const defaultPriceData = { - monthly: 0, - yearly: 0 - }; + return `${subscriptionSettings.fromAddress || 'noreply'}@${this._getDomain()}`; + } - try { - const membersSettings = settingsCache.get('members_subscription_settings'); - const stripeProcessor = membersSettings.paymentProcessors.find( - processor => processor.adapter === 'stripe' + getPublicPlans() { + const CURRENCY_SYMBOLS = { + USD: '$', + AUD: '$', + CAD: '$', + GBP: '£', + EUR: '€' + }; + const defaultPriceData = { + monthly: 0, + yearly: 0 + }; + + try { + const membersSettings = this._settingsCache.get('members_subscription_settings'); + const stripeProcessor = membersSettings.paymentProcessors.find( + processor => processor.adapter === 'stripe' + ); + + const priceData = stripeProcessor.config.plans.reduce((prices, plan) => { + const numberAmount = 0 + plan.amount; + const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0; + return Object.assign(prices, { + [plan.name.toLowerCase()]: dollarAmount + }); + }, {}); + + priceData.currency = String.prototype.toUpperCase.call(stripeProcessor.config.currency || 'usd'); + priceData.currency_symbol = CURRENCY_SYMBOLS[priceData.currency]; + + if (Number.isInteger(priceData.monthly) && Number.isInteger(priceData.yearly)) { + return priceData; + } + + return defaultPriceData; + } catch (err) { + return defaultPriceData; + } + } + + getStripePaymentConfig() { + const subscriptionSettings = this._settingsCache.get('members_subscription_settings'); + + const stripePaymentProcessor = subscriptionSettings.paymentProcessors.find( + paymentProcessor => paymentProcessor.adapter === 'stripe' ); - const priceData = stripeProcessor.config.plans.reduce((prices, plan) => { - const numberAmount = 0 + plan.amount; - const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0; - return Object.assign(prices, { - [plan.name.toLowerCase()]: dollarAmount - }); - }, {}); - - priceData.currency = String.prototype.toUpperCase.call(stripeProcessor.config.currency || 'usd'); - priceData.currency_symbol = CURRENCY_SYMBOLS[priceData.currency]; - - if (Number.isInteger(priceData.monthly) && Number.isInteger(priceData.yearly)) { - return priceData; + if (!stripePaymentProcessor || !stripePaymentProcessor.config) { + return null; } - return defaultPriceData; - } catch (err) { - return defaultPriceData; - } -} - -const getApiUrl = ({version, type}) => { - const {href} = new URL( - urlUtils.getApiPath({version, type}), - urlUtils.urlFor('admin', true) - ); - return href; -}; - -const siteUrl = urlUtils.getSiteUrl(); -const membersApiUrl = getApiUrl({version: 'v3', type: 'members'}); - -function getStripePaymentConfig() { - const subscriptionSettings = settingsCache.get('members_subscription_settings'); - - const stripePaymentProcessor = subscriptionSettings.paymentProcessors.find( - paymentProcessor => paymentProcessor.adapter === 'stripe' - ); - - if (!stripePaymentProcessor || !stripePaymentProcessor.config) { - return null; - } - - if (!stripePaymentProcessor.config.public_token || !stripePaymentProcessor.config.secret_token) { - return null; - } - - // NOTE: "Complimentary" plan has to be first in the queue so it is created even if regular plans are not configured - stripePaymentProcessor.config.plans.unshift(COMPLIMENTARY_PLAN); - - const webhookHandlerUrl = new URL('/members/webhooks/stripe', siteUrl); - - const checkoutSuccessUrl = new URL(siteUrl); - checkoutSuccessUrl.searchParams.set('stripe', 'success'); - const checkoutCancelUrl = new URL(siteUrl); - checkoutCancelUrl.searchParams.set('stripe', 'cancel'); - - const billingSuccessUrl = new URL(siteUrl); - billingSuccessUrl.searchParams.set('stripe', 'billing-update-success'); - const billingCancelUrl = new URL(siteUrl); - billingCancelUrl.searchParams.set('stripe', 'billing-update-cancel'); - - return { - publicKey: stripePaymentProcessor.config.public_token, - secretKey: stripePaymentProcessor.config.secret_token, - checkoutSuccessUrl: checkoutSuccessUrl.href, - checkoutCancelUrl: checkoutCancelUrl.href, - billingSuccessUrl: billingSuccessUrl.href, - billingCancelUrl: billingCancelUrl.href, - webhookHandlerUrl: webhookHandlerUrl.href, - product: stripePaymentProcessor.config.product, - plans: stripePaymentProcessor.config.plans, - appInfo: { - name: 'Ghost', - partner_id: 'pp_partner_DKmRVtTs4j9pwZ', - version: ghostVersion.original, - url: 'https://ghost.org/' + if (!stripePaymentProcessor.config.public_token || !stripePaymentProcessor.config.secret_token) { + return null; } - }; -} -function getAuthSecret() { - const hexSecret = settingsCache.get('members_email_auth_secret'); - if (!hexSecret) { - logging.warn('Could not find members_email_auth_secret, using dynamically generated secret'); - return crypto.randomBytes(64); + // NOTE: "Complimentary" plan has to be first in the queue so it is created even if regular plans are not configured + stripePaymentProcessor.config.plans.unshift(COMPLIMENTARY_PLAN); + + const siteUrl = this._urlUtils.getSiteUrl(); + + const webhookHandlerUrl = new URL('/members/webhooks/stripe', siteUrl); + + const checkoutSuccessUrl = new URL(siteUrl); + checkoutSuccessUrl.searchParams.set('stripe', 'success'); + const checkoutCancelUrl = new URL(siteUrl); + checkoutCancelUrl.searchParams.set('stripe', 'cancel'); + + const billingSuccessUrl = new URL(siteUrl); + billingSuccessUrl.searchParams.set('stripe', 'billing-update-success'); + const billingCancelUrl = new URL(siteUrl); + billingCancelUrl.searchParams.set('stripe', 'billing-update-cancel'); + + return { + publicKey: stripePaymentProcessor.config.public_token, + secretKey: stripePaymentProcessor.config.secret_token, + checkoutSuccessUrl: checkoutSuccessUrl.href, + checkoutCancelUrl: checkoutCancelUrl.href, + billingSuccessUrl: billingSuccessUrl.href, + billingCancelUrl: billingCancelUrl.href, + webhookHandlerUrl: webhookHandlerUrl.href, + product: stripePaymentProcessor.config.product, + plans: stripePaymentProcessor.config.plans, + appInfo: { + name: 'Ghost', + partner_id: 'pp_partner_DKmRVtTs4j9pwZ', + version: this._ghostVersion.original, + url: 'https://ghost.org/' + } + }; } - const secret = Buffer.from(hexSecret, 'hex'); - if (secret.length < 64) { - logging.warn('members_email_auth_secret not large enough (64 bytes), using dynamically generated secret'); - return crypto.randomBytes(64); + + getAuthSecret() { + const hexSecret = this._settingsCache.get('members_email_auth_secret'); + if (!hexSecret) { + this._logging.warn('Could not find members_email_auth_secret, using dynamically generated secret'); + return crypto.randomBytes(64); + } + const secret = Buffer.from(hexSecret, 'hex'); + if (secret.length < 64) { + this._logging.warn('members_email_auth_secret not large enough (64 bytes), using dynamically generated secret'); + return crypto.randomBytes(64); + } + return secret; + } + + getAllowSelfSignup() { + const subscriptionSettings = this._settingsCache.get('members_subscription_settings'); + return subscriptionSettings.allowSelfSignup; + } + + getTokenConfig() { + const {href: membersApiUrl} = new URL( + this._urlUtils.getApiPath({version: 'v3', type: 'members'}), + this._urlUtils.urlFor('admin', true) + ); + + return { + issuer: membersApiUrl, + publicKey: this._settingsCache.get('members_public_key'), + privateKey: this._settingsCache.get('members_private_key') + }; + } + + getSigninURL(token, type) { + const siteUrl = this._urlUtils.getSiteUrl(); + const signinURL = new URL(siteUrl); + signinURL.pathname = path.join(signinURL.pathname, '/members/'); + signinURL.searchParams.set('token', token); + signinURL.searchParams.set('action', type); + return signinURL.href; } - return secret; } -function getAllowSelfSignup() { - const subscriptionSettings = settingsCache.get('members_subscription_settings'); - return subscriptionSettings.allowSelfSignup; -} - -function getTokenConfig() { - return { - issuer: membersApiUrl, - publicKey: settingsCache.get('members_public_key'), - privateKey: settingsCache.get('members_private_key') - }; -} - -function getSigninURL(token, type) { - const signinURL = new URL(siteUrl); - signinURL.pathname = path.join(signinURL.pathname, '/members/'); - signinURL.searchParams.set('token', token); - signinURL.searchParams.set('action', type); - return signinURL.href; -} - -module.exports = { - getEmailFromAddress, - getPublicPlans, - getStripePaymentConfig, - getAllowSelfSignup, - getAuthSecret, - getTokenConfig, - getSigninURL -}; +module.exports = MembersConfigProvider; diff --git a/core/server/services/members/index.js b/core/server/services/members/index.js index 0bbeecb9ea..5b410aaa39 100644 --- a/core/server/services/members/index.js +++ b/core/server/services/members/index.js @@ -1,10 +1,21 @@ const MembersSSR = require('@tryghost/members-ssr'); +const MembersConfigProvider = require('./config'); const createMembersApiInstance = require('./api'); const {events} = require('../../lib/common'); const logging = require('../../../shared/logging'); const urlUtils = require('../../../shared/url-utils'); const settingsCache = require('../settings/cache'); +const config = require('../../../shared/config'); +const ghostVersion = require('../../lib/ghost-version'); + +const membersConfig = new MembersConfigProvider({ + config, + settingsCache, + urlUtils, + logging, + ghostVersion +}); let membersApi; @@ -14,7 +25,7 @@ events.on('settings.edited', function updateSettingFromModel(settingModel) { return; } - const reconfiguredMembersAPI = createMembersApiInstance(); + const reconfiguredMembersAPI = createMembersApiInstance(membersConfig); reconfiguredMembersAPI.bus.on('ready', function () { membersApi = reconfiguredMembersAPI; }); @@ -26,11 +37,11 @@ events.on('settings.edited', function updateSettingFromModel(settingModel) { const membersService = { contentGating: require('./content-gating'), - config: require('./config'), + config: membersConfig, get api() { if (!membersApi) { - membersApi = createMembersApiInstance(); + membersApi = createMembersApiInstance(membersConfig); membersApi.bus.on('error', function (err) { logging.error(err);