walnut.js/lib/apis.js

1210 lines
42 KiB
JavaScript

'use strict';
module.exports.create = function (xconfx, apiFactories, apiDeps) {
var PromiseA = apiDeps.Promise;
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var request = PromiseA.promisify(require('request'));
//var express = require('express');
var express = require('express-lazy');
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path');
var localCache = { rests: {}, pkgs: {}, assets: {} };
var promisableRequest = require('./common').promisableRequest;
var rejectableRequest = require('./common').rejectableRequest;
var crypto = require('crypto');
// TODO xconfx.apispath
xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest');
xconfx.apiPath = path.join(__dirname, '..', '..', 'packages', 'api');
xconfx.appApiGrantsPath = path.join(__dirname, '..', '..', 'packages', 'client-api-grants');
xconfx.appConfigPath = path.join(__dirname, '..', '..', 'var');
function notConfigured(req, res) {
var msg = "api package '" + req.pkgId + "' not configured for client uri '" + req.experienceId + "'"
+ ". To configure it place a new line '" + req.pkgId + "' in the file '/srv/walnut/packages/client-api-grants/" + req.experienceId + "'"
;
res.send({ error: { message: msg } });
}
/*
function isThisPkgInstalled(myConf, pkgId) {
}
*/
function isThisClientAllowedToUseThisPkg(req, myConf, clientUrih, pkgId) {
var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
return text.trim().split(/\n/);
}, function (err) {
if ('ENOENT' !== err.code) {
console.error(err);
}
return [];
}).then(function (apis) {
if (apis.some(function (api) {
if (api === pkgId) {
return true;
}
})) {
return true;
}
console.log('#################################################');
console.log('assets.' + xconfx.setupDomain);
console.log('assets.' + clientUrih);
console.log(req.clientAssetsUri);
console.log(pkgId);
if (req.clientAssetsUri === ('assets.' + clientUrih) && -1 !== [ 'session', 'session@oauth3.org', 'azp@oauth3.org', 'issuer@oauth3.org' ].indexOf(pkgId)) {
// fallthrough
return true;
}
if (clientUrih === ('api.' + xconfx.setupDomain) && -1 !== ['org.oauth3.consumer', 'azp@oauth3.org', 'oauth3.org'].indexOf(pkgId)) {
// fallthrough
return true;
}
return null;
});
}
function getSitePackageConfig(clientUrih, pkgId) {
var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
return mkdirpAsync(siteConfigPath).then(function () {
return fs.readFileAsync(path.join(siteConfigPath, pkgId + '.json'), 'utf8').then(function (text) {
return JSON.parse(text);
}).then(function (data) { return data; }, function (/*err*/) { return {}; });
});
}
function getSiteConfig(clientUrih) {
// TODO test if the requesting package has permission to the root-level site config
var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
return mkdirpAsync(siteConfigPath).then(function () {
return fs.readFileAsync(path.join(siteConfigPath, 'config.json'), 'utf8').then(function (text) {
return JSON.parse(text);
}).then(function (data) { return data; }, function (/*err*/) { return {}; });
});
}
var modelsCache = {};
function getSiteStore(clientUrih, pkgId, dir) {
if (!modelsCache[clientUrih]) {
modelsCache[clientUrih] = apiFactories.systemSqlFactory.create({
init: true
, dbname: clientUrih // '#' is a valid file name character
});
}
// DB scopes:
// system (global)
// experience (per domain)
// api (per api)
// account (per user account)
// client (per 3rd party client)
// scope Experience to db
// scope Api by table
// scope Account and Client by column
return modelsCache[clientUrih].then(function (db) {
var wrap = require('masterquest-sqlite3');
return wrap.wrap(db, dir).then(function (models) {
//modelsCache[clientUrih] = PromiseA.resolve(models);
return models;
});
});
}
function accountRequiredById(req, res, next) {
var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
var tok = req.oauth3.token;
var accountId = req.params.accountId || '__NO_ID_GIVEN__';
var ppid;
var iss = tok.iss;
if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
return ppid === accountId;
}).length) {
ppid = accountId;
}
// Deprecated backwards compat. To be removed.
if (tok.axs && tok.axs.filter(function (acc) {
return acc.id === accountId || acc.appScopedId === accountId;
}).length) {
ppid = accountId;
}
if (tok.acx && accountId === (tok.acx.appScopedId || tok.acx.id || tok.acx)) {
ppid = accountId;
}
if (!ppid) {
return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
}
return req.oauth3.rescope().then(function (accountIdx) {
req.oauth3.accountIdx = accountIdx;
req.oauth3.ppid = ppid;
//console.log('[walnut@daplie.com] accountIdx:', accountIdx);
//console.log('[walnut@daplie.com] ppid:', ppid);
next();
});
});
rejectableRequest(req, res, promise, "[walnut@daplie.com] attach account by id");
}
function accountRequired(req, res, next) {
console.log('[accountRequired] [enter]');
var myIss = req.experienceId;
var isPpid;
// if this already has auth, great
if (req.oauth3.ppid && req.oauth3.accountIdx) {
// except that if it's a ppid, we have to internally exchange it for the real token
isPpid = (myIss === req.oauth3.iss && myIss !== req.oauth3.azp);
if (!isPpid) {
console.log('[accountRequired] has token already');
console.log(req.oauth3);
console.log('');
next();
return;
}
}
if (!req.oauth3.encodedToken) {
// being public does not disallow authentication
if (req.isPublic) {
next();
return;
}
rejectableRequest(
req
, res
, PromiseA.reject(new Error("this secure resource requires an access token"))
, "[walnut@daplie.com] required account (not /public)"
);
return;
}
// verify the auth if it's here
var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
var tok = req.oauth3.token;
var ppid;
var err;
var iss = tok.iss;
if (tok.sub) {
if (tok.sub.split(/,/g).length > 1) {
err = new Error("more than one 'sub' specified in token");
return PromiseA.reject(err);
}
ppid = tok.sub;
}
if (!ppid) {
return PromiseA.reject(new Error("could not determine accountId from access token"));
}
return req.oauth3.rescope().then(function (accountIdx) {
console.log('[accountRequired] req.oauth3');
console.log(accountIdx);
var sub = accountIdx.split('@')[0];
var iss = accountIdx.split('@')[1];
var id = sub + '@' + iss;
req.oauth3.profile = {
id: id
, sub: sub
, iss: iss
};
req.oauth3.id = id;
req.oauth3.sub = sub;
req.oauth3.iss = iss;
req.oauth3.accountIdx = accountIdx;
req.oauth3.ppid = ppid;
next();
});
});
rejectableRequest(req, res, promise, "[walnut@daplie.com] required account (not /public)");
}
function grantsRequired(grants) {
if (!Array.isArray(grants)) {
throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
}
if (!grants.length) {
return function (req, res, next) {
next();
};
}
return function (req, res, next) {
var tokenScopes;
if (!(req.oauth3 || req.oauth3.token)) {
// TODO some error generator for standard messages
res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
return;
}
var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
if ('string' !== typeof scope) {
res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
return;
}
tokenScopes = scope.split(/[,\s]+/mg);
if (-1 !== tokenScopes.indexOf('*')) {
// has full account access
next();
return;
}
// every grant in the array must be present, though some grants can be satisfied
// by multiple scopes.
var missing = grants.filter(function (grant) {
return !grant.split('|').some(function (scp) {
return tokenScopes.indexOf(scp) !== -1;
});
});
if (missing.length) {
res.send({ error: { message: "Token missing required grants: '" + missing.join(',') + "'", code: "E_NO_GRANTS" } });
return;
}
next();
};
}
function loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath) {
var pkgLinks = [];
pkgLinks.push(pkgId);
var pkgRestApi;
var pkgDeps = {};
var myApp;
var pkgPathApi;
pkgPathApi = pkgPath;
if (pkg.walnut) {
pkgPathApi = path.join(pkgPath, pkg.walnut);
}
pkgRestApi = require(pkgPathApi);
Object.keys(apiDeps).forEach(function (key) {
pkgDeps[key] = apiDeps[key];
});
Object.keys(apiFactories).forEach(function (key) {
pkgDeps[key] = apiFactories[key];
});
// TODO pull db stuff from package.json somehow and pass allowed data models as deps
//
// how can we tell which of these would be correct?
// deps.memstore = apiFactories.memstoreFactory.create(pkgId);
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
// let's go with this one for now and the api can choose to scope or not to scope
pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
myApp = express();
myApp.handlePromise = promisableRequest;
myApp.handleRejection = rejectableRequest;
myApp.grantsRequired = grantsRequired;
function getSitePackageStoreProp(otherPkgId) {
var restPath = path.join(myConf.restPath, otherPkgId);
var apiPath = path.join(myConf.apiPath, otherPkgId);
var dir;
// TODO usage package.json as a falback if the standard location is not used
try {
dir = require(path.join(apiPath, 'models.js'));
} catch(e) {
dir = require(path.join(restPath, 'models.js'));
}
return getSiteStore(clientUrih, otherPkgId, dir);
}
function attachOauth3(req, res, next) {
return getSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
return require('./oauth3').attachOauth3(Models, req, res, next);
});
}
myApp.use('/', attachOauth3);
// TODO delete these caches when config changes
var _stripe;
var _stripe_test;
var _mandrill;
var _mailchimp;
var _twilio;
var _get_response;
myApp.use('/', function preHandler(req, res, next) {
//if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
return getSiteConfig(clientUrih).then(function (siteConfig) {
//if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
// Use getSiteCapability('email@daplie.com') instead
Object.defineProperty(req, 'getSiteMailer' /*deprecated*/, {
enumerable: true
, configurable: false
, writable: false
, value: function getSiteMailerProp() {
var nodemailer = require('nodemailer');
var transport = require('nodemailer-mailgun-transport');
//var mailconf = require('../../../com.daplie.mailer/config.mailgun');
var mailconf = siteConfig['mailgun.org'];
var mailer = PromiseA.promisifyAll(nodemailer.createTransport(transport(mailconf)));
return mailer;
}
});
Object.defineProperty(req, 'getSiteConfig', {
enumerable: true
, configurable: false
, writable: false
, value: function getSiteConfigProp(section) {
// deprecated
if ('com.daplie.tel' === section) {
section = 'tel@daplie.com';
}
return PromiseA.resolve((siteConfig || {})[section]);
}
});
Object.defineProperty(req, 'getSitePackageConfig', {
enumerable: true
, configurable: false
, writable: false
, value: function getSitePackageConfigProp() {
return getSitePackageConfig(clientUrih, pkgId);
}
});
Object.defineProperty(req, 'getSitePackageStore', {
enumerable: true
, configurable: false
, writable: false
, value: getSitePackageStoreProp
});
Object.defineProperty(req, 'getSiteStore', {
enumerable: true
, configurable: false
, writable: false
, value: function getSiteStoreProp() {
var restPath = path.join(myConf.restPath, pkgId);
var apiPath = path.join(myConf.apiPath, pkgId);
var dir;
// TODO usage package.json as a falback if the standard location is not used
try {
dir = require(path.join(apiPath, 'models.js'));
} catch(e) {
dir = require(path.join(restPath, 'models.js'));
}
return getSiteStore(clientUrih, pkgId, dir);
}
});
/*
Object.defineProperty(req, 'getSitePayments', {
enumerable: true
, configurable: false
, writable: false
, value: function getSitePaymentsProp() {
}
});
*/
// TODO allow third-party clients stripe ids destination
// https://stripe.com/docs/connect/payments-fees
Object.defineProperty(req, 'Stripe', {
enumerable: true
, configurable: false
, get: function () {
_stripe = _stripe || require('stripe')(siteConfig['stripe.com'].live.secret);
return _stripe;
}
});
Object.defineProperty(req, 'StripeTest', {
enumerable: true
, configurable: false
, get: function () {
_stripe_test = _stripe_test || require('stripe')(siteConfig['stripe.com'].test.secret);
return _stripe_test;
}
});
Object.defineProperty(req, 'Mandrill', {
enumerable: true
, configurable: false
, get: function () {
if (!_mandrill) {
var Mandrill = require('mandrill-api/mandrill');
_mandrill = new Mandrill.Mandrill(siteConfig['mandrill.com'].apiKey);
_mandrill.messages.sendTemplateAsync = function (opts) {
return new PromiseA(function (resolve, reject) {
_mandrill.messages.sendTemplate(opts, resolve, reject);
});
};
}
return _mandrill;
}
});
Object.defineProperty(req, 'Mailchimp', {
enumerable: true
, configurable: false
, get: function () {
var Mailchimp = require('mailchimp-api-v3');
_mailchimp = _mailchimp || new Mailchimp(siteConfig['mailchimp.com'].apiKey);
return _mailchimp;
}
});
Object.defineProperty(req, 'GetResponse', {
enumerable: true
, configurable: false
, get: function () {
if (_get_response) {
return _get_response;
}
_get_response = {
saveSubscriber: function (email, opts) {
var config = siteConfig['getresponse@daplie.com'];
var customFields = [];
Object.keys(config.customFields).forEach(function (name) {
if (typeof opts[name] !== 'undefined') {
customFields.push({
customFieldId: config.customFields[name]
, value: [ String(opts[name]) ]
});
}
});
return request({
method: 'POST'
, url: 'https://api.getresponse.com/v3/contacts'
, headers: { 'X-Auth-Token': 'api-key ' + config.apiKey }
, json: true
, body: {
name: opts.name
, email: email
, ipAddress: opts.ipAddress
, campaign: { campaignId: config.campaignId }
, customFieldValues: customFields
}
}).then(function (resp) {
if (resp.statusCode === 202) {
return;
}
return PromiseA.reject(resp.body.message);
});
}
};
return _get_response;
}
});
var Twilio = require('twilio');
function twilioTel(/*opts*/) {
if (_twilio) {
return apiDeps.Promise.resolve(_twilio);
}
_twilio = new Twilio.RestClient(
siteConfig['twilio.com'].live.id
, siteConfig['twilio.com'].live.auth
);
return apiDeps.Promise.resolve(_twilio);
}
// TODO shared memory db
var mailgunTokens = {};
function validateMailgun(apiKey, timestamp, token, signature) {
// https://gist.github.com/coolaj86/81a3b61353d2f0a2552c
// (realized later)
// HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
var scmp = require('scmp')
, mailgunExpirey = 15 * 60 * 1000
, mailgunHashType = 'sha256'
, mailgunSignatureEncoding = 'hex'
;
var actual
, adjustedTimestamp = parseInt(timestamp, 10) * 1000
, fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey)
;
if (!fresh) {
console.error('[mailgun] Stale Timestamp: this may be an attack');
console.error('[mailgun] However, this is most likely your fault\n');
console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n');
console.error('[mailgun] System Time: ' + new Date().toString());
console.error('[mailgun] Mailgun Time: ' + new Date(adjustedTimestamp).toString(), timestamp);
console.error('[mailgun] Delta: ' + (Date.now() - adjustedTimestamp));
return false;
}
if (mailgunTokens[token]) {
console.error('[mailgun] Replay Attack');
return false;
}
mailgunTokens[token] = true;
setTimeout(function () {
delete mailgunTokens[token];
}, mailgunExpirey + (5 * 1000));
actual = crypto.createHmac(mailgunHashType, apiKey)
.update(new Buffer(timestamp + token, 'utf8'))
.digest(mailgunSignatureEncoding)
;
return scmp(signature, actual);
}
function mailgunMail(/*opts*/) {
return apiDeps.Promise.resolve(req.getSiteMailer());
}
function getResponseList() {
return apiDeps.Promise.resolve(req.GetResponse);
}
// Twilio Parameters are often 26 long
var bodyParserTwilio = require('body-parser').urlencoded({ limit: '4kb', parameterLimit: 100, extended: false });
// Mailgun has something like 50 parameters
var bodyParserMailgun = require('body-parser').urlencoded({ limit: '1024kb', parameterLimit: 500, extended: false });
function bodyMultiParserMailgun (req, res, next) {
var multiparty = require('multiparty');
var form = new multiparty.Form();
form.parse(req, function (err, fields/*, files*/) {
if (err) {
console.error('Error');
console.error(err);
res.end("Couldn't parse form");
return;
}
var body;
req.body = req.body || {};
Object.keys(fields).forEach(function (key) {
// TODO what if there were two of something?
// (even though there won't be)
req.body[key] = fields[key][0];
});
body = req.body;
next();
});
}
function daplieTel() {
return twilioTel().then(function (twilio) {
function sms(opts) {
// opts = { to, from, body }
return new apiDeps.Promise(function (resolve, reject) {
twilio.sendSms(opts, function (err, resp) {
if (err) {
reject(err);
return;
}
resolve(resp);
});
});
}
return {
sms: sms
, mms: function () { throw new Error('MMS Not Implemented'); }
};
});
}
var settingsPromise = PromiseA.resolve();
function manageSiteSettings(section) {
var submanager;
var manager = {
set: function (section, value) {
if ('email@daplie.com' === section) {
section = 'mailgun.org';
}
settingsPromise = settingsPromise.then(function () {
return manager.get().then(function () {
siteConfig[section] = value;
var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
return mkdirpAsync(siteConfigPath).then(function () {
return fs.writeFileAsync(path.join(siteConfigPath, 'config.json'), JSON.stringify(siteConfig), 'utf8');
});
});
});
return settingsPromise;
}
, get: function (section) {
if ('email@daplie.com' === section) {
section = 'mailgun.org';
}
settingsPromise = settingsPromise.then(function () {
return getSiteConfig(clientUrih).then(function (_siteConfig) {
siteConfig = _siteConfig;
return PromiseA.resolve((_siteConfig || {})[section]);
});
});
return settingsPromise;
}
};
submanager = manager;
if (section) {
submanager = {
set: function (value) {
return manager.set(section, value);
}
, get: function () {
return manager.get(section);
}
};
}
return apiDeps.Promise.resolve(submanager);
}
var caps = {
//
// Capabilities for APIs
//
'settings.site@daplie.com': manageSiteSettings
, 'email@daplie.com': mailgunMail // whichever mailer
, 'mailer@daplie.com': mailgunMail // whichever mailer
, 'mailgun@daplie.com': mailgunMail // specifically mailgun
, 'tel@daplie.com': daplieTel // whichever telephony service
, 'twilio@daplie.com': twilioTel // specifically twilio
, 'com.daplie.tel.twilio': twilioTel // deprecated alias
, 'getresponse@daplie.com': getResponseList
//
// Webhook Parsers
//
//, 'mailgun.urlencoded@daplie.com': function (req, res, next) { ... }
, 'mailgun.parsers@daplie.com': function (req, res, next) {
var chunks = [];
req.on('data', function (chunk) {
chunks.push(chunk);
});
req.on('end', function () {
});
function verify() {
var body = req.body;
var mailconf = siteConfig['mailgun.org'];
if (!body.timestamp) {
console.log('mailgun parser req.headers');
console.log(req.headers);
chunks.forEach(function (datum) {
console.log('Length:', datum.length);
//console.log(datum.toString('utf8'));
});
console.log('weird body');
console.log(body);
}
if (!validateMailgun(mailconf.apiKey, body.timestamp, body.token, body.signature)) {
console.error('Request came, but not from Mailgun');
console.error(req.url);
console.error(req.headers);
res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
return;
}
next();
}
if (/urlencoded/.test(req.headers['content-type'])) {
console.log('urlencoded');
bodyParserMailgun(req, res, verify);
}
else if (/multipart/.test(req.headers['content-type'])) {
console.log('multipart');
bodyMultiParserMailgun(req, res, verify);
}
else {
console.log('no parser');
next();
}
}
, 'twilio.urlencoded@daplie.com': function (req, res, next) {
// TODO null for res and Promise instead of next?
return bodyParserTwilio(req, res, function () {
var signature = req.headers['x-twilio-signature'];
var auth = siteConfig['twilio.com'].live.auth;
var fullUrl = 'https://' + req.headers.host + req._walnutOriginalUrl;
var validSig = Twilio.validateRequest(auth, signature, fullUrl, req.body);
/*
console.log('Twilio Signature Check');
console.log('auth', auth);
console.log('sig', signature);
console.log('fullUrl', fullUrl);
console.log(req.body);
console.log('valid', validSig);
*/
if (!validSig) {
res.statusCode = 401;
res.setHeader('Content-Type', 'text/xml');
res.end('<Error>Invalid signature. Are you even Twilio?</Error>');
return;
}
// TODO session via db req.body.CallId req.body.smsId
next();
});
}
};
req.getSiteCapability = function (capname, opts, b, c) {
if (caps[capname]) {
return caps[capname](opts, b, c);
}
if (siteConfig[capname]) {
var service = siteConfig[capname].service || siteConfig[capname];
if (caps[service]) {
return caps[service](opts, b, c);
}
}
return apiDeps.Promise.reject(
new Error("['" + req.clientApiUri + '/' + pkgId + "'] "
+ "capability '" + capname + "' not implemented")
);
};
req._walnutOriginalUrl = req.url;
// "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/');
next();
});
});
myApp.use('/public', function preHandler(req, res, next) {
// TODO authenticate or use guest user
req.isPublic = true;
next();
});
myApp.use('/accounts/:accountId', accountRequiredById);
myApp.use('/acl', accountRequired);
//
// TODO handle /accounts/:accountId
//
return PromiseA.resolve(pkgRestApi.create({
etcpath: xconfx.etcpath
}/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
//if (xconfx.debug) { console.log('[api.js] got handler'); }
myApp.use('/', function postHandler(req, res, next) {
req.url = req._walnutOriginalUrl;
next();
});
localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
pkgLinks.forEach(function (pkgLink) {
localCache.pkgs[pkgLink] = localCache.pkgs[pkgId];
});
return localCache.pkgs[pkgId];
});
}
function loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath) {
var myApp;
var pkgDeps = {};
var pkgRestAssets;
try {
pkgRestAssets = require(path.join(pkgPath, 'assets.js'));
} catch(e) {
return PromiseA.reject(e);
}
Object.keys(apiDeps).forEach(function (key) {
pkgDeps[key] = apiDeps[key];
});
Object.keys(apiFactories).forEach(function (key) {
pkgDeps[key] = apiFactories[key];
});
// TODO pull db stuff from package.json somehow and pass allowed data models as deps
//
// how can we tell which of these would be correct?
// deps.memstore = apiFactories.memstoreFactory.create(pkgId);
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
// let's go with this one for now and the api can choose to scope or not to scope
pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
myApp = express();
myApp.handlePromise = promisableRequest;
myApp.handleRejection = rejectableRequest;
myApp.grantsRequired = grantsRequired;
function otherGetSitePackageStoreProp(otherPkgId) {
var restPath = path.join(myConf.restPath, otherPkgId);
var apiPath = path.join(myConf.apiPath, otherPkgId);
var dir;
// TODO usage package.json as a falback if the standard location is not used
try {
dir = require(path.join(apiPath, 'models.js'));
} catch(e) {
dir = require(path.join(restPath, 'models.js'));
}
return getSiteStore(clientUrih, otherPkgId, dir);
}
myApp.use('/', function cookieAttachOauth3(req, res, next) {
return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
return require('./oauth3').cookieOauth3(Models, req, res, next);
});
});
myApp.use('/', function (req, res, next) {
console.log('########################################### session ###############################');
console.log('req.url', req.url);
console.log('req.oauth3', req.oauth3);
next();
});
function otherAttachOauth3(req, res, next) {
return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) {
return require('./oauth3').attachOauth3(Models, req, res, next);
});
}
myApp.post('/assets/issuer@oauth3.org/session', otherAttachOauth3, function (req, res) {
console.log('get the session');
console.log(req.url);
console.log("req.cookies:");
console.log(req.cookies);
console.log("req.oauth3:");
console.log(req.oauth3);
res.cookie('jwt', req.oauth3.encodedToken, { domain: req.clientAssetsUri, path: '/assets', httpOnly: true });
//req.url;
res.send({ success: true });
});
// TODO delete these caches when config changes
myApp.use('/', function preHandler(req, res, next) {
//if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
return getSiteConfig(clientUrih).then(function (siteConfig) {
//if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
Object.defineProperty(req, 'getSiteConfig', {
enumerable: true
, configurable: false
, writable: false
, value: function getSiteConfigProp(section) {
return PromiseA.resolve((siteConfig || {})[section]);
}
});
Object.defineProperty(req, 'getSitePackageConfig', {
enumerable: true
, configurable: false
, writable: false
, value: function getSitePackageConfigProp() {
return getSitePackageConfig(clientUrih, pkgId);
}
});
Object.defineProperty(req, 'getSiteStore', {
enumerable: true
, configurable: false
, writable: false
, value: function getSiteStoreProp() {
var restPath = path.join(myConf.restPath, pkgId);
var apiPath = path.join(myConf.apiPath, pkgId);
var dir;
// TODO usage package.json as a falback if the standard location is not used
try {
dir = require(path.join(apiPath, 'models.js'));
} catch(e) {
dir = require(path.join(restPath, 'models.js'));
}
return getSiteStore(clientUrih, pkgId, dir);
}
});
req._walnutOriginalUrl = req.url;
// "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
req.url = req.url.replace(/\/(api|assets)\//, '').replace(/.*\/(api|assets)\//, '').replace(/([^\/]*\/+)/, '/');
next();
});
});
myApp.use('/public', function preHandler(req, res, next) {
// TODO authenticate or use guest user
req.isPublic = true;
next();
});
myApp.use('/accounts/:accountId', accountRequiredById);
myApp.use('/acl', accountRequired);
//
// TODO handle /accounts/:accountId
//
function myAppWrapper(req, res, next) {
return myApp(req, res, next);
}
Object.keys(myApp).forEach(function (key) {
myAppWrapper[key] = myApp[key];
});
myAppWrapper.use = function () { myApp.use.apply(myApp, arguments); };
myAppWrapper.get = function () { myApp.get.apply(myApp, arguments); };
myAppWrapper.post = function () { myApp.use(function (req, res, next) { next(); }); /*throw new Error("assets may not handle POST");*/ };
myAppWrapper.put = function () { throw new Error("assets may not handle PUT"); };
myAppWrapper.del = function () { throw new Error("assets may not handle DELETE"); };
myAppWrapper.delete = function () { throw new Error("assets may not handle DELETE"); };
return PromiseA.resolve(pkgRestAssets.create({
etcpath: xconfx.etcpath
}/*pkgConf*/, pkgDeps/*pkgDeps*/, myAppWrapper)).then(function (assetsHandler) {
//if (xconfx.debug) { console.log('[api.js] got handler'); }
myApp.use('/', function postHandler(req, res, next) {
req.url = req._walnutOriginalUrl;
next();
});
return assetsHandler || myApp;
});
}
function loadRestHelper(myConf, clientUrih, pkgId) {
var pkgPath = path.join(myConf.restPath, pkgId);
// TODO allow recursion, but catch cycles
return fs.lstatAsync(pkgPath).then(function (stat) {
if (!stat.isFile()) {
return;
}
return fs.readFileAsync(pkgPath, 'utf8').then(function (text) {
pkgId = text.trim();
pkgPath = path.join(myConf.restPath, pkgId);
});
}, function () {
// ignore error
return;
}).then(function () {
// TODO should not require package.json. Should work with files alone.
return fs.readFileAsync(path.join(pkgPath, 'package.json'), 'utf8').then(function (text) {
var pkg = JSON.parse(text);
return loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (stuff) {
return loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (assetsHandler) {
stuff.assetsHandler = assetsHandler;
return stuff;
}, function (err) {
console.error('[lib/api.js] no assets handler:');
console.error(err);
return stuff;
});
});
});
});
}
// Read packages/apis/sub.sld.tld (forward dns) to find list of apis as tld.sld.sub (reverse dns)
// TODO packages/allowed_apis/sub.sld.tld (?)
// TODO auto-register org.oauth3.consumer for primaryDomain (and all sites?)
function loadRestHandler(myConf, clientUrih, pkgId) {
return PromiseA.resolve().then(function () {
if (!localCache.pkgs[pkgId]) {
return loadRestHelper(myConf, clientUrih, pkgId);
}
return localCache.pkgs[pkgId];
// TODO expire require cache
/*
if (Date.now() - localCache.pkgs[pkgId].createdAt < (5 * 60 * 1000)) {
return;
}
*/
}, function (/*err*/) {
// TODO what kind of errors might we want to handle?
return null;
}).then(function (restPkg) {
return restPkg;
});
}
var CORS = require('connect-cors');
var cors = CORS({ credentials: true, headers: [
'X-Requested-With'
, 'X-HTTP-Method-Override'
, 'Content-Type'
, 'Accept'
, 'Authorization'
], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
var staleAfter = (5 * 60 * 1000);
return function (req, res, next) {
cors(req, res, function () {
//if (xconfx.debug) { console.log('[api.js] after cors'); }
// Canonical client names
// example.com should use api.example.com/api for all requests
// sub.example.com/api should resolve to sub.example.com
// example.com/subapp/api should resolve to example.com#subapp
// sub.example.com/subapp/api should resolve to sub.example.com#subapp
var appUri = req.hostname.replace(/^(api|assets)\./, '') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
var clientUrih = appUri.replace(/\/+/g, '#').replace(/#$/, '');
var clientApiUri = req.hostname.replace(/^(api|assets)\./, 'api.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'assets.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
//var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'api.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
// Canonical package names
// '/api/com.daplie.hello/hello' should resolve to 'com.daplie.hello'
// '/subapp/api/com.daplie.hello/hello' should also 'com.daplie.hello'
// '/subapp/api/com.daplie.hello/' may exist... must be a small api
var pkgId = req.url.replace(/.*\/(api|assets)\//, '').replace(/^\//, '').replace(/\/.*/, '');
var now = Date.now();
var hasBeenHandled = false;
Object.defineProperty(req, 'clientUrl', {
enumerable: true
, configurable: false
, writable: false
, value: (req.headers.referer || ('https://' + appUri)).replace(/\/$/, '').replace(/\?.*/, '')
});
Object.defineProperty(req, 'apiUrlPrefix', {
enumerable: true
, configurable: false
, writable: false
, value: 'https://' + clientApiUri + '/api/' + pkgId
});
Object.defineProperty(req, 'assetsUrlPrefix', {
enumerable: true
, configurable: false
, writable: false
, value: 'https://' + clientAssetsUri + '/assets/' + pkgId
});
Object.defineProperty(req, 'experienceId' /*deprecated*/, {
enumerable: true
, configurable: false
, writable: false
, value: clientUrih
});
Object.defineProperty(req, 'clientApiUri', {
enumerable: true
, configurable: false
, writable: false
, value: clientApiUri
});
Object.defineProperty(req, 'clientAssetsUri', {
enumerable: true
, configurable: false
, writable: false
, value: clientAssetsUri
});
Object.defineProperty(req, 'apiId', {
enumerable: true
, configurable: false
, writable: false
, value: pkgId
});
Object.defineProperty(req, 'clientUrih', {
enumerable: true
, configurable: false
, writable: false
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
// NOTE: probably best to alias the name logically
, value: clientUrih
});
Object.defineProperty(req, 'pkgId', {
enumerable: true
, configurable: false
, writable: false
, value: pkgId
});
// TODO cache permission (although the FS is already cached, NBD)
var promise = isThisClientAllowedToUseThisPkg(req, xconfx, clientUrih, pkgId).then(function (yes) {
//if (xconfx.debug) { console.log('[api.js] azp is allowed?', yes); }
if (!yes) {
notConfigured(req, res);
return null;
}
function handleWithHandler() {
if (/\/assets\//.test(req.url) || /(^|\.)assets\./.test(req.hostname)) {
if (localCache.assets[pkgId]) {
if ('function' !== typeof localCache.assets[pkgId].handler) { console.log('localCache.assets[pkgId]'); console.log(localCache.assets[pkgId]); }
localCache.assets[pkgId].handler(req, res, next);
} else {
next();
return true;
}
} else {
localCache.rests[pkgId].handler(req, res, next);
}
}
if (localCache.rests[pkgId]) {
if (handleWithHandler()) {
return;
}
hasBeenHandled = true;
if (now - localCache.rests[pkgId].createdAt > staleAfter) {
localCache.rests[pkgId] = null;
localCache.assets[pkgId] = null;
}
}
if (!localCache.rests[pkgId]) {
//return doesThisPkgExist
//if (xconfx.debug) { console.log('[api.js] before rest handler'); }
return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
if (!myHandler) {
//if (xconfx.debug) { console.log('[api.js] not configured'); }
notConfigured(req, res);
return;
}
localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
localCache.assets[pkgId] = { handler: myHandler.assetsHandler, createdAt: now };
if (!hasBeenHandled) {
if (handleWithHandler()) {
return;
}
}
});
}
});
rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package");
});
};
};