Ghost/ghost/members-api/index.js
Fabien O'Carroll 1fb969ad36 Refactored to improve logging and error handling
* Installed stripe@7.4.0

refs #38

We were relying on stripe being installed in Ghost, this moves the dep
to the correct package.

* Created exponentialBackoff wrapper for stripe api

refs #38

https://stripe.com/docs/testing#rate-limits The stripe docs suggest to
use exponential backoff when recieving a rate limit error. This wrapper
will wrap stripe api calls, and retry them after 1s,2s,4s,8s,16s until
eventually failing. This gives a total of 5 retries over 31s.

* Added wrappers around the stripe api calls

refs #38

* Ensured all calls to stripe api go via exp backoff

refs #38

* Scaffolding out the error handling for stripe api

* Forwarding all errors

* Refactored stripe api into modules

* Ensured the ready promise object is not replaced

* Added logging setup

- Sets up common logger structure with custom logger passed through

* Ensure logger is kept in module state

* Renamed updateLogger to setLogger

* Removed `logger` param and exposed setLogger method

* Ensured different ids used for test mode

* Ensure setLogger works for prototype methods

* Removed reconfigureSettings method

* Updated payment processer service to keep static ready promise

* Added eventemitter to member api instance to handle errors

* Moved logging of errors to http level
2019-07-17 18:20:13 +08:00

257 lines
7.2 KiB
JavaScript

const {Router} = require('express');
const {EventEmitter} = require('events');
const body = require('body-parser');
const {getData, handleError} = require('./lib/util');
const Cookies = require('./lib/cookies');
const Tokens = require('./lib/tokens');
const Users = require('./lib/users');
const Subscriptions = require('./lib/subscriptions');
const common = require('./lib/common');
module.exports = function MembersApi({
authConfig: {
issuer,
privateKey,
publicKey,
sessionSecret,
ssoOrigin,
accessControl
},
paymentConfig,
createMember,
validateMember,
updateMember,
getMember,
deleteMember,
listMembers,
sendEmail,
siteConfig
}) {
const {encodeToken, decodeToken, getPublicKeys} = Tokens({privateKey, publicKey, issuer});
let subscriptions = new Subscriptions(paymentConfig);
let users = Users({
subscriptions,
createMember,
updateMember,
getMember,
deleteMember,
validateMember,
sendEmail,
encodeToken,
listMembers,
decodeToken
});
const apiRouter = Router();
apiRouter.use(body.json());
/* session */
const {getCookie, setCookie, removeCookie} = Cookies(sessionSecret);
function validateAccess({audience, origin}) {
const audienceLookup = accessControl[origin] || {
[origin]: accessControl['*']
};
const tokenSettings = audienceLookup[audience];
if (tokenSettings) {
return Promise.resolve(tokenSettings);
}
return Promise.reject();
}
/* token */
apiRouter.post('/token', getData('audience'), (req, res) => {
const {signedin} = getCookie(req);
if (!signedin) {
res.writeHead(401, {
'Set-Cookie': removeCookie()
});
return res.end();
}
const {audience, origin} = req.data;
validateAccess({audience, origin})
.then(({tokenLength}) => {
return users.get({id: signedin})
.then(member => encodeToken({
sub: member.id,
plans: member.plans,
exp: tokenLength,
aud: audience
}));
})
.then(token => res.end(token))
.catch(handleError(403, res));
});
apiRouter.get('/config', (req, res) => {
subscriptions.getAdapters()
.then((adapters) => {
return Promise.all(adapters.map((adapter) => {
return subscriptions.getPublicConfig(adapter);
}));
})
.then(paymentConfig => res.json({
paymentConfig,
issuer,
siteConfig
}))
.catch(handleError(500, res));
});
/* security */
function ssoOriginCheck(req, res, next) {
if (!req.data.origin || req.data.origin !== ssoOrigin) {
res.writeHead(403);
return res.end();
}
next();
}
/* subscriptions */
apiRouter.post('/subscription', getData('adapter', 'plan', 'stripeToken', {name: 'coupon', required: false}), ssoOriginCheck, (req, res) => {
const {signedin} = getCookie(req);
if (!signedin) {
res.writeHead(401, {
'Set-Cookie': removeCookie()
});
return res.end();
}
const {plan, adapter, stripeToken, coupon} = req.data;
subscriptions.getAdapters()
.then((adapters) => {
if (!adapters.includes(adapter)) {
throw new Error('Invalid adapter');
}
})
.then(() => users.get({id: signedin}))
.then((member) => {
return subscriptions.createSubscription(member, {
adapter,
plan,
stripeToken,
coupon
});
})
.then(() => {
res.end();
})
.catch(handleError(500, res));
});
/* users, token, emails */
apiRouter.post('/request-password-reset', getData('email'), ssoOriginCheck, (req, res) => {
const {email} = req.data;
users.requestPasswordReset({email}).then(() => {
res.writeHead(200);
res.end();
}).catch(handleError(500, res));
});
/* users, token */
apiRouter.post('/reset-password', getData('token', 'password'), ssoOriginCheck, (req, res) => {
const {token, password} = req.data;
users.resetPassword({token, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
/* users, email */
apiRouter.post('/signup', getData('name', 'email', 'password'), ssoOriginCheck, (req, res) => {
const {name, email, password} = req.data;
// @TODO this should attempt to reset password before creating member
users.create({name, email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(400, res));
});
/* users, session */
apiRouter.post('/signin', getData('email', 'password'), ssoOriginCheck, (req, res) => {
const {email, password} = req.data;
users.validate({email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
/* session */
apiRouter.post('/signout', getData(), (req, res) => {
res.writeHead(200, {
'Set-Cookie': removeCookie()
});
res.end();
});
/* http */
const staticRouter = Router();
staticRouter.get('/gateway', (req, res) => {
res.status(200).send(`
<script>
window.membersApiUrl = "${issuer}";
</script>
<script src="bundle.js"></script>
`);
});
staticRouter.get('/bundle.js', (req, res) => {
res.status(200).sendFile(require('path').join(__dirname, './gateway/bundle.js'));
});
const apiInstance = new Router();
const eventBus = new EventEmitter();
subscriptions._ready.then(() => {
eventBus.emit('ready');
}, (err) => {
eventBus.emit('error', err);
});
apiInstance.bus = eventBus;
apiInstance.use(apiRouter);
apiInstance.use('/static', staticRouter);
apiInstance.apiRouter = apiRouter;
apiInstance.staticRouter = staticRouter;
apiInstance.members = users;
apiInstance.getPublicKeys = getPublicKeys;
apiInstance.getPublicConfig = function () {
return Promise.resolve({
publicKey,
issuer
});
};
apiInstance.getMember = function (id, token) {
return decodeToken(token).then(() => {
return users.get({id});
});
};
apiInstance.setLogger = common.logging.setLogger;
return apiInstance;
};