gadael/gadael

View on GitHub
api/Company.api.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

const models = require('../models');
const helmet = require('helmet');
const routes = require('../rest/routes');
const apputil = require('../modules/apputil');
const passportHelper = require('../modules/passport');
const connectMongodbSession = require('connect-mongodb-session');
const passport = require('passport');
const express = require('express');
const session = require('express-session');
const async = require('async');
const mongoose = require('mongoose');
const csrf = require('csurf');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const compression = require('compression');
const serveStatic = require('serve-static');
const cookieParser = require('cookie-parser');
const http = require('http');
const gadaelMiddleware = require('../modules/gadaelMiddleware');
const flash = require('connect-flash-plus');
const schedule = require('node-schedule');
const child_process = require('child_process');
const approbalert = require('../modules/approbalert');
const googlecalendar = require('../rest/user/googlecalendar');
const decodeUrlEncodedBody = require('body-parser').urlencoded({ extended: false });
const loginPromise = require('../modules/login');

/**
 * Load models into an external mongo connexion
 * for actions on databases
 * Resolve when indexation is done on models
 *
 * @param    object    app     the headless mock app
 * @param    object    db        Mongoose connexion
 *
 * @return {Promise}
 */
function gadael_loadMockModels(app, db)
{
    if (Object.keys(db.models).length > 0) {
        return Promise.reject(new Error('Models already loaded for db connexion'));
    }

    models.requirements = {
        mongoose: app.mongoose,
        db: db,
        autoIndex: true,
        removeIndex: false,
        embeddedSchemas: [],
        app: app
    };

    apputil(app);
    return models.load()
    .catch(err => {
        console.error(err);
    });
}



/**
 * The company API
 * for action on multiples databases, application initialisation on server
 * @module api/Company
 *
 */
exports = module.exports = {


    /**
     * Get a database name from a company name
     * Used for auto-conversion in a form
     * @function
     *
     * @todo this will be done client side
     *
     * @return {string | false}
     */
    dbName: function dbName(name) {
        var str = name.replace(/[^a-z0-9]/gi, '').toLowerCase();

        if (str.length <= 2)
        {
            return false;
        }

        return str;
    },


    /**
     * db name for field validity
     * this is the server side valididty test
     * @function
     */
    isDbNameValid: function isDbNameValid(app, dbName, callback) {


        if (!dbName.match(/[a-z0-9]+/gi) || dbName.length <= 2 || dbName.length > 100)
        {
            callback(false);
            return;
        }

        this.listDatabases(app, function(databases) {

            for(var i =0; i< databases.length; i++)
            {
                if (databases[i].name === dbName)
                {
                    callback(false);
                    return;
                }
            }


            callback(true);
        });
    },






    /**
     * Get all databases
     * @function
     *
     * @param    {object}    app            headless application
     * @param    {function}     callback    function to receive results
     *
     */
    listDatabases: function listDatabases(app, callback) {
        const Admin = app.mongoose.mongo.Admin(app.db.db);
        Admin.listDatabases(function(err, result) {

            if (err) {
                console.error(err);
                return;
            }

            callback(result.databases);
        });

    },


    /**
     * Empty collections before initialization
     * @param {Object} db
     * @return {Promise}
     */
    emptyInitCollection: function(db) {

        let m = db.models;

        return Promise.all([
            m.Type.deleteMany({}),
            m.Calendar.deleteMany({}),
            m.RightCollection.deleteMany({}),
            m.RecoverQuantity.deleteMany({}),
            m.Right.deleteMany({})
        ]);
    },


    /**
     * Get list of promises for initialization
     * @param {Object} db
     * @param {Company} company
     * @return {Array}
     */
    execInitTasks: function(db, company) {

        let m = db.models;

        return [
            m.Type.getInitTask(company)(),
            m.Calendar.getInitTask(company)(),
            m.RightCollection.getInitTask(company)(),
            m.RecoverQuantity.getInitTask(company)(),
            m.Right.getInitTask(company)()
        ];
    },


    /**
     * Run init scripts on already initialized database
     *
     * @param    {Object}         app            express app or mock headless app variable
     * @param    {string}        dbname        database name, verified with this.isDbNameValid
     *
     * @return {Promise}
     */
    runInit: function runInit(app, dbName) {

        let companyApi = this;

        return companyApi.getOpenDbPromise(app, dbName)
        .then(db => {

            return Promise.all([
                db.models.Company.findOne().exec(),
                companyApi.emptyInitCollection(db)
            ])
            .then(all => {

                let company = all[0];

                if (null === company) {
                    throw new Error('No company');
                }

                return Promise.all(companyApi.execInitTasks(db, company))
                .then(() => {
                    db.close();
                    return true;
                });
            });
        });
    },


    /**
     * Create a new virgin database with initialized Company infos
     * Additional connexion to database is used
     * @function
     *
     * @param    {Object}         app            express app or mock headless app variable
     * @param    {string}        dbname        database name, verified with this.isDbNameValid
     * @param    {object}        company     A company document object
     *
     * @return {Promise}
     */
    createDb: function createDb(app, dbName, company) {
        if (undefined !== app.db && Object.keys(app.db.models).length > 0) {
            return Promise.reject(new Error('app object already initialized with models'));
        }

        apputil(app);

        // createConnection

        let db;
        let companyApi = this;
        let Company;


        /**
         * Create the company document and init tasks
         *
         * @return {Promise} resolve to company document
         */
        function saveCompanyDoc() {
            return gadael_loadMockModels(app, db)
            .then(() => {
                Company = db.models.Company;
                return Company.countDocuments()
                .exec();
            })
            .then(count => {

                if (0 !== count) {
                    throw new Error('createDb: Database already initialized with company object: '+dbName);
                }



                // create the company entry
                let companyDoc = new Company(company);
                let promises = companyApi.execInitTasks(db, companyDoc);

                promises.push(companyDoc.save());
                return Promise.all(promises);


            })
            .then(arr => {
                return arr[arr.length-1];
            });
        }


        return app.mongoose.disconnect()
        .then(() => {
            return new Promise((resolve, reject) => {
                db = app.mongoose.createConnection('mongodb://' + app.config.mongodb.prefix + dbName,
                    { useNewUrlParser: true, useCreateIndex: true });

                db.on('error', function(err) {
                    reject(new Error('CompanyApi.createDb mongoose connection error: '+err.message));
                });

                db.once('open', function() {
                    if (Object.keys(db.models).length > 0) {
                        throw new Error('once open: Models already loaded for db connexion');
                    }

                    saveCompanyDoc()
                    .then(company => {
                        //return app.mongoose.disconnect()
                        resolve(company);
                    })
                    .catch(err => {
                        // db.close();
                        console.error(err);
                    });
                });

            });
        });
    },


    /**
     * Delete a database
     * @function
     *
     * @param    {Object}    app
     * @param    {String}    dbName
     * @param    {function}    callback
     */
    dropDb: function dropDb(app, dbName, callback) {
        var db = app.mongoose.createConnection('mongodb://' + app.config.mongodb.prefix + dbName,
            { useNewUrlParser: true, useCreateIndex: true });
        db.on('error', console.error.bind(console, 'CompanyApi.deleteDb mongoose connection error: '));

        db.once('open', function() {
            db.db.dropDatabase(function(err, result) {
                if (err) {
                    return console.error(err);
                }
                app.mongoose.disconnect().then(callback);
            });
        });
    },


    /**
     * Use a database in the app
     * @param    {Object}    app            Express app or headless mock app
     * @param    {String}    dbName
     * @param    {Function}    callback     once connected
     *
     */
    bindToDb: function bindToDb(app, dbName, callback) {


        app.mongoose = mongoose;

        //setup mongoose
        app.db = mongoose.createConnection('mongodb://' + app.config.mongodb.prefix + dbName,
            { useNewUrlParser: true, useCreateIndex: true });
        app.db.on('error', console.error.bind(console, 'mongoose connection error: '));
        app.db.once('open', callback);
    },


    /**
     * Get the db object in a promise
     * @return {Promise}
     */
    getOpenDbPromise: function getOpenDbPromise(app, dbName) {
        let db;
        return new Promise((resolve, reject) => {
            db = app.mongoose.createConnection('mongodb://' + app.config.mongodb.prefix + dbName,
                { useNewUrlParser: true, useCreateIndex: true });
            db.on('error', err => {
                return reject('CompanyApi mongoose connection error: '+err, null);
            });

            db.once('open', () => {
                gadael_loadMockModels(app, db)
                .then(() => {
                    resolve(db);
                });
            });
        });
    },


    /**
     * Open the company document in a spcific database object
     * the db object is not closed
     * @returns {Promise} database object and company document
     */
    openCompany: function openCompany(app, dbName) {

        let output = {
            db: null,
            company: null
        };

        return this.getOpenDbPromise(app, dbName)
        .then(db => {
            output.db = db;

            let promise = db.models.Company.find().exec()
            .then(docs => {

                if (docs.length === 0) {
                    return output;
                }

                output.company = docs[0];
                return output;
            });

            promise.catch(err => {
                db.close();
                throw err;
            });

            return promise;
        });
    },


    /**
     * get company document from a database using an external connexion
     * Read only api, database connexion is closed afterward
     * @return {Promise}
     */
    getCompany: function getCompany(app, dbName, callback) {

        let promise = this.openCompany(app, dbName)
        .then(o => {
            if (undefined !== callback) {
                callback(null, o.company);
            }
            o.db.close();
            return o.company;
        });

        if (undefined !== callback) {
            promise.catch(err => {
                callback(err);
            });
        }

        return promise;
    },


    /*jshint loopfunc: true */

    /**
     * Get all company documents from all the databases
     * Promise resolve to an associated object wher keys are dbnames
     *
     * The schemas will be loaded for each company, if a database is not a gadael
     * db it can be ignored with the ignoreList param
     *
     * @param {Object} app
     * @param {Array} ignoreList
     * @return {Promise}
     */
    getCompanies: function getCompanies(app, ignoreList) {

        let api = this;
        if (undefined === ignoreList) {
            ignoreList = [];
        }

        return new Promise((resolve, reject) => {

            api.listDatabases(app, function(databases) {

                let tasks = [];
                let asyncTasks = [];
                for(var i=0; i<databases.length; i++) {

                    if (databases[i] && -1 === ignoreList.indexOf(databases[i].name)) {
                        var task = {
                            db: databases[i].name,
                            getCompany: function(async_done) {
                                api.getCompany(app, this.db, async_done);
                            }
                        };

                        tasks.push(task);
                        asyncTasks.push(task.getCompany.bind(task));
                    }
                }

                async.parallel(asyncTasks, function(err, results) {

                    let companies = {};

                    // databases without the company document are ignored
                    // add database name to resultset

                    for (var i=0; i<results.length; i++) {

                        if (null === results[i]) {
                            continue;
                        }

                        companies[tasks[i].db] = results[i];
                    }

                    resolve(companies);
                });
            });
        });
    },



    /**
     * Get the hightest port number
     *
     */
    getHighestPort: function getHighestPort(app, callback) {
        this.getCompanies(app).then(companies => {

            var max = 0;
            for (let dbName in companies) {
                if (companies.hasOwnProperty(dbName)) {
                    if (max < companies[dbName].port) {
                        max = companies[dbName].port;
                    }
                }
            }

            callback(max);
        });
    },



    /**
     *Get Express Application
     * @return {Object}
     */
    getExpress: function getExpress(config, models) {

        let mongoStore = connectMongodbSession(session);


        let csrfProtection = null;
        if (config.csrfProtection) {
            const defaultCsrfMiddleware = csrf({ cookie: true });
            csrfProtection = (req, res, next) => {
                // ignore CSRF protection for API
                if('/login/oauth-token' === req.path || req.path.startsWith('/api/')) {
                    next();
                    return;
                }

                return defaultCsrfMiddleware(req, res, next);
            };
        }

        //create express app
        var app = express();

        //keep reference to config
        app.config = config;

        this.bindToDb(app, config.mongodb.dbname, function() {
            // db connexion ready
        });

        models.requirements = {
            mongoose: app.mongoose,
            db: app.db,
            autoIndex: config.mongodb.autoIndex,
            removeIndex: config.mongodb.removeIndex,
            embeddedSchemas: [],
            app: app
        };

        apputil(app);
        models.load()
        .catch(err => {
            console.error(err);
        });

        //settings
        app.disable('x-powered-by');
        app.set('port', config.port);

        //middleware

        if (config.loghttp) {
            // logging HTTP requests
            app.use(morgan('dev'));
        }

        app.use(compression());
        app.use(serveStatic(config.staticPath, {
            'index': [config.indexFile]
        }));

        app.use(bodyParser.json());
        app.use(cookieParser());


        // the mongostore lock the gracefull stop of the app
        app.session_mongoStore = new mongoStore({
            uri: 'mongodb://' + app.config.mongodb.prefix + app.config.mongodb.dbname,
            collection: 'sessions'
        });


        app.use(session({
          secret: config.cryptoKey,
          store: app.session_mongoStore,
          saveUninitialized: true,
          resave: true
        }));

        app.use(flash());
        app.use(passport.initialize());
        app.use(passport.session());


        helmet.defaults(app);

        if (null !== csrfProtection) {
            app.use(csrfProtection);
        }


        //response locals
        app.use(function(req, res, next) {
            if (null !== csrfProtection && req.csrfToken) {
                // XSRF-TOKEN is the cookie used by angularjs to forward the X-SRF-TOKEN header with a $http request
                res.cookie('XSRF-TOKEN', req.csrfToken());
            }
            res.locals.user = {};
            res.locals.user.defaultReturnUrl = req.user && req.user.defaultReturnUrl();
            res.locals.user.username = req.user && req.user.username;
            next();
        });

        app.use(gadaelMiddleware(app));


        const oauth = require('../modules/oauth')(app);
        const authenticate = oauth.authenticate();

        app.use('/api/*', decodeUrlEncodedBody, (req, res, next) => {
            authenticate(req, res, (err) => {
                if (err) { return next(err); }
                loginPromise(req, res.locals.oauth.token.user)
                .then(() => { next(); })
                .catch(next);
            });
        });

        //setup routes
        routes(app, passport);

        return app;
    },





    /**
     * Start server on localhost
     *
     * @param app   Express app
     *
     * @return server
     */
    startServer: function startServer(app, callback) {

        const server = http.createServer(app);
        const companyModel = app.db.models.Company;

        companyModel.findOne({}, (err, company) => {

            if (err) {
                throw err;
            }

            if (null === company) {
                throw new Error('The company document is required to start the server');
            }

            app.config.company = company;
            if (company.country === 'UK') {
                app.config.language = 'en';
            }

            googlecalendar.init(app.config);

            //setup passport
            passportHelper(app, passport);

            server.listen(app.config.port, app.config.host);

            schedule.scheduleJob({ hour: 12, minute: 30 }, () => {
                approbalert(app).catch(console.error);
            });

            schedule.scheduleJob({ hour: 0, minute: 0 }, () => {
                console.log('Saving lunch breaks...');
                app.db.models.Account.find().exec()
                .then(accounts => Promise.all(accounts.map(a => a.saveLunchBreaks())))
                .catch(console.error);
            });

            if (app.config.useSchudeledRefreshStat) {
                const args = process.argv.slice();
                const node = args.shift();
                const file = args.shift().replace('app.js', 'refreshstat.js');
                args.unshift(file);
                schedule.scheduleJob({ minute: 0 }, () => {
                    child_process.execFile(node, args, (error, stdout, stderr) => {
                        if (error) {
                            throw error;
                        }
                        if (stdout) {
                            console.log('[refreshstat]', stdout);
                        }
                    });
                });
            }

            if (callback) {
                server.on('listening', callback);
            }
        });

        app.server = server;
        return server;
    }
};