Ghost/ghost/members-api/lib/controllers/router.js

483 lines
16 KiB
JavaScript
Raw Normal View History

const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging');
const _ = require('lodash');
const {BadRequestError, NoPermissionError, NotFoundError, UnauthorizedError} = require('@tryghost/errors');
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
const errors = require('@tryghost/errors');
const messages = {
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
emailRequired: 'Email is required.',
badRequest: 'Bad Request.',
notFound: 'Not Found.',
offerArchived: 'This offer is archived.',
tierArchived: 'This tier is archived.',
existingSubscription: 'A subscription exists for this Member.',
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
unableToCheckout: 'Unable to initiate checkout session',
inviteOnly: 'This site is invite-only, contact the owner for access.',
memberNotFound: 'No member exists with this e-mail address.',
memberNotFoundSignUp: 'No member exists with this e-mail address. Please sign up first.'
};
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
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
* @param {import('@tryghost/member-attribution')} deps.memberAttributionService
* @param {any} deps.tokenService
* @param {any} deps.sendEmailWithMagicLink
* @param {{isSet(name: string): boolean}} deps.labsService
*/
constructor({
offersAPI,
paymentsService,
productRepository,
memberRepository,
StripePrice,
allowSelfSignup,
magicLinkService,
stripeAPIService,
tokenService,
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
memberAttributionService,
sendEmailWithMagicLink,
labsService
}) {
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;
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
this._memberAttributionService = memberAttributionService;
this.labsService = labsService;
}
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, {
'Content-Type': 'text/plain;charset=UTF-8'
});
return 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,
cancelUrl: req.body.cancelUrl,
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 tierId = req.body.tierId;
const cadence = req.body.cadence;
const identity = req.body.identity;
const offerId = req.body.offerId;
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
const metadata = req.body.metadata ?? {};
if (!ghostPriceId && !offerId && !tierId && !cadence) {
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (offerId && (ghostPriceId || (tierId && cadence))) {
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (ghostPriceId && tierId && cadence) {
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (tierId && !cadence) {
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
if (cadence && cadence !== 'month' && cadence !== 'year') {
throw new BadRequestError({
message: tpl(messages.badRequest)
});
}
let couponId = null;
let trialDays;
if (offerId) {
const offer = await this._offersAPI.getOffer({id: offerId});
const tier = (await this._productRepository.get(offer.tier)).toJSON();
if (offer.status === 'archived') {
throw new NoPermissionError({
message: tpl(messages.offerArchived)
});
}
if (offer.cadence === 'month') {
ghostPriceId = tier.monthly_price_id;
} else {
ghostPriceId = tier.yearly_price_id;
}
// Free trial offers don't have a stripe coupon
if (offer.type === 'trial') {
trialDays = offer.amount;
} else {
const coupon = await this._paymentsService.getCouponForOffer(offerId);
couponId = coupon.id;
}
metadata.offer = offer.id;
}
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
// Don't allow to set the source manually
delete metadata.attribution_id;
delete metadata.attribution_url;
delete metadata.attribution_type;
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
if (metadata.urlHistory) {
// The full attribution history doesn't fit in the Stripe metadata (can't store objects + limited to 50 keys and 500 chars values)
// So we need to add top-level attributes with string values
const urlHistory = metadata.urlHistory;
delete metadata.urlHistory;
const attribution = await this._memberAttributionService.getAttribution(urlHistory);
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
// Don't set null properties
if (attribution.id) {
metadata.attribution_id = attribution.id;
}
if (attribution.url) {
metadata.attribution_url = attribution.url;
}
if (attribution.type) {
metadata.attribution_type = attribution.type;
}
if (attribution.referrerSource) {
metadata.referrer_source = attribution.referrerSource;
}
if (attribution.referrerMedium) {
metadata.referrer_medium = attribution.referrerMedium;
}
if (attribution.referrerUrl) {
metadata.referrer_url = attribution.referrerUrl;
}
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
}
if (!ghostPriceId) {
const tier = await this._productRepository.get({id: tierId});
if (tier) {
if (cadence === 'month') {
ghostPriceId = tier.get('monthly_price_id');
} else {
ghostPriceId = tier.get('yearly_price_id');
}
}
}
const price = await this._StripePrice.findOne({
id: ghostPriceId
});
if (!price) {
throw new NotFoundError({
message: tpl(messages.notFound)
});
}
const priceId = price.get('stripe_price_id');
const product = await this._productRepository.get({stripe_price_id: priceId});
if (this.labsService.isSet('freeTrial') && !trialDays) {
trialDays = product.get('trial_days');
}
if (product.get('active') !== true) {
throw new NoPermissionError({
message: tpl(messages.tierArchived)
});
}
let member = null;
if (identity) {
try {
const claims = await this._tokenService.decodeToken(identity);
const email = claims && claims.sub;
if (email) {
member = await this._memberRepository.get({email}, {withRelated: ['stripeCustomers', 'products']});
}
} catch (err) {
throw new UnauthorizedError({err});
}
} else if (req.body.customerEmail) {
member = await this._memberRepository.get({email: req.body.customerEmail}, {withRelated: ['stripeCustomers', 'products']});
}
let successUrl = req.body.successUrl;
let cancelUrl = req.body.cancelUrl;
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: {
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
email: req.body.customerEmail,
attribution: {
id: metadata.attribution_id ?? null,
type: metadata.attribution_type ?? null,
url: metadata.attribution_url ?? null
}
},
type: 'signup'
});
}
}
if (!member) {
const customer = null;
const session = await this._stripeAPIService.createCheckoutSession(priceId, customer, {
coupon: couponId,
successUrl,
cancelUrl,
trialDays,
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));
}
let restrictCheckout = false;
if (!this.labsService.isSet('compExpiring')) {
restrictCheckout = member.related('products').length !== 0;
} else {
restrictCheckout = member.get('status') === 'paid';
}
if (restrictCheckout) {
if (!identity && req.body.customerEmail) {
try {
await this._sendEmailWithMagicLink({email: req.body.customerEmail, requestedType: 'signin'});
} catch (err) {
logging.warn(err);
}
}
throw new NoPermissionError({
message: messages.existingSubscription,
code: 'CANNOT_CHECKOUT_WITH_EXISTING_SUBSCRIPTION'
});
}
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,
trialDays,
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 (err) {
throw new BadRequestError({
err,
message: tpl(messages.unableToCheckout)
});
}
}
async sendMagicLink(req, res) {
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
const {email, autoRedirect} = req.body;
let {emailType} = req.body;
let referer = req.get('referer');
if (autoRedirect === false){
referer = null;
}
if (!email) {
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
throw new errors.BadRequestError({
message: tpl(messages.emailRequired)
});
}
if (!emailType) {
// Default to subscribe form that also allows to login (safe fallback for older clients)
if (!this._allowSelfSignup()) {
emailType = 'signin';
} else {
emailType = 'subscribe';
}
}
if (!['signin', 'signup', 'subscribe'].includes(emailType)) {
res.writeHead(400);
return res.end('Bad Request.');
}
try {
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
if (emailType === 'signup' || emailType === 'subscribe') {
if (!this._allowSelfSignup()) {
throw new errors.BadRequestError({
message: tpl(messages.inviteOnly)
});
}
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
// Someone tries to signup with a user that already exists
// -> doesn't really matter: we'll send a login link
const tokenData = _.pick(req.body, ['labels', 'name', 'newsletters']);
if (req.ip) {
tokenData.reqIp = req.ip;
}
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
// Save attribution data in the tokenData
tokenData.attribution = await this._memberAttributionService.getAttribution(req.body.urlHistory);
Added member attribution events and storage (#15243) refs https://github.com/TryGhost/Team/issues/1808 refs https://github.com/TryGhost/Team/issues/1809 refs https://github.com/TryGhost/Team/issues/1820 refs https://github.com/TryGhost/Team/issues/1814 ### Changes in `member-events` package - Added MemberCreatedEvent (event, not model) - Added SubscriptionCreatedEvent (event, not model) ### Added `member-attribution` package (new) - Added the AttributionBuilder class which is able to convert a url history to an attribution object (exposed as getAttribution on the service itself, which handles the dependencies) ``` [{ "path": "/", "time": 123 }] ``` to ``` { "url": "/", "id": null, "type": "url" } ``` - event handler listens for MemberCreatedEvent and SubscriptionCreatedEvent and creates the corresponding models in the database. ### Changes in `members-api` package - Added urlHistory to `sendMagicLink` endpoint body + convert the urlHistory to an attribution object that is stored in the tokenData of the magic link (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added urlHistory to `createCheckoutSession` endpoint + convert the urlHistory to attribution keys that are saved in the Stripe Session metadata (sent by Portal in this PR: https://github.com/TryGhost/Portal/pull/256). - Added attribution data property to member repository's create method (when a member is created) - Dispatch MemberCreatedEvent with attribution ### Changes in `members-stripe-service` package (`ghost/stripe`) - Dispatch SubscriptionCreatedEvent in WebhookController on subscription checkout (with attribution from session metadata)
2022-08-18 18:38:42 +03:00
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, referrer: referer});
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
res.writeHead(201);
return res.end('Created.');
}
// Signin
const member = await this._memberRepository.get({email});
if (member) {
const tokenData = {};
await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, referrer: referer});
res.writeHead(201);
return res.end('Created.');
}
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
throw new errors.BadRequestError({
message: this._allowSelfSignup() ? tpl(messages.memberNotFoundSignUp) : tpl(messages.memberNotFound)
});
} catch (err) {
if (err.code === 'EENVELOPE') {
logging.error(err);
res.writeHead(400);
return res.end('Bad Request.');
}
🔒 Prevented member creation when logging in (#15526) fixes https://github.com/TryGhost/Ghost/issues/14508 This change requires the frontend to send an explicit `emailType` when sending a magic link. We default to `subscribe` (`signin` for invite only sites) for now to remain compatible with the existing behaviour. **Problem:** When a member tries to login and that member doesn't exist, we created a new member in the past. - This caused the creation of duplicate accounts when members were guessing the email address they used. - This caused the creation of new accounts when using an old impersonation token, login link or email change link that was sent before member deletion. **Fixed:** - Trying to login with an email address that doesn't exist will throw an error now. - Added new and separate rate limiting to login (to prevent user enumeration). This rate limiting has a higher default limit of 8. I think it needs a higher default limit (because it is rate limited on every call instead of per email address. And it should be configurable independent from administrator rate limiting. It also needs a lower lifetime value because it is never reset. - Updated error responses in the `sendMagicLink` endpoint to use the default error encoding middleware. - The type (`signin`, `signup`, `updateEmail` or `subscribe`) is now stored in the magic link. This is used to prevent signups with a sign in token. **Notes:** - Between tests, we truncate the database, but this is not enough for the rate limits to be truly reset. I had to add a method to the spam prevention service to reset all the instances between tests. Not resetting them caused random failures because every login in every test was hitting those spam prevention middlewares and somehow left a trace of that in those instances (even when the brute table is reset). Maybe those instances were doing some in memory caching.
2022-10-05 13:42:42 +03:00
// Let the normal error middleware handle this error
throw err;
}
}
};