Fixed checkout session creation for existing members (#403)

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

We weren't using the `req.body.customerEmail` to load a member and
check their existing tiers, this meant that existing members which
were signed out and attempted to create a stripe checkout session were
able to.
This commit is contained in:
Fabien 'egg' O'Carroll 2022-06-01 10:40:52 +01:00 committed by GitHub
parent 5bef2a1068
commit 8e2c600612
2 changed files with 93 additions and 61 deletions

View File

@ -246,30 +246,38 @@ module.exports = function MembersAPI({
return getMemberIdentityData(email);
}
const forwardError = fn => async (req, res, next) => {
try {
await fn(req, res, next);
} catch (err) {
next(err);
}
};
const middleware = {
sendMagicLink: Router().use(
body.json(),
(req, res) => routerController.sendMagicLink(req, res)
forwardError((req, res) => routerController.sendMagicLink(req, res))
),
createCheckoutSession: Router().use(
body.json(),
(req, res) => routerController.createCheckoutSession(req, res)
forwardError((req, res) => routerController.createCheckoutSession(req, res))
),
createCheckoutSetupSession: Router().use(
body.json(),
(req, res) => routerController.createCheckoutSetupSession(req, res)
forwardError((req, res) => routerController.createCheckoutSetupSession(req, res))
),
createEvents: Router().use(
body.json(),
(req, res) => MembersAnalyticsIngress.createEvents(req, res)
forwardError((req, res) => MembersAnalyticsIngress.createEvents(req, res))
),
updateEmailAddress: Router().use(
body.json(),
(req, res) => memberController.updateEmailAddress(req, res)
forwardError((req, res) => memberController.updateEmailAddress(req, res))
),
updateSubscription: Router({mergeParams: true}).use(
body.json(),
(req, res) => memberController.updateSubscription(req, res)
forwardError((req, res) => memberController.updateSubscription(req, res))
),
wellKnown: Router()
.get('/jwks.json',

View File

@ -1,5 +1,16 @@
const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging');
const _ = require('lodash');
const {BadRequestError, NoPermissionError, NotFoundError, UnauthorizedError} = require('@tryghost/errors');
const messages = {
badRequest: 'Bad Request.',
notFound: 'Not Found.',
offerArchived: 'This offer is archived.',
tierArchived: 'This tier is archived.',
existingSubscription: 'A subscription exists for this Member.',
unableToCheckout: 'Unable to initiate checkout session'
};
module.exports = class RouterController {
/**
@ -127,55 +138,56 @@ module.exports = class RouterController {
const metadata = req.body.metadata;
if (!ghostPriceId && !offerId && !tierId && !cadence) {
res.writeHead(400);
return res.end('Bad Request.');
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (offerId && (ghostPriceId || (tierId && cadence))) {
res.writeHead(400);
return res.end('Bad Request.');
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (ghostPriceId && tierId && cadence) {
res.writeHead(400);
return res.end('Bad Request.');
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (tierId && !cadence) {
res.writeHead(400);
return res.end('Bad Request.');
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (cadence && cadence !== 'month' && cadence !== 'year') {
res.writeHead(400);
return res.end('Bad Request.');
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
let couponId = null;
if (offerId) {
try {
const offer = await this._offersAPI.getOffer({id: offerId});
const tier = (await this._productRepository.get(offer.tier)).toJSON();
const offer = await this._offersAPI.getOffer({id: offerId});
const tier = (await this._productRepository.get(offer.tier)).toJSON();
if (offer.status === 'archived') {
res.writeHead(403);
return res.end('Offer is archived.');
}
if (offer.cadence === 'month') {
ghostPriceId = tier.monthly_price_id;
} else {
ghostPriceId = tier.yearly_price_id;
}
const coupon = await this._paymentsService.getCouponForOffer(offerId);
couponId = coupon.id;
metadata.offer = offer.id;
} catch (err) {
res.writeHead(500);
return res.end('Could not use Offer.');
if (offer.status === 'archived') {
throw new NoPermissionError({
message: tpl(messages.offerArchived)
});
}
if (offer.cadence === 'month') {
ghostPriceId = tier.monthly_price_id;
} else {
ghostPriceId = tier.yearly_price_id;
}
const coupon = await this._paymentsService.getCouponForOffer(offerId);
couponId = coupon.id;
metadata.offer = offer.id;
}
if (!ghostPriceId) {
@ -194,8 +206,9 @@ module.exports = class RouterController {
});
if (!price) {
res.writeHead(404);
return res.end('Not Found.');
throw new NotFoundError({
message: tpl(messages.notFound)
});
}
const priceId = price.get('stripe_price_id');
@ -203,25 +216,26 @@ module.exports = class RouterController {
const product = await this._productRepository.get({stripe_price_id: priceId});
if (product.get('active') !== true) {
res.writeHead(403);
return res.end('Tier is archived.');
throw new NoPermissionError({
message: tpl(messages.tierArchived)
});
}
let email;
try {
if (!identity) {
email = null;
} else {
let member = null;
if (identity) {
try {
const claims = await this._tokenService.decodeToken(identity);
email = claims && claims.sub;
const email = claims && claims.sub;
if (email) {
member = await this._memberRepository.get({email}, {withRelated: ['stripeCustomers', 'products']});
}
} catch (err) {
throw new UnauthorizedError({err});
}
} catch (err) {
res.writeHead(401);
return res.end('Unauthorized');
} else if (req.body.customerEmail) {
member = await this._memberRepository.get({email: req.body.customerEmail}, {withRelated: ['stripeCustomers', 'products']});
}
const member = email ? await this._memberRepository.get({email}, {withRelated: ['stripeCustomers', 'products']}) : null;
let successUrl = req.body.successUrl;
let cancelUrl = req.body.cancelUrl;
@ -261,8 +275,17 @@ module.exports = class RouterController {
}
if (member.related('products').length !== 0) {
res.writeHead(403);
return res.end('No permission');
if (!identity && req.body.customerEmail) {
try {
await this._sendEmailWithMagicLink({email: req.body.customerEmail, requestedType: 'signin'});
} catch (err) {
logging.warn(err);
}
}
throw new NoPermissionError({
message: messages.existingSubscription,
code: 'CANNOT_CHECKOUT_WITH_EXISTING_SUBSCRIPTION'
});
}
let stripeCustomer;
@ -302,15 +325,16 @@ module.exports = class RouterController {
});
return res.end(JSON.stringify(sessionInfo));
} catch (e) {
const error = e.message || 'Unable to initiate checkout session';
res.writeHead(400);
return res.end(error);
} catch (err) {
throw new BadRequestError({
err,
message: tpl(messages.unableToCheckout)
});
}
}
async sendMagicLink(req, res) {
const {email, emailType, requestSrc} = req.body;
const {email, emailType} = req.body;
if (!email) {
res.writeHead(400);
return res.end('Bad Request.');
@ -321,11 +345,11 @@ module.exports = class RouterController {
const member = await this._memberRepository.get({email});
if (member) {
const tokenData = {};
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc});
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType});
}
} else {
const tokenData = _.pick(req.body, ['labels', 'name', 'newsletters']);
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc});
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType});
}
res.writeHead(201);
return res.end('Created.');