'use strict'; module.exports.create = function (xconfx, apiFactories, apiDeps) { var PromiseA = apiDeps.Promise; var mkdirpAsync = PromiseA.promisify(require('mkdirp')); //var express = require('express'); var express = require('express-lazy'); var fs = PromiseA.promisifyAll(require('fs')); var path = require('path'); var localCache = { rests: {}, pkgs: {} }; // TODO xconfx.apispath xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest'); 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(myConf, clientUrih, pkgId) { var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih); return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) { console.log('sanity', 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) { console.log(api, pkgId, api === pkgId); return true; } })) { return true; } if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) { // fallthrough return true; } else { 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 {}; }); }); } function loadRestHelper(myConf, clientUrih, pkgId) { var pkgPath = path.join(myConf.restPath, pkgId); // 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); var pkgDeps = {}; var myApp; if (pkg.walnut) { pkgPath = path.join(pkgPath, pkg.walnut); } 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); console.log('DEBUG pkgPath', pkgPath); myApp = express(); /* var pkgConf = { pagespath: path.join(__dirname, '..', '..', 'packages', 'pages') + path.sep , apipath: path.join(__dirname, '..', '..', 'packages', 'apis') + path.sep , servicespath: path.join(__dirname, '..', '..', 'packages', 'services') , vhostsMap: vhostsMap , vhostPatterns: null , server: webserver , externalPort: info.conf.externalPort , primaryNameserver: info.conf.primaryNameserver , nameservers: info.conf.nameservers , privkey: info.conf.privkey , pubkey: info.conf.pubkey , redirects: info.conf.redirects , apiPrefix: '/api' , 'org.oauth3.consumer': info.conf['org.oauth3.consumer'] , 'org.oauth3.provider': info.conf['org.oauth3.provider'] , keys: info.conf.keys }; */ var _getOauth3Controllers = pkgDeps.getOauth3Controllers = require('oauthcommon/example-oauthmodels').create( { sqlite3Sock: xconfx.sqlite3Sock, ipcKey: xconfx.ipcKey } ).getControllers; //require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps); require('oauthcommon').inject(_getOauth3Controllers, myApp/*, pkgConf, pkgDeps*/); myApp.use('/public', function preHandler(req, res, next) { // TODO authenticate or use guest user next(); }); myApp.use('/', function preHandler(req, res, next) { return getSiteConfig(clientUrih).then(function (siteConfig) { /* Object.defineProperty(req, 'siteConfig', { enumerable: true , configurable: false , writable: false , value: siteConfig }); */ Object.defineProperty(req, 'getSiteMailer', { 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 getSiteMailerProp(section) { return PromiseA.resolve((siteConfig || {})[section]); } }); Object.defineProperty(req, 'getSitePackageConfig', { enumerable: true , configurable: false , writable: false , value: function getSitePackageConfigProp() { return getSitePackageConfig(clientUrih, pkgId); } }); req._walnutOriginalUrl = req.url; // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello' req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/'); console.log('[prehandler] req.url', req.url); next(); }); }); // // TODO handle /accounts/:accountId // return PromiseA.resolve(require(pkgPath).create({ etcpath: xconfx.etcpath }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) { myApp.use('/', function postHandler(req, res, next) { req.url = req._walnutOriginalUrl; console.log('[posthandler] req.url', req.url); next(); }); localCache.pkgs[pkgId] = { pkg: pkg, handler: handler || myApp, createdAt: Date.now() }; return localCache.pkgs[pkgId]; }); }); } // 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 () { console.log('[sanity check]', req.url); // 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/subpath/api should resolve to example.com#subapp // sub.example.com/subpath/api should resolve to sub.example.com#subapp var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').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\//, '').replace(/^\//, '').replace(/\/.*/, ''); var now = Date.now(); var hasBeenHandled = false; // Existing (Deprecated) Object.defineProperty(req, 'experienceId', { enumerable: true , configurable: false , writable: false , value: clientUrih }); Object.defineProperty(req, 'apiId', { enumerable: true , configurable: false , writable: false , value: pkgId }); // New 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) return isThisClientAllowedToUseThisPkg(xconfx, clientUrih, pkgId).then(function (yes) { if (!yes) { notConfigured(req, res); return null; } if (localCache.rests[pkgId]) { localCache.rests[pkgId].handler(req, res, next); hasBeenHandled = true; if (now - localCache.rests[pkgId].createdAt > staleAfter) { localCache.rests[pkgId] = null; } } if (!localCache.rests[pkgId]) { //return doesThisPkgExist return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) { if (!myHandler) { notConfigured(req, res); return; } localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now }; if (!hasBeenHandled) { myHandler.handler(req, res, next); } }); } }); }); }; };