0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00
ghost/core/server/services/members/middleware.js
Rishabh Garg c7b247a079
Added member endpoints for managing newsletter subscriptions (#14624)
refs TryGhost/Team#1495

With multiple newsletters, members are allowed to manage their newsletter pref via email unsubscribe link with member uuid. Since Portal needs to manage member's newsletter pref via their UUID, we need new endpoints on members that allow fetch/update of newsletter subscriptions via only uuid. The endpoints return only limited data for a member that are needed for the UI.

- adds endpoint to fetch newsletter subscriptions for member via uuid
- adds endpoint to update newsletter subscriptions for member via uuid
2022-04-28 17:14:17 +05:30

339 lines
12 KiB
JavaScript

const _ = require('lodash');
const logging = require('@tryghost/logging');
const membersService = require('./service');
const models = require('../../models');
const offersService = require('../offers/service');
const urlUtils = require('../../../shared/url-utils');
const ghostVersion = require('@tryghost/version');
const settingsCache = require('../../../shared/settings-cache');
const {formattedMemberResponse} = require('./utils');
const labsService = require('../../../shared/labs');
const config = require('../../../shared/config');
const newslettersService = require('../newsletters');
// @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();
}
};
const getIdentityToken = async function (req, res) {
try {
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
res.writeHead(200);
res.end(token);
} catch (err) {
res.writeHead(204);
res.end();
}
};
const deleteSession = async function (req, res) {
try {
await membersService.ssr.deleteSession(req, res);
res.writeHead(204);
res.end();
} catch (err) {
res.writeHead(err.statusCode);
res.end(err.message);
}
};
const getMemberData = async function (req, res) {
try {
const member = await membersService.ssr.getMemberDataFromSession(req, res);
if (member) {
res.json(formattedMemberResponse(member));
} else {
res.json(null);
}
} catch (err) {
res.writeHead(204);
res.end();
}
};
const getOfferData = async function (req, res) {
const offerId = req.params.id;
const offer = await offersService.api.getOffer({id: offerId});
return res.json({
offers: [offer]
});
};
const getMemberNewsletters = async function (req, res) {
try {
const memberUuid = req.query.uuid;
if (!memberUuid) {
res.writeHead(400);
return res.end('Invalid member uuid');
}
const memberData = await membersService.api.members.get({
uuid: memberUuid
}, {
withRelated: ['newsletters']
});
if (!memberData) {
res.writeHead(404);
return res.end('Email address not found.');
}
const data = _.pick(memberData.toJSON(), 'uuid', 'email', 'name', 'newsletters', 'status');
return res.json(data);
} catch (err) {
res.writeHead(400);
res.end('Failed to unsubscribe this email address');
}
};
const updateMemberNewsletters = async function (req, res) {
try {
const memberUuid = req.query.uuid;
if (!memberUuid) {
res.writeHead(400);
return res.end('Invalid member uuid');
}
const data = _.pick(req.body, 'newsletters');
const memberData = await membersService.api.members.get({
uuid: memberUuid
});
if (!memberData) {
res.writeHead(404);
return res.end('Email address not found.');
}
const options = {
id: memberData.get('id'),
withRelated: ['newsletters']
};
const updatedMember = await membersService.api.members.update(data, options);
const updatedMemberData = _.pick(updatedMember.toJSON(), ['uuid', 'email', 'name', 'newsletters', 'status']);
res.json(updatedMemberData);
} catch (err) {
res.writeHead(400);
res.end('Failed to update newsletters');
}
};
const updateMemberData = async function (req, res) {
try {
const data = _.pick(req.body, 'name', 'subscribed', 'newsletters');
const member = await membersService.ssr.getMemberDataFromSession(req, res);
if (member) {
const options = {
id: member.id,
withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'newsletters']
};
const updatedMember = await membersService.api.members.update(data, options);
res.json(formattedMemberResponse(updatedMember.toJSON()));
} else {
res.json(null);
}
} catch (err) {
res.writeHead(err.statusCode);
res.end(err.message);
}
};
const getPortalProductPrices = async function () {
const page = await membersService.api.productRepository.list({
withRelated: ['monthlyPrice', 'yearlyPrice', 'benefits']
});
const products = page.data.map((productModel) => {
const product = productModel.toJSON();
const productPrices = [];
if (product.monthlyPrice) {
productPrices.push(product.monthlyPrice);
}
if (product.yearlyPrice) {
productPrices.push(product.yearlyPrice);
}
return {
id: product.id,
name: product.name,
description: product.description || '',
monthlyPrice: product.monthlyPrice,
yearlyPrice: product.yearlyPrice,
benefits: product.benefits,
active: product.active,
type: product.type,
visibility: product.visibility,
prices: productPrices
};
}).filter((product) => {
return !!product.active;
});
const defaultProduct = products.find((product) => {
return product.type === 'paid';
});
const defaultPrices = defaultProduct ? defaultProduct.prices : [];
let portalProducts = defaultProduct ? [defaultProduct] : [];
if (labsService.isSet('multipleProducts')) {
portalProducts = products;
}
return {
prices: defaultPrices,
products: portalProducts
};
};
const getSiteNewsletters = async function () {
try {
return await newslettersService.browse({filter: 'status:active', limit: 'all'});
} catch (err) {
logging.warn('Failed to fetch site newsletters');
logging.warn(err.message);
return [];
}
};
const getMemberSiteData = async function (req, res) {
const isStripeConfigured = membersService.config.isStripeConnected();
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
const firstpromoterId = settingsCache.get('firstpromoter') ? settingsCache.get('firstpromoter_id') : '';
const blogDomain = domain && domain[1];
let supportAddress = settingsCache.get('members_support_address') || 'noreply';
if (!supportAddress.includes('@')) {
supportAddress = `${supportAddress}@${blogDomain}`;
}
const {products = [], prices = []} = await getPortalProductPrices() || {};
const portalVersion = config.get('portal:version');
const newsletters = await getSiteNewsletters();
const response = {
title: settingsCache.get('title'),
description: settingsCache.get('description'),
logo: settingsCache.get('logo'),
icon: settingsCache.get('icon'),
accent_color: settingsCache.get('accent_color'),
url: urlUtils.urlFor('home', true),
version: ghostVersion.safe,
portal_version: portalVersion,
free_price_name: settingsCache.get('members_free_price_name'),
free_price_description: settingsCache.get('members_free_price_description'),
allow_self_signup: membersService.config.getAllowSelfSignup(),
members_signup_access: settingsCache.get('members_signup_access'),
is_stripe_configured: isStripeConfigured,
portal_button: settingsCache.get('portal_button'),
portal_name: settingsCache.get('portal_name'),
portal_plans: settingsCache.get('portal_plans'),
portal_button_icon: settingsCache.get('portal_button_icon'),
portal_button_signup_text: settingsCache.get('portal_button_signup_text'),
portal_button_style: settingsCache.get('portal_button_style'),
firstpromoter_id: firstpromoterId,
members_support_address: supportAddress,
prices,
products
};
if (labsService.isSet('multipleNewsletters')) {
response.newsletters = newsletters;
}
if (labsService.isSet('multipleProducts')) {
response.portal_products = settingsCache.get('portal_products');
}
if (config.get('portal_sentry') && !config.get('portal_sentry').disabled) {
response.portal_sentry = {
dsn: config.get('portal_sentry').dsn,
env: config.get('env')
};
}
res.json({site: response});
};
const createSessionFromMagicLink = async function (req, res, next) {
if (!req.url.includes('token=')) {
return next();
}
// 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]);
}
});
try {
const member = await membersService.ssr.exchangeTokenForSession(req, res);
const subscriptions = member && member.subscriptions || [];
const action = req.query.action;
if (action === 'signup' || action === 'signup-paid' || action === 'subscribe') {
let customRedirect = '';
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) {
if (labsService.isSet('tierWelcomePages')) {
customRedirect = mostRecentActiveSubscription.tier.welcome_page_url;
} else {
customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
}
} else {
if (labsService.isSet('tierWelcomePages')) {
const freeTier = await models.Product.findOne({type: 'free'});
customRedirect = freeTier && freeTier.get('welcome_page_url') || '';
} else {
customRedirect = settingsCache.get('members_free_signup_redirect') || '';
}
}
if (customRedirect && customRedirect !== '/') {
const baseUrl = urlUtils.getSiteUrl();
const ensureEndsWith = (string, endsWith) => (string.endsWith(endsWith) ? string : string + endsWith);
const removeLeadingSlash = string => string.replace(/^\//, '');
const redirectUrl = new URL(removeLeadingSlash(ensureEndsWith(customRedirect, '/')), ensureEndsWith(baseUrl, '/'));
return res.redirect(redirectUrl.href);
}
}
// Do a standard 302 redirect to the homepage, with success=true
searchParams.set('success', true);
res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);
} catch (err) {
logging.warn(err.message);
// Do a standard 302 redirect to the homepage, with success=false
searchParams.set('success', false);
res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);
}
};
// Set req.member & res.locals.member if a cookie is set
module.exports = {
loadMemberSession,
createSessionFromMagicLink,
getIdentityToken,
getMemberNewsletters,
getMemberData,
getOfferData,
updateMemberData,
updateMemberNewsletters,
getMemberSiteData,
deleteSession
};