arrow-admin
Version:
Arrow Admin Website
277 lines (236 loc) • 8.16 kB
JavaScript
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;