UNPKG

arrow-admin

Version:
277 lines (236 loc) 8.16 kB
var fs = require('fs'), path = require('path'), events = require('events'), utillib = require('util'), util = require('./util'), chalk = require('chalk'), DocGen = require('arrow-docgen'), apiutil = require('arrow-util'), AppC = require('appc-platform-sdk'), wrench = DocGen.wrench, logger, express; utillib.inherits(Admin, events.EventEmitter); function Admin(exp, arrow, app, objectmodel, config, authCallback, callback) { express = exp; logger = arrow.logger; this.configure(arrow, app, objectmodel, config, config && config.prefix || '/arrow', authCallback || createDefaultAuthCallback(config||{}), callback); } /** * create a default authorization handler in the case that one isn't provided */ function createDefaultAuthCallback(config) { var validEmails = config.validEmails || [], validOrgs = config.validOrgs || [], isDevelopment = config.env==='development'; if (config.disableAuth && !isDevelopment) { logger.warn(chalk.bold.red('User authentication is disabled for admin.')); return; } if ((!config.env || isDevelopment || config.enableAdminInProduction)) { if (validEmails.length===0 && validOrgs.length===0) { var prefs = util.preferences(); if (prefs.username) { validEmails.push(prefs.username); logger.info('setting default admin username as',chalk.bold.magenta(prefs.username)); logger.info('to change admin access, edit your config file at '+chalk.green('./conf')+' and add the following:'); var example = { admin: { validEmails: [prefs.username] } }; logger.info('\n'+chalk.bold.yellow(JSON.stringify(example,null,2))); } } } return function authCallback(username, org_id, callback) { if (validOrgs.indexOf(org_id)!==-1) { return callback(); } if (validEmails.indexOf(username)!==-1) { return callback(); } callback("Unauthorized access for user or organization: username="+username+", org_id="+org_id); }; } /** * configure the restify routes */ Admin.prototype.configure = function configure(arrow, app, objectModel, config, prefix, authCallback, callback) { var dist_dir = path.join(__dirname,'../dist'), doc_base_dir = path.join(require('os').tmpdir(), util.md5(arrow.config.dir)), doc_dir = path.join(doc_base_dir,'auth'), doc2_dir = path.join(doc_base_dir,'unauth'), initialized = false, self = this; if (!fs.existsSync(doc_dir)) { wrench.mkdirSyncRecursive(doc_dir); } function reload(om) { var startTime = Date.now(); arrow.logger.debug('Generating documentation...',doc_dir); DocGen.generate(config, om, doc_dir, function(err, result) { if (err) { arrow.logger.error("Documentation generation error",err); } if (!initialized) { initialized = true; init.call(self); } arrow.logger.debug('Documentation generated in ' + (Date.now() - startTime) + 'ms'); }); } reload(objectModel); function init() { var templateEnv = { APPC_REGISTRY_SERVER: AppC.registryurl, APPC_PLATFORM_SERVER: AppC.baseurl, APPC_SECURITY_SERVER: AppC.securityurl, ADMIN_URL: objectModel.adminurl, ENDPOINT_URL: objectModel.baseurl, objectmodel:objectModel }; function makeIndexPage(name) { return fs.readFileSync(path.join(dist_dir,name)).toString() .replace(/{{APPC_PLATFORM_SERVER}}/g,AppC.baseurl) .replace(/{{APPC_SECURITY_SERVER}}/g,AppC.securityurl); } var index_html = makeIndexPage('index.html'), apidoc_html = makeIndexPage('apidoc.html'), createIndexRenderer = function(useAPIDoc) { return function(req, resp, next) { if (useAPIDoc) { util.html(resp,apidoc_html); } else { util.html(resp,index_html); } }; }; ['config','generate'].forEach(function(name){ try { require('./routes/'+name).configure(arrow, app, config, prefix, authCallback); } catch (E){ arrow.logger.fatal(E); } }); app.get(prefix+'/moment.js', function(req,resp){ util.redirect(resp, prefix+'/moment/moment.js'); }); function setupRoutes(routePrefix, useAPIDoc, useDocDir) { // we need to redirect /arrow to have a filename so that our menu logic works correctly app.get(new RegExp('^'+routePrefix+'/?$'), function(req, resp, next){ util.redirect(resp, routePrefix + (useAPIDoc ? '/docs.html' : '/index.html')); }); app.get(routePrefix+'/index.html', createIndexRenderer(useAPIDoc)); // handle these files special ['intro','logs','build','docs','cms','unauthorized'].forEach(function(name){ app.get(routePrefix+'/'+name+'.html', createIndexRenderer(useAPIDoc)); if (name === 'docs') { // Docs need a special handler. return app.get(routePrefix+'/'+name, function(req, resp, next){ if (!req.xhr) { // if not coming from XHR, redirect return resp.redirect(routePrefix+'/docs.html'); } next(); }); } app.get(routePrefix+'/'+name, function(req, resp, next){ var p = path.join(dist_dir,name+'.html'); if (fs.existsSync(p)) { resp.set('Content-Type','text/html'); return fs.createReadStream(p).pipe(resp); } p = path.join(dist_dir,name+'.ejs'); if (fs.existsSync(p)) { try { templateEnv.filename = p; var content = apiutil.content.generate(fs.readFileSync(p).toString(), templateEnv, {markdown:false}); return util.html(resp, content.markup); } catch (E) { return next(E); } } util.html(resp,'<h1>'+name+'</h1>'); }); }); // for some reason jquery forms this url and our routing can't handle it, strip off ; app.get(routePrefix+'/jquery.min.map;',function(req,resp,next){ req.url = req.url.replace(/;$/,''); next(); }); //TODO: add a security middleware here to validate session app.use(routePrefix+'/docs', express.static(useDocDir)); app.use(routePrefix,express.static(dist_dir)); } function escapeAPIKey(key, key2) { return (key || key2 || '').replace(/\+/g,'\\+'); } function deployUnauthorizedDocs() { if (!fs.existsSync(doc_dir)) { return; } wrench.copyDirSyncRecursive(doc_dir, doc2_dir, {forceDelete: true}); var regexString = ''; ['apikey_development', 'apikey_production', 'apikey'].forEach(function (key) { if (!config[key]) { return; } if (regexString !== '') { regexString += '|'; } regexString += escapeAPIKey(config[key]); }); if (regexString !== '') { var regex = new RegExp('(' + regexString + ')', 'g'); // replace the apikeys for unauthorized var apidir = path.join(doc2_dir, 'apis'); if (fs.existsSync(apidir)) { wrench.readdirSyncRecursive(apidir).forEach(function (fn) { if (path.extname(fn) === '.html') { var p = path.join(apidir, fn), html = fs.readFileSync(p).toString(); html = html.replace(regex, 'APIKEY'); fs.writeFileSync(p, html); } }); } } // removed protected try { wrench.rmdirSyncRecursive(path.join(doc2_dir,'models')); } catch (E){} try { wrench.rmdirSyncRecursive(path.join(doc2_dir,'blocks')); } catch (E){} try { wrench.rmdirSyncRecursive(path.join(doc2_dir,'connectors')); } catch (E){} try { fs.unlinkSync(path.join(doc2_dir,'authentication.html')); } catch (E){} // fix menus var menuFn = path.join(doc2_dir,'menu.json'); var menu = JSON.parse(fs.readFileSync(menuFn)); menu[0].pages.splice(1,1); menu.splice(2,menu.length-2); fs.writeFileSync(menuFn,JSON.stringify(menu)); } deployUnauthorizedDocs(); // only enable admin if explicitly enabled or in development if (!config.disableAuth && (config.env==='development' || config.enableAdminInProduction)) { setupRoutes(prefix, false, doc_dir); } else { wrench.rmdirSyncRecursive(doc_dir); } if (!config.disableAPIDoc) { setupRoutes(config.apiDocPrefix || '/apidocs', true, doc2_dir); } else if (config.disableAuth) { // don't even generate doc, we're totally disabled wrench.rmdirSyncRecursive(doc2_dir); return callback(); } this.on('reload', reload); callback(); } }; module.exports = Admin; // map AppC into Admin module Admin.AppC = AppC;