Skip to content

Commit

Permalink
Fixed checkout session creation for existing members (#403)
Browse files Browse the repository at this point in the history
refs TryGhost/Product#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.
  • Loading branch information
allouis committed Jun 1, 2022
1 parent 5bef2a1 commit 8e2c600
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 59 deletions.
20 changes: 14 additions & 6 deletions ghost/members-api/lib/MembersAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
130 changes: 77 additions & 53 deletions ghost/members-api/lib/controllers/router.js
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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.status === 'archived') {
throw new NoPermissionError({
message: tpl(messages.offerArchived)
});
}

if (offer.cadence === 'month') {
ghostPriceId = tier.monthly_price_id;
} else {
ghostPriceId = tier.yearly_price_id;
}
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;
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.');
}
metadata.offer = offer.id;
}

if (!ghostPriceId) {
Expand All @@ -194,34 +206,36 @@ 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');

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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.');
Expand All @@ -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.');
Expand Down

0 comments on commit 8e2c600

Please sign in to comment.