UNPKG

arrow-admin

Version:
301 lines (256 loc) 9.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(email, org_id, callback) { var hasValidEmail = validEmails.indexOf(email) !== -1, hasValidOrg = validOrgs.indexOf(org_id) !== -1; if (hasValidEmail || hasValidOrg) { return callback(); } callback("Unauthorized access for user or organization: email=" + email + ", 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(config.docsDir || arrow.config.docsDir || config.dir || arrow.config.dir)), doc_dir = path.join(doc_base_dir, 'auth'), doc2_dir = path.join(doc_base_dir, 'unauth'), initialized = false, self = this; self.objectModel = objectModel; 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, APIS_URL: objectModel.baseurl + objectModel.config.apiPrefix, 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', 'configurations', '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 + '/swagger.json', require('./routes/swagger').route(objectModel)); app.get(routePrefix + '/index.html', createIndexRenderer(useAPIDoc)); // handle these files special ['intro', 'logs', 'build', 'docs', 'config', '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)); app.use(function (req, res, next) { if (res._flushBodyCalled) { return; // already handled. } res.status(404); if (req.accepts('html')) { var p = path.join(dist_dir, '404.html'); res.set('Content-Type', 'text/html'); fs.createReadStream(p).pipe(res); } else if (req.accepts('json')) { res.send({success: false, code: 404, error: 'Not found'}); } else { res.type('txt').send('Not found'); } }); } 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(); } }; Admin.prototype.swaggerDefinition = function () { return require('./routes/swagger').definition(this.objectModel); }; module.exports = Admin; // map AppC into Admin module Admin.AppC = AppC;