Ghost/ghost/members-api/lib/controllers/router.js
Fabien "egg" O'Carroll 69df4b7c05 Added support for dynamic allowSelfSignup config
refs https://github.com/TryGhost/Team/issues/1257

This gets us closer to not having to reload the MembersAPI when config
is changed which will help stop bugs arising from multiple instances of
the MembersAPI being created.
2022-01-10 17:53:30 +02:00

313 lines
10 KiB
JavaScript

const logging = require('@tryghost/logging');
const _ = require('lodash');
module.exports = class RouterController {
/**
* RouterController
*
* @param {object} deps
* @param {any} deps.offersAPI
* @param {any} deps.paymentsService
* @param {any} deps.productRepository
* @param {any} deps.memberRepository
* @param {any} deps.StripePrice
* @param {() => boolean} deps.allowSelfSignup
* @param {any} deps.magicLinkService
* @param {import('@tryghost/members-stripe-service')} deps.stripeAPIService
* @param {any} deps.tokenService
* @param {{isSet(name: string): boolean}} deps.labsService
* @param {any} deps.config
*/
constructor({
offersAPI,
paymentsService,
productRepository,
memberRepository,
StripePrice,
allowSelfSignup,
magicLinkService,
stripeAPIService,
tokenService,
sendEmailWithMagicLink,
labsService,
config
}) {
this._offersAPI = offersAPI;
this._paymentsService = paymentsService;
this._productRepository = productRepository;
this._memberRepository = memberRepository;
this._StripePrice = StripePrice;
this._allowSelfSignup = allowSelfSignup;
this._magicLinkService = magicLinkService;
this._stripeAPIService = stripeAPIService;
this._tokenService = tokenService;
this._sendEmailWithMagicLink = sendEmailWithMagicLink;
this.labsService = labsService;
this._config = config;
}
async ensureStripe(_req, res, next) {
if (!this._stripeAPIService.configured) {
res.writeHead(400);
return res.end('Stripe not configured');
}
try {
await this._stripeAPIService.ready();
next();
} catch (err) {
res.writeHead(500);
return res.end('There was an error configuring stripe');
}
}
async createCheckoutSetupSession(req, res) {
const identity = req.body.identity;
if (!identity) {
res.writeHead(400);
return res.end();
}
let email;
try {
if (!identity) {
email = null;
} else {
const claims = await this._tokenService.decodeToken(identity);
email = claims && claims.sub;
}
} catch (err) {
res.writeHead(401);
return res.end('Unauthorized');
}
const member = email ? await this._memberRepository.get({email}) : null;
if (!member) {
res.writeHead(403);
return res.end('Bad Request.');
}
let customer;
if (!req.body.subscription_id) {
customer = await this._stripeAPIService.getCustomerForMemberCheckoutSession(member);
} else {
const subscriptions = await member.related('stripeSubscriptions').fetch();
const subscription = subscriptions.models.find((sub) => {
return sub.get('subscription_id') === req.body.subscription_id;
});
if (!subscription) {
res.writeHead(404);
res.end(`Could not find subscription ${req.body.subscription_id}`);
}
customer = await this._stripeAPIService.getCustomer(subscription.get('customer_id'));
}
const session = await this._stripeAPIService.createCheckoutSetupSession(customer, {
successUrl: req.body.successUrl || this._config.billingSuccessUrl,
cancelUrl: req.body.cancelUrl || this._config.billingCancelUrl,
subscription_id: req.body.subscription_id
});
const publicKey = this._stripeAPIService.getPublicKey();
const sessionInfo = {
sessionId: session.id,
publicKey
};
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify(sessionInfo));
}
async createCheckoutSession(req, res) {
let ghostPriceId = req.body.priceId;
const identity = req.body.identity;
const offerId = req.body.offerId;
const metadata = req.body.metadata;
if (!ghostPriceId && !offerId) {
res.writeHead(400);
return res.end('Bad Request.');
}
if (offerId && ghostPriceId) {
res.writeHead(400);
return res.end('Bad Request.');
}
let couponId = null;
if (offerId) {
try {
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.');
}
}
const price = await this._StripePrice.findOne({
id: ghostPriceId
});
if (!price) {
res.writeHead(404);
return res.end('Not Found.');
}
const priceId = price.get('stripe_price_id');
let email;
try {
if (!identity) {
email = null;
} else {
const claims = await this._tokenService.decodeToken(identity);
email = claims && claims.sub;
}
} catch (err) {
res.writeHead(401);
return res.end('Unauthorized');
}
const member = email ? await this._memberRepository.get({email}, {withRelated: ['stripeCustomers', 'products']}) : null;
let successUrl = req.body.successUrl || this._config.checkoutSuccessUrl;
let cancelUrl = req.body.cancelUrl || this._config.checkoutCancelUrl;
if (!member && req.body.customerEmail && !req.body.successUrl) {
const memberExistsForCustomer = await this._memberRepository.get({email: req.body.customerEmail});
if (!memberExistsForCustomer) {
successUrl = await this._magicLinkService.getMagicLink({
tokenData: {
email: req.body.customerEmail
},
type: 'signup'
});
}
}
if (!member) {
const customer = null;
const session = await this._stripeAPIService.createCheckoutSession(priceId, customer, {
coupon: couponId,
successUrl,
cancelUrl,
customerEmail: req.body.customerEmail,
metadata: metadata
});
const publicKey = this._stripeAPIService.getPublicKey();
const sessionInfo = {
publicKey,
sessionId: session.id
};
res.writeHead(200, {
'Content-Type': 'application/json'
});
return res.end(JSON.stringify(sessionInfo));
}
if (member.related('products').length !== 0) {
res.writeHead(403);
return res.end('No permission');
}
let stripeCustomer;
for (const customer of member.related('stripeCustomers').models) {
try {
const fetchedCustomer = await this._stripeAPIService.getCustomer(customer.get('customer_id'));
if (!fetchedCustomer.deleted) {
stripeCustomer = fetchedCustomer;
break;
}
} catch (err) {
logging.info('Ignoring error for fetching customer for checkout');
}
}
if (!stripeCustomer) {
stripeCustomer = await this._stripeAPIService.createCustomer({email: member.get('email')});
}
try {
const session = await this._stripeAPIService.createCheckoutSession(priceId, stripeCustomer, {
coupon: couponId,
successUrl,
cancelUrl,
metadata: metadata
});
const publicKey = this._stripeAPIService.getPublicKey();
const sessionInfo = {
publicKey,
sessionId: session.id
};
res.writeHead(200, {
'Content-Type': 'application/json'
});
return res.end(JSON.stringify(sessionInfo));
} catch (e) {
const error = e.message || 'Unable to initiate checkout session';
res.writeHead(400);
return res.end(error);
}
}
async sendMagicLink(req, res) {
const {email, emailType, requestSrc} = req.body;
if (!email) {
res.writeHead(400);
return res.end('Bad Request.');
}
try {
if (!this._allowSelfSignup()) {
const member = await this._memberRepository.get({email});
if (member) {
const tokenData = {};
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc});
}
} else {
const tokenData = _.pick(req.body, ['labels', 'name']);
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc});
}
res.writeHead(201);
return res.end('Created.');
} catch (err) {
if (err.code === 'EENVELOPE') {
logging.error(err);
res.writeHead(400);
return res.end('Bad Request.');
}
const statusCode = (err && err.statusCode) || 500;
logging.error(err);
res.writeHead(statusCode);
return res.end('Internal Server Error.');
}
}
};