Ghost/ghost/members-api/lib/controllers/router.js
Fabien O'Carroll 5172e40646 Used OffersAPI over OfferRepository in MembersAPI
no-issue

The OfferRepository deals with domain objects in the Offers module, and
as such is not suitable for use with "external" services. This update
means that MembersAPI can deal with POJO DTOs so that there is not a
dependency on the internals of the Offers module. Just on the contract
it holds with the outside world.
2021-10-13 11:11:12 +02:00

284 lines
9.2 KiB
JavaScript

const common = require('../../lib/common');
const _ = require('lodash');
/**
* RouterController
*
* @param {object} deps
* @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
* @param {any} deps.logging
*/
module.exports = class RouterController {
constructor({
offersAPI,
productRepository,
memberRepository,
StripePrice,
allowSelfSignup,
magicLinkService,
stripeAPIService,
tokenService,
sendEmailWithMagicLink,
labsService,
config,
logging
}) {
this._offersAPI = offersAPI;
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;
this._logging = logging;
}
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;
if (!ghostPriceId && !offerId) {
res.writeHead(400);
return res.end('Bad Request.');
}
if (offerId && ghostPriceId) {
res.writeHead(400);
return res.end('Bad Request.');
}
let coupon = null;
if (offerId && this.labsService.isSet('offers')) {
try {
const offer = await this._offersAPI.getOffer({id: offerId});
const tier = (await this._productRepository.get(offer.tier)).toJSON();
if (offer.cadence === 'month') {
ghostPriceId = tier.monthly_price_id;
} else {
ghostPriceId = tier.yearly_price_id;
}
coupon = {
id: offer.stripeCouponId
};
} 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;
if (!member) {
const customer = null;
const session = await this._stripeAPIService.createCheckoutSession(priceId, customer, {
coupon,
successUrl: req.body.successUrl || this._config.checkoutSuccessUrl,
cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl,
customerEmail: req.body.customerEmail,
metadata: req.body.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) {
this._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,
successUrl: req.body.successUrl || this._config.checkoutSuccessUrl,
cancelUrl: req.body.cancelUrl || this._config.checkoutCancelUrl,
metadata: req.body.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) {
const statusCode = (err && err.statusCode) || 500;
common.logging.error(err);
res.writeHead(statusCode);
return res.end('Internal Server Error.');
}
}
};