diff --git a/ghost/members-api/lib/MembersAPI.js b/ghost/members-api/lib/MembersAPI.js index 3d011ee721..db91643ba0 100644 --- a/ghost/members-api/lib/MembersAPI.js +++ b/ghost/members-api/lib/MembersAPI.js @@ -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', diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index 2d617e5071..9f5905407a 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -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.');