const {Router} = require('express'); const body = require('body-parser'); const MagicLink = require('@tryghost/magic-link'); const errors = require('@tryghost/errors'); const MemberAnalyticsService = require('@tryghost/member-analytics-service'); const MembersAnalyticsIngress = require('@tryghost/members-analytics-ingress'); const PaymentsService = require('@tryghost/members-payments'); const TokenService = require('./services/token'); const GeolocationSerice = require('./services/geolocation'); const MemberBREADService = require('./services/member-bread'); const MemberRepository = require('./repositories/member'); const EventRepository = require('./repositories/event'); const ProductRepository = require('./repositories/product'); const RouterController = require('./controllers/router'); const MemberController = require('./controllers/member'); const WellKnownController = require('./controllers/well-known'); module.exports = function MembersAPI({ tokenConfig: { issuer, privateKey, publicKey }, auth: { allowSelfSignup = () => true, getSigninURL, tokenProvider }, mail: { transporter, getText, getHTML, getSubject }, models: { EmailRecipient, StripeCustomer, StripeCustomerSubscription, Member, MemberCancelEvent, MemberSubscribeEvent, MemberLoginEvent, MemberPaidSubscriptionEvent, MemberPaymentEvent, MemberStatusEvent, MemberProductEvent, MemberEmailChangeEvent, MemberAnalyticEvent, Offer, OfferRedemption, StripeProduct, StripePrice, Product, Settings }, stripeAPIService, offersAPI, labsService }) { const tokenService = new TokenService({ privateKey, publicKey, issuer }); const memberAnalyticsService = MemberAnalyticsService.create(MemberAnalyticEvent); memberAnalyticsService.eventHandler.setupSubscribers(); const productRepository = new ProductRepository({ Product, Settings, StripeProduct, StripePrice, stripeAPIService }); const memberRepository = new MemberRepository({ stripeAPIService, tokenService, productRepository, Member, MemberCancelEvent, MemberSubscribeEvent, MemberPaidSubscriptionEvent, MemberEmailChangeEvent, MemberStatusEvent, MemberProductEvent, OfferRedemption, StripeCustomer, StripeCustomerSubscription }); const eventRepository = new EventRepository({ EmailRecipient, MemberSubscribeEvent, MemberPaidSubscriptionEvent, MemberPaymentEvent, MemberStatusEvent, MemberLoginEvent, labsService }); const memberBREADService = new MemberBREADService({ offersAPI, memberRepository, emailService: { async sendEmailWithMagicLink({email, requestedType}) { return sendEmailWithMagicLink({ email, requestedType, options: { forceEmailType: true } }); } }, labsService, stripeService: stripeAPIService }); const geolocationService = new GeolocationSerice(); const magicLinkService = new MagicLink({ transporter, tokenProvider, getSigninURL, getText, getHTML, getSubject }); const memberController = new MemberController({ memberRepository, productRepository, StripePrice, tokenService, sendEmailWithMagicLink }); const paymentsService = new PaymentsService({ Offer, offersAPI, stripeAPIService }); const routerController = new RouterController({ offersAPI, paymentsService, productRepository, memberRepository, StripePrice, allowSelfSignup, magicLinkService, stripeAPIService, tokenService, sendEmailWithMagicLink, labsService }); const wellKnownController = new WellKnownController({ tokenService }); async function hasActiveStripeSubscriptions() { const firstActiveSubscription = await StripeCustomerSubscription.findOne({ status: 'active' }); if (firstActiveSubscription) { return true; } const firstTrialingSubscription = await StripeCustomerSubscription.findOne({ status: 'trialing' }); if (firstTrialingSubscription) { return true; } const firstUnpaidSubscription = await StripeCustomerSubscription.findOne({ status: 'unpaid' }); if (firstUnpaidSubscription) { return true; } const firstPastDueSubscription = await StripeCustomerSubscription.findOne({ status: 'past_due' }); if (firstPastDueSubscription) { return true; } return false; } const users = memberRepository; async function sendEmailWithMagicLink({email, requestedType, tokenData, options = {forceEmailType: false}, requestSrc = ''}) { let type = requestedType; if (!options.forceEmailType) { const member = await users.get({email}); if (member) { type = 'signin'; } else if (type !== 'subscribe') { type = 'signup'; } } return magicLinkService.sendMagicLink({email, type, requestSrc, tokenData: Object.assign({email}, tokenData)}); } function getMagicLink(email) { return magicLinkService.getMagicLink({tokenData: {email}, type: 'signin'}); } async function getMemberDataFromMagicLinkToken(token) { const {email, labels = [], name = '', oldEmail} = await magicLinkService.getDataFromToken(token); if (!email) { return null; } const member = oldEmail ? await getMemberIdentityData(oldEmail) : await getMemberIdentityData(email); if (member) { await MemberLoginEvent.add({member_id: member.id}); if (oldEmail) { // user exists but wants to change their email address if (oldEmail) { member.email = email; } await users.update(member, {id: member.id}); return getMemberIdentityData(email); } return member; } const newMember = await users.create({name, email, labels}); await MemberLoginEvent.add({member_id: newMember.id}); return getMemberIdentityData(email); } async function getMemberIdentityData(email) { return memberBREADService.read({email}); } async function getMemberIdentityToken(email) { const member = await getMemberIdentityData(email); if (!member) { return null; } return tokenService.encodeIdentityToken({sub: member.email}); } async function setMemberGeolocationFromIp(email, ip) { if (!email || !ip) { throw new errors.IncorrectUsageError({ message: 'setMemberGeolocationFromIp() expects email and ip arguments to be present' }); } // toJSON() is needed here otherwise users.update() will pick methods off // the model object rather than data and fail to edit correctly const member = (await users.get({email})).toJSON(); if (!member) { throw new errors.NotFoundError({ message: `Member with email address ${email} does not exist` }); } // max request time is 500ms so shouldn't slow requests down too much let geolocation = JSON.stringify(await geolocationService.getGeolocationFromIP(ip)); if (geolocation) { member.geolocation = geolocation; await users.update(member, {id: member.id}); } return getMemberIdentityData(email); } const middleware = { sendMagicLink: Router().use( body.json(), (req, res) => routerController.sendMagicLink(req, res) ), createCheckoutSession: Router().use( body.json(), (req, res) => routerController.createCheckoutSession(req, res) ), createCheckoutSetupSession: Router().use( body.json(), (req, res) => routerController.createCheckoutSetupSession(req, res) ), createEvents: Router().use( body.json(), (req, res) => MembersAnalyticsIngress.createEvents(req, res) ), updateEmailAddress: Router().use( body.json(), (req, res) => memberController.updateEmailAddress(req, res) ), updateSubscription: Router({mergeParams: true}).use( body.json(), (req, res) => memberController.updateSubscription(req, res) ), wellKnown: Router() .get('/jwks.json', (req, res) => wellKnownController.getPublicKeys(req, res) ) }; const getPublicConfig = function () { return Promise.resolve({ publicKey, issuer }); }; const bus = new (require('events').EventEmitter)(); bus.emit('ready'); return { middleware, getMemberDataFromMagicLinkToken, getMemberIdentityToken, getMemberIdentityData, setMemberGeolocationFromIp, getPublicConfig, bus, sendEmailWithMagicLink, getMagicLink, hasActiveStripeSubscriptions, members: users, memberBREADService, events: eventRepository, productRepository }; };