2020-01-20 09:25:17 +03:00
|
|
|
const _ = require('lodash');
|
2019-05-07 16:46:20 +03:00
|
|
|
const {Router} = require('express');
|
2018-12-11 09:47:44 +03:00
|
|
|
const body = require('body-parser');
|
2019-09-03 07:32:00 +03:00
|
|
|
const MagicLink = require('@tryghost/magic-link');
|
2019-09-06 08:14:56 +03:00
|
|
|
const StripePaymentProcessor = require('./lib/stripe');
|
2019-05-07 17:34:41 +03:00
|
|
|
const Tokens = require('./lib/tokens');
|
|
|
|
const Users = require('./lib/users');
|
2020-01-13 11:43:26 +03:00
|
|
|
const Metadata = require('./lib/metadata');
|
2019-07-17 13:20:13 +03:00
|
|
|
const common = require('./lib/common');
|
2020-02-27 13:29:36 +03:00
|
|
|
const {getGeolocationFromIP} = require('./lib/geolocation');
|
2018-12-11 09:47:44 +03:00
|
|
|
|
|
|
|
module.exports = function MembersApi({
|
2019-09-03 07:32:00 +03:00
|
|
|
tokenConfig: {
|
2018-12-11 09:47:44 +03:00
|
|
|
issuer,
|
|
|
|
privateKey,
|
2019-09-03 07:32:00 +03:00
|
|
|
publicKey
|
|
|
|
},
|
|
|
|
auth: {
|
2019-10-06 13:14:22 +03:00
|
|
|
allowSelfSignup = true,
|
2019-10-10 12:56:18 +03:00
|
|
|
getSigninURL,
|
2020-09-18 14:45:51 +03:00
|
|
|
tokenProvider
|
2019-09-03 07:32:00 +03:00
|
|
|
},
|
2019-09-06 08:14:56 +03:00
|
|
|
paymentConfig,
|
2019-09-03 07:32:00 +03:00
|
|
|
mail: {
|
2019-10-01 10:35:09 +03:00
|
|
|
transporter,
|
|
|
|
getText,
|
2019-10-10 16:20:15 +03:00
|
|
|
getHTML,
|
|
|
|
getSubject
|
2018-12-11 09:47:44 +03:00
|
|
|
},
|
2020-07-09 17:40:48 +03:00
|
|
|
models: {
|
|
|
|
StripeWebhook,
|
|
|
|
StripeCustomer,
|
|
|
|
StripeCustomerSubscription,
|
|
|
|
Member
|
|
|
|
},
|
2019-10-02 06:57:23 +03:00
|
|
|
logger
|
2018-12-11 09:47:44 +03:00
|
|
|
}) {
|
2019-10-02 06:57:23 +03:00
|
|
|
if (logger) {
|
|
|
|
common.logging.setLogger(logger);
|
|
|
|
}
|
|
|
|
|
2019-09-06 08:14:56 +03:00
|
|
|
const {encodeIdentityToken, decodeToken} = Tokens({privateKey, publicKey, issuer});
|
2020-07-09 17:40:48 +03:00
|
|
|
const metadata = Metadata({
|
2020-07-24 16:39:01 +03:00
|
|
|
Member,
|
2020-07-09 17:40:48 +03:00
|
|
|
StripeWebhook,
|
|
|
|
StripeCustomer,
|
|
|
|
StripeCustomerSubscription
|
|
|
|
});
|
2019-09-06 08:14:56 +03:00
|
|
|
|
2020-06-18 19:01:04 +03:00
|
|
|
async function hasActiveStripeSubscriptions() {
|
2020-07-09 17:40:48 +03:00
|
|
|
const firstActiveSubscription = await StripeCustomerSubscription.findOne({
|
2020-06-18 19:01:04 +03:00
|
|
|
status: 'active'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (firstActiveSubscription) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-09 17:40:48 +03:00
|
|
|
const firstTrialingSubscription = await StripeCustomerSubscription.findOne({
|
2020-06-18 19:01:04 +03:00
|
|
|
status: 'trialing'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (firstTrialingSubscription) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-27 12:45:23 +03:00
|
|
|
const firstUnpaidSubscription = await StripeCustomerSubscription.findOne({
|
|
|
|
status: 'unpaid'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (firstUnpaidSubscription) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const firstPastDueSubscription = await StripeCustomerSubscription.findOne({
|
|
|
|
status: 'past_due'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (firstPastDueSubscription) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-18 19:01:04 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-09-25 06:15:31 +03:00
|
|
|
const stripeStorage = {
|
|
|
|
async get(member) {
|
2020-01-13 11:43:26 +03:00
|
|
|
return metadata.getMetadata('stripe', member);
|
2019-09-25 06:15:31 +03:00
|
|
|
},
|
2020-10-27 13:29:08 +03:00
|
|
|
async set(data, options) {
|
|
|
|
return metadata.setMetadata('stripe', data, options);
|
2019-09-25 06:15:31 +03:00
|
|
|
}
|
|
|
|
};
|
2020-11-23 19:28:35 +03:00
|
|
|
/** @type {StripePaymentProcessor} */
|
|
|
|
const stripe = (paymentConfig.stripe ? new StripePaymentProcessor(paymentConfig.stripe, stripeStorage, common.logging) : null);
|
2019-09-06 08:14:56 +03:00
|
|
|
|
|
|
|
async function ensureStripe(_req, res, next) {
|
|
|
|
if (!stripe) {
|
|
|
|
res.writeHead(400);
|
|
|
|
return res.end('Stripe not configured');
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
await stripe.ready();
|
|
|
|
next();
|
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(500);
|
|
|
|
return res.end('There was an error configuring stripe');
|
|
|
|
}
|
|
|
|
}
|
2019-02-07 12:41:39 +03:00
|
|
|
|
2019-09-03 07:32:00 +03:00
|
|
|
const magicLinkService = new MagicLink({
|
|
|
|
transporter,
|
2020-09-18 14:45:51 +03:00
|
|
|
tokenProvider,
|
2019-10-01 10:35:09 +03:00
|
|
|
getSigninURL,
|
|
|
|
getText,
|
2019-10-10 16:20:15 +03:00
|
|
|
getHTML,
|
|
|
|
getSubject
|
2019-09-03 07:32:00 +03:00
|
|
|
});
|
Updated theme layer to use members-ssr (#10676)
* Removed support for cookies in members auth middleware
no-issue
The members middleware will no longer be supporting cookies, the cookie
will be handled by a new middleware specific for serverside rendering,
more informations can be found here:
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Removed members auth middleware from site app
no-issue
The site app no longer needs the members auth middleware as it doesn't
support cookies, and will be replaced by ssr specific middleware.
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Added comment for session_secret setting
no-issue
We are going to have multiple concepts of sessions, so adding a comment
here to be specific that this is for the Ghost Admin client
* Added theme_session_secret setting dynamic default
no-issue
Sessions for the theme layer will be signed, so we generate a random hex
string to use as a signing key
* Added getPublicConfig method
* Replaced export of httpHandler with POJO apiInstance
no-issue
This is mainly to reduce the public api, so it's easier to document.
* Renamed memberUserObject -> members
no-issue
Simplifies the interface, and is more inline with what we would want to export as an api library.
* Removed use of require options inside members
no-issue
This was too tight of a coupling between Ghost and Members
* Simplified apiInstance definition
no-issue
* Added getMember method to members api
* Added MembersSSR instance to members service
* Wired up routes for members ssr
* Updated members auth middleware to use getPublicConfig
* Removed publicKey static export from members service
* Used real session secret
no-issue
* Added DELETE /members/ssr handler
no-issue
This allows users to log out of the theme layer
* Fixed missing code property
no-issue
Ignition uses the statusCode property to forward status codes to call sites
* Removed superfluous error middleware
no-issue
Before we used generic JWT middleware which would reject, now the
middleware catches it's own error and doesn't error, thus this
middleware is unecessary.
* Removed console.logs
no-issue
* Updated token expirty to hardcoded 20 minutes
no-issue
This returns to our previous state of using short lived tokens, both for
security and simplicity.
* Removed hardcoded default member settings
no-issue
This is no longer needed, as defaults are in default-settings.json
* Removed stripe from default payment processor
no-issue
* Exported `getSiteUrl` method from url utils
no-issue
This keeps inline with newer naming conventions
* Updated how audience access control works
no-issue
Rather than being passed a function, members api now receives an object
which describes which origins have access to which audiences, and how
long those tokens should be allowed to work for. It also allows syntax
for default tokens where audience === origin requesting it. This can be
set to undefined or null to disable this functionality.
{
"http://site.com": {
"http://site.com": {
tokenLength: '5m'
},
"http://othersite.com": {
tokenLength: '1h'
}
},
"*": {
tokenLength: '30m'
}
}
* Updated members service to use access control feature
no-issue
This also cleans up a lot of unecessary variable definitions, and some
other minor cleanups.
* Added status code to auth pages html response
no-issue
This was missing, probably default but better to be explicit
* Updated gateway to have membersApiUrl from config
no-issue
Previously we were parsing the url, this was not very safe as we can
have Ghost hosted on a subdomain, and this would have failed.
* Added issuer to public config for members
no-issue
This can be used to request SSR tokens in the client
* Fixed path for gateway bundle
no-issue
* Updated settings model tests
no-issue
* Revert "Removed stripe from default payment processor"
This reverts commit 1d88d9b6d73a10091070bcc1b7f5779d071c7845.
* Revert "Removed hardcoded default member settings"
This reverts commit 9d899048ba7d4b272b9ac65a95a52af66b30914a.
* Installed @tryghost/members-ssr
* Fixed tests for settings model
2019-04-16 17:50:25 +03:00
|
|
|
|
2020-08-12 14:57:28 +03:00
|
|
|
const users = Users({
|
|
|
|
stripe,
|
2020-10-27 14:00:47 +03:00
|
|
|
Member,
|
|
|
|
StripeCustomer
|
2020-08-12 14:57:28 +03:00
|
|
|
});
|
|
|
|
|
2020-10-29 09:42:15 +03:00
|
|
|
async function sendEmailWithMagicLink({email, requestedType, tokenData, options = {forceEmailType: false}, requestSrc = ''}) {
|
2020-09-17 17:42:01 +03:00
|
|
|
let type = requestedType;
|
|
|
|
if (!options.forceEmailType) {
|
|
|
|
const member = await users.get({email});
|
|
|
|
if (member) {
|
|
|
|
type = 'signin';
|
|
|
|
} else if (type !== 'subscribe') {
|
|
|
|
type = 'signup';
|
|
|
|
}
|
2019-10-06 11:57:48 +03:00
|
|
|
}
|
2020-10-28 15:46:45 +03:00
|
|
|
return magicLinkService.sendMagicLink({email, type, requestSrc, tokenData: Object.assign({email}, tokenData)});
|
Updated theme layer to use members-ssr (#10676)
* Removed support for cookies in members auth middleware
no-issue
The members middleware will no longer be supporting cookies, the cookie
will be handled by a new middleware specific for serverside rendering,
more informations can be found here:
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Removed members auth middleware from site app
no-issue
The site app no longer needs the members auth middleware as it doesn't
support cookies, and will be replaced by ssr specific middleware.
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Added comment for session_secret setting
no-issue
We are going to have multiple concepts of sessions, so adding a comment
here to be specific that this is for the Ghost Admin client
* Added theme_session_secret setting dynamic default
no-issue
Sessions for the theme layer will be signed, so we generate a random hex
string to use as a signing key
* Added getPublicConfig method
* Replaced export of httpHandler with POJO apiInstance
no-issue
This is mainly to reduce the public api, so it's easier to document.
* Renamed memberUserObject -> members
no-issue
Simplifies the interface, and is more inline with what we would want to export as an api library.
* Removed use of require options inside members
no-issue
This was too tight of a coupling between Ghost and Members
* Simplified apiInstance definition
no-issue
* Added getMember method to members api
* Added MembersSSR instance to members service
* Wired up routes for members ssr
* Updated members auth middleware to use getPublicConfig
* Removed publicKey static export from members service
* Used real session secret
no-issue
* Added DELETE /members/ssr handler
no-issue
This allows users to log out of the theme layer
* Fixed missing code property
no-issue
Ignition uses the statusCode property to forward status codes to call sites
* Removed superfluous error middleware
no-issue
Before we used generic JWT middleware which would reject, now the
middleware catches it's own error and doesn't error, thus this
middleware is unecessary.
* Removed console.logs
no-issue
* Updated token expirty to hardcoded 20 minutes
no-issue
This returns to our previous state of using short lived tokens, both for
security and simplicity.
* Removed hardcoded default member settings
no-issue
This is no longer needed, as defaults are in default-settings.json
* Removed stripe from default payment processor
no-issue
* Exported `getSiteUrl` method from url utils
no-issue
This keeps inline with newer naming conventions
* Updated how audience access control works
no-issue
Rather than being passed a function, members api now receives an object
which describes which origins have access to which audiences, and how
long those tokens should be allowed to work for. It also allows syntax
for default tokens where audience === origin requesting it. This can be
set to undefined or null to disable this functionality.
{
"http://site.com": {
"http://site.com": {
tokenLength: '5m'
},
"http://othersite.com": {
tokenLength: '1h'
}
},
"*": {
tokenLength: '30m'
}
}
* Updated members service to use access control feature
no-issue
This also cleans up a lot of unecessary variable definitions, and some
other minor cleanups.
* Added status code to auth pages html response
no-issue
This was missing, probably default but better to be explicit
* Updated gateway to have membersApiUrl from config
no-issue
Previously we were parsing the url, this was not very safe as we can
have Ghost hosted on a subdomain, and this would have failed.
* Added issuer to public config for members
no-issue
This can be used to request SSR tokens in the client
* Fixed path for gateway bundle
no-issue
* Updated settings model tests
no-issue
* Revert "Removed stripe from default payment processor"
This reverts commit 1d88d9b6d73a10091070bcc1b7f5779d071c7845.
* Revert "Removed hardcoded default member settings"
This reverts commit 9d899048ba7d4b272b9ac65a95a52af66b30914a.
* Installed @tryghost/members-ssr
* Fixed tests for settings model
2019-04-16 17:50:25 +03:00
|
|
|
}
|
2019-09-26 12:53:07 +03:00
|
|
|
|
2020-02-06 12:08:39 +03:00
|
|
|
function getMagicLink(email) {
|
2020-09-18 19:43:42 +03:00
|
|
|
return magicLinkService.getMagicLink({tokenData: {email}, type: 'signin'});
|
2020-02-06 12:08:39 +03:00
|
|
|
}
|
|
|
|
|
2020-02-27 13:29:36 +03:00
|
|
|
async function getMemberDataFromMagicLinkToken(token) {
|
2020-09-17 17:42:01 +03:00
|
|
|
const {email, labels = [], name = '', oldEmail} = await magicLinkService.getDataFromToken(token);
|
2019-09-03 07:32:00 +03:00
|
|
|
if (!email) {
|
|
|
|
return null;
|
2018-12-11 09:47:44 +03:00
|
|
|
}
|
2020-02-12 14:12:49 +03:00
|
|
|
|
2020-05-28 17:07:03 +03:00
|
|
|
const member = oldEmail ? await getMemberIdentityData(oldEmail) : await getMemberIdentityData(email);
|
2020-02-27 13:29:36 +03:00
|
|
|
|
2019-09-03 07:32:00 +03:00
|
|
|
if (member) {
|
2020-06-04 15:20:19 +03:00
|
|
|
if (oldEmail) {
|
2020-05-28 17:07:03 +03:00
|
|
|
// user exists but wants to change their email address
|
|
|
|
if (oldEmail) {
|
|
|
|
member.email = email;
|
|
|
|
}
|
2020-02-27 13:29:36 +03:00
|
|
|
await users.update(member, {id: member.id});
|
|
|
|
return getMemberIdentityData(email);
|
|
|
|
}
|
2019-09-03 07:32:00 +03:00
|
|
|
return member;
|
2018-12-11 09:47:44 +03:00
|
|
|
}
|
2020-02-27 13:29:36 +03:00
|
|
|
|
2020-06-04 15:20:19 +03:00
|
|
|
await users.create({name, email, labels});
|
2019-09-09 08:44:50 +03:00
|
|
|
return getMemberIdentityData(email);
|
2019-09-03 07:32:00 +03:00
|
|
|
}
|
2020-06-04 15:20:19 +03:00
|
|
|
|
2020-07-09 17:40:48 +03:00
|
|
|
async function getMemberIdentityData(email) {
|
2020-08-12 14:57:28 +03:00
|
|
|
const model = await users.get({email}, {withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer']});
|
|
|
|
if (!model) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return model.toJSON();
|
2019-09-03 07:32:00 +03:00
|
|
|
}
|
2020-06-04 15:20:19 +03:00
|
|
|
|
2020-07-09 17:40:48 +03:00
|
|
|
async function getMemberIdentityToken(email) {
|
2019-09-03 07:32:00 +03:00
|
|
|
const member = await getMemberIdentityData(email);
|
2019-10-02 14:10:43 +03:00
|
|
|
if (!member) {
|
|
|
|
return null;
|
|
|
|
}
|
2019-09-03 07:32:00 +03:00
|
|
|
return encodeIdentityToken({sub: member.email});
|
2018-12-11 09:47:44 +03:00
|
|
|
}
|
|
|
|
|
2020-06-04 15:20:19 +03:00
|
|
|
async function setMemberGeolocationFromIp(email, ip) {
|
|
|
|
if (!email || !ip) {
|
2020-09-28 15:06:58 +03:00
|
|
|
throw new common.errors.IncorrectUsageError({
|
2020-06-04 15:20:19 +03:00
|
|
|
message: 'setMemberGeolocationFromIp() expects email and ip arguments to be present'
|
2020-09-28 15:06:58 +03:00
|
|
|
});
|
2020-06-04 15:20:19 +03:00
|
|
|
}
|
|
|
|
|
2020-09-28 15:08:03 +03:00
|
|
|
const member = await users.get({email}, {
|
2020-09-17 17:42:01 +03:00
|
|
|
withRelated: ['labels']
|
|
|
|
});
|
2020-06-04 15:20:19 +03:00
|
|
|
|
|
|
|
if (!member) {
|
2020-09-28 15:06:58 +03:00
|
|
|
throw new common.errors.NotFoundError({
|
2020-06-04 15:20:19 +03:00
|
|
|
message: `Member with email address ${email} does not exist`
|
2020-09-28 15:06:58 +03:00
|
|
|
});
|
2020-06-04 15:20:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// max request time is 500ms so shouldn't slow requests down too much
|
|
|
|
let geolocation = JSON.stringify(await getGeolocationFromIP(ip));
|
|
|
|
if (geolocation) {
|
|
|
|
member.geolocation = geolocation;
|
|
|
|
await users.update(member, {id: member.id});
|
|
|
|
}
|
|
|
|
|
|
|
|
return getMemberIdentityData(email);
|
|
|
|
}
|
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
const middleware = {
|
|
|
|
sendMagicLink: Router(),
|
|
|
|
createCheckoutSession: Router(),
|
2020-02-26 07:09:09 +03:00
|
|
|
createCheckoutSetupSession: Router(),
|
2019-12-12 11:19:36 +03:00
|
|
|
handleStripeWebhook: Router(),
|
|
|
|
updateSubscription: Router({mergeParams: true})
|
2019-09-25 12:32:33 +03:00
|
|
|
};
|
2019-07-17 13:20:13 +03:00
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
middleware.sendMagicLink.use(body.json(), async function (req, res) {
|
2020-10-28 15:46:45 +03:00
|
|
|
const {email, emailType, oldEmail, requestSrc} = req.body;
|
2020-10-29 15:23:07 +03:00
|
|
|
let forceEmailType = false;
|
2019-09-03 07:32:00 +03:00
|
|
|
if (!email) {
|
|
|
|
res.writeHead(400);
|
|
|
|
return res.end('Bad Request.');
|
|
|
|
}
|
2020-02-27 13:29:36 +03:00
|
|
|
|
2019-09-03 07:32:00 +03:00
|
|
|
try {
|
2020-05-28 17:07:03 +03:00
|
|
|
if (oldEmail) {
|
|
|
|
const existingMember = await users.get({email});
|
|
|
|
if (existingMember) {
|
|
|
|
throw new common.errors.BadRequestError({
|
|
|
|
message: 'This email is already associated with a member'
|
|
|
|
});
|
|
|
|
}
|
2020-10-29 15:23:07 +03:00
|
|
|
forceEmailType = true;
|
2020-05-28 17:07:03 +03:00
|
|
|
}
|
2020-09-17 17:42:01 +03:00
|
|
|
|
2019-10-06 13:11:37 +03:00
|
|
|
if (!allowSelfSignup) {
|
2020-10-15 13:49:41 +03:00
|
|
|
const member = oldEmail ? await users.get({oldEmail}) : await users.get({email});
|
2019-10-06 13:11:37 +03:00
|
|
|
if (member) {
|
2020-09-17 17:42:01 +03:00
|
|
|
const tokenData = _.pick(req.body, ['oldEmail']);
|
2020-10-28 15:46:45 +03:00
|
|
|
await sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc, options: {forceEmailType}});
|
2019-10-06 13:11:37 +03:00
|
|
|
}
|
|
|
|
} else {
|
2020-09-17 17:42:01 +03:00
|
|
|
const tokenData = _.pick(req.body, ['labels', 'name', 'oldEmail']);
|
2020-10-29 15:24:18 +03:00
|
|
|
await sendEmailWithMagicLink({email, tokenData, requestedType: emailType, requestSrc, options: {forceEmailType}});
|
2019-10-06 13:11:37 +03:00
|
|
|
}
|
2019-09-03 07:32:00 +03:00
|
|
|
res.writeHead(201);
|
|
|
|
return res.end('Created.');
|
|
|
|
} catch (err) {
|
2020-07-22 13:36:58 +03:00
|
|
|
const statusCode = (err && err.statusCode) || 500;
|
2019-09-15 06:48:11 +03:00
|
|
|
common.logging.error(err);
|
2020-07-22 13:36:58 +03:00
|
|
|
res.writeHead(statusCode);
|
2019-09-03 07:32:00 +03:00
|
|
|
return res.end('Internal Server Error.');
|
|
|
|
}
|
2019-07-17 13:20:13 +03:00
|
|
|
});
|
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
middleware.createCheckoutSession.use(ensureStripe, body.json(), async function (req, res) {
|
2019-09-06 08:14:56 +03:00
|
|
|
const plan = req.body.plan;
|
|
|
|
const identity = req.body.identity;
|
|
|
|
|
2019-09-25 06:51:45 +03:00
|
|
|
if (!plan) {
|
2019-09-06 08:14:56 +03:00
|
|
|
res.writeHead(400);
|
|
|
|
return res.end('Bad Request.');
|
|
|
|
}
|
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
// NOTE: never allow "Complimentary" plan to be subscribed to from the client
|
2020-01-27 08:34:22 +03:00
|
|
|
if (plan.toLowerCase() === 'complimentary') {
|
|
|
|
res.writeHead(400);
|
|
|
|
return res.end('Bad Request.');
|
|
|
|
}
|
|
|
|
|
2019-09-06 08:14:56 +03:00
|
|
|
let email;
|
|
|
|
try {
|
2019-09-25 06:51:45 +03:00
|
|
|
if (!identity) {
|
|
|
|
email = null;
|
|
|
|
} else {
|
|
|
|
const claims = await decodeToken(identity);
|
2020-11-23 19:28:35 +03:00
|
|
|
email = claims && claims.sub;
|
2019-09-25 06:51:45 +03:00
|
|
|
}
|
2019-09-06 08:14:56 +03:00
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(401);
|
|
|
|
return res.end('Unauthorized');
|
|
|
|
}
|
|
|
|
|
2020-08-12 14:57:28 +03:00
|
|
|
const member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
|
2019-09-06 08:14:56 +03:00
|
|
|
|
2019-09-25 06:51:45 +03:00
|
|
|
// Do not allow members already with a subscription to initiate a new checkout session
|
2020-08-12 14:57:28 +03:00
|
|
|
if (member && member.related('stripeSubscriptions').length > 0) {
|
2019-09-06 08:14:56 +03:00
|
|
|
res.writeHead(403);
|
|
|
|
return res.end('No permission');
|
|
|
|
}
|
|
|
|
|
2020-06-10 13:59:27 +03:00
|
|
|
try {
|
|
|
|
const sessionInfo = await stripe.createCheckoutSession(member, plan, {
|
|
|
|
successUrl: req.body.successUrl,
|
|
|
|
cancelUrl: req.body.cancelUrl,
|
|
|
|
customerEmail: req.body.customerEmail,
|
|
|
|
metadata: req.body.metadata
|
|
|
|
});
|
|
|
|
|
|
|
|
res.writeHead(200, {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
});
|
|
|
|
|
|
|
|
res.end(JSON.stringify(sessionInfo));
|
|
|
|
} catch (e) {
|
|
|
|
const error = e.message || 'Unable to initiate checkout session';
|
|
|
|
res.writeHead(400);
|
|
|
|
return res.end(error);
|
|
|
|
}
|
2019-09-06 08:14:56 +03:00
|
|
|
});
|
|
|
|
|
2020-02-26 07:09:09 +03:00
|
|
|
middleware.createCheckoutSetupSession.use(ensureStripe, body.json(), async function (req, res) {
|
|
|
|
const identity = req.body.identity;
|
|
|
|
|
|
|
|
let email;
|
|
|
|
try {
|
|
|
|
if (!identity) {
|
|
|
|
email = null;
|
|
|
|
} else {
|
|
|
|
const claims = await decodeToken(identity);
|
2020-11-23 19:28:35 +03:00
|
|
|
email = claims && claims.sub;
|
2020-02-26 07:09:09 +03:00
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(401);
|
|
|
|
return res.end('Unauthorized');
|
|
|
|
}
|
|
|
|
|
|
|
|
const member = email ? await users.get({email}) : null;
|
|
|
|
|
|
|
|
if (!member) {
|
|
|
|
res.writeHead(403);
|
|
|
|
return res.end('Bad Request.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const sessionInfo = await stripe.createCheckoutSetupSession(member, {
|
|
|
|
successUrl: req.body.successUrl,
|
|
|
|
cancelUrl: req.body.cancelUrl
|
|
|
|
});
|
|
|
|
|
|
|
|
res.writeHead(200, {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
});
|
|
|
|
|
|
|
|
res.end(JSON.stringify(sessionInfo));
|
|
|
|
});
|
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
middleware.handleStripeWebhook.use(ensureStripe, body.raw({type: 'application/json'}), async function (req, res) {
|
2019-10-08 14:14:59 +03:00
|
|
|
let event;
|
|
|
|
try {
|
|
|
|
event = await stripe.parseWebhook(req.body, req.headers['stripe-signature']);
|
|
|
|
} catch (err) {
|
|
|
|
common.logging.error(err);
|
|
|
|
res.writeHead(401);
|
|
|
|
return res.end();
|
|
|
|
}
|
2020-07-21 12:50:10 +03:00
|
|
|
common.logging.info(`Handling webhook ${event.type}`);
|
2019-09-25 06:27:57 +03:00
|
|
|
try {
|
2019-10-08 10:22:26 +03:00
|
|
|
if (event.type === 'customer.subscription.deleted') {
|
|
|
|
await stripe.handleCustomerSubscriptionDeletedWebhook(event.data.object);
|
2019-09-25 06:27:57 +03:00
|
|
|
}
|
|
|
|
|
2019-10-08 10:22:26 +03:00
|
|
|
if (event.type === 'customer.subscription.updated') {
|
|
|
|
await stripe.handleCustomerSubscriptionUpdatedWebhook(event.data.object);
|
|
|
|
}
|
2019-09-25 06:27:57 +03:00
|
|
|
|
2020-10-15 13:55:36 +03:00
|
|
|
if (event.type === 'customer.subscription.created') {
|
|
|
|
await stripe.handleCustomerSubscriptionCreatedWebhook(event.data.object);
|
|
|
|
}
|
|
|
|
|
2019-10-08 10:22:26 +03:00
|
|
|
if (event.type === 'invoice.payment_succeeded') {
|
|
|
|
await stripe.handleInvoicePaymentSucceededWebhook(event.data.object);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'invoice.payment_failed') {
|
|
|
|
await stripe.handleInvoicePaymentFailedWebhook(event.data.object);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'checkout.session.completed') {
|
2020-07-21 12:50:10 +03:00
|
|
|
if (event.data.object.mode === 'setup') {
|
|
|
|
common.logging.info('Handling "setup" mode Checkout Session');
|
2020-02-26 07:09:09 +03:00
|
|
|
const setupIntent = await stripe.getSetupIntent(event.data.object.setup_intent);
|
|
|
|
const customer = await stripe.getCustomer(setupIntent.metadata.customer_id);
|
|
|
|
const member = await users.get({email: customer.email});
|
2019-10-08 10:22:26 +03:00
|
|
|
|
2020-02-26 07:09:09 +03:00
|
|
|
await stripe.handleCheckoutSetupSessionCompletedWebhook(setupIntent, member);
|
2020-07-21 12:50:10 +03:00
|
|
|
} else if (event.data.object.mode === 'subscription') {
|
|
|
|
common.logging.info('Handling "subscription" mode Checkout Session');
|
2020-02-26 07:09:09 +03:00
|
|
|
const customer = await stripe.getCustomer(event.data.object.customer, {
|
|
|
|
expand: ['subscriptions.data.default_payment_method']
|
|
|
|
});
|
2020-05-19 11:20:40 +03:00
|
|
|
let member = await users.get({email: customer.email});
|
2020-10-28 15:36:04 +03:00
|
|
|
const checkoutType = _.get(event, 'data.object.metadata.checkoutType');
|
2020-10-29 09:42:15 +03:00
|
|
|
const requestSrc = _.get(event, 'data.object.metadata.requestSrc') || '';
|
2020-05-19 11:20:40 +03:00
|
|
|
if (!member) {
|
2020-10-19 14:44:01 +03:00
|
|
|
const metadataName = _.get(event, 'data.object.metadata.name');
|
|
|
|
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
|
|
|
const name = metadataName || payerName || null;
|
2020-05-19 11:20:40 +03:00
|
|
|
member = await users.create({email: customer.email, name});
|
2020-10-19 14:44:01 +03:00
|
|
|
} else {
|
|
|
|
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
2020-01-20 09:25:17 +03:00
|
|
|
|
2020-10-19 14:44:01 +03:00
|
|
|
if (payerName && !member.get('name')) {
|
|
|
|
await users.update({name: payerName}, {id: member.get('id')});
|
|
|
|
}
|
2020-02-26 07:09:09 +03:00
|
|
|
}
|
|
|
|
|
2020-10-19 14:44:01 +03:00
|
|
|
await stripe.handleCheckoutSessionCompletedWebhook(member, customer);
|
2020-10-28 15:36:04 +03:00
|
|
|
if (checkoutType !== 'upgrade') {
|
|
|
|
const emailType = 'signup';
|
|
|
|
await sendEmailWithMagicLink({email: customer.email, requestedType: emailType, requestSrc, options: {forceEmailType: true}, tokenData: {}});
|
|
|
|
}
|
2020-07-21 12:50:10 +03:00
|
|
|
} else if (event.data.object.mode === 'payment') {
|
|
|
|
common.logging.info('Ignoring "payment" mode Checkout Session');
|
2020-02-26 07:09:09 +03:00
|
|
|
}
|
2019-10-08 10:22:26 +03:00
|
|
|
}
|
2019-09-25 06:27:57 +03:00
|
|
|
|
|
|
|
res.writeHead(200);
|
|
|
|
res.end();
|
|
|
|
} catch (err) {
|
2019-10-08 14:14:59 +03:00
|
|
|
common.logging.error(`Error handling webhook ${event.type}`, err);
|
2019-09-25 06:27:57 +03:00
|
|
|
res.writeHead(400);
|
|
|
|
res.end();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-12-12 11:19:36 +03:00
|
|
|
middleware.updateSubscription.use(ensureStripe, body.json(), async function (req, res) {
|
|
|
|
const identity = req.body.identity;
|
2020-11-23 19:28:35 +03:00
|
|
|
const subscriptionId = req.params.id;
|
2019-12-12 11:19:36 +03:00
|
|
|
const cancelAtPeriodEnd = req.body.cancel_at_period_end;
|
2020-11-23 19:28:35 +03:00
|
|
|
const cancellationReason = req.body.cancellation_reason;
|
2020-05-19 10:29:39 +03:00
|
|
|
const planName = req.body.planName;
|
2019-12-12 11:19:36 +03:00
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
if (cancelAtPeriodEnd === undefined && planName === undefined) {
|
|
|
|
throw new common.errors.BadRequestError({
|
|
|
|
message: 'Updating subscription failed!',
|
|
|
|
help: 'Request should contain "cancel_at_period_end" or "planName" field.'
|
|
|
|
});
|
|
|
|
}
|
2019-12-12 11:19:36 +03:00
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
if ((cancelAtPeriodEnd === undefined || cancelAtPeriodEnd === false) && cancellationReason !== undefined) {
|
|
|
|
throw new common.errors.BadRequestError({
|
|
|
|
message: 'Updating subscription failed!',
|
|
|
|
help: '"cancellation_reason" field requires the "cancel_at_period_end" field to be true.'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cancellationReason && cancellationReason.length > 500) {
|
|
|
|
throw new common.errors.BadRequestError({
|
|
|
|
message: 'Updating subscription failed!',
|
|
|
|
help: '"cancellation_reason" field can be a maximum of 500 characters.'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let email;
|
2019-12-12 11:19:36 +03:00
|
|
|
try {
|
|
|
|
if (!identity) {
|
|
|
|
throw new common.errors.BadRequestError({
|
2020-05-19 10:29:39 +03:00
|
|
|
message: 'Updating subscription failed! Could not find member'
|
2019-12-12 11:19:36 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const claims = await decodeToken(identity);
|
2020-11-23 19:28:35 +03:00
|
|
|
email = claims && claims.sub;
|
2019-12-12 11:19:36 +03:00
|
|
|
} catch (err) {
|
|
|
|
res.writeHead(401);
|
|
|
|
return res.end('Unauthorized');
|
|
|
|
}
|
2020-11-23 19:28:35 +03:00
|
|
|
|
|
|
|
const member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
|
|
|
|
|
|
|
|
if (!member) {
|
2020-05-19 10:29:39 +03:00
|
|
|
throw new common.errors.BadRequestError({
|
2020-11-23 19:28:35 +03:00
|
|
|
message: 'Updating subscription failed! Could not find member'
|
2020-05-19 10:29:39 +03:00
|
|
|
});
|
|
|
|
}
|
2020-11-23 19:28:35 +03:00
|
|
|
|
|
|
|
// Don't allow removing subscriptions that don't belong to the member
|
2020-08-12 14:57:28 +03:00
|
|
|
const subscription = member.related('stripeSubscriptions').models.find(
|
|
|
|
subscription => subscription.get('subscription_id') === subscriptionId
|
|
|
|
);
|
2019-12-12 11:19:36 +03:00
|
|
|
if (!subscription) {
|
|
|
|
res.writeHead(403);
|
|
|
|
return res.end('No permission');
|
|
|
|
}
|
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
const subscriptionUpdateData = {
|
|
|
|
id: subscriptionId
|
2020-05-19 10:29:39 +03:00
|
|
|
};
|
|
|
|
if (cancelAtPeriodEnd !== undefined) {
|
2020-11-23 19:28:35 +03:00
|
|
|
subscriptionUpdateData.cancel_at_period_end = cancelAtPeriodEnd;
|
|
|
|
subscriptionUpdateData.cancellation_reason = cancellationReason;
|
2020-05-19 10:29:39 +03:00
|
|
|
}
|
2019-12-12 11:19:36 +03:00
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
if (planName !== undefined) {
|
|
|
|
const plan = stripe.findPlanByNickname(planName);
|
|
|
|
if (!plan) {
|
|
|
|
throw new common.errors.BadRequestError({
|
|
|
|
message: 'Updating subscription failed! Could not find plan'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
subscriptionUpdateData.plan = plan.id;
|
2020-05-19 10:29:39 +03:00
|
|
|
}
|
2019-12-12 11:19:36 +03:00
|
|
|
|
2020-11-23 19:28:35 +03:00
|
|
|
await stripe.updateSubscriptionFromClient(subscriptionUpdateData);
|
2019-12-12 11:19:36 +03:00
|
|
|
|
|
|
|
res.writeHead(204);
|
|
|
|
res.end();
|
|
|
|
});
|
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
const getPublicConfig = function () {
|
Updated theme layer to use members-ssr (#10676)
* Removed support for cookies in members auth middleware
no-issue
The members middleware will no longer be supporting cookies, the cookie
will be handled by a new middleware specific for serverside rendering,
more informations can be found here:
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Removed members auth middleware from site app
no-issue
The site app no longer needs the members auth middleware as it doesn't
support cookies, and will be replaced by ssr specific middleware.
https://paper.dropbox.com/doc/Members-Auth-II-4WP4vF6coMqDYbSMIajo5
* Added comment for session_secret setting
no-issue
We are going to have multiple concepts of sessions, so adding a comment
here to be specific that this is for the Ghost Admin client
* Added theme_session_secret setting dynamic default
no-issue
Sessions for the theme layer will be signed, so we generate a random hex
string to use as a signing key
* Added getPublicConfig method
* Replaced export of httpHandler with POJO apiInstance
no-issue
This is mainly to reduce the public api, so it's easier to document.
* Renamed memberUserObject -> members
no-issue
Simplifies the interface, and is more inline with what we would want to export as an api library.
* Removed use of require options inside members
no-issue
This was too tight of a coupling between Ghost and Members
* Simplified apiInstance definition
no-issue
* Added getMember method to members api
* Added MembersSSR instance to members service
* Wired up routes for members ssr
* Updated members auth middleware to use getPublicConfig
* Removed publicKey static export from members service
* Used real session secret
no-issue
* Added DELETE /members/ssr handler
no-issue
This allows users to log out of the theme layer
* Fixed missing code property
no-issue
Ignition uses the statusCode property to forward status codes to call sites
* Removed superfluous error middleware
no-issue
Before we used generic JWT middleware which would reject, now the
middleware catches it's own error and doesn't error, thus this
middleware is unecessary.
* Removed console.logs
no-issue
* Updated token expirty to hardcoded 20 minutes
no-issue
This returns to our previous state of using short lived tokens, both for
security and simplicity.
* Removed hardcoded default member settings
no-issue
This is no longer needed, as defaults are in default-settings.json
* Removed stripe from default payment processor
no-issue
* Exported `getSiteUrl` method from url utils
no-issue
This keeps inline with newer naming conventions
* Updated how audience access control works
no-issue
Rather than being passed a function, members api now receives an object
which describes which origins have access to which audiences, and how
long those tokens should be allowed to work for. It also allows syntax
for default tokens where audience === origin requesting it. This can be
set to undefined or null to disable this functionality.
{
"http://site.com": {
"http://site.com": {
tokenLength: '5m'
},
"http://othersite.com": {
tokenLength: '1h'
}
},
"*": {
tokenLength: '30m'
}
}
* Updated members service to use access control feature
no-issue
This also cleans up a lot of unecessary variable definitions, and some
other minor cleanups.
* Added status code to auth pages html response
no-issue
This was missing, probably default but better to be explicit
* Updated gateway to have membersApiUrl from config
no-issue
Previously we were parsing the url, this was not very safe as we can
have Ghost hosted on a subdomain, and this would have failed.
* Added issuer to public config for members
no-issue
This can be used to request SSR tokens in the client
* Fixed path for gateway bundle
no-issue
* Updated settings model tests
no-issue
* Revert "Removed stripe from default payment processor"
This reverts commit 1d88d9b6d73a10091070bcc1b7f5779d071c7845.
* Revert "Removed hardcoded default member settings"
This reverts commit 9d899048ba7d4b272b9ac65a95a52af66b30914a.
* Installed @tryghost/members-ssr
* Fixed tests for settings model
2019-04-16 17:50:25 +03:00
|
|
|
return Promise.resolve({
|
|
|
|
publicKey,
|
|
|
|
issuer
|
|
|
|
});
|
|
|
|
};
|
2019-07-17 13:20:13 +03:00
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
const bus = new (require('events').EventEmitter)();
|
2019-09-06 08:14:56 +03:00
|
|
|
|
|
|
|
if (stripe) {
|
|
|
|
stripe.ready().then(() => {
|
2019-09-25 12:32:33 +03:00
|
|
|
bus.emit('ready');
|
2019-09-06 08:14:56 +03:00
|
|
|
}).catch((err) => {
|
2019-09-25 12:32:33 +03:00
|
|
|
bus.emit('error', err);
|
2019-09-06 08:14:56 +03:00
|
|
|
});
|
|
|
|
} else {
|
2019-09-25 12:32:33 +03:00
|
|
|
process.nextTick(() => bus.emit('ready'));
|
2019-09-06 08:14:56 +03:00
|
|
|
}
|
2018-12-11 09:47:44 +03:00
|
|
|
|
2019-09-25 12:32:33 +03:00
|
|
|
return {
|
|
|
|
middleware,
|
|
|
|
getMemberDataFromMagicLinkToken,
|
|
|
|
getMemberIdentityToken,
|
|
|
|
getMemberIdentityData,
|
2020-06-04 15:20:19 +03:00
|
|
|
setMemberGeolocationFromIp,
|
2019-09-25 12:32:33 +03:00
|
|
|
getPublicConfig,
|
|
|
|
bus,
|
2020-01-15 11:35:15 +03:00
|
|
|
sendEmailWithMagicLink,
|
2020-02-06 12:08:39 +03:00
|
|
|
getMagicLink,
|
2020-06-18 19:01:04 +03:00
|
|
|
hasActiveStripeSubscriptions,
|
2019-09-25 12:32:33 +03:00
|
|
|
members: users
|
|
|
|
};
|
2018-12-11 09:47:44 +03:00
|
|
|
};
|