2019-11-07 13:13:20 +03:00
|
|
|
const {URL} = require('url');
|
|
|
|
const crypto = require('crypto');
|
2021-01-28 21:07:45 +03:00
|
|
|
const createKeypair = require('keypair');
|
2020-04-29 20:23:55 +03:00
|
|
|
const path = require('path');
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2020-01-28 07:25:00 +03:00
|
|
|
const COMPLIMENTARY_PLAN = {
|
|
|
|
name: 'Complimentary',
|
|
|
|
currency: 'usd',
|
|
|
|
interval: 'year',
|
|
|
|
amount: '0'
|
|
|
|
};
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
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;
|
|
|
|
}
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_getDomain() {
|
2021-03-24 21:01:00 +03:00
|
|
|
const url = this._urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
|
|
|
const domain = (url && url[1]) || '';
|
2021-03-24 21:03:49 +03:00
|
|
|
if (domain.startsWith('www.')) {
|
2021-03-24 21:01:00 +03:00
|
|
|
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
|
|
|
|
}
|
|
|
|
return domain;
|
2020-05-28 18:55:23 +03:00
|
|
|
}
|
2020-04-17 12:29:23 +03:00
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
getEmailFromAddress() {
|
2020-06-29 17:22:42 +03:00
|
|
|
const fromAddress = this._settingsCache.get('members_from_address') || 'noreply';
|
2020-04-17 12:29:23 +03:00
|
|
|
|
2020-06-05 19:20:04 +03:00
|
|
|
// Any fromAddress without domain uses site domain, like default setting `noreply`
|
|
|
|
if (fromAddress.indexOf('@') < 0) {
|
|
|
|
return `${fromAddress}@${this._getDomain()}`;
|
|
|
|
}
|
|
|
|
return fromAddress;
|
2020-05-28 18:55:23 +03:00
|
|
|
}
|
2020-04-17 12:29:23 +03:00
|
|
|
|
2020-09-03 08:00:09 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-26 11:00:16 +03:00
|
|
|
getAuthEmailFromAddress() {
|
2020-09-03 08:00:09 +03:00
|
|
|
return this.getEmailSupportAddress() || this.getEmailFromAddress();
|
2020-08-26 11:00:16 +03:00
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
getPublicPlans() {
|
|
|
|
const defaultPriceData = {
|
|
|
|
monthly: 0,
|
2020-06-29 17:22:42 +03:00
|
|
|
yearly: 0,
|
2021-02-25 12:49:07 +03:00
|
|
|
currency: 'USD'
|
2020-05-28 18:55:23 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
2020-07-07 14:10:10 +03:00
|
|
|
const plans = this._settingsCache.get('stripe_plans') || [];
|
2020-05-28 18:55:23 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
const priceData = plans.reduce((prices, plan) => {
|
2020-05-28 18:55:23 +03:00
|
|
|
const numberAmount = 0 + plan.amount;
|
|
|
|
const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0;
|
|
|
|
return Object.assign(prices, {
|
|
|
|
[plan.name.toLowerCase()]: dollarAmount
|
|
|
|
});
|
|
|
|
}, {});
|
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
priceData.currency = plans[0].currency || 'USD';
|
2020-05-28 18:55:23 +03:00
|
|
|
|
|
|
|
if (Number.isInteger(priceData.monthly) && Number.isInteger(priceData.yearly)) {
|
|
|
|
return priceData;
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultPriceData;
|
|
|
|
} catch (err) {
|
|
|
|
return defaultPriceData;
|
2020-04-17 12:29:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-28 18:58:51 +03:00
|
|
|
/**
|
2020-06-29 17:22:42 +03:00
|
|
|
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
|
|
|
|
* @returns {{publicKey: string, secretKey: string} | null}
|
2020-05-28 18:58:51 +03:00
|
|
|
*/
|
2020-06-29 17:22:42 +03:00
|
|
|
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;
|
2020-05-28 18:58:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2020-06-29 17:22:42 +03:00
|
|
|
secretKey,
|
|
|
|
publicKey
|
2020-05-28 18:58:51 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
/**
|
|
|
|
* @returns {{publicKey: string, secretKey: string} | null}
|
|
|
|
*/
|
|
|
|
getActiveStripeKeys() {
|
|
|
|
const stripeDirect = this._config.get('stripeDirect');
|
2020-06-09 14:02:38 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
if (stripeDirect) {
|
|
|
|
return this.getStripeKeys('direct');
|
|
|
|
}
|
2020-06-09 14:02:38 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
const connectKeys = this.getStripeKeys('connect');
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
if (!connectKeys) {
|
|
|
|
return this.getStripeKeys('direct');
|
2020-05-28 18:55:23 +03:00
|
|
|
}
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
return connectKeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
isStripeConnected() {
|
|
|
|
return this.getActiveStripeKeys() !== null;
|
|
|
|
}
|
2020-05-28 18:55:23 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
getStripeUrlConfig() {
|
2020-05-28 18:55:23 +03:00
|
|
|
const siteUrl = this._urlUtils.getSiteUrl();
|
|
|
|
|
2020-07-10 13:07:11 +03:00
|
|
|
const webhookHandlerUrl = new URL('members/webhooks/stripe/', siteUrl);
|
2020-05-28 18:55:23 +03:00
|
|
|
|
|
|
|
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');
|
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
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();
|
2020-05-28 18:58:51 +03:00
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
if (!stripeApiKeys) {
|
2020-06-12 19:38:06 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
return {
|
2020-05-28 18:58:51 +03:00
|
|
|
publicKey: stripeApiKeys.publicKey,
|
|
|
|
secretKey: stripeApiKeys.secretKey,
|
2020-06-29 17:22:42 +03:00
|
|
|
checkoutSuccessUrl: urls.checkoutSuccess,
|
|
|
|
checkoutCancelUrl: urls.checkoutCancel,
|
|
|
|
billingSuccessUrl: urls.billingSuccess,
|
|
|
|
billingCancelUrl: urls.billingCancel,
|
|
|
|
webhookHandlerUrl: urls.webhookHandler,
|
2020-07-09 17:17:54 +03:00
|
|
|
webhook: {
|
|
|
|
id: this._settingsCache.get('members_stripe_webhook_id'),
|
|
|
|
secret: this._settingsCache.get('members_stripe_webhook_secret')
|
|
|
|
},
|
2020-09-21 15:15:41 +03:00
|
|
|
enablePromoCodes: this._config.get('enableStripePromoCodes'),
|
2020-06-29 17:22:42 +03:00
|
|
|
product: {
|
|
|
|
name: this._settingsCache.get('stripe_product_name')
|
|
|
|
},
|
2020-07-07 14:10:10 +03:00
|
|
|
plans: [COMPLIMENTARY_PLAN].concat(this._settingsCache.get('stripe_plans') || []),
|
2020-05-28 18:55:23 +03:00
|
|
|
appInfo: {
|
|
|
|
name: 'Ghost',
|
|
|
|
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
|
|
|
|
version: this._ghostVersion.original,
|
|
|
|
url: 'https://ghost.org/'
|
|
|
|
}
|
|
|
|
};
|
2019-11-07 13:13:20 +03:00
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
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);
|
2019-11-07 13:13:20 +03:00
|
|
|
}
|
2020-05-28 18:55:23 +03:00
|
|
|
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;
|
2019-11-07 13:13:20 +03:00
|
|
|
}
|
2020-05-28 18:55:23 +03:00
|
|
|
|
|
|
|
getAllowSelfSignup() {
|
2021-04-27 18:22:43 +03:00
|
|
|
// '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;
|
2019-11-07 13:13:20 +03:00
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
getTokenConfig() {
|
|
|
|
const {href: membersApiUrl} = new URL(
|
2021-03-03 04:42:03 +03:00
|
|
|
this._urlUtils.getApiPath({version: 'v4', type: 'members'}),
|
2020-05-28 18:55:23 +03:00
|
|
|
this._urlUtils.urlFor('admin', true)
|
|
|
|
);
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2021-01-28 21:07:45 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
return {
|
|
|
|
issuer: membersApiUrl,
|
2021-01-28 21:07:45 +03:00
|
|
|
publicKey,
|
|
|
|
privateKey
|
2020-05-28 18:55:23 +03:00
|
|
|
};
|
|
|
|
}
|
2019-11-07 13:13:20 +03:00
|
|
|
|
2021-01-22 15:53:34 +03:00
|
|
|
getSigninURL(token, type) {
|
2020-05-28 18:55:23 +03:00
|
|
|
const siteUrl = this._urlUtils.getSiteUrl();
|
|
|
|
const signinURL = new URL(siteUrl);
|
|
|
|
signinURL.pathname = path.join(signinURL.pathname, '/members/');
|
|
|
|
signinURL.searchParams.set('token', token);
|
2021-01-22 15:53:34 +03:00
|
|
|
signinURL.searchParams.set('action', type);
|
2020-05-28 18:55:23 +03:00
|
|
|
return signinURL.href;
|
|
|
|
}
|
2019-11-07 13:13:20 +03:00
|
|
|
}
|
|
|
|
|
2020-05-28 18:55:23 +03:00
|
|
|
module.exports = MembersConfigProvider;
|