arrow-admin
Version:
Arrow Admin Website
289 lines (248 loc) • 8.82 kB
JavaScript
// jscs:disable jsDoc
var fs = require('fs-extra'),
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'),
klawSync = require('klaw-sync'),
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;
fs.ensureDirSync(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,
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);
}
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);
}
});
function setupRoutes(routePrefix, useAPIDoc, useDocDir) {
// we need to redirect /routePrefix 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 + '/moment.js', function (req, resp) {
util.redirect(resp, prefix + '/moment/moment.js');
});
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('/arrow-static', express.static(dist_dir));
}
function escapeAPIKey(key, key2) {
return (key || key2 || '').replace(/\+/g, '\\+');
}
function deployUnauthorizedDocs() {
if (!fs.existsSync(doc_dir)) {
return;
}
fs.copySync(doc_dir, doc2_dir, {overwrite: 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)) {
klawSync(apidir).forEach(function (fn) {
if (path.extname(fn.path) === '.html') {
var html = fs.readFileSync(fn.path).toString();
html = html.replace(regex, 'APIKEY');
fs.writeFileSync(fn.path, html);
}
});
}
}
// removed protected
try { fs.removeSync(path.join(doc2_dir, 'models')); } catch (E) {}
try { fs.removeSync(path.join(doc2_dir, 'blocks')); } catch (E) {}
try { fs.removeSync(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 {
fs.removeSync(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
fs.removeSync(doc2_dir);
return callback();
}
if (!config.disableDefault404) {
app.use(function (req, res, next) {
if (res.bodyFlushed) {
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');
}
});
}
this.on('reload', reload);
callback();
}
};
module.exports = Admin;
// map AppC into Admin module
Admin.AppC = AppC;