<commit_before>const express = require('express');
const config = require('../config/config');
const jwt = require('jwt-simple');
const passport = require('passport');
require('../util/passport')(passport);
const User = require('../models/user');
const Pixel = require('../models/pixel');
const path = require("path");
const fs = require('fs');

function APIRouter(app) {
    let router = express.Router();

    // Normal APIs

    router.post('/signup', function(req, res) {
        return res.status(503).json({ success: false, error: { message: "API signup is no longer available.", code: "unavailable" } });
    });

    router.post('/identify', function(req, res, next) {
        if (!req.body.username || !req.body.password) return res.status(403).json({ success: false, error: { message: 'A username and password are required.', code: 'invalid_parameters' } });
        passport.authenticate('local', { session: false }, function(err, user, info) {
            if (!user) return res.status(500).json({ success: false, error: info.error || { message: "An unknown error occurred." } });
            let token = jwt.encode(user, config.secret);
            res.json({ success: true, token: 'JWT ' + token }); // create and return jwt token here
        })(req, res, next);
    });

    router.get('/session', function(req, res, next) {
        if (req.user) return res.send({ success: true, user: req.user.toInfo() });
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) {
                app.reportError("Session auth error: " + info.error);
                return res.status(500).json({ success: false, error: info.error });
            }
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return res.send({ success: true, user: user.toInfo() });
        })(req, res, next);
    });

    router.get('/board-image', function(req, res, next) {
        if (!app.paintingHandler.hasImage) return res.status(503).json({ success: false, error: { message: "We are not yet ready to take requests.", code: "not_ready" } });
        app.paintingHandler.getOutputImage().then((image) => {
            return res.set({ 'Content-Type': 'image/png' }).send(image);
        }).catch((err) => {
            app.reportError("Error while serving board image: " + err);
            return res.status(500).json({ success: false, error: { message: "We could not retrieve the current board image.", code: "image_fail" } });
        });
    });

    router.post('/place', function(req, res, next) {
         if (fs.existsSync(path.join(__dirname, '../util/', 'legit.js'))) {
             const legit = require('../util/legit');
             if (!legit.verify(req)) return res.status(403).json({ success: false, error: { message: "You cannot do that.", code: "unauthorized" } });
         }
        function paintWithUser(user) {
            if (!user.canPlace()) return res.status(429).json({ success: false, error: { message: "You cannot place yet.", code: "slow_down" } });
            if (!req.body.x || !req.body.y || !req.body.colour) return res.status(400).json({ success: false, error: { message: "You need to include all paramaters", code: "invalid_parameters" } });
            let rgb = app.paintingHandler.getColourRGB(req.body.colour);
            if (!rgb) return res.status(400).json({ success: false, error: { message: "Invalid color code specified.", code: "invalid_parameters" } });
            app.paintingHandler.doPaint(rgb, req.body.x, req.body.y, user).then((pixel) => {
                return User.findById(user.id).then(user => {
                    let seconds = user.getPlaceSecondsRemaining();
                    let countData = { canPlace: seconds <= 0, seconds: seconds };
                    return res.json({ success: true, timer: countData })
                }).catch(err => res.json({ success: true }));
            }).catch(err => res.status(500).json({ success: false, error: err }));
        }
        if (req.user) return paintWithUser(req.user);
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) {
                app.reportError("Session auth error: " + info.error);
                return res.status(500).json({ success: false, error: info.error });
            }
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return paintWithUser(user);
        })(req, res, next);
    });

    router.get('/timer', function(req, res, next) {
        function getTimerPayload(user) {
            let seconds = user.getPlaceSecondsRemaining();
            let countData = { canPlace: seconds <= 0, seconds: seconds };
            return { success: true, timer: countData };
        }
        if (req.user) return res.send(getTimerPayload(req.user));
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) return res.status(500).json({ success: false, error: info.error });
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return res.send(getTimerPayload(user));
        })(req, res, next);
    });

    router.get('/online', function(req, res, next) {
        return res.json({ success: true, online: { count: app.websocketServer.connectedClients } });
    });

    router.get('/pixel', function(req, res, next) {
        function fail(err) {
            app.reportError("Pixel data retrieve error: " + err);
            return res.status(500).json({ success: false, error: { message: "An error occurred while trying to look up information about that pixel." } })
        }
        if(!req.query.x || !req.query.y) return res.status(400).json( { success: false, error: { message: "You did not specify the coordinates of the pixel to look up.", code: "bad_request" } });
        Pixel.find({xPos: req.query.x, yPos: req.query.y}).then(pixels => {
            if (pixels.length <= 0) return res.json( {success: true, pixel: null });
            pixels[0].getInfo().then(info => {
                res.json({ success: true, pixel: info })
            }).catch(err => fail(err));
        }).catch(err => fail(err));
    });

    // Admin APIs

    router.get("/admin/stats", app.modMiddleware, function(req, res, next) {
        var signups24h = null, pixelsPlaced24h = null, pixelsPerMin = null;
        let dateBack24h = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
        let dateBack20m = new Date(new Date().getTime() - (20 * 60 * 1000));
        function finish() {
            return res.json({ success: true, stats: { online: app.websocketServer.connectedClients, signups24h: signups24h, pixelsPlaced24h: pixelsPlaced24h, pixelsPerMin: pixelsPerMin } });
        }
        function doPixelsPerMin() {
            Pixel.count({lastModified: {$gt: dateBack20m}}).then(c => {
                pixelsPerMin = Math.round(c / 20);
                finish()
            }).catch(err => finish());
        }
        function doPixelsPlaced24h() {
            Pixel.count({lastModified: {$gt: dateBack24h}}).then(c => {
                pixelsPlaced24h = c;
                doPixelsPerMin()
            }).catch(err => doPixelsPerMin());
        }
        function doSignups24h() {
            User.count({creationDate: {$gt: dateBack24h}}).then(c => {
                signups24h = c;
                doPixelsPlaced24h()
            }).catch(err => doPixelsPlaced24h());
        }
        doSignups24h();
    });

    router.get("/admin/refresh_clients", app.adminMiddleware, function(req, res, next) {
        app.websocketServer.broadcast("reload_client");
        res.json({success: true});
    });

    router.post("/admin/users", app.modMiddleware, function(req, res, next) {
        let searchValue = req.body.search ? req.body.search.value || "" : "";
        var sort = { creationDate: "desc" };
        if(req.body.order && req.body.order.length > 0 && req.body.columns) {
            if(req.body.columns.length > req.body.order[0].column || 1) {
                let colName = req.body.columns[req.body.order[0].column].data;
                sort = {}, sort[colName] = req.body.order[0].dir || "desc";
            }
        }
        User.dataTables({
            limit: req.body.length || 25,
            skip: req.body.start || 0,
            select: ["id", "name", "creationDate", "admin", "moderator", "banned", "lastPlace", "placeCount"],
            search: {
                value: searchValue,
                fields: ['name']
            }, sort: sort
        }, (err, table) => {
            if(err) {
                app.reportError("Error trying to receive admin user table data.");
                return res.status(500).json({success: false});
            }
            User.find().count().then(c => res.json(Object.assign({success: true, recordsTotal: c}, table))).catch(err => {
                app.reportError("Error trying to receive user count for admin user table.");
                res.status(500).json({success: false});
            });
        });
    });

    router.get("/admin/toggle_mod", app.adminMiddleware, function(req, res, next) {
        if(!req.query.id) return res.status(400).json({success: false, error: {message: "No user ID specified.", code: "bad_request"}});
        if(req.query.id == req.user.id) return res.status(400).json({success: false, error: {message: "You may not change your own moderator status.", code: "cant_modify_self"}});
        User.findById(req.query.id).then(user => {
            user.moderator = !user.moderator;
            user.save().then(user => res.json({success: true, moderator: user.moderator})).catch(err => {
                app.reportError("Error trying to save moderator status on user.");
                res.status(500).json({success: false});
            });
        }).catch(err => {
            app.reportError("Error trying to get user to set moderator status on.");
            res.status(500).json({success: false})
        });
    });

    // Mod APIs

    router.get("/mod/toggle_ban", app.modMiddleware, function(req, res, next) {
        if(!req.query.id) return res.status(400).json({success: false, error: {message: "No user ID specified.", code: "bad_request"}});
        if(req.query.id == req.user.id) return res.status(400).json({success: false, error: {message: "You may not ban yourself.", code: "cant_modify_self"}});
        User.findById(req.query.id).then(user => {
            user.banned = !user.banned;
            user.save().then(user => res.json({success: true, banned: user.banned})).catch(err => {
                app.reportError("Error trying to save banned status on user.");
                res.status(500).json({success: false})
            });
        }).catch(err => {
            app.reportError("Error trying to get user to set banned status on.");
            res.status(500).json({success: false});
        });
    });

    // Debug APIs

    if(config.debug) {

        router.get("/trigger-error", function(req, res, next) {
            app.reportError("Oh no! An error has happened!");
            res.status(500).json({ success: false, error: { message: "The server done fucked up.", code: "debug" } });
        });
        router.get("/trigger-error-report", function(req, res, next) {
            app.errorTracker.handleErrorCheckingInterval();
            res.json({ success: true });
        });

    }

    return router;
}

APIRouter.prototype = Object.create(APIRouter.prototype);

module.exports = APIRouter;<commit_msg>Add API call for similar users<commit_after>const express = require('express');
const config = require('../config/config');
const jwt = require('jwt-simple');
const passport = require('passport');
require('../util/passport')(passport);
const User = require('../models/user');
const Pixel = require('../models/pixel');
const path = require("path");
const fs = require('fs');

function APIRouter(app) {
    let router = express.Router();

    // Normal APIs

    router.post('/signup', function(req, res) {
        return res.status(503).json({ success: false, error: { message: "API signup is no longer available.", code: "unavailable" } });
    });

    router.post('/identify', function(req, res, next) {
        if (!req.body.username || !req.body.password) return res.status(403).json({ success: false, error: { message: 'A username and password are required.', code: 'invalid_parameters' } });
        passport.authenticate('local', { session: false }, function(err, user, info) {
            if (!user) return res.status(500).json({ success: false, error: info.error || { message: "An unknown error occurred." } });
            let token = jwt.encode(user, config.secret);
            res.json({ success: true, token: 'JWT ' + token }); // create and return jwt token here
        })(req, res, next);
    });

    router.get('/session', function(req, res, next) {
        if (req.user) return res.send({ success: true, user: req.user.toInfo() });
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) {
                app.reportError("Session auth error: " + info.error);
                return res.status(500).json({ success: false, error: info.error });
            }
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return res.send({ success: true, user: user.toInfo() });
        })(req, res, next);
    });

    router.get('/board-image', function(req, res, next) {
        if (!app.paintingHandler.hasImage) return res.status(503).json({ success: false, error: { message: "We are not yet ready to take requests.", code: "not_ready" } });
        app.paintingHandler.getOutputImage().then((image) => {
            return res.set({ 'Content-Type': 'image/png' }).send(image);
        }).catch((err) => {
            app.reportError("Error while serving board image: " + err);
            return res.status(500).json({ success: false, error: { message: "We could not retrieve the current board image.", code: "image_fail" } });
        });
    });

    router.post('/place', function(req, res, next) {
         if (fs.existsSync(path.join(__dirname, '../util/', 'legit.js'))) {
             const legit = require('../util/legit');
             if (!legit.verify(req)) return res.status(403).json({ success: false, error: { message: "You cannot do that.", code: "unauthorized" } });
         }
        function paintWithUser(user) {
            if (!user.canPlace()) return res.status(429).json({ success: false, error: { message: "You cannot place yet.", code: "slow_down" } });
            if (!req.body.x || !req.body.y || !req.body.colour) return res.status(400).json({ success: false, error: { message: "You need to include all paramaters", code: "invalid_parameters" } });
            let rgb = app.paintingHandler.getColourRGB(req.body.colour);
            if (!rgb) return res.status(400).json({ success: false, error: { message: "Invalid color code specified.", code: "invalid_parameters" } });
            app.paintingHandler.doPaint(rgb, req.body.x, req.body.y, user).then((pixel) => {
                return User.findById(user.id).then(user => {
                    let seconds = user.getPlaceSecondsRemaining();
                    let countData = { canPlace: seconds <= 0, seconds: seconds };
                    return res.json({ success: true, timer: countData })
                }).catch(err => res.json({ success: true }));
            }).catch(err => res.status(500).json({ success: false, error: err }));
        }
        if (req.user) return paintWithUser(req.user);
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) {
                app.reportError("Session auth error: " + info.error);
                return res.status(500).json({ success: false, error: info.error });
            }
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return paintWithUser(user);
        })(req, res, next);
    });

    router.get('/timer', function(req, res, next) {
        function getTimerPayload(user) {
            let seconds = user.getPlaceSecondsRemaining();
            let countData = { canPlace: seconds <= 0, seconds: seconds };
            return { success: true, timer: countData };
        }
        if (req.user) return res.send(getTimerPayload(req.user));
        passport.authenticate('jwt', { session: false }, function(err, user, info) {
            if (!user && info.error) return res.status(500).json({ success: false, error: info.error });
            if (!user) return res.status(403).json({ success: false, error: { message: "You do not have a valid session", code: "invalid_session" } });
            return res.send(getTimerPayload(user));
        })(req, res, next);
    });

    router.get('/online', function(req, res, next) {
        return res.json({ success: true, online: { count: app.websocketServer.connectedClients } });
    });

    router.get('/pixel', function(req, res, next) {
        function fail(err) {
            app.reportError("Pixel data retrieve error: " + err);
            return res.status(500).json({ success: false, error: { message: "An error occurred while trying to look up information about that pixel." } })
        }
        if(!req.query.x || !req.query.y) return res.status(400).json( { success: false, error: { message: "You did not specify the coordinates of the pixel to look up.", code: "bad_request" } });
        Pixel.find({xPos: req.query.x, yPos: req.query.y}).then(pixels => {
            if (pixels.length <= 0) return res.json( {success: true, pixel: null });
            pixels[0].getInfo().then(info => {
                res.json({ success: true, pixel: info })
            }).catch(err => fail(err));
        }).catch(err => fail(err));
    });

    // Admin APIs

    router.get("/admin/stats", app.modMiddleware, function(req, res, next) {
        var signups24h = null, pixelsPlaced24h = null, pixelsPerMin = null;
        let dateBack24h = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
        let dateBack20m = new Date(new Date().getTime() - (20 * 60 * 1000));
        function finish() {
            return res.json({ success: true, stats: { online: app.websocketServer.connectedClients, signups24h: signups24h, pixelsPlaced24h: pixelsPlaced24h, pixelsPerMin: pixelsPerMin } });
        }
        function doPixelsPerMin() {
            Pixel.count({lastModified: {$gt: dateBack20m}}).then(c => {
                pixelsPerMin = Math.round(c / 20);
                finish()
            }).catch(err => finish());
        }
        function doPixelsPlaced24h() {
            Pixel.count({lastModified: {$gt: dateBack24h}}).then(c => {
                pixelsPlaced24h = c;
                doPixelsPerMin()
            }).catch(err => doPixelsPerMin());
        }
        function doSignups24h() {
            User.count({creationDate: {$gt: dateBack24h}}).then(c => {
                signups24h = c;
                doPixelsPlaced24h()
            }).catch(err => doPixelsPlaced24h());
        }
        doSignups24h();
    });

    router.get("/admin/refresh_clients", app.adminMiddleware, function(req, res, next) {
        app.websocketServer.broadcast("reload_client");
        res.json({success: true});
    });

    router.post("/admin/users", app.modMiddleware, function(req, res, next) {
        let searchValue = req.body.search ? req.body.search.value || "" : "";
        var sort = { creationDate: "desc" };
        if(req.body.order && req.body.order.length > 0 && req.body.columns) {
            if(req.body.columns.length > req.body.order[0].column || 1) {
                let colName = req.body.columns[req.body.order[0].column].data;
                sort = {}, sort[colName] = req.body.order[0].dir || "desc";
            }
        }
        User.dataTables({
            limit: req.body.length || 25,
            skip: req.body.start || 0,
            select: ["id", "name", "creationDate", "admin", "moderator", "banned", "lastPlace", "placeCount"],
            search: {
                value: searchValue,
                fields: ['name']
            }, sort: sort
        }, (err, table) => {
            if(err) {
                app.reportError("Error trying to receive admin user table data.");
                return res.status(500).json({success: false});
            }
            User.find().count().then(c => res.json(Object.assign({success: true, recordsTotal: c}, table))).catch(err => {
                app.reportError("Error trying to receive user count for admin user table.");
                res.status(500).json({success: false});
            });
        });
    });

    router.get("/admin/toggle_mod", app.adminMiddleware, function(req, res, next) {
        if(!req.query.id) return res.status(400).json({success: false, error: {message: "No user ID specified.", code: "bad_request"}});
        if(req.query.id == req.user.id) return res.status(400).json({success: false, error: {message: "You may not change your own moderator status.", code: "cant_modify_self"}});
        User.findById(req.query.id).then(user => {
            user.moderator = !user.moderator;
            user.save().then(user => res.json({success: true, moderator: user.moderator})).catch(err => {
                app.reportError("Error trying to save moderator status on user.");
                res.status(500).json({success: false});
            });
        }).catch(err => {
            app.reportError("Error trying to get user to set moderator status on.");
            res.status(500).json({success: false})
        });
    });

    // Mod APIs

    router.get("/mod/toggle_ban", app.modMiddleware, function(req, res, next) {
        if(!req.query.id) return res.status(400).json({success: false, error: {message: "No user ID specified.", code: "bad_request"}});
        if(req.query.id == req.user.id) return res.status(400).json({success: false, error: {message: "You may not ban yourself.", code: "cant_modify_self"}});
        User.findById(req.query.id).then(user => {
            user.banned = !user.banned;
            user.save().then(user => res.json({success: true, banned: user.banned})).catch(err => {
                app.reportError("Error trying to save banned status on user.");
                res.status(500).json({success: false})
            });
        }).catch(err => {
            app.reportError("Error trying to get user to set banned status on.");
            res.status(500).json({success: false});
        });
    });

    router.get('/mod/similar_users/:userID', app.modMiddleware, function(req, res) {
        if(!req.params.userID || req.params.userID == "") return res.redirect("/admin/users");
        User.findById(req.params.userID).then(user => {
            // Find similar IP accesses
            user.findSimilarIPUsers().then(users => {
                var identifiedAccounts = users.map(user => { return { user: user, reasons: ["ip"] } });
                return res.json({ success: true, identifiedAccounts: identifiedAccounts })
            }).catch(err => {
                app.reportError("Error finding similar accounts: " + err);
                res.status(500).json({ success: false });
            });
        }).catch(err => res.status(400).json({ success: false }));
    });

    // Debug APIs

    if(config.debug) {
        router.get("/trigger-error", function(req, res, next) {
            app.reportError("Oh no! An error has happened!");
            res.status(500).json({ success: false, error: { message: "The server done fucked up.", code: "debug" } });
        });
        router.get("/trigger-error-report", function(req, res, next) {
            app.errorTracker.handleErrorCheckingInterval();
            res.json({ success: true });
        });
    }

    return router;
}

APIRouter.prototype = Object.create(APIRouter.prototype);

module.exports = APIRouter;<|endoftext|>


<commit_before>var glob = require('glob');
var path = require('path');
var _ = require('lodash');

module.exports = function(app, appConfig, customDebug) {

  var debug = customDebug || function(){};

  var seedNodules = [], routes = {};

  var config = {
    // NOTE: the three route config params below must be specified in the nodule init method, they cannot be mutated at request-time
    // REQUIRED, must be unique within express app, can be String or RegExp or an array of either to handle multiple routes
    route: null,

    // get/post/put/delete
    routeVerb: 'get',

    // use to load routes before or after main group (use negative #s for to load route first - like z-index)
    routeIndex: 0,

    debugToConsole: false
  };

  var defaultConfig = _.extend(_.cloneDeep(config), appConfig);

  // TODO - look for a cleaner way to do this inside the nodules
  //        change name from module to nodule in all files
  app.initModule = initModule;

  return {
    seedNodules: seedNodules,
    loadNodules: loadNodules,
    registerRoutes: registerRoutes
  };

  function loadNodules(dir, exclude) {
    var root = dir || process.cwd(); // TOOD - should this be process.cwd() + '/app' ?
    glob.sync('./**/*.js', { cwd: root })
      .filter(doesntMatch.apply(this, exclude))
      .forEach(function(nodule) { require(path.join(root, nodule))(app); });
  }

  function initModule(file, config) {
    var seedNodule = _.extend(_.cloneDeep(defaultConfig), config); // merge config properties onto default config
    seedNodule.path = path.dirname(file);
    seedNodule.name = path.basename(file, '.js');

    // nodules can have multiple routes
    var routeArray = (typeof seedNodule.route === 'string') || (seedNodule.route instanceof RegExp) ? [seedNodule.route] : seedNodule.route;
    _.each(routeArray, function(routePath) {
      seedNodules[routePath] = seedNodule; // routes must me unique

      if (!routes[seedNodule.routeIndex])
        routes[seedNodule.routeIndex] = [];
      routes[seedNodule.routeIndex].push({path:routePath, verb:seedNodule.routeVerb});
    });
  }

  // register routes in order based on nodule.routeIndex (default 0, can be negative)
  function registerRoutes(middlewares) {
    var sortedRouteKeys = _.sortBy(_.keys(routes), function(num){ return 1*num; });
    _.each(sortedRouteKeys, function(key) {
      _.each(routes[key], function(route) {

        debug('registering route: ' + route.verb + ' ' + route.path);
        if (defaultConfig.debugToConsole) console.log('registering route: ' + route.verb + ' ' + route.path);

        app[route.verb].apply(app, [route.path].concat(middlewares));
      });
    });
  }
};

function doesntMatch() {
  var matchers = Array.prototype.slice.call(arguments, 0);
  return function(str) {
    var matches = !matchers.some(function(matcher) {
      return str.indexOf(matcher) !== -1;
    });
    return matches;
  };
}

<commit_msg>Move initRequest back into framework<commit_after>var glob = require('glob');
var path = require('path');
var _ = require('lodash');

module.exports = function(app, appConfig, customDebug) {

  var debug = customDebug || function(){};

  var seedNodules = [], routes = {};

  var config = {
    // NOTE: the three route config params below must be specified in the nodule init method, they cannot be mutated at request-time
    // REQUIRED, must be unique within express app, can be String or RegExp or an array of either to handle multiple routes
    route: null,

    // get/post/put/delete
    routeVerb: 'get',

    // use to load routes before or after main group (use negative #s for to load route first - like z-index)
    routeIndex: 0,

    debugToConsole: false
  };

  var defaultConfig = _.extend(_.cloneDeep(config), appConfig);

  // TODO - look for a cleaner way to do this inside the nodules
  //        change name from module to nodule in all files
  app.initModule = initModule;

  return {
    seedNodules: seedNodules,
    loadNodules: loadNodules,
    registerRoutes: registerRoutes
  };

  function loadNodules(dir, exclude) {
    var root = dir || process.cwd(); // TOOD - should this be process.cwd() + '/app' ?
    glob.sync('./**/*.js', { cwd: root })
      .filter(doesntMatch.apply(this, exclude))
      .forEach(function(nodule) { require(path.join(root, nodule))(app); });
  }

  function initModule(file, config) {
    var seedNodule = _.extend(_.cloneDeep(defaultConfig), config); // merge config properties onto default config
    seedNodule.path = path.dirname(file);
    seedNodule.name = path.basename(file, '.js');

    // nodules can have multiple routes
    var routeArray = (typeof seedNodule.route === 'string') || (seedNodule.route instanceof RegExp) ? [seedNodule.route] : seedNodule.route;
    _.each(routeArray, function(routePath) {
      seedNodules[routePath] = seedNodule; // routes must me unique

      if (!routes[seedNodule.routeIndex])
        routes[seedNodule.routeIndex] = [];
      routes[seedNodule.routeIndex].push({path:routePath, verb:seedNodule.routeVerb});
    });
  }

  // register routes in order based on nodule.routeIndex (default 0, can be negative)
  function registerRoutes(middlewares) {
    var sortedRouteKeys = _.sortBy(_.keys(routes), function(num){ return 1*num; });
    _.each(sortedRouteKeys, function(key) {
      _.each(routes[key], function(route) {

        debug('registering route: ' + route.verb + ' ' + route.path);
        if (defaultConfig.debugToConsole) console.log('registering route: ' + route.verb + ' ' + route.path);

        app[route.verb].apply(app, [route.path, initRequest].concat(middlewares));
      });
    });
  }

  // attach new nodule instance to each incoming request
  function initRequest(req, res, next) {
    req.module = _.cloneDeep(seedNodules[req.route.path]);
    next();
  }
};

function doesntMatch() {
  var matchers = Array.prototype.slice.call(arguments, 0);
  return function(str) {
    var matches = !matchers.some(function(matcher) {
      return str.indexOf(matcher) !== -1;
    });
    return matches;
  };
}

<|endoftext|>


<commit_before>
/*
 * handle api endpoints.
 */

var mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

exports.collectionObject = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        objectId = req.route.params.objectId,
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    if (!collection) {
        res.status(400);
        res.send({ error: 'bad request' });
        return;
    }

    var ops = { GET: 'r', POST: 'c', PUT: 'u', DELETE: 'd' };
    if (!global.helpers.hasPermission(req.session.user, collectionName, ops[req.method])) {
        res.status(403);
        res.send({ error: req.session.user.name + ' has no enough permissions for perform this operation' });
        return;
    }

    switch (req.method) {
        case 'GET':
            if ('default' === objectId) {
                res.send(collection.backboneForms.defaults||{});
            } else {
                var populateFields = _(collection.relations)
                    .map(function (relation, key) { return 'fs.files' !== relation.relatedCollection ? key : ''; })
                    .join(' ');
                global.getModel(collectionName)
                    .findOne({ _id: objectId })
                    .populate(populateFields)
                    .exec()
                    .then(function (data) {
                        res.send(data);
                    })
                    .reject(function () {
                        res.send(arguments);
                    });
            }
            break;
        case 'POST':

            var attributes = _.clone(req.body);
            _(collection.relations).each(function (data, relKey) {
                if (_.isArray(req.body[relKey]) && req.body[relKey].length) {
                    attributes[relKey] = _(req.body[relKey]).map(function (val, key) {
                        if ('string' === typeof val ) {
                            return val;
                        }
                        return val['_id'].toString();
                    });
                    if (0 === attributes[relKey].length) {
                        delete attributes[relKey];
                    }
                } else if (_.isObject(req.body[relKey]) && req.body[relKey]['_id']) {
                    attributes[relKey] = req.body[relKey]['_id'].toString();
                }
            });
            var responseData = _.clone(attributes);
            delete attributes['_id'];
            // TODO skip all attributes not specified in schema
            var attributesToSet = global.helpers.toFlat(attributes);

            var model = new global.getModel(collectionName)();
            model.set(attributesToSet);
            model.set(collection.createdField.key, new global[collection.createdField.type||'Date']());
            model.set(collection.updatedField.key, new global[collection.createdField.type||'Date']());
            model.save(function (err, model) {
                if (err) {
                    res.send(err);
                } else {
                    var responseData = model.toObject();
                    delete responseData.__v;

                    if (collection.revisionable) {
                        // mongoose hooks doesn't have  support for update, so here is the "hook"
                        var RevisionModel = global.getRevisionModel(collectionName);
                        var revisionModel = new RevisionModel();
                        revisionModel.set({
                            objectId: model.get('_id'),
                            collectionName: collectionName,
                            user: req.session.user.username,
                            created: new Date(),
                            modelSnapshot: model
                        });
                        revisionModel.save();
                    }

                    res.send(responseData);
                }
            });

            break;
        case 'PUT':

            var attributes = _.clone(req.body);
            _(collection.relations).each(function (data, relKey) {
                if (_.isArray(req.body[relKey]) && req.body[relKey].length) {
                    attributes[relKey] = _(req.body[relKey]).map(function (val, key) {
                        if ('string' === typeof val ) {
                            return val;
                        }
                        return val['_id'].toString();
                    });
                    if (0 === attributes[relKey].length) {
                        delete attributes[relKey];
                    }
                } else if (_.isObject(req.body[relKey]) && req.body[relKey]['_id']) {
                    attributes[relKey] = req.body[relKey]['_id'].toString();
                }
            });
            var responseData = _.clone(attributes);
            delete attributes['_id'];
            // TODO skip all attributes not specified in schema
            var attributesToSet = global.helpers.toFlat(attributes);
            attributesToSet[collection.updatedField.key] = new global[collection.createdField.type||'Date']().toISOString();

            global.getModel(collectionName)
                .findByIdAndUpdate(objectId, { $set: attributesToSet }, function (err, model) {
                    if (err) {
                        res.send(err);
                    } else {

                        if (collection.revisionable) {
                            // mongoose hooks doesn't have  support for update, so here is the "hook"
                            var RevisionModel = global.getRevisionModel(collectionName);
                            var revisionModel = new RevisionModel();
                            revisionModel.set({
                                objectId: model.get('_id'),
                                collectionName: collectionName,
                                user: req.session.user.username,
                                created: new Date(),
                                modelSnapshot: model
                            });
                            revisionModel.save();
                        }

                        res.send(responseData);
                    }
                });

            break;
        case 'DELETE':

            global.getModel(collectionName)
                .findByIdAndRemove(objectId, function (err, model) {
                    if (err) {
                        res.send(err);
                    } else {
                        res.send(model);
                    }
                });

            break;
    }
};

exports.collectionSearch = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        url_parts = url.parse(req.url, true),
        q = (url_parts.query.q||'').sanitize().makeSafeForRegex(),
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    var columnsHumanNames = _(collection.fastSearch.columns).map(function (col) {
        if (collection.backboneForms.schema[col]) {
            return collection.backboneForms.schema[col].title || col;
        }
        return col;
    });

    var findParams = global.helpers.toJS(_(collection.fastSearch.find).clone(), function (arg) {
        return arg.replace(/\$\{q\}/g, q);
    });

    global.getModel(collectionName)
        .find(findParams, collection.fastSearch.columns.join(' ') + ' ' + collection.toStringField)
        .sort(collection.fastSearch.sort)
        .limit(collection.fastSearch.limit)
        .exec()
        .then(function (results) {
            res.send({
                collectionName: collectionName,
                q: q,
                columns: collection.fastSearch.columns,
                columnsHumanNames: columnsHumanNames,
                data: results
            });
        });

};

exports.fileObject = function(req, res){
    var url = require('url'),
        objectId = req.route.params.objectId,
        gfs = req.app.get('gfs'),
        _ = require('underscore');

    switch (req.method) {
        case 'GET':
            var view = req.route.params.view;
            if ('raw' === view) {
                var readStream = gfs.createReadStream({ _id: objectId });
                if (readStream) {
                    try {
                        readStream.on('end', function() {
                            res.send();
                        })
                        .on('error', function() {
                            res.send();
                        })
                        .pipe(res);
                    } catch (error) {
                        res.status(404);
                        res.send('file not found');
                    }
                } else {
                    res.status(404);
                    res.send('file not found');
                }
            } else {
                gfs.collection('fs')
                    .findOne({ _id: new mongoose.mongo.ObjectID(objectId) }, function (err, data) {
                        res.send(data);
                    });
            }
            break;
        case 'POST':
            _(req.files).each(function (file, modelPath) {
                // TODO store multiple files
                require('fs').exists(file.path, function (exists) {
                    if(!exists) {
                        res.send({ error: 'Ah crap! Something bad happened' });
                        return;
                    }
                    var collectionName = modelPath.replace(/^([^\.]+)\..*$/, '$1'),
                        path = modelPath.replace(/^[^\.]+\.(.*)$/, '$1'),
                        collection = _(global.config.collections).find(function (col) {
                            return col.name === collectionName;
                        });
                    if (collection
                        && collection.backboneForms.schema[path]
                        && collection.backboneForms.schema[path].pushToS3) {
                            console.log('pushToS3'); // TODO
                    }
                    var writestream = gfs.createWriteStream({
                        metadata: {
                            collection: collectionName,
                            path: path,
                            s3_url: null
                        },
                        filename: file.name
                    });
                    require('fs').createReadStream(file.path).pipe(writestream);
                    writestream.on('close', function (file) {
                        res.send(file);
                    });
                });
            });
            break;
        case 'PUT':
            break;
        case 'DELETE':
            gfs.remove({ _id: ObjectId }, function (err) {
                if (err) {
                    res.status(400);
                    return res.send(err);
                }
                res.status(200);
                res.send({ ok: true });
            });
            break;
    }
};

exports.revisions = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        objectId = req.route.params.objectId,
        url_parts = url.parse(req.url, true),
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    global.getRevisionModel(collectionName)
        .find({ objectId: objectId })
        .sort({ created: -1 })
        .exec()
        .then(function (data) {
            res.send(data);
        })
        .reject(function () {
            res.send(arguments);
        });
};
<commit_msg>files upload refactor<commit_after>
/*
 * handle api endpoints.
 */

var mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

exports.collectionObject = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        objectId = req.route.params.objectId,
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    if (!collection) {
        res.status(400);
        res.send({ error: 'bad request' });
        return;
    }

    var ops = { GET: 'r', POST: 'c', PUT: 'u', DELETE: 'd' };
    if (!global.helpers.hasPermission(req.session.user, collectionName, ops[req.method])) {
        res.status(403);
        res.send({ error: req.session.user.name + ' has no enough permissions for perform this operation' });
        return;
    }

    switch (req.method) {
        case 'GET':
            if ('default' === objectId) {
                res.send(collection.backboneForms.defaults||{});
            } else {
                var populateFields = _(collection.relations)
                    .map(function (relation, key) { return 'fs.files' !== relation.relatedCollection ? key : ''; })
                    .join(' ');
                global.getModel(collectionName)
                    .findOne({ _id: objectId })
                    .populate(populateFields)
                    .exec()
                    .then(function (data) {
                        res.send(data);
                    })
                    .reject(function () {
                        res.send(arguments);
                    });
            }
            break;
        case 'POST':

            var attributes = _.clone(req.body);
            _(collection.relations).each(function (data, relKey) {
                if (_.isArray(req.body[relKey]) && req.body[relKey].length) {
                    attributes[relKey] = _(req.body[relKey]).map(function (val, key) {
                        if ('string' === typeof val ) {
                            return val;
                        }
                        return val['_id'].toString();
                    });
                    if (0 === attributes[relKey].length) {
                        delete attributes[relKey];
                    }
                } else if (_.isObject(req.body[relKey]) && req.body[relKey]['_id']) {
                    attributes[relKey] = req.body[relKey]['_id'].toString();
                }
            });
            var responseData = _.clone(attributes);
            delete attributes['_id'];
            // TODO skip all attributes not specified in schema
            var attributesToSet = global.helpers.toFlat(attributes);

            var model = new global.getModel(collectionName)();
            model.set(attributesToSet);
            model.set(collection.createdField.key, new global[collection.createdField.type||'Date']());
            model.set(collection.updatedField.key, new global[collection.createdField.type||'Date']());
            model.save(function (err, model) {
                if (err) {
                    res.send(err);
                } else {
                    var responseData = model.toObject();
                    delete responseData.__v;

                    if (collection.revisionable) {
                        // mongoose hooks doesn't have  support for update, so here is the "hook"
                        var RevisionModel = global.getRevisionModel(collectionName);
                        var revisionModel = new RevisionModel();
                        revisionModel.set({
                            objectId: model.get('_id'),
                            collectionName: collectionName,
                            user: req.session.user.username,
                            created: new Date(),
                            modelSnapshot: model
                        });
                        revisionModel.save();
                    }

                    res.send(responseData);
                }
            });

            break;
        case 'PUT':

            var attributes = _.clone(req.body);
            _(collection.relations).each(function (data, relKey) {
                if (_.isArray(req.body[relKey]) && req.body[relKey].length) {
                    attributes[relKey] = _(req.body[relKey]).map(function (val, key) {
                        if ('string' === typeof val ) {
                            return val;
                        }
                        return val['_id'].toString();
                    });
                    if (0 === attributes[relKey].length) {
                        delete attributes[relKey];
                    }
                } else if (_.isObject(req.body[relKey]) && req.body[relKey]['_id']) {
                    attributes[relKey] = req.body[relKey]['_id'].toString();
                }
            });
            var responseData = _.clone(attributes);
            delete attributes['_id'];
            // TODO skip all attributes not specified in schema
            var attributesToSet = global.helpers.toFlat(attributes);
            attributesToSet[collection.updatedField.key] = new global[collection.createdField.type||'Date']().toISOString();

            global.getModel(collectionName)
                .findByIdAndUpdate(objectId, { $set: attributesToSet }, function (err, model) {
                    if (err) {
                        res.send(err);
                    } else {

                        if (collection.revisionable) {
                            // mongoose hooks doesn't have  support for update, so here is the "hook"
                            var RevisionModel = global.getRevisionModel(collectionName);
                            var revisionModel = new RevisionModel();
                            revisionModel.set({
                                objectId: model.get('_id'),
                                collectionName: collectionName,
                                user: req.session.user.username,
                                created: new Date(),
                                modelSnapshot: model
                            });
                            revisionModel.save();
                        }

                        res.send(responseData);
                    }
                });

            break;
        case 'DELETE':

            global.getModel(collectionName)
                .findByIdAndRemove(objectId, function (err, model) {
                    if (err) {
                        res.send(err);
                    } else {
                        res.send(model);
                    }
                });

            break;
    }
};

exports.collectionSearch = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        url_parts = url.parse(req.url, true),
        q = (url_parts.query.q||'').sanitize().makeSafeForRegex(),
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    var columnsHumanNames = _(collection.fastSearch.columns).map(function (col) {
        if (collection.backboneForms.schema[col]) {
            return collection.backboneForms.schema[col].title || col;
        }
        return col;
    });

    var findParams = global.helpers.toJS(_(collection.fastSearch.find).clone(), function (arg) {
        return arg.replace(/\$\{q\}/g, q);
    });

    global.getModel(collectionName)
        .find(findParams, collection.fastSearch.columns.join(' ') + ' ' + collection.toStringField)
        .sort(collection.fastSearch.sort)
        .limit(collection.fastSearch.limit)
        .exec()
        .then(function (results) {
            res.send({
                collectionName: collectionName,
                q: q,
                columns: collection.fastSearch.columns,
                columnsHumanNames: columnsHumanNames,
                data: results
            });
        });

};

exports.fileObject = function(req, res){
    var url = require('url'),
        objectId = req.route.params.objectId,
        gfs = req.app.get('gfs'),
        _ = require('underscore');

    switch (req.method) {
        case 'GET':
            var view = req.route.params.view;
            if ('raw' === view) {
                var readStream = gfs.createReadStream({ _id: objectId });
                if (readStream) {
                    try {
                        readStream.on('end', function() {
                            res.send();
                        })
                        .on('error', function() {
                            res.send();
                        })
                        .pipe(res);
                    } catch (error) {
                        res.status(404);
                        res.send('file not found');
                    }
                } else {
                    res.status(404);
                    res.send('file not found');
                }
            } else {
                gfs.collection('fs')
                    .findOne({ _id: new mongoose.mongo.ObjectID(objectId) }, function (err, data) {
                        res.send(data);
                    });
            }
            break;
        case 'POST':
            var filePromises = [];
            _(req.files).each(function (file, modelPath) {
                var filePromise = new mongoose.Promise();
                require('fs').exists(file.path, function (exists) {
                    if(!exists) {
                        filePromise.resolve('tmp file do not exists', null);
                        return;
                    }
                    var collectionName = modelPath.replace(/^([^\.]+)\..*$/, '$1'),
                        path = modelPath.replace(/^[^\.]+\.(.*)$/, '$1'),
                        collection = _(global.config.collections).find(function (col) {
                            return col.name === collectionName;
                        });
                    if (collection
                        && collection.backboneForms.schema[path]
                        && collection.backboneForms.schema[path].pushToS3) {
                            console.log('pushToS3'); // TODO
                    }
                    var writestream = gfs.createWriteStream({
                        metadata: {
                            collection: collectionName,
                            path: path,
                            s3_url: null
                        },
                        filename: file.name
                    });
                    require('fs').createReadStream(file.path).pipe(writestream);
                    writestream.on('close', function (file) {
                        res.send(file);
                        filePromise.resolve(null, file);
                    });
                });
                filePromises.push(filePromise);
            });
            mongoose.Promise
                .when.apply(null, filePromises)
                .addBack(function (err) {
                    if (err) {
                        res.status(409);
                        res.send({ error: err });
                    } else {
                        var files = _(arguments).toArray().slice(1);
                        res.send({ data: files });
                    }
                });
            break;
        case 'PUT':
            break;
        case 'DELETE':
            gfs.remove({ _id: ObjectId }, function (err) {
                if (err) {
                    res.status(400);
                    return res.send(err);
                }
                res.status(200);
                res.send({ ok: true });
            });
            break;
    }
};

exports.revisions = function(req, res){
    var url = require('url'),
        collectionName = req.route.params.collectionName,
        objectId = req.route.params.objectId,
        url_parts = url.parse(req.url, true),
        _ = require('underscore');

    var collection = _(global.config.collections).find(function (col) {
        return col.name === collectionName;
    });

    global.getRevisionModel(collectionName)
        .find({ objectId: objectId })
        .sort({ created: -1 })
        .exec()
        .then(function (data) {
            res.send(data);
        })
        .reject(function () {
            res.send(arguments);
        });
};
<|endoftext|>


<commit_before>"use strict";

/**
 * Module dependencies.
 */

var _ = require("lodash")
  , inquirer = require("inquirer")
  , EventEmitter = require("events").EventEmitter
  , VantageUtil = require("./util")
  ;

var ui = {

  /**
   * Sets intial variables and registers
   * listeners. This is called once in a
   * process thread regardless of how many
   * instances of Vantage have been generated.
   *
   * @api private
   */

  _init: function() {

    var self = this;

    // Attached vantage instance. The UI can
    // only attach to one instance of Vantage
    // at a time, and directs all events to that
    // instance.
    this.parent = void 0;

    // Hook to reference active inquirer prompt.
    this._activePrompt = void 0;

    // Fail-safe to ensure there is no double
    // prompt in odd situations.
    this._midPrompt = false;

    // Handle for inquirer's prompt.
    this.inquirer = inquirer;

    // Whether a prompt is currently in cancel mode.
    this._cancelled = false;

    // Middleware for piping stdout through.
    this._pipeFn = void 0;

    // Custom function on sigint event.
    this._sigint = void 0;

    // Hook in to steal inquirer's keypress.
    inquirer.prompt.prompts.input.prototype.onKeypress = function(e) {
      self.emit("client_keypress", e);
      return self._keypressHandler(e, this);
    };

    // Extend the render function to steal the active prompt object,
    // as inquirer doesn't expose it and we need it.
    (function(render){
      inquirer.prompt.prompts.input.prototype.render = function() {
        self._activePrompt = this;
        return render.call(this);
      };
    })(inquirer.prompt.prompts.input.prototype.render);

    // Sigint handling - make it more graceful.
    process.on('SIGINT', function() {

      if (_.isFunction(self._sigint)) {
        self._sigint.call(self.parent);
      } else {
        process.exit(0);
      }
    });

  },

  /**
   * Hook for sigint event.
   *
   * @param {Object} options
   * @param {Function} cb
   * @api public
   */

  sigint: function(fn) {
    if (_.isFunction(fn)) {
      this._sigint = fn;
    } else {
      throw new Error('vantage.ui.sigint must be passed in a valid function.');
    }
    return this;
  },

  /**
   * Creates an inquirer prompt on the TTY.
   *
   * @param {Object} options
   * @param {Function} cb
   * @api public
   */

  prompt: function(options, cb) {
    var self = this;
    options = options || {};
    if (!this.parent) { return; }
    if (options.delimiter) {
      this.setDelimiter(options.delimiter);
    }
    if (options.message) {
      this.setDelimiter(options.message);
    }
    if (self._midPrompt) {
      console.log("Prompt called when mid prompt...");
      throw new Error("UI Prompt called when already mid prompt.");
    }
    self._midPrompt = true;
    try {
      inquirer.prompt(options, function(result) {
        self._midPrompt = false;
        cb(result);
      });
    } catch(e) {
      console.log("Vantage Prompt error:", e);
    }
  },

  /**
   * Returns a boolean as to whether user
   * is mid another pr ompt.
   *
   * @return {Boolean}
   * @api public
   */

  midPrompt: function() {
    return (
      (this._midPrompt && this.parent) ? true : false
    );
  },

  /**
   * Sets the temporarily delimiter based
   * on the delimiter provided by another
   * vantage server to this instance's client
   * upon the establishment of a session.
   *
   * @param {String} str
   * @api public
   */

  setDelimiter: function(str) {
    if (!this.parent) { return; }
    str = String(str).trim() + " ";
    this._lastDelimiter = str;
    inquirer.prompt.prompts.password.prototype.prefix = function(){
      return str;
    };
    inquirer.prompt.prompts.input.prototype.prefix = function(){
      return str;
    };
  },

  /**
   * Event handler for keypresses - deals with command history
   * and tabbed auto-completion.
   *
   * @param {Event} e
   * @param {Prompt} prompt
   * @api private
   */

  _keypressHandler: function(e, prompt) {
    if (!this.parent) { return; }
    this._activePrompt = prompt;
    var key = (e.key || {}).name;
    var value = (prompt) ? String(prompt.rl.line).trim() : void 0;
    this.emit("vantage_ui_keypress", { key: key, value: value });
  },

  /**
   * Refreshes active prompt.
   *
   * @return {UI}
   * @api public
   */

  refresh: function() {
    if (!this.parent) { return false; }
    if (!this._activePrompt) { return false; }
    if (!this._midPrompt) { return false; }
    this._activePrompt.clean();
    this._midPrompt = false;
    this._cancelled = true;
    if (this._activePrompt.status !== "answered") { // huh?
      this._activePrompt.status = "answered";
      this._activePrompt.done();
    }
    this._cancelled = false;
    this.parent._prompt();
    return this;
  },

  /**
   * Pauses active prompt, returning
   * the value of what had been typed so far.
   *
   * @return {String} val
   * @api public
   */

  pause: function() {
    if (!this.parent) { return false; }
    if (!this._activePrompt) { return false; }
    if (!this._midPrompt) { return false; }
    var val = this._activePrompt.rl.line;
    this._midPrompt = false;
    this._cancelled = true;
    this._activePrompt.clean();
    this._activePrompt.status = "answered";
    this._activePrompt.done();
    return val;
  },

  /**
   * Resumes active prompt, accepting
   * a string, which will fill the prompt
   * with that text and put the cursor at
   * the end.
   *
   * @param {String} val
   * @api public
   */

  resume: function(val) {
    if (!this.parent) { return this; }
    val = val || "";
    if (!this._activePrompt) { return this; }
    if (this._midPrompt) { return this; }
    this.parent._prompt();
    this._activePrompt.rl.line = val;
    this._activePrompt.rl.cursor = val.length;
    this._activePrompt.cacheCursorPos();
    this._activePrompt.clean().render().write( this._activePrompt.rl.line );
    this._activePrompt.restoreCursorPos();
    return this;
  },

  /**
   * Logs the current delimiter and typed data.
   *
   * @return {UI}
   * @api public
   */

  imprint: function(str) {
    if (!this.parent) { return this; }
    var val = this._activePrompt.rl.line;
    var delimiter = this._lastDelimiter || "";
    this.log(delimiter + val);
    return this;
  },

  /**
   * Redraws the inquirer prompt with a new string.
   *
   * @param {String} str
   * @return {UI}
   * @api private
   */

  redraw: function(str) {
    if (!this.parent) { return this; }
    this._activePrompt.rl.line = str;
    this._activePrompt.rl.cursor = str.length;
    this._activePrompt.cacheCursorPos();
    this._activePrompt.clean().render().write( this._activePrompt.rl.line );
    this._activePrompt.restoreCursorPos();
    return this;
  },

  /**
   * Attaches TTY prompt to a given Vantage instance.
   *
   * @param {Vantage} vantage
   * @return {UI}
   * @api public
   */

  attach: function(vantage) {
    this.parent = vantage;
    this.refresh();
    this.parent._prompt();
    return this;
  },

  /**
   * Receives and runs logging through
   * a piped function is one is provided
   * through ui.pipe(). Pauses any active
   * prompts, logs the data and then if
   * paused, resumes the prompt.
   *
   * @return {UI}
   * @api public
   */

  log: function() {
    var args = VantageUtil.fixArgsForApply(arguments);
    args = (_.isFunction(this._pipeFn))
      ? this._pipeFn.call(this, args)
      : args;
    if (args === "") {
      return this;
    }
    args = VantageUtil.fixArgsForApply(args);
    if (this.midPrompt()) {
      var data = this.pause();
      console.log.apply(console.log, args);
      if (typeof data !== "undefined" && data !== false) {
        this.resume(data);
      } else {
        console.log("Log got back 'false' as data. This shouldn't happen.", data);
      }
    } else {
      console.log.apply(console.log, args);
    }
    return this;
  },

  /**
   * Detaches UI from a given Vantage instance.
   *
   * @param {Vantage} vantage
   * @return {UI}
   * @api public
   */

  detach: function(vantage) {
    if (vantage === this.parent) {
      this.parent = void 0;
    }
    return this;
  }
};

/**
 * Make UI an EventEmitter.
 */

_.assign(ui, EventEmitter.prototype);

/**
 * Expose `ui`.
 *
 * Modifying global? WTF?!? Yes. It is evil.
 * However node.js prompts are also quite
 * evil in a way. Nothing prevents dual prompts
 * between applications in the same terminal,
 * and inquirer doesn't catch or deal with this, so
 * if you want to start two independent instances of
 * vantage, you need to know that prompt listeners
 * have already been initiated, and that you can
 * only attach the tty to one vantage instance
 * at a time.
 * When you fire inqurier twice, you get a double-prompt,
 * where every keypress fires twice and it's just a
 * total mess. So forgive me.
 */

global.__vantage = global.__vantage || {};
global.__vantage.ui = global.__vantage.ui || {
  exists: false,
  exports: void 0
};

if (!global.__vantage.ui.exists) {
  global.__vantage.ui.exists = true;
  global.__vantage.ui.exports = ui;
  module.exports = exports = ui;
  ui._init();
} else {
  module.exports = global.__vantage.ui.exports;
}
<commit_msg>graceful sigint handling<commit_after>"use strict";

/**
 * Module dependencies.
 */

var _ = require("lodash")
  , inquirer = require("inquirer")
  , EventEmitter = require("events").EventEmitter
  , VantageUtil = require("./util")
  ;

var ui = {

  /**
   * Sets intial variables and registers
   * listeners. This is called once in a
   * process thread regardless of how many
   * instances of Vantage have been generated.
   *
   * @api private
   */

  _init: function() {

    var self = this;

    // Attached vantage instance. The UI can
    // only attach to one instance of Vantage
    // at a time, and directs all events to that
    // instance.
    this.parent = void 0;

    // Hook to reference active inquirer prompt.
    this._activePrompt = void 0;

    // Fail-safe to ensure there is no double
    // prompt in odd situations.
    this._midPrompt = false;

    // Handle for inquirer's prompt.
    this.inquirer = inquirer;

    // Whether a prompt is currently in cancel mode.
    this._cancelled = false;

    // Middleware for piping stdout through.
    this._pipeFn = void 0;

    // Custom function on sigint event.
    this._sigint = void 0;
    this._sigintCalled = false;

    // Hook in to steal inquirer's keypress.
    inquirer.prompt.prompts.input.prototype.onKeypress = function(e) {
      //console.log(e)
      self.emit("client_keypress", e);
      return self._keypressHandler(e, this);
    };

    process.stdin.on('keypress', function(letter, key) {
      if (key.ctrl === true && key.shift === false && key.meta === false && ['c', 'C'].indexOf(key.name) > -1) {
        if (self._sigint && !self._sigintCalled) {
          self._sigintCalled = true;
          self._sigint.call(self.parent);
        } else {
          process.exit(0);
        }
      }
    });

    // Extend the render function to steal the active prompt object,
    // as inquirer doesn't expose it and we need it.
    (function(render){
      inquirer.prompt.prompts.input.prototype.render = function() {
        self._activePrompt = this;
        return render.call(this);
      };
    })(inquirer.prompt.prompts.input.prototype.render);

    // Sigint handling - make it more graceful.
    process.on('SIGINT', function() {
      if (_.isFunction(self._sigint) && !self._sigintCalled) {
        self._sigintCalled = true;
        self._sigint.call(self.parent);
      } else {
        process.exit(0);
      }
    });

  },

  /**
   * Hook for sigint event.
   *
   * @param {Object} options
   * @param {Function} cb
   * @api public
   */

  sigint: function(fn) {
    if (_.isFunction(fn)) {
      this._sigint = fn;
    } else {
      throw new Error('vantage.ui.sigint must be passed in a valid function.');
    }
    return this;
  },

  /**
   * Creates an inquirer prompt on the TTY.
   *
   * @param {Object} options
   * @param {Function} cb
   * @api public
   */

  prompt: function(options, cb) {
    var self = this;
    options = options || {};
    if (!this.parent) { return; }
    if (options.delimiter) {
      this.setDelimiter(options.delimiter);
    }
    if (options.message) {
      this.setDelimiter(options.message);
    }
    if (self._midPrompt) {
      console.log("Prompt called when mid prompt...");
      throw new Error("UI Prompt called when already mid prompt.");
    }
    self._midPrompt = true;
    try {
      inquirer.prompt(options, function(result) {
        self._midPrompt = false;
        cb(result);
      });
    } catch(e) {
      console.log("Vantage Prompt error:", e);
    }
  },

  /**
   * Returns a boolean as to whether user
   * is mid another pr ompt.
   *
   * @return {Boolean}
   * @api public
   */

  midPrompt: function() {
    return (
      (this._midPrompt && this.parent) ? true : false
    );
  },

  /**
   * Sets the temporarily delimiter based
   * on the delimiter provided by another
   * vantage server to this instance's client
   * upon the establishment of a session.
   *
   * @param {String} str
   * @api public
   */

  setDelimiter: function(str) {
    if (!this.parent) { return; }
    str = String(str).trim() + " ";
    this._lastDelimiter = str;
    inquirer.prompt.prompts.password.prototype.prefix = function(){
      return str;
    };
    inquirer.prompt.prompts.input.prototype.prefix = function(){
      return str;
    };
  },

  /**
   * Event handler for keypresses - deals with command history
   * and tabbed auto-completion.
   *
   * @param {Event} e
   * @param {Prompt} prompt
   * @api private
   */

  _keypressHandler: function(e, prompt) {
    if (!this.parent) { return; }
    this._activePrompt = prompt;
    var key = (e.key || {}).name;
    var value = (prompt) ? String(prompt.rl.line).trim() : void 0;
    this.emit("vantage_ui_keypress", { key: key, value: value });
  },

  /**
   * Refreshes active prompt.
   *
   * @return {UI}
   * @api public
   */

  refresh: function() {
    if (!this.parent) { return false; }
    if (!this._activePrompt) { return false; }
    if (!this._midPrompt) { return false; }
    this._activePrompt.clean();
    this._midPrompt = false;
    this._cancelled = true;
    if (this._activePrompt.status !== "answered") { // huh?
      this._activePrompt.status = "answered";
      this._activePrompt.done();
    }
    this._cancelled = false;
    this.parent._prompt();
    return this;
  },

  /**
   * Pauses active prompt, returning
   * the value of what had been typed so far.
   *
   * @return {String} val
   * @api public
   */

  pause: function() {
    if (!this.parent) { return false; }
    if (!this._activePrompt) { return false; }
    if (!this._midPrompt) { return false; }
    var val = this._activePrompt.rl.line;
    this._midPrompt = false;
    this._cancelled = true;
    this._activePrompt.clean();
    this._activePrompt.status = "answered";
    this._activePrompt.done();
    return val;
  },

  /**
   * Resumes active prompt, accepting
   * a string, which will fill the prompt
   * with that text and put the cursor at
   * the end.
   *
   * @param {String} val
   * @api public
   */

  resume: function(val) {
    if (!this.parent) { return this; }
    val = val || "";
    if (!this._activePrompt) { return this; }
    if (this._midPrompt) { return this; }
    this.parent._prompt();
    this._activePrompt.rl.line = val;
    this._activePrompt.rl.cursor = val.length;
    this._activePrompt.cacheCursorPos();
    this._activePrompt.clean().render().write( this._activePrompt.rl.line );
    this._activePrompt.restoreCursorPos();
    return this;
  },

  /**
   * Logs the current delimiter and typed data.
   *
   * @return {UI}
   * @api public
   */

  imprint: function(str) {
    if (!this.parent) { return this; }
    var val = this._activePrompt.rl.line;
    var delimiter = this._lastDelimiter || "";
    this.log(delimiter + val);
    return this;
  },

  /**
   * Redraws the inquirer prompt with a new string.
   *
   * @param {String} str
   * @return {UI}
   * @api private
   */

  redraw: function(str) {
    if (!this.parent) { return this; }
    this._activePrompt.rl.line = str;
    this._activePrompt.rl.cursor = str.length;
    this._activePrompt.cacheCursorPos();
    this._activePrompt.clean().render().write( this._activePrompt.rl.line );
    this._activePrompt.restoreCursorPos();
    return this;
  },

  /**
   * Attaches TTY prompt to a given Vantage instance.
   *
   * @param {Vantage} vantage
   * @return {UI}
   * @api public
   */

  attach: function(vantage) {
    this.parent = vantage;
    this.refresh();
    this.parent._prompt();
    return this;
  },

  /**
   * Receives and runs logging through
   * a piped function is one is provided
   * through ui.pipe(). Pauses any active
   * prompts, logs the data and then if
   * paused, resumes the prompt.
   *
   * @return {UI}
   * @api public
   */

  log: function() {
    var args = VantageUtil.fixArgsForApply(arguments);
    args = (_.isFunction(this._pipeFn))
      ? this._pipeFn.call(this, args)
      : args;
    if (args === "") {
      return this;
    }
    args = VantageUtil.fixArgsForApply(args);
    if (this.midPrompt()) {
      var data = this.pause();
      console.log.apply(console.log, args);
      if (typeof data !== "undefined" && data !== false) {
        this.resume(data);
      } else {
        console.log("Log got back 'false' as data. This shouldn't happen.", data);
      }
    } else {
      console.log.apply(console.log, args);
    }
    return this;
  },

  /**
   * Detaches UI from a given Vantage instance.
   *
   * @param {Vantage} vantage
   * @return {UI}
   * @api public
   */

  detach: function(vantage) {
    if (vantage === this.parent) {
      this.parent = void 0;
    }
    return this;
  }
};

/**
 * Make UI an EventEmitter.
 */

_.assign(ui, EventEmitter.prototype);

/**
 * Expose `ui`.
 *
 * Modifying global? WTF?!? Yes. It is evil.
 * However node.js prompts are also quite
 * evil in a way. Nothing prevents dual prompts
 * between applications in the same terminal,
 * and inquirer doesn't catch or deal with this, so
 * if you want to start two independent instances of
 * vantage, you need to know that prompt listeners
 * have already been initiated, and that you can
 * only attach the tty to one vantage instance
 * at a time.
 * When you fire inqurier twice, you get a double-prompt,
 * where every keypress fires twice and it's just a
 * total mess. So forgive me.
 */

global.__vantage = global.__vantage || {};
global.__vantage.ui = global.__vantage.ui || {
  exists: false,
  exports: void 0
};

if (!global.__vantage.ui.exists) {
  global.__vantage.ui.exists = true;
  global.__vantage.ui.exports = ui;
  module.exports = exports = ui;
  ui._init();
} else {
  module.exports = global.__vantage.ui.exports;
}
<|endoftext|>