strong-arc
Version:
A visual suite for the StrongLoop API Platform
412 lines (386 loc) • 13.2 kB
JavaScript
var request = require('request');
var async = require('async');
var KeyStore = require('./key-store');
var debug = require('debug')('strong-arc:subscription');
// Extract the access token from the request
function getAccessToken(req) {
var token = req.query.access_token;
if (token) {
return token;
}
token = req.get && req.get('authorization');
if (typeof token === 'string') {
// Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750
if (token.indexOf('Bearer ') === 0) {
token = token.substring(7);
// Decode from base64
var buf = new Buffer(token, 'base64');
token = buf.toString('utf8');
} else if (/^Basic /i.test(token)) {
token = token.substring(6);
token = (new Buffer(token, 'base64')).toString('utf8');
// The spec says the string is user:pass, so if we see both parts
// we will assume the longer of the two is the token, so we will
// extract "a2b2c3" from:
// "a2b2c3"
// "a2b2c3:" (curl http://a2b2c3@localhost:3000/)
// "token:a2b2c3" (curl http://token:a2b2c3@localhost:3000/)
// ":a2b2c3"
var parts = /^([^:]*):(.*)$/.exec(token);
if (parts) {
token = parts[2].length > parts[1].length ? parts[2] : parts[1];
}
}
return token;
}
}
// Parse the http response
function handleRes(err, res, body, done) {
if (err) {
return done(err);
}
if (res.statusCode !== 200 && res.statusCode !== 201
&& res.statusCode !== 204) {
debug('Error %d: %j', res.statusCode, body);
if (body) {
if (typeof body.error === 'object') {
err = new Error();
for (var p in body.error) {
err[p] = body.error[p];
}
} else if (typeof body === 'string') {
err = new Error(body);
err.statusCode = res.statusCode;
}
}
if (!err) {
err = new Error('Error status: ' + res.statusCode);
}
return done(err);
}
return done(err, body);
}
module.exports = function(Subscription) {
var store = new KeyStore();
function loadSubscriptionsFromStore(userId, cb) {
return store.load(userId, function(err, content) {
if (err) {
return cb(err);
} else {
return cb(null, (content && content.licenses) || []);
}
});
}
function loadProductsFromStore(userId, cb) {
return store.load(userId, function(err, content) {
if (err) {
return cb(err);
} else {
return cb(null, (content && content.products) || {});
}
});
}
Subscription.provisionSubscriptions = function(cb) {
store.load(null, function(err, content) {
if (err) return cb(err);
if (content && content.userId) {
Subscription.provisionTrials(content.userId, content.accessToken,
function(err, result) {
if (err) return cb(err);
debug('Trial subscriptions provisioned:', result);
reloadSubscriptions(content.userId, content.accessToken, null, cb);
});
}
});
};
Subscription.provisionTrials = function(userId, accessToken, cb) {
var url = Subscription.settings.authUrl;
request.post({
url: url + 'users/' + userId + '/provisionTrials',
qs: {
access_token: accessToken
},
json: true
}, function(err, res, body) {
handleRes(err, res, body, cb);
});
};
/**
* Get all product subscriptions for a given user
* @param {Number|String} userId User id
* @param {String} mode Operation mode: online/offline/...
* @param {Request} req HTTP request object
* @param {Function} cb callback
*/
Subscription.getSubscriptions = function(userId, mode, req, cb) {
if (mode === 'offline') {
debug('Loading subscriptions from the local key store (offline)');
// Read from the local key store
return loadSubscriptionsFromStore(userId, cb);
}
var tasks = {};
var accessToken = getAccessToken(req);
tasks.subscriptions = function(done) {
debug('Loading subscriptions from auth service (online)');
var url = Subscription.settings.authUrl;
request.get({
url: url + 'users/' + userId + '/subscriptions',
qs: {
access_token: accessToken
},
json: true
}, function(err, res, body) {
handleRes(err, res, body, done);
});
};
tasks.products = function(done) {
debug('Loading products from auth service (online)');
Subscription.getProducts(mode, req, done);
};
async.parallel(tasks, function(err, results) {
if (err) {
if (mode === 'online') {
// Report error for online mode
return cb(err);
} else {
debug('Falling back to load subscriptions from the local key store');
// Fall back to offline mode
return loadSubscriptionsFromStore(userId, cb);
}
} else {
var products = results.products;
var subscriptions = results.subscriptions;
// Refresh the local key store
store.save({
products: products,
licenses: subscriptions,
userId: userId,
accessToken: accessToken,
modified: new Date()}, userId, function(err) {
cb(err, subscriptions);
});
}
});
};
function reloadSubscriptions(userId, accessToken, result, cb) {
Subscription.getSubscriptions(userId, 'online', {
query: {access_token: accessToken}
}, function(err) {
if (err) {
return cb(err);
} else {
return cb(null, result);
}
});
}
/**
* Renew a trial subscription for a given user/product/features
* @param {Number|String} userId User id
* @param {String} product Product name
* @param {String|String[]} features Feature names
* @param {Request} req HTTP request object
* @param {Function} cb callback
*/
Subscription.renewTrial = function(userId, product, features, req, cb) {
var url = Subscription.settings.authUrl;
var accessToken = getAccessToken(req);
request.post({
url: url + 'users/' + userId + '/renewTrial',
qs: {
access_token: accessToken
},
json: {
product: product,
features: features
}
}, function(err, res, body) {
handleRes(err, res, body, function(err) {
if (err) {
return cb(err);
}
reloadSubscriptions(userId, accessToken, body, cb);
});
});
};
/**
* List product descriptions
* @param {String} mode Operation mode
* @param {Request} req HTTP request object
* @param {Function} cb callback
*/
Subscription.getProducts = function(mode, req, cb) {
if (mode === 'offline') {
return loadProductsFromStore(null, cb);
}
var url = Subscription.settings.authUrl;
request.get({
url: url + 'subscriptions/products',
qs: {
access_token: getAccessToken(req)
},
json: true
}, function(err, res, body) {
if (err && mode !== 'online') {
debug('Falling back to load products from the local key store');
// Fall back to offline mode
return loadProductsFromStore(null, cb);
}
handleRes(err, res, body, cb);
});
};
/**
* Track usages for a given user/product/features
* @param {Number|String} userId User id
* @param {Object|Object[]} usages Product usages
* @param {Request} req HTTP request object
* @param {Function} cb callback
*/
Subscription.trackUsages = function(userId, usages, req, cb) {
var url = Subscription.settings.authUrl;
request.post({
url: url + 'users/' + userId + '/trackUsages',
qs: {
access_token: getAccessToken(req)
},
json: usages
}, function(err, res, body) {
handleRes(err, res, body, cb);
});
};
/**
* Log in with credentials to get the access token
* @param {Object} credentials username/password
* @param {String} include Set to 'user' to include the user information
* @param {Function} cb callback
*/
Subscription.login = function(credentials, include, cb) {
var url = Subscription.settings.authUrl;
request.post({
url: url + 'users/login',
qs: {
include: include
},
json: credentials
}, function(err, res, body) {
handleRes(err, res, body, function(err) {
if (err) {
return cb(err);
}
var userId = body.userId;
var accessToken = body.id;
reloadSubscriptions(userId, accessToken, body, cb);
});
});
};
Subscription.remoteMethod('login', {
isStatic: true,
description: 'Login a user with username/email and password',
accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
{arg: 'include', type: 'string', http: {source: 'query' },
description: 'Related objects to include in the response. ' +
'See the description of return value for more details.'}
],
returns: {
arg: 'accessToken', type: 'object', root: true,
description: 'The response body contains properties of the AccessToken created on login.\n' +
'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' +
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
},
http: {verb: 'post', path: '/login'}
}
);
Subscription.remoteMethod('getSubscriptions', {
isStatic: true,
description: 'List subscriptions for a given username or email',
accepts: [
{arg: 'userId', type: 'number', description: 'User id',
http: {source: 'path'}, required: true},
{arg: 'mode', type: 'string', description: 'Operation mode',
http: {source: 'query'}},
{arg: 'req', type: 'object', http: {source: 'req'}}
],
returns: {arg: 'data', type: ['subscription'], root: true},
http: {verb: 'get', path: '/:userId/getSubscriptions'}});
Subscription.remoteMethod('getProducts', {
isStatic: true,
description: 'List products',
accepts: [
{arg: 'mode', type: 'string', description: 'Operation mode',
http: {source: 'query'}},
{arg: 'req', type: 'object', http: {source: 'req'}}
],
returns: {arg: 'data', type: 'object', root: true},
http: {verb: 'get', path: '/products'}});
Subscription.remoteMethod('trackUsages', {
isStatic: true,
description: 'Track usages',
accepts: [
{arg: 'userId', type: 'number', description: 'User id',
http: {source: 'path'}, required: true},
{arg: 'usages', type: 'array', description: 'Usage records',
http: {source: 'body'}},
{arg: 'req', type: 'object', http: {source: 'req'}}
],
returns: {arg: 'count', type: 'number', root: true},
http: {verb: 'post', path: '/:userId/trackUsages'}});
Subscription.remoteMethod('renewTrial', {
isStatic: true,
description: 'Renew the trial subscription',
accepts: [
{arg: 'userId', type: 'number', description: 'User id',
http: {source: 'path'}, required: true},
{arg: 'product', type: 'string', description: 'product',
http: {source: 'form'}, required: true},
{arg: 'features', type: 'string', description: 'features',
http: {source: 'form'}},
{arg: 'req', type: 'object', http: {source: 'req'}}
],
returns: {arg: 'subscription', type: 'subscription', root: true},
http: {verb: 'post', path: '/:userId/renewTrial'}});
// FIXME: [rfeng] This is hack to prevent gulp build from hanging due to
// active handles in the server
if (process.env.GULP_ANGULAR_CODEGEN) {
return;
}
var app = require('../../../server/server');
app.meshProxy.models.ManagerHost.observe('before action',
function(ctx, next) {
debug('Trapping strong-pm command:', ctx.data);
if (['stop', 'start', 'restart', 'cluster-restart', 'license-push']
.indexOf(ctx.data.cmd) === -1) {
return next();
}
Subscription.getSubscriptions(null, 'offline', null,
function(err, subscriptions) {
if (err) {
return next(err);
}
debug('Subscriptions:', subscriptions);
if (Array.isArray(subscriptions)) {
var keys = subscriptions.map(function(s) {
return s.licenseKey;
}).join(':');
debug('Pushing license keys:', keys);
var ServerService = ctx.instance.getPMClient().models.ServerService;
ServerService.findById(ctx.service.serverServiceId, function(err, svc) {
if (err) {
return next(err);
}
svc.setEnv('STRONGLOOP_LICENSE', keys, function(err, res) {
debug('Response from strong-pm: %j %j', err, res);
if (!err && res && res.result && res.result.error) {
next(new Error(res.result.error));
} else {
next(err, res);
}
});
});
} else {
next();
}
});
});
};