0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Moved StripeAPIService to its own service

refs https://github.com/TryGhost/Team/issues/1083

The Offers service is going to need access to the StripeAPIService too,
so we need to move it out of the @tryghost/members-api module and make
it accessible to both.
This commit is contained in:
Fabien O'Carroll 2021-10-04 13:18:22 +02:00
parent 025faec7c3
commit cda041d424
11 changed files with 246 additions and 140 deletions

View file

@ -177,6 +177,7 @@ async function initServices({config}) {
debug(`Default API Version: ${defaultApiVersion}`);
debug('Begin: Services');
const stripe = require('./server/services/stripe');
const members = require('./server/services/members');
const permissions = require('./server/services/permissions');
const xmlrpc = require('./server/services/xmlrpc');
@ -193,6 +194,10 @@ async function initServices({config}) {
// in case it limits initialization of any other service (e.g. webhooks)
await limits.init();
// NOTE: stripe service has to be initialized before members
// as it is a dependency
await stripe.init();
await Promise.all([
members.init(),
permissions.init(),

View file

@ -5,42 +5,43 @@ const config = require('../../../../shared/config');
let UNO_MEMBERINO;
async function createMiddleware() {
const url = require('url');
const {protocol, host} = url.parse(config.get('url'));
const siteOrigin = `${protocol}//${host}`;
const membersConfig = await membersService.api.getPublicConfig();
return jwt({
credentialsRequired: false,
requestProperty: 'member',
audience: siteOrigin,
issuer: membersConfig.issuer,
algorithms: ['RS512'],
secret: membersConfig.publicKey,
getToken(req) {
if (!req.get('authorization')) {
return null;
}
const [scheme, credentials] = req.get('authorization').split(/\s+/);
if (scheme !== 'GhostMembers') {
return null;
}
return credentials;
}
});
}
module.exports = {
get authenticateMembersToken() {
if (!UNO_MEMBERINO) {
const url = require('url');
const {protocol, host} = url.parse(config.get('url'));
const siteOrigin = `${protocol}//${host}`;
UNO_MEMBERINO = membersService.api.getPublicConfig().then(({issuer}) => jwt({
credentialsRequired: false,
requestProperty: 'member',
audience: siteOrigin,
issuer,
algorithms: ['RS512'],
secret(req, payload, done) {
membersService.api.getPublicConfig().then(({publicKey}) => {
done(null, publicKey);
}).catch(done);
},
getToken(req) {
if (!req.get('authorization')) {
return null;
}
const [scheme, credentials] = req.get('authorization').split(/\s+/);
if (scheme !== 'GhostMembers') {
return null;
}
return credentials;
}
}));
}
return async function (req, res, next) {
if (!UNO_MEMBERINO) {
UNO_MEMBERINO = await createMiddleware();
}
try {
const middleware = await UNO_MEMBERINO;
const middleware = UNO_MEMBERINO;
middleware(req, res, function (err, ...rest) {
if (err && err.name === 'UnauthorizedError') {

View file

@ -1,3 +1,4 @@
const stripeService = require('../stripe');
const settingsCache = require('../../../shared/settings-cache');
const MembersApi = require('@tryghost/members-api');
const logging = require('@tryghost/logging');
@ -183,6 +184,7 @@ function createApiInstance(config) {
Product: models.Product,
Settings: models.Settings
},
stripeAPIService: stripeService.api,
logger: logging,
labsService: labsService
});

View file

@ -174,8 +174,6 @@ class MembersConfigProvider {
}
return {
publicKey: stripeApiKeys.publicKey,
secretKey: stripeApiKeys.secretKey,
checkoutSuccessUrl: urls.checkoutSuccess,
checkoutCancelUrl: urls.checkoutCancel,
billingSuccessUrl: urls.billingSuccess,
@ -185,17 +183,10 @@ class MembersConfigProvider {
id: this._settingsCache.get('members_stripe_webhook_id'),
secret: this._settingsCache.get('members_stripe_webhook_secret')
},
enablePromoCodes: this._config.get('enableStripePromoCodes'),
product: {
name: this._settingsCache.get('stripe_product_name')
},
plans: this._settingsCache.get('stripe_plans') || [],
appInfo: {
name: 'Ghost',
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
version: this._ghostVersion.original,
url: 'https://ghost.org/'
}
plans: this._settingsCache.get('stripe_plans') || []
};
}

View file

@ -17,6 +17,7 @@ const ghostVersion = require('@tryghost/version');
const _ = require('lodash');
const {GhostMailer} = require('../mail');
const jobsService = require('../jobs');
const stripeService = require('../stripe');
const messages = {
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
@ -135,15 +136,8 @@ events.on('settings.edited', function updateSettingFromModel(settingModel) {
'members_from_address',
'members_support_address',
'members_reply_address',
'stripe_publishable_key',
'stripe_secret_key',
'stripe_product_name',
'stripe_plans',
'stripe_connect_publishable_key',
'stripe_connect_secret_key',
'stripe_connect_livemode',
'stripe_connect_display_name',
'stripe_connect_account_id'
'stripe_plans'
].includes(settingModel.get('key'))) {
return;
}
@ -151,32 +145,27 @@ events.on('settings.edited', function updateSettingFromModel(settingModel) {
debouncedReconfigureMembersAPI();
});
events.on('services.stripe.reconfigured', reconfigureMembersAPI);
const membersService = {
async init() {
const env = config.get('env');
const paymentConfig = membersConfig.getStripePaymentConfig();
if (env !== 'production') {
if (!process.env.WEBHOOK_SECRET && membersConfig.isStripeConnected()) {
if (!process.env.WEBHOOK_SECRET && stripeService.api.configured) {
process.env.WEBHOOK_SECRET = 'DEFAULT_WEBHOOK_SECRET';
logging.warn(tpl(messages.remoteWebhooksInDevelopment));
}
if (paymentConfig && paymentConfig.secretKey.startsWith('sk_live')) {
if (stripeService.api.configured && stripeService.api.mode === 'live') {
throw new errors.IncorrectUsageError(tpl(messages.noLiveKeysInDevelopment));
}
} else {
const siteUrl = urlUtils.getSiteUrl();
if (!/^https/.test(siteUrl) && membersConfig.isStripeConnected()) {
if (!/^https/.test(siteUrl) && stripeService.api.configured) {
throw new errors.IncorrectUsageError(tpl(messages.sslRequiredForStripe));
}
}
},
contentGating: require('./content-gating'),
config: membersConfig,
get api() {
if (!membersApi) {
membersApi = createMembersApiInstance(membersConfig);
@ -184,6 +173,12 @@ const membersService = {
logging.error(err);
});
}
},
contentGating: require('./content-gating'),
config: membersConfig,
get api() {
return membersApi;
},

View file

@ -0,0 +1,57 @@
const ghostVersion = require('@tryghost/version');
module.exports = {
getConfig(settings, config) {
/**
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
* @returns {{publicKey: string, secretKey: string} | null}
*/
function getStripeKeys(type) {
const secretKey = settings.get(`stripe_${type === 'connect' ? 'connect_' : ''}secret_key`);
const publicKey = settings.get(`stripe_${type === 'connect' ? 'connect_' : ''}publishable_key`);
if (!secretKey || !publicKey) {
return null;
}
return {
secretKey,
publicKey
};
}
/**
* @returns {{publicKey: string, secretKey: string} | null}
*/
function getActiveStripeKeys() {
const stripeDirect = config.get('stripeDirect');
if (stripeDirect) {
return getStripeKeys('direct');
}
const connectKeys = getStripeKeys('connect');
if (!connectKeys) {
return getStripeKeys('direct');
}
return connectKeys;
}
const keys = getActiveStripeKeys();
if (!keys) {
return null;
}
return {
secretKey: keys.secretKey,
publicKey: keys.publicKey,
appInfo: {
name: 'Ghost',
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
version: ghostVersion.original,
url: 'https://ghost.org/'
},
enablePromoCodes: config.get('enableStripePromoCodes')
};
}
};

View file

@ -0,0 +1,45 @@
const _ = require('lodash');
const logging = require('@tryghost/logging');
const StripeAPIService = require('@tryghost/members-stripe-service');
const config = require('../../../shared/config');
const settings = require('../../../shared/settings-cache');
const events = require('../../lib/common/events');
const {getConfig} = require('./config');
const api = new StripeAPIService({
logger: logging,
config: {}
});
const stripeKeySettings = [
'stripe_publishable_key',
'stripe_secret_key',
'stripe_connect_publishable_key',
'stripe_connect_secret_key'
];
function configureApi() {
const cfg = getConfig(settings, config);
if (cfg) {
api.configure(cfg);
}
}
const debouncedConfigureApi = _.debounce(configureApi, 600);
module.exports = {
async init() {
configureApi();
events.on('settings.edited', function (model) {
if (!stripeKeySettings.includes(model.get('key'))) {
return;
}
debouncedConfigureApi();
events.emit('services.stripe.reconfigured');
});
},
api
};

View file

@ -75,7 +75,7 @@
"@tryghost/limit-service": "0.6.4",
"@tryghost/logging": "0.1.7",
"@tryghost/magic-link": "1.0.13",
"@tryghost/members-api": "1.39.1",
"@tryghost/members-api": "2.0.0",
"@tryghost/members-csv": "1.1.7",
"@tryghost/members-importer": "0.3.3",
"@tryghost/members-ssr": "1.0.14",

View file

@ -71,81 +71,6 @@ describe('Members - config', function () {
afterEach(function () {
configUtils.restore();
});
it('Uses direct keys when stripeDirect is true, regardles of which keys exist', function () {
configUtils.set({stripeDirect: true});
const settingsCache = createSettingsMock({setDirect: true, setConnect: Math.random() < 0.5});
const urlUtils = createUrlUtilsMock();
const membersConfig = new MembersConfigProvider({
config: configUtils.config,
settingsCache,
urlUtils,
ghostVersion: {original: 'v7357'},
logging: console
});
const paymentConfig = membersConfig.getStripePaymentConfig();
should.equal(paymentConfig.publicKey, 'direct_publishable');
should.equal(paymentConfig.secretKey, 'direct_secret');
});
it('Does not use connect keys if stripeDirect is true, and the direct keys do not exist', function () {
configUtils.set({stripeDirect: true});
const settingsCache = createSettingsMock({setDirect: false, setConnect: true});
const urlUtils = createUrlUtilsMock();
const membersConfig = new MembersConfigProvider({
config: configUtils.config,
settingsCache,
urlUtils,
ghostVersion: {original: 'v7357'},
logging: console
});
const paymentConfig = membersConfig.getStripePaymentConfig();
should.equal(paymentConfig, null);
});
it('Uses connect keys when stripeDirect is false, and the connect keys exist', function () {
configUtils.set({stripeDirect: false});
const settingsCache = createSettingsMock({setDirect: true, setConnect: true});
const urlUtils = createUrlUtilsMock();
const membersConfig = new MembersConfigProvider({
config: configUtils.config,
settingsCache,
urlUtils,
ghostVersion: {original: 'v7357'},
logging: console
});
const paymentConfig = membersConfig.getStripePaymentConfig();
should.equal(paymentConfig.publicKey, 'connect_publishable');
should.equal(paymentConfig.secretKey, 'connect_secret');
});
it('Uses direct keys when stripeDirect is false, but the connect keys do not exist', function () {
configUtils.set({stripeDirect: false});
const settingsCache = createSettingsMock({setDirect: true, setConnect: false});
const urlUtils = createUrlUtilsMock();
const membersConfig = new MembersConfigProvider({
config: configUtils.config,
settingsCache,
urlUtils,
ghostVersion: {original: 'v7357'},
logging: console
});
const paymentConfig = membersConfig.getStripePaymentConfig();
should.equal(paymentConfig.publicKey, 'direct_publishable');
should.equal(paymentConfig.secretKey, 'direct_secret');
});
it('Includes the subdirectory in the webhookHandlerUrl', function () {
configUtils.set({

View file

@ -0,0 +1,85 @@
const should = require('should');
const sinon = require('sinon');
const {getConfig} = require('../../../../core/server/services/stripe/config');
describe('Stripe - config', function () {
it('Uses direct keys when stripeDirect is true, regardles of which keys exist', function () {
const fakeSettings = {
get: sinon.stub()
};
const fakeConfig = {
get: sinon.stub()
};
fakeSettings.get.withArgs('stripe_connect_secret_key').returns('connect_secret');
fakeSettings.get.withArgs('stripe_connect_publishable_key').returns('connect_publishable');
fakeSettings.get.withArgs('stripe_secret_key').returns('direct_secret');
fakeSettings.get.withArgs('stripe_publishable_key').returns('direct_publishable');
fakeConfig.get.withArgs('stripeDirect').returns(true);
const config = getConfig(fakeSettings, fakeConfig);
should.equal(config.publicKey, 'direct_publishable');
should.equal(config.secretKey, 'direct_secret');
});
it('Does not use connect keys if stripeDirect is true, and the direct keys do not exist', function () {
const fakeSettings = {
get: sinon.stub()
};
const fakeConfig = {
get: sinon.stub()
};
fakeSettings.get.withArgs('stripe_connect_secret_key').returns('connect_secret');
fakeSettings.get.withArgs('stripe_connect_publishable_key').returns('connect_publishable');
fakeSettings.get.withArgs('stripe_secret_key').returns(null);
fakeSettings.get.withArgs('stripe_publishable_key').returns(null);
fakeConfig.get.withArgs('stripeDirect').returns(true);
const config = getConfig(fakeSettings, fakeConfig);
should.equal(config, null);
});
it('Uses connect keys when stripeDirect is false, and the connect keys exist', function () {
const fakeSettings = {
get: sinon.stub()
};
const fakeConfig = {
get: sinon.stub()
};
fakeSettings.get.withArgs('stripe_connect_secret_key').returns('connect_secret');
fakeSettings.get.withArgs('stripe_connect_publishable_key').returns('connect_publishable');
fakeSettings.get.withArgs('stripe_secret_key').returns('direct_secret');
fakeSettings.get.withArgs('stripe_publishable_key').returns('direct_publishable');
fakeConfig.get.withArgs('stripeDirect').returns(false);
const config = getConfig(fakeSettings, fakeConfig);
should.equal(config.publicKey, 'connect_publishable');
should.equal(config.secretKey, 'connect_secret');
});
it('Uses direct keys when stripeDirect is false, but the connect keys do not exist', function () {
const fakeSettings = {
get: sinon.stub()
};
const fakeConfig = {
get: sinon.stub()
};
fakeSettings.get.withArgs('stripe_connect_secret_key').returns(null);
fakeSettings.get.withArgs('stripe_connect_publishable_key').returns(null);
fakeSettings.get.withArgs('stripe_secret_key').returns('direct_secret');
fakeSettings.get.withArgs('stripe_publishable_key').returns('direct_publishable');
fakeConfig.get.withArgs('stripeDirect').returns(false);
const config = getConfig(fakeSettings, fakeConfig);
should.equal(config.publicKey, 'direct_publishable');
should.equal(config.secretKey, 'direct_secret');
});
});

View file

@ -1493,10 +1493,10 @@
"@tryghost/domain-events" "^0.1.2"
"@tryghost/member-events" "^0.2.1"
"@tryghost/members-api@1.39.1":
version "1.39.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-1.39.1.tgz#5afcb41db0dad037c7ff42ba0ebaaaa60789448b"
integrity sha512-FCtI81Lhu/LbM1eQ2LxRKvK7wIM0lRv22WK/l4BUC5cYFMUhlbhtyhr45s9TH6P4/7B3YoXJkmBjEfq6MYntlA==
"@tryghost/members-api@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-2.0.0.tgz#01a7d46004705be86243e0f77f20685829b09fe0"
integrity sha512-lDGJtf6lO2vNVK4cqn7X5nJUIpZbRSnc3z32/3r5dL7b4xTR0wwHrDR9WvsROBkCxbPVvg7XyxGO9t316l5jDw==
dependencies:
"@tryghost/debug" "^0.1.2"
"@tryghost/errors" "^0.2.9"