2021-07-14 15:32:09 +03:00
|
|
|
const {Router} = require('express');
|
|
|
|
const body = require('body-parser');
|
|
|
|
const MagicLink = require('@tryghost/magic-link');
|
2021-12-02 17:46:58 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2022-08-25 22:15:34 +03:00
|
|
|
const logging = require('@tryghost/logging');
|
2021-07-14 15:32:09 +03:00
|
|
|
|
2021-10-21 14:35:29 +03:00
|
|
|
const PaymentsService = require('@tryghost/members-payments');
|
2021-09-21 14:37:24 +03:00
|
|
|
|
2021-07-15 19:54:24 +03:00
|
|
|
const TokenService = require('./services/token');
|
|
|
|
const GeolocationSerice = require('./services/geolocation');
|
2021-08-25 14:30:49 +03:00
|
|
|
const MemberBREADService = require('./services/member-bread');
|
2021-07-15 19:54:24 +03:00
|
|
|
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');
|
2021-07-19 15:02:45 +03:00
|
|
|
const WellKnownController = require('./controllers/well-known');
|
2021-07-14 15:32:09 +03:00
|
|
|
|
2022-12-05 12:56:01 +03:00
|
|
|
const {EmailSuppressedEvent} = require('@tryghost/email-suppression-list');
|
|
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
module.exports = function MembersAPI({
|
|
|
|
tokenConfig: {
|
|
|
|
issuer,
|
|
|
|
privateKey,
|
|
|
|
publicKey
|
|
|
|
},
|
|
|
|
auth: {
|
2022-01-10 12:42:05 +03:00
|
|
|
allowSelfSignup = () => true,
|
2021-07-14 15:32:09 +03:00
|
|
|
getSigninURL,
|
|
|
|
tokenProvider
|
|
|
|
},
|
|
|
|
mail: {
|
|
|
|
transporter,
|
|
|
|
getText,
|
|
|
|
getHTML,
|
|
|
|
getSubject
|
|
|
|
},
|
|
|
|
models: {
|
2022-01-18 17:53:51 +03:00
|
|
|
EmailRecipient,
|
2021-07-14 15:32:09 +03:00
|
|
|
StripeCustomer,
|
|
|
|
StripeCustomerSubscription,
|
|
|
|
Member,
|
2022-11-16 10:29:00 +03:00
|
|
|
MemberNewsletter,
|
2022-03-12 00:44:28 +03:00
|
|
|
MemberCancelEvent,
|
2021-07-14 15:32:09 +03:00
|
|
|
MemberSubscribeEvent,
|
|
|
|
MemberLoginEvent,
|
|
|
|
MemberPaidSubscriptionEvent,
|
|
|
|
MemberPaymentEvent,
|
|
|
|
MemberStatusEvent,
|
2021-08-23 13:00:19 +03:00
|
|
|
MemberProductEvent,
|
2021-07-14 15:32:09 +03:00
|
|
|
MemberEmailChangeEvent,
|
2022-08-24 17:11:25 +03:00
|
|
|
MemberCreatedEvent,
|
|
|
|
SubscriptionCreatedEvent,
|
2022-09-21 11:25:51 +03:00
|
|
|
MemberLinkClickEvent,
|
2022-11-30 15:16:13 +03:00
|
|
|
EmailSpamComplaintEvent,
|
2021-10-20 15:40:34 +03:00
|
|
|
Offer,
|
2021-10-18 16:27:17 +03:00
|
|
|
OfferRedemption,
|
2021-07-14 15:32:09 +03:00
|
|
|
StripeProduct,
|
|
|
|
StripePrice,
|
2022-01-24 14:10:14 +03:00
|
|
|
Product,
|
2022-07-25 18:48:23 +03:00
|
|
|
Settings,
|
2022-10-17 16:44:18 +03:00
|
|
|
Comment,
|
|
|
|
MemberFeedback
|
2021-07-14 15:32:09 +03:00
|
|
|
},
|
2022-10-21 12:28:09 +03:00
|
|
|
tiersService,
|
2021-10-04 14:34:17 +03:00
|
|
|
stripeAPIService,
|
2021-10-13 12:11:12 +03:00
|
|
|
offersAPI,
|
2022-04-05 20:26:18 +03:00
|
|
|
labsService,
|
2022-08-18 18:38:42 +03:00
|
|
|
newslettersService,
|
2022-11-18 12:28:13 +03:00
|
|
|
memberAttributionService,
|
|
|
|
emailSuppressionList
|
2021-07-14 15:32:09 +03:00
|
|
|
}) {
|
2021-09-17 12:25:57 +03:00
|
|
|
const tokenService = new TokenService({
|
|
|
|
privateKey,
|
|
|
|
publicKey,
|
|
|
|
issuer
|
|
|
|
});
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
const productRepository = new ProductRepository({
|
|
|
|
Product,
|
2022-01-24 14:10:14 +03:00
|
|
|
Settings,
|
2021-07-14 15:32:09 +03:00
|
|
|
StripeProduct,
|
|
|
|
StripePrice,
|
|
|
|
stripeAPIService
|
|
|
|
});
|
|
|
|
|
|
|
|
const memberRepository = new MemberRepository({
|
|
|
|
stripeAPIService,
|
2021-09-17 12:25:57 +03:00
|
|
|
tokenService,
|
2022-04-05 20:26:18 +03:00
|
|
|
newslettersService,
|
|
|
|
labsService,
|
2021-07-14 15:32:09 +03:00
|
|
|
productRepository,
|
|
|
|
Member,
|
2022-11-16 10:29:00 +03:00
|
|
|
MemberNewsletter,
|
2022-03-12 00:44:28 +03:00
|
|
|
MemberCancelEvent,
|
2022-10-03 17:50:28 +03:00
|
|
|
MemberSubscribeEventModel: MemberSubscribeEvent,
|
2021-07-14 15:32:09 +03:00
|
|
|
MemberPaidSubscriptionEvent,
|
|
|
|
MemberEmailChangeEvent,
|
|
|
|
MemberStatusEvent,
|
2021-08-23 13:00:19 +03:00
|
|
|
MemberProductEvent,
|
2021-10-18 16:27:17 +03:00
|
|
|
OfferRedemption,
|
2021-07-14 15:32:09 +03:00
|
|
|
StripeCustomer,
|
2022-04-19 10:15:33 +03:00
|
|
|
StripeCustomerSubscription,
|
|
|
|
offerRepository: offersAPI.repository
|
2021-07-14 15:32:09 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const eventRepository = new EventRepository({
|
2022-01-18 17:53:51 +03:00
|
|
|
EmailRecipient,
|
2021-07-14 15:32:09 +03:00
|
|
|
MemberSubscribeEvent,
|
|
|
|
MemberPaidSubscriptionEvent,
|
|
|
|
MemberPaymentEvent,
|
|
|
|
MemberStatusEvent,
|
2022-01-18 17:53:51 +03:00
|
|
|
MemberLoginEvent,
|
2022-08-24 17:11:25 +03:00
|
|
|
MemberCreatedEvent,
|
|
|
|
SubscriptionCreatedEvent,
|
2022-09-21 11:25:51 +03:00
|
|
|
MemberLinkClickEvent,
|
2022-10-17 16:44:18 +03:00
|
|
|
MemberFeedback,
|
2022-11-30 15:16:13 +03:00
|
|
|
EmailSpamComplaintEvent,
|
2022-07-25 18:48:23 +03:00
|
|
|
Comment,
|
2022-08-24 17:11:25 +03:00
|
|
|
labsService,
|
|
|
|
memberAttributionService
|
2021-07-14 15:32:09 +03:00
|
|
|
});
|
|
|
|
|
2021-08-25 14:30:49 +03:00
|
|
|
const memberBREADService = new MemberBREADService({
|
2021-10-20 15:32:41 +03:00
|
|
|
offersAPI,
|
2021-09-14 14:18:34 +03:00
|
|
|
memberRepository,
|
|
|
|
emailService: {
|
2021-11-02 13:37:07 +03:00
|
|
|
async sendEmailWithMagicLink({email, requestedType}) {
|
|
|
|
return sendEmailWithMagicLink({
|
|
|
|
email,
|
|
|
|
requestedType,
|
|
|
|
options: {
|
|
|
|
forceEmailType: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-09-14 14:18:34 +03:00
|
|
|
},
|
|
|
|
labsService,
|
2022-08-19 23:39:18 +03:00
|
|
|
stripeService: stripeAPIService,
|
2022-11-18 12:28:13 +03:00
|
|
|
memberAttributionService,
|
|
|
|
emailSuppressionList
|
2021-08-25 14:30:49 +03:00
|
|
|
});
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
const geolocationService = new GeolocationSerice();
|
|
|
|
|
|
|
|
const magicLinkService = new MagicLink({
|
|
|
|
transporter,
|
|
|
|
tokenProvider,
|
|
|
|
getSigninURL,
|
|
|
|
getText,
|
|
|
|
getHTML,
|
|
|
|
getSubject
|
|
|
|
});
|
|
|
|
|
2021-10-21 14:35:29 +03:00
|
|
|
const paymentsService = new PaymentsService({
|
2022-10-21 11:13:32 +03:00
|
|
|
StripeProduct,
|
|
|
|
StripePrice,
|
|
|
|
StripeCustomer,
|
2021-10-21 14:35:29 +03:00
|
|
|
Offer,
|
|
|
|
offersAPI,
|
|
|
|
stripeAPIService
|
|
|
|
});
|
|
|
|
|
2022-11-07 11:19:26 +03:00
|
|
|
const memberController = new MemberController({
|
|
|
|
memberRepository,
|
|
|
|
productRepository,
|
|
|
|
paymentsService,
|
|
|
|
tiersService,
|
|
|
|
StripePrice,
|
|
|
|
tokenService,
|
|
|
|
sendEmailWithMagicLink
|
|
|
|
});
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
const routerController = new RouterController({
|
2021-10-13 12:11:12 +03:00
|
|
|
offersAPI,
|
2021-10-21 14:35:29 +03:00
|
|
|
paymentsService,
|
2022-10-21 12:28:09 +03:00
|
|
|
tiersService,
|
2021-07-14 15:32:09 +03:00
|
|
|
memberRepository,
|
|
|
|
StripePrice,
|
|
|
|
allowSelfSignup,
|
|
|
|
magicLinkService,
|
|
|
|
stripeAPIService,
|
|
|
|
tokenService,
|
|
|
|
sendEmailWithMagicLink,
|
2022-08-18 18:38:42 +03:00
|
|
|
memberAttributionService,
|
2022-02-09 16:00:39 +03:00
|
|
|
labsService
|
2021-07-14 15:32:09 +03:00
|
|
|
});
|
|
|
|
|
2021-07-19 15:02:45 +03:00
|
|
|
const wellKnownController = new WellKnownController({
|
2021-12-02 17:46:58 +03:00
|
|
|
tokenService
|
2021-07-19 15:02:45 +03:00
|
|
|
});
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
const users = memberRepository;
|
|
|
|
|
2022-07-15 13:02:58 +03:00
|
|
|
async function sendEmailWithMagicLink({email, requestedType, tokenData, options = {forceEmailType: false}, referrer = null}) {
|
2021-07-14 15:32:09 +03:00
|
|
|
let type = requestedType;
|
|
|
|
if (!options.forceEmailType) {
|
|
|
|
const member = await users.get({email});
|
|
|
|
if (member) {
|
|
|
|
type = 'signin';
|
|
|
|
} else if (type !== 'subscribe') {
|
|
|
|
type = 'signup';
|
|
|
|
}
|
|
|
|
}
|
2022-10-05 13:42:42 +03:00
|
|
|
return magicLinkService.sendMagicLink({email, type, tokenData: Object.assign({email, type}, tokenData), referrer});
|
2021-07-14 15:32:09 +03:00
|
|
|
}
|
|
|
|
|
2022-10-05 13:42:42 +03:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} email
|
|
|
|
* @param {'signin'|'signup'} type When you specify 'signin' this will prevent the creation of a new member if no member is found with the provided email
|
|
|
|
* @param {*} [tokenData] Optional token data to add to the token
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
function getMagicLink(email, type, tokenData = {}) {
|
|
|
|
return magicLinkService.getMagicLink({
|
|
|
|
tokenData: {email, ...tokenData},
|
|
|
|
type
|
|
|
|
});
|
2022-08-18 18:38:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getTokenDataFromMagicLinkToken(token) {
|
|
|
|
return await magicLinkService.getDataFromToken(token);
|
2021-07-14 15:32:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMemberDataFromMagicLinkToken(token) {
|
2022-10-05 13:42:42 +03:00
|
|
|
const {email, labels = [], name = '', oldEmail, newsletters, attribution, reqIp, type} = await getTokenDataFromMagicLinkToken(token);
|
2021-07-14 15:32:09 +03:00
|
|
|
if (!email) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const member = oldEmail ? await getMemberIdentityData(oldEmail) : await getMemberIdentityData(email);
|
|
|
|
|
|
|
|
if (member) {
|
|
|
|
await MemberLoginEvent.add({member_id: member.id});
|
2022-10-05 13:42:42 +03:00
|
|
|
if (oldEmail && (!type || type === 'updateEmail')) {
|
2021-07-14 15:32:09 +03:00
|
|
|
// user exists but wants to change their email address
|
2022-04-28 14:16:30 +03:00
|
|
|
await users.update({email}, {id: member.id});
|
2021-07-14 15:32:09 +03:00
|
|
|
return getMemberIdentityData(email);
|
|
|
|
}
|
|
|
|
return member;
|
|
|
|
}
|
2022-08-25 22:15:34 +03:00
|
|
|
|
2022-10-05 13:42:42 +03:00
|
|
|
// Note: old tokens can still have a missing type (we can remove this after a couple of weeks)
|
|
|
|
if (type && !['signup', 'subscribe'].includes(type)) {
|
|
|
|
// Don't allow sign up
|
|
|
|
// Note that we use the type from inside the magic token so this behaviour can't be changed
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-08-25 22:15:34 +03:00
|
|
|
let geolocation;
|
|
|
|
if (reqIp) {
|
|
|
|
try {
|
|
|
|
geolocation = JSON.stringify(await geolocationService.getGeolocationFromIP(reqIp));
|
|
|
|
} catch (err) {
|
|
|
|
logging.warn(err);
|
|
|
|
// no-op, we don't want to stop anything working due to
|
|
|
|
// geolocation lookup failing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const newMember = await users.create({name, email, labels, newsletters, attribution, geolocation});
|
2022-08-25 10:28:26 +03:00
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
await MemberLoginEvent.add({member_id: newMember.id});
|
|
|
|
return getMemberIdentityData(email);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMemberIdentityData(email) {
|
2021-08-26 17:07:06 +03:00
|
|
|
return memberBREADService.read({email});
|
2021-07-14 15:32:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2021-12-02 17:46:58 +03:00
|
|
|
throw new errors.IncorrectUsageError({
|
2021-07-14 15:32:09 +03:00
|
|
|
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) {
|
2021-12-02 17:46:58 +03:00
|
|
|
throw new errors.NotFoundError({
|
2021-07-14 15:32:09 +03:00
|
|
|
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) {
|
2022-04-28 14:16:30 +03:00
|
|
|
await users.update({geolocation}, {id: member.id});
|
2021-07-14 15:32:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return getMemberIdentityData(email);
|
|
|
|
}
|
|
|
|
|
2022-06-01 12:40:52 +03:00
|
|
|
const forwardError = fn => async (req, res, next) => {
|
|
|
|
try {
|
|
|
|
await fn(req, res, next);
|
|
|
|
} catch (err) {
|
|
|
|
next(err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
const middleware = {
|
|
|
|
sendMagicLink: Router().use(
|
|
|
|
body.json(),
|
2022-06-01 12:40:52 +03:00
|
|
|
forwardError((req, res) => routerController.sendMagicLink(req, res))
|
2021-07-14 15:32:09 +03:00
|
|
|
),
|
|
|
|
createCheckoutSession: Router().use(
|
|
|
|
body.json(),
|
2022-06-01 12:40:52 +03:00
|
|
|
forwardError((req, res) => routerController.createCheckoutSession(req, res))
|
2021-07-14 15:32:09 +03:00
|
|
|
),
|
|
|
|
createCheckoutSetupSession: Router().use(
|
|
|
|
body.json(),
|
2022-06-01 12:40:52 +03:00
|
|
|
forwardError((req, res) => routerController.createCheckoutSetupSession(req, res))
|
2021-07-14 15:32:09 +03:00
|
|
|
),
|
2021-09-22 14:32:02 +03:00
|
|
|
updateEmailAddress: Router().use(
|
|
|
|
body.json(),
|
2022-06-01 12:40:52 +03:00
|
|
|
forwardError((req, res) => memberController.updateEmailAddress(req, res))
|
2021-09-22 14:32:02 +03:00
|
|
|
),
|
2021-07-14 15:32:09 +03:00
|
|
|
updateSubscription: Router({mergeParams: true}).use(
|
|
|
|
body.json(),
|
2022-06-01 12:40:52 +03:00
|
|
|
forwardError((req, res) => memberController.updateSubscription(req, res))
|
2021-07-14 15:32:09 +03:00
|
|
|
),
|
2021-07-19 15:02:45 +03:00
|
|
|
wellKnown: Router()
|
|
|
|
.get('/jwks.json',
|
|
|
|
(req, res) => wellKnownController.getPublicKeys(req, res)
|
|
|
|
)
|
2021-07-14 15:32:09 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const getPublicConfig = function () {
|
|
|
|
return Promise.resolve({
|
|
|
|
publicKey,
|
|
|
|
issuer
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const bus = new (require('events').EventEmitter)();
|
|
|
|
|
2022-01-17 13:10:57 +03:00
|
|
|
bus.emit('ready');
|
2021-07-14 15:32:09 +03:00
|
|
|
|
2022-12-05 12:56:01 +03:00
|
|
|
DomainEvents.subscribe(EmailSuppressedEvent, async function (event) {
|
2022-12-09 06:30:44 +03:00
|
|
|
if (!labsService.isSet('suppressionList')) {
|
|
|
|
return;
|
|
|
|
}
|
2022-12-05 12:56:01 +03:00
|
|
|
const member = await memberRepository.get({email: event.data.emailAddress});
|
|
|
|
if (!member) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await memberRepository.update({newsletters: []}, {id: member.id});
|
|
|
|
});
|
|
|
|
|
2021-07-14 15:32:09 +03:00
|
|
|
return {
|
|
|
|
middleware,
|
|
|
|
getMemberDataFromMagicLinkToken,
|
|
|
|
getMemberIdentityToken,
|
|
|
|
getMemberIdentityData,
|
|
|
|
setMemberGeolocationFromIp,
|
|
|
|
getPublicConfig,
|
|
|
|
bus,
|
|
|
|
sendEmailWithMagicLink,
|
|
|
|
getMagicLink,
|
|
|
|
members: users,
|
2021-08-25 14:30:49 +03:00
|
|
|
memberBREADService,
|
2021-07-14 15:32:09 +03:00
|
|
|
events: eventRepository,
|
2022-08-18 18:38:42 +03:00
|
|
|
productRepository,
|
2022-08-25 10:28:26 +03:00
|
|
|
|
2022-08-18 18:38:42 +03:00
|
|
|
// Test helpers
|
|
|
|
getTokenDataFromMagicLinkToken
|
2021-07-14 15:32:09 +03:00
|
|
|
};
|
|
|
|
};
|