mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 16:42:17 +03:00
104f84f252
As discussed with the product team we want to enforce kebab-case file names for all files, with the exception of files which export a single class, in which case they should be PascalCase and reflect the class which they export. This will help find classes faster, and should push better naming for them too. Some files and packages have been excluded from this linting, specifically when a library or framework depends on the naming of a file for the functionality e.g. Ember, knex-migrator, adapter-manager
381 lines
11 KiB
JavaScript
381 lines
11 KiB
JavaScript
const {Router} = require('express');
|
|
const body = require('body-parser');
|
|
const MagicLink = require('@tryghost/magic-link');
|
|
const errors = require('@tryghost/errors');
|
|
const logging = require('@tryghost/logging');
|
|
|
|
const PaymentsService = require('@tryghost/members-payments');
|
|
|
|
const TokenService = require('./services/TokenService');
|
|
const GeolocationService = require('./services/GeolocationService');
|
|
const MemberBREADService = require('./services/MemberBREADService');
|
|
const MemberRepository = require('./repositories/MemberRepository');
|
|
const EventRepository = require('./repositories/EventRepository');
|
|
const ProductRepository = require('./repositories/ProductRepository');
|
|
const RouterController = require('./controllers/RouterController');
|
|
const MemberController = require('./controllers/MemberController');
|
|
const WellKnownController = require('./controllers/WellKnownController');
|
|
|
|
const {EmailSuppressedEvent} = require('@tryghost/email-suppression-list');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
|
|
module.exports = function MembersAPI({
|
|
tokenConfig: {
|
|
issuer,
|
|
privateKey,
|
|
publicKey
|
|
},
|
|
auth: {
|
|
allowSelfSignup = () => true,
|
|
getSigninURL,
|
|
tokenProvider
|
|
},
|
|
mail: {
|
|
transporter,
|
|
getText,
|
|
getHTML,
|
|
getSubject
|
|
},
|
|
models: {
|
|
EmailRecipient,
|
|
StripeCustomer,
|
|
StripeCustomerSubscription,
|
|
Member,
|
|
MemberNewsletter,
|
|
MemberCancelEvent,
|
|
MemberSubscribeEvent,
|
|
MemberLoginEvent,
|
|
MemberPaidSubscriptionEvent,
|
|
MemberPaymentEvent,
|
|
MemberStatusEvent,
|
|
MemberProductEvent,
|
|
MemberEmailChangeEvent,
|
|
MemberCreatedEvent,
|
|
SubscriptionCreatedEvent,
|
|
MemberLinkClickEvent,
|
|
EmailSpamComplaintEvent,
|
|
Offer,
|
|
OfferRedemption,
|
|
StripeProduct,
|
|
StripePrice,
|
|
Product,
|
|
Settings,
|
|
Comment,
|
|
MemberFeedback
|
|
},
|
|
tiersService,
|
|
stripeAPIService,
|
|
offersAPI,
|
|
labsService,
|
|
newslettersService,
|
|
memberAttributionService,
|
|
emailSuppressionList
|
|
}) {
|
|
const tokenService = new TokenService({
|
|
privateKey,
|
|
publicKey,
|
|
issuer
|
|
});
|
|
|
|
const productRepository = new ProductRepository({
|
|
Product,
|
|
Settings,
|
|
StripeProduct,
|
|
StripePrice,
|
|
stripeAPIService
|
|
});
|
|
|
|
const memberRepository = new MemberRepository({
|
|
stripeAPIService,
|
|
tokenService,
|
|
newslettersService,
|
|
labsService,
|
|
productRepository,
|
|
Member,
|
|
MemberNewsletter,
|
|
MemberCancelEvent,
|
|
MemberSubscribeEventModel: MemberSubscribeEvent,
|
|
MemberPaidSubscriptionEvent,
|
|
MemberEmailChangeEvent,
|
|
MemberStatusEvent,
|
|
MemberProductEvent,
|
|
OfferRedemption,
|
|
StripeCustomer,
|
|
StripeCustomerSubscription,
|
|
offerRepository: offersAPI.repository
|
|
});
|
|
|
|
const eventRepository = new EventRepository({
|
|
EmailRecipient,
|
|
MemberSubscribeEvent,
|
|
MemberPaidSubscriptionEvent,
|
|
MemberPaymentEvent,
|
|
MemberStatusEvent,
|
|
MemberLoginEvent,
|
|
MemberCreatedEvent,
|
|
SubscriptionCreatedEvent,
|
|
MemberLinkClickEvent,
|
|
MemberFeedback,
|
|
EmailSpamComplaintEvent,
|
|
Comment,
|
|
labsService,
|
|
memberAttributionService
|
|
});
|
|
|
|
const memberBREADService = new MemberBREADService({
|
|
offersAPI,
|
|
memberRepository,
|
|
emailService: {
|
|
async sendEmailWithMagicLink({email, requestedType}) {
|
|
return sendEmailWithMagicLink({
|
|
email,
|
|
requestedType,
|
|
options: {
|
|
forceEmailType: true
|
|
}
|
|
});
|
|
}
|
|
},
|
|
labsService,
|
|
stripeService: stripeAPIService,
|
|
memberAttributionService,
|
|
emailSuppressionList
|
|
});
|
|
|
|
const geolocationService = new GeolocationService();
|
|
|
|
const magicLinkService = new MagicLink({
|
|
transporter,
|
|
tokenProvider,
|
|
getSigninURL,
|
|
getText,
|
|
getHTML,
|
|
getSubject
|
|
});
|
|
|
|
const paymentsService = new PaymentsService({
|
|
StripeProduct,
|
|
StripePrice,
|
|
StripeCustomer,
|
|
Offer,
|
|
offersAPI,
|
|
stripeAPIService
|
|
});
|
|
|
|
const memberController = new MemberController({
|
|
memberRepository,
|
|
productRepository,
|
|
paymentsService,
|
|
tiersService,
|
|
StripePrice,
|
|
tokenService,
|
|
sendEmailWithMagicLink
|
|
});
|
|
|
|
const routerController = new RouterController({
|
|
offersAPI,
|
|
paymentsService,
|
|
tiersService,
|
|
memberRepository,
|
|
StripePrice,
|
|
allowSelfSignup,
|
|
magicLinkService,
|
|
stripeAPIService,
|
|
tokenService,
|
|
sendEmailWithMagicLink,
|
|
memberAttributionService,
|
|
labsService
|
|
});
|
|
|
|
const wellKnownController = new WellKnownController({
|
|
tokenService
|
|
});
|
|
|
|
const users = memberRepository;
|
|
|
|
async function sendEmailWithMagicLink({email, requestedType, tokenData, options = {forceEmailType: false}, referrer = null}) {
|
|
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, tokenData: Object.assign({email, type}, tokenData), referrer});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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
|
|
});
|
|
}
|
|
|
|
async function getTokenDataFromMagicLinkToken(token) {
|
|
return await magicLinkService.getDataFromToken(token);
|
|
}
|
|
|
|
async function getMemberDataFromMagicLinkToken(token) {
|
|
const {email, labels = [], name = '', oldEmail, newsletters, attribution, reqIp, type} = await getTokenDataFromMagicLinkToken(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 && (!type || type === 'updateEmail')) {
|
|
// user exists but wants to change their email address
|
|
await users.update({email}, {id: member.id});
|
|
return getMemberIdentityData(email);
|
|
}
|
|
return member;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
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});
|
|
|
|
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) {
|
|
await users.update({geolocation}, {id: member.id});
|
|
}
|
|
|
|
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(),
|
|
forwardError((req, res) => routerController.sendMagicLink(req, res))
|
|
),
|
|
createCheckoutSession: Router().use(
|
|
body.json(),
|
|
forwardError((req, res) => routerController.createCheckoutSession(req, res))
|
|
),
|
|
createCheckoutSetupSession: Router().use(
|
|
body.json(),
|
|
forwardError((req, res) => routerController.createCheckoutSetupSession(req, res))
|
|
),
|
|
updateEmailAddress: Router().use(
|
|
body.json(),
|
|
forwardError((req, res) => memberController.updateEmailAddress(req, res))
|
|
),
|
|
updateSubscription: Router({mergeParams: true}).use(
|
|
body.json(),
|
|
forwardError((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');
|
|
|
|
DomainEvents.subscribe(EmailSuppressedEvent, async function (event) {
|
|
const member = await memberRepository.get({email: event.data.emailAddress});
|
|
if (!member) {
|
|
return;
|
|
}
|
|
await memberRepository.update({newsletters: []}, {id: member.id});
|
|
});
|
|
|
|
return {
|
|
middleware,
|
|
getMemberDataFromMagicLinkToken,
|
|
getMemberIdentityToken,
|
|
getMemberIdentityData,
|
|
setMemberGeolocationFromIp,
|
|
getPublicConfig,
|
|
bus,
|
|
sendEmailWithMagicLink,
|
|
getMagicLink,
|
|
members: users,
|
|
memberBREADService,
|
|
events: eventRepository,
|
|
productRepository,
|
|
|
|
// Test helpers
|
|
getTokenDataFromMagicLinkToken,
|
|
paymentsService
|
|
};
|
|
};
|