2020-05-20 12:07:58 +03:00
|
|
|
const _ = require('lodash');
|
2021-06-15 17:36:27 +03:00
|
|
|
const logging = require('@tryghost/logging');
|
2021-05-12 15:02:27 +03:00
|
|
|
const membersService = require('./service');
|
2022-01-27 14:07:27 +03:00
|
|
|
const models = require('../../models');
|
2021-10-15 09:16:27 +03:00
|
|
|
const offersService = require('../offers/service');
|
2020-05-28 13:57:02 +03:00
|
|
|
const urlUtils = require('../../../shared/url-utils');
|
2021-06-16 11:36:58 +03:00
|
|
|
const ghostVersion = require('@tryghost/version');
|
2021-06-30 16:56:57 +03:00
|
|
|
const settingsCache = require('../../../shared/settings-cache');
|
2020-05-20 12:07:58 +03:00
|
|
|
const {formattedMemberResponse} = require('./utils');
|
2021-07-07 23:41:34 +03:00
|
|
|
const labsService = require('../../../shared/labs');
|
2021-06-21 15:29:20 +03:00
|
|
|
const config = require('../../../shared/config');
|
2019-11-21 06:01:24 +03:00
|
|
|
|
2020-04-30 21:33:09 +03:00
|
|
|
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
|
|
|
|
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
|
|
|
|
const loadMemberSession = async function (req, res, next) {
|
|
|
|
try {
|
|
|
|
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
|
|
|
Object.assign(req, {member});
|
|
|
|
res.locals.member = req.member;
|
|
|
|
next();
|
|
|
|
} catch (err) {
|
|
|
|
Object.assign(req, {member: null});
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-11-26 18:44:25 +03:00
|
|
|
const getIdentityToken = async function (req, res) {
|
2019-11-21 06:01:24 +03:00
|
|
|
try {
|
|
|
|
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
|
|
|
|
res.writeHead(200);
|
|
|
|
res.end(token);
|
|
|
|
} catch (err) {
|
2021-07-30 08:03:06 +03:00
|
|
|
res.writeHead(204);
|
|
|
|
res.end();
|
2019-11-21 06:01:24 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-11-28 10:53:30 +03:00
|
|
|
const deleteSession = async function (req, res) {
|
2019-11-21 06:01:24 +03:00
|
|
|
try {
|
|
|
|
await membersService.ssr.deleteSession(req, res);
|
|
|
|
res.writeHead(204);
|
|
|
|
res.end();
|
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(err.statusCode);
|
|
|
|
res.end(err.message);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-20 09:03:15 +03:00
|
|
|
const getMemberData = async function (req, res) {
|
|
|
|
try {
|
|
|
|
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
|
|
|
if (member) {
|
2020-05-20 12:07:58 +03:00
|
|
|
res.json(formattedMemberResponse(member));
|
|
|
|
} else {
|
|
|
|
res.json(null);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
2021-05-13 12:51:58 +03:00
|
|
|
res.writeHead(204);
|
|
|
|
res.end();
|
2020-05-20 12:07:58 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-10-15 09:16:27 +03:00
|
|
|
const getOfferData = async function (req, res) {
|
|
|
|
const offerId = req.params.id;
|
|
|
|
const offer = await offersService.api.getOffer({id: offerId});
|
|
|
|
return res.json({
|
|
|
|
offers: [offer]
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-05-20 12:07:58 +03:00
|
|
|
const updateMemberData = async function (req, res) {
|
|
|
|
try {
|
2020-05-28 17:01:00 +03:00
|
|
|
const data = _.pick(req.body, 'name', 'subscribed');
|
2020-05-20 12:07:58 +03:00
|
|
|
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
|
|
|
if (member) {
|
2020-09-04 14:41:04 +03:00
|
|
|
const options = {
|
|
|
|
id: member.id,
|
2021-07-01 20:59:59 +03:00
|
|
|
withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice']
|
2020-09-04 14:41:04 +03:00
|
|
|
};
|
|
|
|
const updatedMember = await membersService.api.members.update(data, options);
|
|
|
|
|
2020-08-24 11:08:06 +03:00
|
|
|
res.json(formattedMemberResponse(updatedMember.toJSON()));
|
2020-04-20 09:03:15 +03:00
|
|
|
} else {
|
|
|
|
res.json(null);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(err.statusCode);
|
|
|
|
res.end(err.message);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-06-04 10:33:33 +03:00
|
|
|
const getPortalProductPrices = async function () {
|
2021-05-04 18:32:20 +03:00
|
|
|
const page = await membersService.api.productRepository.list({
|
2021-06-30 12:21:29 +03:00
|
|
|
withRelated: ['monthlyPrice', 'yearlyPrice', 'benefits']
|
2021-05-04 18:32:20 +03:00
|
|
|
});
|
2021-06-04 10:33:33 +03:00
|
|
|
|
|
|
|
const products = page.data.map((productModel) => {
|
|
|
|
const product = productModel.toJSON();
|
2021-06-08 17:11:55 +03:00
|
|
|
const productPrices = [];
|
|
|
|
if (product.monthlyPrice) {
|
|
|
|
productPrices.push(product.monthlyPrice);
|
|
|
|
}
|
|
|
|
if (product.yearlyPrice) {
|
|
|
|
productPrices.push(product.yearlyPrice);
|
|
|
|
}
|
2021-05-07 12:44:56 +03:00
|
|
|
return {
|
2021-06-04 10:33:33 +03:00
|
|
|
id: product.id,
|
|
|
|
name: product.name,
|
|
|
|
description: product.description || '',
|
2021-06-11 10:06:54 +03:00
|
|
|
monthlyPrice: product.monthlyPrice,
|
|
|
|
yearlyPrice: product.yearlyPrice,
|
2021-06-30 12:21:29 +03:00
|
|
|
benefits: product.benefits,
|
2022-01-11 16:57:20 +03:00
|
|
|
type: product.type,
|
2021-06-08 17:11:55 +03:00
|
|
|
prices: productPrices
|
2021-05-07 12:44:56 +03:00
|
|
|
};
|
2021-06-04 10:33:33 +03:00
|
|
|
});
|
2022-01-13 11:38:51 +03:00
|
|
|
const defaultProduct = products.find((product) => {
|
|
|
|
return product.type === 'paid';
|
|
|
|
});
|
2021-06-11 10:06:54 +03:00
|
|
|
const defaultPrices = defaultProduct ? defaultProduct.prices : [];
|
|
|
|
let portalProducts = defaultProduct ? [defaultProduct] : [];
|
|
|
|
if (labsService.isSet('multipleProducts')) {
|
|
|
|
portalProducts = products;
|
|
|
|
}
|
2021-06-04 10:33:33 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
prices: defaultPrices,
|
2021-06-11 10:06:54 +03:00
|
|
|
products: portalProducts
|
2021-06-04 10:33:33 +03:00
|
|
|
};
|
2021-05-04 18:32:20 +03:00
|
|
|
};
|
|
|
|
|
2020-04-30 21:50:40 +03:00
|
|
|
const getMemberSiteData = async function (req, res) {
|
2020-06-29 17:22:42 +03:00
|
|
|
const isStripeConfigured = membersService.config.isStripeConnected();
|
2020-09-17 10:02:53 +03:00
|
|
|
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
2021-01-14 11:49:16 +03:00
|
|
|
const firstpromoterId = settingsCache.get('firstpromoter') ? settingsCache.get('firstpromoter_id') : '';
|
2020-09-17 10:02:53 +03:00
|
|
|
const blogDomain = domain && domain[1];
|
2020-10-15 12:14:03 +03:00
|
|
|
let supportAddress = settingsCache.get('members_support_address') || 'noreply';
|
2020-09-17 10:02:53 +03:00
|
|
|
if (!supportAddress.includes('@')) {
|
|
|
|
supportAddress = `${supportAddress}@${blogDomain}`;
|
|
|
|
}
|
2021-06-04 10:33:33 +03:00
|
|
|
const {products = [], prices = []} = await getPortalProductPrices() || {};
|
2021-06-21 15:29:20 +03:00
|
|
|
const portalVersion = config.get('portal:version');
|
|
|
|
|
2020-04-30 21:50:40 +03:00
|
|
|
const response = {
|
|
|
|
title: settingsCache.get('title'),
|
|
|
|
description: settingsCache.get('description'),
|
|
|
|
logo: settingsCache.get('logo'),
|
2020-07-23 14:34:04 +03:00
|
|
|
icon: settingsCache.get('icon'),
|
2020-06-23 15:33:57 +03:00
|
|
|
accent_color: settingsCache.get('accent_color'),
|
2020-04-30 21:50:40 +03:00
|
|
|
url: urlUtils.urlFor('home', true),
|
|
|
|
version: ghostVersion.safe,
|
2021-06-21 15:29:20 +03:00
|
|
|
portal_version: portalVersion,
|
2021-05-07 20:26:16 +03:00
|
|
|
free_price_name: settingsCache.get('members_free_price_name'),
|
|
|
|
free_price_description: settingsCache.get('members_free_price_description'),
|
2020-06-19 17:04:40 +03:00
|
|
|
allow_self_signup: membersService.config.getAllowSelfSignup(),
|
2021-04-27 18:32:22 +03:00
|
|
|
members_signup_access: settingsCache.get('members_signup_access'),
|
2020-06-19 17:04:40 +03:00
|
|
|
is_stripe_configured: isStripeConfigured,
|
|
|
|
portal_button: settingsCache.get('portal_button'),
|
|
|
|
portal_name: settingsCache.get('portal_name'),
|
2020-07-07 11:11:16 +03:00
|
|
|
portal_plans: settingsCache.get('portal_plans'),
|
|
|
|
portal_button_icon: settingsCache.get('portal_button_icon'),
|
|
|
|
portal_button_signup_text: settingsCache.get('portal_button_signup_text'),
|
2020-09-08 18:18:57 +03:00
|
|
|
portal_button_style: settingsCache.get('portal_button_style'),
|
2021-01-14 11:49:16 +03:00
|
|
|
firstpromoter_id: firstpromoterId,
|
2021-06-11 10:06:54 +03:00
|
|
|
members_support_address: supportAddress,
|
|
|
|
prices,
|
|
|
|
products
|
2020-04-30 21:50:40 +03:00
|
|
|
};
|
2021-06-21 12:11:15 +03:00
|
|
|
if (labsService.isSet('multipleProducts')) {
|
|
|
|
response.portal_products = settingsCache.get('portal_products');
|
|
|
|
}
|
2021-06-21 15:29:20 +03:00
|
|
|
if (config.get('portal_sentry') && !config.get('portal_sentry').disabled) {
|
|
|
|
response.portal_sentry = {
|
2021-06-22 10:01:20 +03:00
|
|
|
dsn: config.get('portal_sentry').dsn,
|
|
|
|
env: config.get('env')
|
2021-06-21 15:29:20 +03:00
|
|
|
};
|
|
|
|
}
|
2020-04-30 21:50:40 +03:00
|
|
|
res.json({site: response});
|
|
|
|
};
|
|
|
|
|
2020-04-29 20:23:55 +03:00
|
|
|
const createSessionFromMagicLink = async function (req, res, next) {
|
2019-11-21 06:01:24 +03:00
|
|
|
if (!req.url.includes('token=')) {
|
|
|
|
return next();
|
|
|
|
}
|
2020-04-29 20:23:55 +03:00
|
|
|
|
2020-05-07 23:55:50 +03:00
|
|
|
// req.query is a plain object, copy it to a URLSearchParams object so we can call toString()
|
|
|
|
const searchParams = new URLSearchParams('');
|
|
|
|
Object.keys(req.query).forEach((param) => {
|
|
|
|
// don't copy the token param
|
|
|
|
if (param !== 'token') {
|
|
|
|
searchParams.set(param, req.query[param]);
|
|
|
|
}
|
|
|
|
});
|
2020-04-29 20:23:55 +03:00
|
|
|
|
2020-05-07 23:55:50 +03:00
|
|
|
try {
|
2020-11-19 12:58:32 +03:00
|
|
|
const member = await membersService.ssr.exchangeTokenForSession(req, res);
|
2021-01-28 20:25:38 +03:00
|
|
|
const subscriptions = member && member.subscriptions || [];
|
2020-11-19 12:58:32 +03:00
|
|
|
|
2021-01-22 15:53:34 +03:00
|
|
|
const action = req.query.action;
|
2020-11-23 12:36:45 +03:00
|
|
|
|
2022-02-10 17:56:43 +03:00
|
|
|
if (action === 'signup' || action === 'signup-paid') {
|
2020-11-23 12:36:45 +03:00
|
|
|
let customRedirect = '';
|
2022-01-27 14:07:27 +03:00
|
|
|
const mostRecentActiveSubscription = subscriptions
|
|
|
|
.sort((a, b) => {
|
|
|
|
const aStartDate = new Date(a.start_date);
|
|
|
|
const bStartDate = new Date(b.start_date);
|
|
|
|
return bStartDate.valueOf() - aStartDate.valueOf();
|
|
|
|
})
|
|
|
|
.find(sub => ['active', 'trialing'].includes(sub.status));
|
|
|
|
if (mostRecentActiveSubscription) {
|
2022-02-09 19:42:40 +03:00
|
|
|
if (labsService.isSet('tierWelcomePages')) {
|
2022-01-27 14:07:27 +03:00
|
|
|
customRedirect = mostRecentActiveSubscription.tier.welcome_page_url;
|
|
|
|
} else {
|
|
|
|
customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
|
|
|
|
}
|
2020-11-19 12:58:32 +03:00
|
|
|
} else {
|
2022-02-09 19:42:40 +03:00
|
|
|
if (labsService.isSet('tierWelcomePages')) {
|
2022-01-27 14:07:27 +03:00
|
|
|
const freeTier = await models.Product.findOne({type: 'free'});
|
|
|
|
customRedirect = freeTier && freeTier.get('welcome_page_url') || '';
|
|
|
|
} else {
|
|
|
|
customRedirect = settingsCache.get('members_free_signup_redirect') || '';
|
|
|
|
}
|
2020-11-19 12:58:32 +03:00
|
|
|
}
|
|
|
|
|
2020-11-23 12:36:45 +03:00
|
|
|
if (customRedirect && customRedirect !== '/') {
|
|
|
|
const baseUrl = urlUtils.getSiteUrl();
|
|
|
|
const ensureEndsWith = (string, endsWith) => (string.endsWith(endsWith) ? string : string + endsWith);
|
|
|
|
const removeLeadingSlash = string => string.replace(/^\//, '');
|
2020-11-19 12:58:32 +03:00
|
|
|
|
2020-11-23 12:36:45 +03:00
|
|
|
const redirectUrl = new URL(removeLeadingSlash(ensureEndsWith(customRedirect, '/')), ensureEndsWith(baseUrl, '/'));
|
2020-04-29 20:23:55 +03:00
|
|
|
|
2020-11-23 12:36:45 +03:00
|
|
|
return res.redirect(redirectUrl.href);
|
|
|
|
}
|
2020-11-19 17:47:08 +03:00
|
|
|
}
|
|
|
|
|
2020-11-23 12:36:45 +03:00
|
|
|
// Do a standard 302 redirect to the homepage, with success=true
|
|
|
|
searchParams.set('success', true);
|
|
|
|
res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);
|
2019-11-21 06:01:24 +03:00
|
|
|
} catch (err) {
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(err.message);
|
2020-11-19 12:58:32 +03:00
|
|
|
|
|
|
|
// Do a standard 302 redirect to the homepage, with success=false
|
2020-05-08 15:03:44 +03:00
|
|
|
searchParams.set('success', false);
|
2020-11-23 12:36:45 +03:00
|
|
|
res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);
|
2019-11-21 06:01:24 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set req.member & res.locals.member if a cookie is set
|
|
|
|
module.exports = {
|
2020-04-29 20:23:55 +03:00
|
|
|
loadMemberSession,
|
|
|
|
createSessionFromMagicLink,
|
2020-04-21 18:48:42 +03:00
|
|
|
getIdentityToken,
|
2020-04-20 09:03:15 +03:00
|
|
|
getMemberData,
|
2021-10-15 09:16:27 +03:00
|
|
|
getOfferData,
|
2020-05-20 12:07:58 +03:00
|
|
|
updateMemberData,
|
2020-04-30 21:50:40 +03:00
|
|
|
getMemberSiteData,
|
2022-01-18 18:56:47 +03:00
|
|
|
deleteSession
|
2019-11-21 06:01:24 +03:00
|
|
|
};
|