Ghost/core/server/services/members/config.js
Fabien O'Carroll a138586c83 Updated stripe_plans & portal_plans settings
refs https://github.com/TryGhost/Team/issues/586

We are no longer using the `stripe_plans` setting, instead we are using
the `stripe_prices` database table. However, we must keep the setting as
the migration from the setting to the database is not done as a standard
migration, but in code. This means our code has to still read and pass
the setting because we will never know if the migration in code has run
yet.

The `portal_plans` setting has been updated to only include 'free' by
default, because the setting must include id's now rather than names.
2021-05-07 16:31:08 +01:00

266 lines
8.6 KiB
JavaScript

const {URL} = require('url');
const crypto = require('crypto');
const createKeypair = require('keypair');
const path = require('path');
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;
}
/**
* @private
*/
_getDomain() {
const url = this._urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
const domain = (url && url[1]) || '';
if (domain.startsWith('www.')) {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
}
getEmailFromAddress() {
const fromAddress = this._settingsCache.get('members_from_address') || 'noreply';
// Any fromAddress without domain uses site domain, like default setting `noreply`
if (fromAddress.indexOf('@') < 0) {
return `${fromAddress}@${this._getDomain()}`;
}
return fromAddress;
}
getEmailSupportAddress() {
const supportAddress = this._settingsCache.get('members_support_address') || 'noreply';
// Any fromAddress without domain uses site domain, like default setting `noreply`
if (supportAddress.indexOf('@') < 0) {
return `${supportAddress}@${this._getDomain()}`;
}
return supportAddress;
}
getAuthEmailFromAddress() {
return this.getEmailSupportAddress() || this.getEmailFromAddress();
}
getPublicPlans() {
const defaultPriceData = {
monthly: 0,
yearly: 0,
currency: 'USD'
};
try {
const plans = this._settingsCache.get('stripe_plans') || [];
const priceData = 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 = plans[0].currency || 'USD';
if (Number.isInteger(priceData.monthly) && Number.isInteger(priceData.yearly)) {
return priceData;
}
return defaultPriceData;
} catch (err) {
return defaultPriceData;
}
}
/**
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
* @returns {{publicKey: string, secretKey: string} | null}
*/
getStripeKeys(type) {
if (type !== 'direct' && type !== 'connect') {
throw new Error();
}
const secretKey = this._settingsCache.get(`stripe_${type === 'connect' ? 'connect_' : ''}secret_key`);
const publicKey = this._settingsCache.get(`stripe_${type === 'connect' ? 'connect_' : ''}publishable_key`);
if (!secretKey || !publicKey) {
return null;
}
return {
secretKey,
publicKey
};
}
/**
* @returns {{publicKey: string, secretKey: string} | null}
*/
getActiveStripeKeys() {
const stripeDirect = this._config.get('stripeDirect');
if (stripeDirect) {
return this.getStripeKeys('direct');
}
const connectKeys = this.getStripeKeys('connect');
if (!connectKeys) {
return this.getStripeKeys('direct');
}
return connectKeys;
}
isStripeConnected() {
return this.getActiveStripeKeys() !== null;
}
getStripeUrlConfig() {
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 {
checkoutSuccess: checkoutSuccessUrl.href,
checkoutCancel: checkoutCancelUrl.href,
billingSuccess: billingSuccessUrl.href,
billingCancel: billingCancelUrl.href,
webhookHandler: webhookHandlerUrl.href
};
}
getStripePaymentConfig() {
if (!this.isStripeConnected()) {
return null;
}
const stripeApiKeys = this.getActiveStripeKeys();
const urls = this.getStripeUrlConfig();
if (!stripeApiKeys) {
return null;
}
return {
publicKey: stripeApiKeys.publicKey,
secretKey: stripeApiKeys.secretKey,
checkoutSuccessUrl: urls.checkoutSuccess,
checkoutCancelUrl: urls.checkoutCancel,
billingSuccessUrl: urls.billingSuccess,
billingCancelUrl: urls.billingCancel,
webhookHandlerUrl: urls.webhookHandler,
webhook: {
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/'
}
};
}
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() {
// 'invite' and 'none' members signup access disables all signup
if (this._settingsCache.get('members_signup_access') !== 'all') {
return false;
}
// if stripe is not connected then selected plans mean nothing.
// disabling signup would be done by switching to "invite only" mode
if (!this.isStripeConnected()) {
return true;
}
// self signup must be available for free plan signup to work
const hasFreePlan = this._settingsCache.get('portal_plans').includes('free');
if (hasFreePlan) {
return true;
}
// signup access is enabled but there's no free plan, don't allow self signup
return false;
}
getTokenConfig() {
const {href: membersApiUrl} = new URL(
this._urlUtils.getApiPath({version: 'v4', type: 'members'}),
this._urlUtils.urlFor('admin', true)
);
let privateKey = this._settingsCache.get('members_private_key');
let publicKey = this._settingsCache.get('members_public_key');
if (!privateKey || !publicKey) {
this._logging.warn('Could not find members_private_key, using dynamically generated keypair');
const keypair = createKeypair({bits: 1024});
privateKey = keypair.private;
publicKey = keypair.public;
}
return {
issuer: membersApiUrl,
publicKey,
privateKey
};
}
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;
}
}
module.exports = MembersConfigProvider;