app.js

/**
 * Main dashboard process app.js
 * @module frontend/express/app
 */

// Set process name
process.title = "countly: dashboard node " + process.argv[1];

var fs = require('fs');
var path = require('path');
var IS_FLEX = false;

if (fs.existsSync(path.resolve('/opt/deployment_env.json'))) {
    var deploymentConf = fs.readFileSync('/opt/deployment_env.json', 'utf8');
    try {
        if (JSON.parse(deploymentConf).DEPLOYMENT_ID) {
            IS_FLEX = true;
        }
    }
    catch (e) {
        IS_FLEX = false;
    }
}

var versionInfo = require('./version.info'),
    pack = require('../../package.json'),
    COUNTLY_VERSION = versionInfo.version,
    COUNTLY_COMPANY = versionInfo.company || '',
    COUNTLY_TYPE = versionInfo.type,
    COUNTLY_PAGE = versionInfo.page = (!versionInfo.title) ? "http://count.ly" : null,
    COUNTLY_NAME = versionInfo.title = versionInfo.title || "Countly",
    COUNTLY_DOCUMENTATION_LINK = (typeof versionInfo.documentationLink === "undefined") ? true : (typeof versionInfo.documentationLink === "string") ? versionInfo.documentationLink : (typeof versionInfo.documentationLink === "boolean") ? versionInfo.documentationLink : true,
    COUNTLY_FEEDBACK_LINK = (typeof versionInfo.feedbackLink === "undefined") ? true : (typeof versionInfo.feedbackLink === "string") ? versionInfo.feedbackLink : (typeof versionInfo.feedbackLink === "boolean") ? versionInfo.feedbackLink : true,
    COUNTLY_HELPCENTER_LINK = (typeof versionInfo.helpCenterLink === "undefined") ? true : (typeof versionInfo.helpCenterLink === "string") ? versionInfo.helpCenterLink : (typeof versionInfo.helpCenterLink === "boolean") ? versionInfo.helpCenterLink : true,
    COUNTLY_FEATUREREQUEST_LINK = (typeof versionInfo.featureRequestLink === "undefined") ? true : (typeof versionInfo.featureRequestLink === "string") ? versionInfo.featureRequestLink : (typeof versionInfo.featureRequestLink === "boolean") ? versionInfo.featureRequestLink : true,
    express = require('express'),
    https = require('https'),
    SkinStore = require('./libs/connect-mongo.js'),
    expose = require('./libs/express-expose.js'),
    dollarDefender = require('./libs/dollar-defender.js')({
        message: "Dollar sign is not allowed in keys",
        hook: function(req) {
            console.log("Possible Dollar sign injection", req.originalUrl, req.query, req.params, req.body);
        }
    }),
    crypto = require('crypto'),
    jimp = require('jimp'),
    flash = require('connect-flash'),
    cookieParser = require('cookie-parser'),
    formidable = require('formidable'),
    session = require('express-session'),
    methodOverride = require('method-override'),
    csrf = require('csurf')(),
    errorhandler = require('errorhandler'),
    basicAuth = require('basic-auth'),
    bodyParser = require('body-parser'),
    _ = require('underscore'),
    countlyMail = require('../../api/parts/mgmt/mail.js'),
    // countlyStats = require('../../api/parts/data/stats.js'),
    countlyFs = require('../../api/utils/countlyFs.js'),
    common = require('../../api/utils/common.js'),
    preventBruteforce = require('./libs/preventBruteforce.js'),
    plugins = require('../../plugins/pluginManager.js'),
    request = require('countly-request')(plugins.getConfig("security")),
    countlyConfig = require('./config', 'dont-enclose'),
    log = require('../../api/utils/log.js')('core:app'),
    url = require('url'),
    authorize = require('../../api/utils/authorizer.js'), //for token validations
    languages = require('../../frontend/express/locale.conf'),
    rateLimit = require("express-rate-limit"),
    membersUtility = require("./libs/members.js"),
    argon2 = require('argon2'),
    countlyCommon = require('../../api/lib/countly.common.js'),
    timezones = require('../../api/utils/timezones.js').getTimeZones,
    { validateCreate } = require('../../api/utils/rights.js'),
    tracker = require('../../api/parts/mgmt/tracker.js');

console.log("Starting Countly", "version", versionInfo.version, "package", pack.version);

var COUNTLY_NAMED_TYPE = "Countly Lite v" + COUNTLY_VERSION;
var COUNTLY_TYPE_CE = true;
var COUNTLY_TRIAL = (versionInfo.trial) ? true : false;
var COUNTLY_TRACK_TYPE = "OSS";

if (IS_FLEX) {
    COUNTLY_NAMED_TYPE = "Countly v" + COUNTLY_VERSION;
    COUNTLY_TYPE_CE = false;
    COUNTLY_TRACK_TYPE = "Flex";
}
else if (versionInfo.footer) {
    COUNTLY_NAMED_TYPE = versionInfo.footer;
    COUNTLY_TYPE_CE = false;
    if (COUNTLY_NAMED_TYPE === "Countly Cloud") {
        COUNTLY_TRACK_TYPE = "Cloud";
    }
    else if (COUNTLY_TYPE !== "777a2bf527a18e0fffe22fb5b3e322e68d9c07a6") {
        COUNTLY_TRACK_TYPE = "Enterprise";
    }
}
else if (COUNTLY_TYPE !== "777a2bf527a18e0fffe22fb5b3e322e68d9c07a6") {
    COUNTLY_NAMED_TYPE = "Countly Enterprise v" + COUNTLY_VERSION;
    COUNTLY_TYPE_CE = false;
    COUNTLY_TRACK_TYPE = "Enterprise";
}

/**
* Create params object for validation
* @param {obj} obj - express request object
* @returns {object} params object
**/
function paramsGenerator(obj) {
    var params = {
        req: obj.req,
        res: obj.res,
        qstring: obj.req.query,
        fullPath: url.parse(obj.req.url, true).pathname
    };

    params.qstring.auth_token = obj.req.session.auth_token;
    params.qstring.app_id = obj.req.body.app_id;

    return params;
}

if (!countlyConfig.cookie) {
    countlyConfig.cookie = {
        httpOnly: true,
        maxAge: 1000 * 60 * 60 * 24,
        secure: countlyConfig.web.secure_cookies || false,
        maxAgeLogin: 1000 * 60 * 60 * 24 * 365
    };
}

plugins.setConfigs("frontend", {
    production: true,
    theme: countlyConfig.web.theme || "",
    session_timeout: 30,
    use_google: true,
    code: true,
    offline_mode: false
});

plugins.setUserConfigs("frontend", {
    production: false,
    theme: false,
    session_timeout: false,
    use_google: false,
    code: false,
});

plugins.setConfigs("security", {
    login_tries: 3,
    login_wait: 5 * 60,
    password_min: 8,
    password_char: true,
    password_number: true,
    password_symbol: true,
    password_expiration: 0,
    password_rotation: 3,
    password_autocomplete: true,
    robotstxt: "User-agent: *\nDisallow: /",
    dashboard_additional_headers: "X-Frame-Options:deny\nX-XSS-Protection:1; mode=block\nStrict-Transport-Security:max-age=31536000; includeSubDomains; preload\nX-Content-Type-Options: nosniff",
    api_additional_headers: "X-Frame-Options:deny\nX-XSS-Protection:1; mode=block\nStrict-Transport-Security:max-age=31536000; includeSubDomains; preload\nAccess-Control-Allow-Origin:*",
    dashboard_rate_limit_window: 60,
    dashboard_rate_limit_requests: 500
});

process.on('uncaughtException', (err) => {
    console.log('Caught exception: %j', err, err.stack);
    if (log && log.e) {
        log.e('Logging caught exception');
    }
    process.exit(1);
});

process.on('unhandledRejection', (reason, p) => {
    console.log("Unhandled rejection at: Promise ", p, " reason: ", reason);
    if (log && log.e) {
        log.e("Logging unhandled rejection");
    }
});

if (countlyConfig.web && countlyConfig.web.track === "all") {
    countlyConfig.web.track = null;
}

Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_fs")]).then(function(dbs) {
    var countlyDb = dbs[0];
    //reference for consistency between app and api processes
    membersUtility.db = common.db = countlyDb;
    countlyFs.setHandler(dbs[1]);
    tracker.enable();

    /**
    * Create sha1 hash string
    * @param {string} str - string to hash
    * @param {boolean} addSalt - should salt be added
    * @returns {string} hashed string
    **/
    function sha1Hash(str, addSalt) {
        var salt = (addSalt) ? new Date().getTime() : "";
        return crypto.createHmac('sha1', salt + "").update(str + "").digest('hex');
    }

    /**
    * Create sha512 hash string
    * @param {string} str - string to hash
    * @param {boolean} addSalt - should salt be added
    * @returns {string} hashed string
    **/
    function sha512Hash(str, addSalt) {
        var salt = (addSalt) ? new Date().getTime() : "";
        return crypto.createHmac('sha512', salt + "").update(str + "").digest('hex');
    }

    /**
    * Create argon2 hash string
    * @param {string} str - string to hash
    * @returns {promise} hash promise
    **/
    function argon2Hash(str) {
        return argon2.hash(str);
    }

    /**
    * Verify argon2 hash string
    * @param {string} hashedStr - argon2 hashed string
    * @param {string} str - string for verify
    * @returns {promise} verify promise
    **/
    function verifyArgon2Hash(hashedStr, str) {
        return argon2.verify(hashedStr, str);
    }

    /**
    * Is hashed string argon2?
    * @param {string} hashedStr | argon2 hashed string
    * @returns {boolean} return true if string hashed by argon2
    */
    function isArgon2Hash(hashedStr) {
        return hashedStr.includes("$argon2");
    }

    /**
    * Verify member for Argon2 Hash
    * @param {string} username | User name
    * @param {password} password | Password string
    * @param {Function} callback | Callback function
    */
    function verifyMemberArgon2Hash(username, password, callback) {
        var secret = countlyConfig.passwordSecret || "";
        password = password + secret;
        countlyDb.collection('members').findOne({$and: [{ $or: [ {"username": username}, {"email": username}]}]}, (err, member) => {
            if (member) {
                if (isArgon2Hash(member.password)) {
                    verifyArgon2Hash(member.password, password).then(match => {
                        if (match) {
                            callback(undefined, member);
                        }
                        else {
                            callback("Password is wrong!");
                        }
                    }).catch(function() {
                        callback("Password is wrong!");
                    });
                }
                else {
                    var password_SHA1 = sha1Hash(password);
                    var password_SHA5 = sha512Hash(password);

                    if (member.password === password_SHA1 || member.password === password_SHA5) {
                        argon2Hash(password).then(password_ARGON2 => {
                            updateUserPasswordToArgon2(member._id, password_ARGON2);
                            callback(undefined, member);
                        }).catch(function() {
                            callback("Password is wrong!");
                        });
                    }
                    else {
                        callback("Password is wrong!");
                    }
                }
            }
            else {
                callback("Username is wrong!");
            }
        });
    }

    /**
    * Update user password to new sha512 hash
    * @param {string} id - id of the user document
    * @param {string} password - password to hash
    **/
    function updateUserPasswordToArgon2(id, password) {
        countlyDb.collection('members').update({ _id: id}, { $set: { password: password}});
    }

    /**
    * Check if user is global admin
    * @param {object} req - request object
    * @returns {boolean} true if global admin
    **/
    function isGlobalAdmin(req) {
        return (req.session.gadm);
    }

    /**
    * Sort array by list of
    * @param {array} arrayToSort - array to sort
    * @param {array} sortList - list of values by which to sort
    * @returns {array} sorted array
    **/
    function sortBy(arrayToSort, sortList) {
        if (!sortList.length) {
            return arrayToSort;
        }

        var tmpArr = [],
            retArr = [];

        for (let i = 0; i < arrayToSort.length; i++) {
            var objId = arrayToSort[i]._id + "";
            if (sortList.indexOf(objId) !== -1) {
                tmpArr[sortList.indexOf(objId)] = arrayToSort[i];
            }
        }

        for (let i = 0; i < tmpArr.length; i++) {
            if (tmpArr[i]) {
                retArr[retArr.length] = tmpArr[i];
            }
        }

        for (let i = 0; i < arrayToSort.length; i++) {
            if (retArr.indexOf(arrayToSort[i]) === -1) {
                retArr[retArr.length] = arrayToSort[i];
            }
        }

        return retArr;
    }

    var app = express();
    app = expose(app);
    app.enable('trust proxy');
    app.set('x-powered-by', false);
    const limiter = rateLimit({
        keyGenerator: common.getIpAddress,
        windowMs: parseInt(plugins.getConfig("security").dashboard_rate_limit_window) * 1000,
        max: parseInt(plugins.getConfig("security").dashboard_rate_limit_requests),
        headers: false,
        //limit only in production mode
        skip: function() {
            return !plugins.getConfig("frontend").production || plugins.getConfig("security").dashboard_rate_limit_requests <= 0;
        }
    });

    //  apply to all requests
    app.use(limiter);

    var loadedThemes = {};
    var curTheme = countlyConfig.web.theme || "";

    /**
    * Load theme files
    * @param {string} theme - theme name
    * @param {function} callback - when loading files done
    **/
    app.loadThemeFiles = function(theme, callback) {
        if (!loadedThemes[theme]) {
            var tempThemeFiles = {css: [], js: []};
            if (theme && theme.length) {
                var themeDir = path.resolve(__dirname, "public/themes/" + common.sanitizeFilename(theme) + "/");
                fs.readdir(themeDir, function(err, list) {
                    if (err) {
                        if (callback) {
                            callback(tempThemeFiles);
                        }
                        return ;
                    }
                    var ext;
                    for (var i = 0; i < list.length; i++) {
                        ext = list[i].split(".").pop();
                        if (!tempThemeFiles[ext]) {
                            tempThemeFiles[ext] = [];
                        }
                        tempThemeFiles[ext].push(countlyConfig.path + '/themes/' + theme + "/" + list[i]);
                    }
                    if (callback) {
                        callback(tempThemeFiles);
                    }
                    loadedThemes[theme] = tempThemeFiles;
                });
            }
            else if (callback) {
                callback(tempThemeFiles);
            }
        }
        else if (callback) {
            callback(loadedThemes[theme]);
        }
    };

    plugins.loadConfigs(countlyDb, function() {
        tracker.enable();
        curTheme = plugins.getConfig("frontend").theme;
        app.loadThemeFiles(curTheme);
        app.dashboard_headers = plugins.getConfig("security").dashboard_additional_headers;

        var overriddenCountlyNamedType = COUNTLY_NAMED_TYPE;
        var whiteLabelingConfig = plugins.getConfig("white-labeling");
        if (whiteLabelingConfig && whiteLabelingConfig.footerLabel && whiteLabelingConfig.footerLabel.length) {
            overriddenCountlyNamedType = whiteLabelingConfig.footerLabel;
        }

        COUNTLY_NAMED_TYPE = overriddenCountlyNamedType;

        if (typeof plugins.getConfig('frontend').countly_tracking !== 'boolean' && plugins.isPluginEnabled('tracker')) {
            plugins.updateConfigs(countlyDb, 'frontend', { countly_tracking: true });
        }
    });

    app.engine('html', require('ejs').renderFile);
    app.set('views', __dirname + '/views');
    app.set('view engine', 'html');
    app.set('view options', {layout: false});

    app.use('/stylesheets/ionicons/fonts/', function(req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    app.use('/fonts/', function(req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    app.use('*.svg', function(req, res, next) {
        res.setHeader('Content-Type', 'image/svg+xml; charset=UTF-8');
        next();
    });

    /**
    * Add headers to request
    * @param {object} req - request object
    * @param {object} res - response object
    **/
    function add_headers(req, res) {
        if (countlyConfig.web.secure_cookies) {
        //we can't detect if it uses https behind nginx, without specific nginx configuration, so we assume it does
            req.headers["x-forwarded-proto"] = "https";
        }
        //set provided in configuration headers
        if (app.dashboard_headers) {
            var headers = app.dashboard_headers.replace(/\r\n|\r|\n/g, "\n").split("\n");
            var parts;
            for (let i = 0; i < headers.length; i++) {
                if (headers[i] && headers[i].length) {
                    parts = headers[i].split(/:(.+)?/);
                    if (parts.length === 3) {
                        res.header(parts[0], parts[1]);
                    }
                }
            }
        }
    }

    app.use(function(req, res, next) {
        add_headers(req, res);
        next();
    });

    plugins.loadAppStatic(app, countlyDb, express);
    app.use(cookieParser());
    //server theme images
    app.use(function(req, res, next) {
        var urlPath = req.url.replace(countlyConfig.path, "");
        var theme = req.cookies.theme || curTheme;
        if (theme && theme.length && (req.url.indexOf(countlyConfig.path + '/images/') === 0 || req.url.indexOf(countlyConfig.path + '/geodata/') === 0)) {
            fs.exists(__dirname + '/public/themes/' + theme + urlPath, function(exists) {
                if (exists) {
                    res.sendFile(__dirname + '/public/themes/' + theme + urlPath);
                }
                else {
                    next();
                }
            });
        }
        else {
            next();
        }
    });
    //serve app images
    app.get(countlyConfig.path + '/appimages/*', function(req, res) {

        if (!req.params || !req.params[0] || req.params[0] === '') {
            res.sendFile(__dirname + '/public/images/default_app_icon.png');
        }
        else {
            countlyFs.getStats("appimages", __dirname + '/public/appimages/' + req.params[0], {id: req.params[0]}, function(err, stats) {
                if (err || !stats || !stats.size) {
                    res.sendFile(__dirname + '/public/images/default_app_icon.png');
                }
                else {
                    countlyFs.getStream("appimages", __dirname + '/public/appimages/' + req.params[0], {id: req.params[0]}, function(err2, stream) {
                        if (err2 || !stream) {
                            res.sendFile(__dirname + '/public/images/default_app_icon.png');
                        }
                        else {
                            res.writeHead(200, {
                                'Accept-Ranges': 'bytes',
                                'Cache-Control': 'public, max-age=31536000',
                                'Connection': 'keep-alive',
                                'Date': new Date().toUTCString(),
                                'Last-Modified': stats.mtime.toUTCString(),
                                'Content-Type': 'image/png',
                                'Content-Length': stats.size
                            });
                            stream.pipe(res);
                        }
                    });
                }
            });
        }
    });

    //serve member images
    app.get(countlyConfig.path + '/memberimages/*', function(req, res) {

        if (!req.params || !req.params[0] || req.params[0] === '') {
            res.sendFile(__dirname + '/public/images/default_member_icon.png');
        }
        else {
            countlyFs.getStats("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err, stats) {
                if (err || !stats || !stats.size) {
                    res.sendFile(__dirname + '/public/images/default_member_icon.png');
                }
                else {
                    countlyFs.getStream("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err2, stream) {
                        if (err2 || !stream) {
                            res.sendFile(__dirname + '/public/images/default_member_icon.png');
                        }
                        else {
                            res.writeHead(200, {
                                'Accept-Ranges': 'bytes',
                                'Cache-Control': 'public, max-age=31536000',
                                'Connection': 'keep-alive',
                                'Date': new Date().toUTCString(),
                                'Last-Modified': stats.mtime.toUTCString(),
                                'Content-Type': 'image/png',
                                'Content-Length': stats.size
                            });
                            stream.pipe(res);
                        }
                    });
                }
            });
        }
    });

    app.get(countlyConfig.path + "*/screenshots/*", function(req, res) {
        countlyFs.getStats("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err, stats) {
            if (err || !stats || !stats.size) {
                return res.send(false);
            }

            countlyFs.getStream("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err2, stream) {
                if (err2 || !stream) {
                    return res.send(false);
                }

                res.writeHead(200, {
                    'Accept-Ranges': 'bytes',
                    'Cache-Control': 'public, max-age=31536000',
                    'Connection': 'keep-alive',
                    'Date': new Date().toUTCString(),
                    'Last-Modified': stats.mtime.toUTCString(),
                    'Content-Type': 'image/png',
                    'Content-Length': stats.size
                });
                stream.pipe(res);
            });
        });
    });

    var oneYear = 31557600000;
    app.use(countlyConfig.path, express.static(__dirname + '/public', { maxAge: oneYear }));

    app.use(bodyParser.json()); // to support JSON-encoded bodies
    app.use(bodyParser.urlencoded({ // to support URL-encoded bodies
        extended: true
    }));

    const sessionMiddleware = session({
        secret: countlyConfig.web.session_secret || 'countlyss',
        name: countlyConfig.web.session_name || 'connect.sid',
        cookie: countlyConfig.cookie,
        store: new SkinStore(countlyDb),
        saveUninitialized: false,
        resave: true,
        rolling: true,
        proxy: true,
        unset: "destroy"
    });
    app.use((req, res, next) => {
        if (!plugins.callMethod("skipSession", {req: req, res: res, next: next})) {
            return sessionMiddleware(req, res, next);
        }
        else {
            return next();
        }
    });

    app.use(function(req, res, next) {
        var contentType = req.headers['content-type'];
        if (req.method.toLowerCase() === 'post' && contentType && contentType.indexOf('multipart/form-data') >= 0) {
            if (!req.session?.uid || Date.now() > req.session?.expires) {
                res.status(401).send('Unauthorized');
                return;
            }
            var form = new formidable.IncomingForm();
            form.uploadDir = __dirname + '/uploads';
            form.parse(req, function(err, fields, files) {
                //handle bakcwards compatability with formiddble v1
                for (let i in files) {
                    if (files[i].filepath) {
                        files[i].path = files[i].filepath;
                    }
                    if (files[i].mimetype) {
                        files[i].type = files[i].mimetype;
                    }
                    if (files[i].originalFilename) {
                        files[i].name = files[i].originalFilename;
                    }
                }

                req.files = files;
                if (!req.body) {
                    req.body = {};
                }
                for (let i in fields) {
                    if (typeof req.body[i] === "undefined") {
                        req.body[i] = fields[i];
                    }
                }
                next();
            });
        }
        else {
            next();
        }
    });
    var convertLink = function(val, defaultVal) {
        if (typeof val === "undefined" || val === true) {
            return defaultVal;
        }
        return val;
    };
    app.use(flash());
    app.use(function(req, res, next) {
        req.template = {};
        req.template.html = "";
        req.template.js = "";
        req.template.css = "";
        req.template.form = "";
        req.countly = {
            company: COUNTLY_COMPANY,
            version: COUNTLY_VERSION,
            type: COUNTLY_TYPE,
            page: COUNTLY_PAGE,
            title: COUNTLY_NAME,
            favicon: "images/favicon.png",
            documentationLink: convertLink(versionInfo.documentationLink, "https://support.count.ly/hc/en-us/categories/360002373332-Knowledge-Base"),
            helpCenterLink: convertLink(versionInfo.helpCenterLink, "https://support.count.ly/hc/en-us"),
            featureRequestLink: convertLink(versionInfo.featureRequestLink, "https://discord.com/channels/1088398296789299280/1088721958218248243"),
            feedbackLink: convertLink(versionInfo.feedbackLink, "https://count.ly/legal/privacy-policy"),
        };
        plugins.loadConfigs(countlyDb, function() {
            var securityConf = plugins.getConfig("security");
            app.dashboard_headers = securityConf.dashboard_additional_headers;
            add_headers(req, res);
            preventBruteforce.fails = Number.isInteger(securityConf.login_tries) ? securityConf.login_tries : 3;
            preventBruteforce.wait = securityConf.login_wait || 5 * 60;

            curTheme = plugins.getConfig("frontend", req.session && req.session.settings).theme;
            app.loadThemeFiles(req.cookies.theme || curTheme, function(themeFiles) {
                res.locals.flash = req.flash.bind(req);
                req.config = plugins.getConfig("frontend", req.session && req.session.settings);
                req.themeFiles = themeFiles;
                var _render = res.render;
                res.render = function(view, opts, fn, parent, sub) {
                    if (!opts) {
                        opts = {};
                    }
                    if (!opts.path) {
                        opts.path = countlyConfig.path || "";
                    }
                    if (!opts.cdn) {
                        opts.cdn = countlyConfig.cdn || "";
                    }
                    if (!opts.themeFiles) {
                        opts.themeFiles = themeFiles;
                    }
                    _render.call(res, view, opts, fn, parent, sub);
                };
                next();
            });
        });
    });
    app.use(methodOverride());
    app.use(function(req, res, next) {
        if (!plugins.callMethod("skipCSRF", {req: req, res: res, next: next})) {
        //none of the plugins requested to skip csrf for this request
            csrf(req, res, next);
        }
        else {
        //skipping csrf step, some plugin needs it without csrf
            next();
        }
    });

    app.use(function(req, res, next) {
        if (!plugins.callMethod("skipDollarCheck", {req: req, res: res, next: next})) {
        //none of the plugins requested to skip dollar sign check
            dollarDefender(req, res, next);
        }
        else {
        //skipping dollar sign check, some plugin needs mongo object as parameters
            next();
        }
    });

    //for csrf error handling. redirect to login if getting bad token while logging in(not show forbidden page)
    app.use(function(err, req, res, next) { // eslint-disable-line no-unused-vars
        var mylink = req.url.split('?');
        mylink = mylink[0];
        if (err.code === 'EBADCSRFTOKEN' && mylink === countlyConfig.path + "/login") {
            res.status(403);
            res.redirect(countlyConfig.path + '/login?message=login.token-expired');
        }
        else {
            res.status(403).send("Forbidden Token");
        }
    });


    //prevent bruteforce attacks
    preventBruteforce.db = countlyDb;
    preventBruteforce.mail = countlyMail;

    for (let pathPart of ["/login", "/mobile/login"]) {
        const absPath = countlyConfig.path + pathPart;
        preventBruteforce.pathIdentifiers[absPath] = "login";
        preventBruteforce.userIdentifiers[absPath] = (req) => req.body.username;
    }

    preventBruteforce.blockHooks.login = function(uid, req, res) { // eslint-disable-line no-unused-vars
        preventBruteforce.db.collection("members").findOne({username: uid}, function(err, member) {
            if (member) {
                preventBruteforce.mail.sendTimeBanWarning(member, preventBruteforce.db);
            }
        });
    };

    preventBruteforce.pathIdentifiers[countlyConfig.path + "/forgot"] = "forgot";

    app.use(preventBruteforce.middleware);

    plugins.loadAppPlugins(app, countlyDb, express);

    var env = process.env.NODE_ENV || 'development';
    if ('development' === env) {
        app.use(errorhandler(true));
    }

    app.get(countlyConfig.path + '/', function(req, res) {
        res.redirect(countlyConfig.path + '/login');
    });

    var extendSession = function(req) {
        membersUtility.extendSession(req);
    };
    var checkRequestForSession = function(req, res, next) {
        if (parseInt(plugins.getConfig("frontend", req.session && req.session.settings).session_timeout)) {
            if (req.session.uid) {
                if (Date.now() > req.session.expires) {
                    membersUtility.logout(req, res);
                    res.redirect(countlyConfig.path + '/login?message=logout.inactivity');
                }
                else {
                //extend session
                    extendSession(req, res, next);
                    next();
                }
            }
            else {
                next();
            }
        }
        else {
            next();
        }
    };

    app.get(countlyConfig.path + '/ping', function(req, res) {
        countlyDb.collection("plugins").findOne({_id: "plugins"}, function(err) {
            if (err) {
                res.status(404).send("DB Error");
            }
            else {
                res.send("Success");
            }
        });
    });

    app.get(countlyConfig.path + '/robots.txt', function(req, res) {
        res.contentType('text/plain');
        res.send(plugins.getConfig("security").robotstxt);
    });

    app.get(countlyConfig.path + '/session', function(req, res, next) {
        if (req.session.auth_token) {
            authorize.verify_return({
                db: countlyDb,
                token: req.session.auth_token,
                req_path: "",
                callback: function(valid) {
                    if (!valid) {
                        //logout user
                        res.send("logout");
                    }
                    else {
                        if (req.session.uid) {
                            if (Date.now() > req.session.expires) {
                                //logout user
                                membersUtility.logout(req, res);
                                res.send("logout");
                            }
                            else {
                                //extend session
                                if (req.query.check_session) {
                                    res.send("success");
                                }
                                else {
                                    extendSession(req, res, next);
                                    res.send("success");
                                }
                            }
                        }
                        else {
                            res.send("login");
                        }
                    }
                }
            });
        }
        else {
            res.send("login");
        }
    });
    app.get(countlyConfig.path + '/dashboard', checkRequestForSession);
    app.post('*', checkRequestForSession);

    app.get(countlyConfig.path + '/logout', function(req, res) {
        if (req.query.message) {
            res.redirect(countlyConfig.path + '/login?message=' + req.query.message);
        }
        else {
            res.redirect(countlyConfig.path + '/login');
        }
    });

    app.post(countlyConfig.path + '/logout', function(req, res/*, next*/) {
        membersUtility.logout(req, res);
        if (req.query.message) {
            res.redirect(countlyConfig.path + '/login?message=' + req.query.message);
        }
        else {
            res.redirect(countlyConfig.path + '/login');
        }
    });

    /**
     * Stringify all object nested properties named `prop`
     *
     * @param {object} obj object to fix
     * @param {string} prop property name
     */
    function stringifyIds(obj, prop = '_id') {
        for (let k in obj) {
            if (k === prop && common.dbext.isoid(obj[k])) {
                obj[k] = obj[k].toString();
            }
            else if (typeof obj[k] === 'object') {
                stringifyIds(obj[k]);
            }
        }
    }

    /**
    * Render dashboard
    * @param {object} req - request object
    * @param {object} res - response object
    * @param {function} next - callback for next middleware
    * @param {object} member - dashboard member document
    * @param {array} adminOfApps - list of apps member is admin of
    * @param {array} userOfApps - list of apps member is user of
    * @param {object} countlyGlobalApps - all apps user has any access to, where key is app id and value is app document
    * @param {object} countlyGlobalAdminApps - all apps user has write access to, where key is app id and value is app document
    **/
    function renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps) {
        var configs = plugins.getConfig("frontend", member.settings),
            countly_domain = plugins.getConfig('api').domain,
            licenseNotification, licenseError;
        var isLocked = false;
        configs.export_limit = plugins.getConfig("api").export_limit;

        var currentWhiteLabelingConfig = plugins.getConfig("white-labeling");
        var overriddenCountlyNamedType = COUNTLY_NAMED_TYPE;
        if (currentWhiteLabelingConfig && currentWhiteLabelingConfig.footerLabel && currentWhiteLabelingConfig.footerLabel.length) {
            overriddenCountlyNamedType = currentWhiteLabelingConfig.footerLabel;
        }
        app.loadThemeFiles(configs.theme, async function(theme) {
            if (configs._user.theme) {
                res.cookie("theme", configs.theme);
            }
            req.session.uid = member._id;
            req.session.gadm = (member.global_admin === true);
            req.session.email = member.email;
            req.session.settings = member.settings;
            res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
            res.header('Expires', '0');
            res.header('Pragma', 'no-cache');
            if (member.upgrade) {
                countlyDb.collection('members').update({"_id": member._id}, {$unset: {upgrade: ""}}, function() {});
            }
            if (IS_FLEX) {
                let locked = await countlyDb.collection('mycountly').findOne({_id: 'lockServer'});
                if (locked?.isLocked === true) {
                    isLocked = true;
                }

            }

            if (req.session.licenseError) {
                licenseError = req.session.licenseError;
            }
            if (req.session.licenseNotification) {
                try {
                    licenseNotification = JSON.parse(req.session.licenseNotification);
                }
                catch (e) {
                    log.e('Failed to parse notify', e);
                }
            }

            member._id += "";
            delete member.password;

            adminOfApps = sortBy(adminOfApps, member.appSortList || []);
            userOfApps = sortBy(userOfApps, member.appSortList || []);

            stringifyIds(adminOfApps);
            stringifyIds(userOfApps);
            stringifyIds(countlyGlobalApps);
            stringifyIds(countlyGlobalAdminApps);

            plugins.reloadEnabledPluginList(common.db, function() {

                var defaultApp = userOfApps[0];
                var serverSideRendering = req.query.ssr;
                _.extend(req.config, configs);
                var countlyGlobal = {
                    COUNTLY_CONTAINER: process.env.COUNTLY_CONTAINER,
                    countlyTitle: req.countly.title,
                    company: req.countly.company,
                    languages: languages,
                    countlyVersion: req.countly.version,
                    countlyFavicon: req.countly.favicon,
                    pluginsSHA: sha1Hash(plugins.getPlugins()),
                    apps: countlyGlobalApps,
                    defaultApp: defaultApp,
                    admin_apps: countlyGlobalAdminApps,
                    csrf_token: req.csrfToken(),
                    auth_token: req.session.auth_token,
                    member: member,
                    config: req.config,
                    security: plugins.getConfig("security"),
                    tracking: plugins.getConfig("tracking"),
                    plugins: plugins.getPlugins(),
                    pluginsFull: plugins.getPlugins(true),
                    path: countlyConfig.path || "",
                    cdn: countlyConfig.cdn || "",
                    message: req.flash("message"),
                    licenseNotification,
                    licenseError,
                    ssr: serverSideRendering,
                    timezones: timezones,
                    countlyTypeName: overriddenCountlyNamedType,
                    countlyTypeTrack: COUNTLY_TRACK_TYPE,
                    countlyTypeCE: COUNTLY_TYPE_CE,
                    countly_domain,
                    frontend_app: versionInfo.frontend_app || "9c28c347849f2c03caf1b091ec7be8def435e85e",
                    frontend_server: versionInfo.frontend_server || 'https://stats.count.ly/',
                    usermenu: {
                        feedbackLink: COUNTLY_FEEDBACK_LINK,
                        documentationLink: COUNTLY_DOCUMENTATION_LINK,
                        helpCenterLink: COUNTLY_HELPCENTER_LINK,
                        featureRequestLink: COUNTLY_FEATUREREQUEST_LINK,
                    },
                    mycountly: IS_FLEX,
                    isLocked: isLocked,
                };


                var toDashboard = {
                    countlyTitle: req.countly.title,
                    languages: languages,
                    countlyFavicon: req.countly.favicon,
                    adminOfApps: adminOfApps,
                    userOfApps: userOfApps,
                    defaultApp: defaultApp,
                    member: member,
                    intercom: countlyConfig.web.use_intercom,
                    track: countlyConfig.web.track || false,
                    installed: req.session.install || false,
                    cpus: require('os').cpus().length,
                    countlyVersion: req.countly.version,
                    countlyType: COUNTLY_TYPE_CE,
                    countlyTrial: COUNTLY_TRIAL,
                    countlyTypeName: overriddenCountlyNamedType,
                    feedbackLink: COUNTLY_FEEDBACK_LINK,
                    documentationLink: COUNTLY_DOCUMENTATION_LINK,
                    helpCenterLink: COUNTLY_HELPCENTER_LINK,
                    featureRequestLink: COUNTLY_FEATUREREQUEST_LINK,
                    countlyTypeTrack: COUNTLY_TRACK_TYPE,
                    frontend_app: versionInfo.frontend_app,
                    frontend_server: versionInfo.frontend_server,
                    production: configs.production || false,
                    pluginsSHA: sha1Hash(plugins.getPlugins()),
                    plugins: plugins.getPlugins(),
                    config: req.config,
                    path: countlyConfig.path || "",
                    cdn: countlyConfig.cdn || "",
                    use_google: configs.use_google || false,
                    themeFiles: theme,
                    inject_template: req.template,
                    javascripts: [],
                    stylesheets: [],
                    offline_mode: configs.offline_mode || false
                };
                // google services cannot work when offline mode enable
                if (toDashboard.offline_mode) {
                    toDashboard.use_google = false;
                }
                if (countlyGlobal.config.offline_mode) {
                    countlyGlobal.config.use_google = false;
                }


                var plgns = [].concat(plugins.getPlugins());
                if (plgns.indexOf('push') !== -1) {
                    plgns.splice(plgns.indexOf('push'), 1);
                    plgns.unshift('push');
                }
                plgns.forEach(plugin => {
                    try {
                        let contents = fs.readdirSync(__dirname + `/../../plugins/${common.sanitizeFilename(plugin)}/frontend/public/javascripts`) || [];
                        toDashboard.javascripts.push.apply(toDashboard.javascripts, contents.filter(n => typeof n === 'string' && n.includes('.js') && n.length > 3 && n.indexOf('.js') === n.length - 3).map(n => `${plugin}/javascripts/${n}`));
                    }
                    catch (e) {
                        console.log('Error while reading folder of plugin %s: %j', plugin, e.stack);
                    }
                    try {
                        let contents = fs.readdirSync(__dirname + `/../../plugins/${common.sanitizeFilename(plugin)}/frontend/public/stylesheets`) || [];
                        toDashboard.stylesheets.push.apply(toDashboard.stylesheets, contents.filter(n => typeof n === 'string' && n.includes('.css') && n.length > 4 && n.indexOf('.css') === n.length - 4).map(n => `${plugin}/stylesheets/${n}`));
                    }
                    catch (e) {
                        console.log('Error while reading folder of plugin %s: %j', plugin, e.stack);
                    }
                });

                if (req.session.install) {
                    req.session.install = null;
                    res.clearCookie('install');
                }
                plugins.callMethod("renderDashboard", {req: req, res: res, next: next, data: {member: member, adminApps: countlyGlobalAdminApps, userApps: countlyGlobalApps, countlyGlobal: countlyGlobal, toDashboard: toDashboard}});

                res.expose(countlyGlobal, 'countlyGlobal');

                res.render('dashboard', toDashboard);
            });
        });
    }

    app.get(countlyConfig.path + '/dashboard', function(req, res, next) {
        if (!req.session.uid) {
            res.redirect(countlyConfig.path + '/login');
        }
        else {
            countlyDb.collection('members').findOne({"_id": countlyDb.ObjectID(req.session.uid + "")}, function(err, member) {
                if (member) {
                    plugins.callPromisedAppMethod('checkMemberLicense', { req, member }).then(licenseCheck => {
                        common.licenseAssign(req, licenseCheck);
                        req.session.cookie.maxAge = countlyConfig.cookie.maxAgeLogin;
                        var adminOfApps = [],
                            userOfApps = [],
                            countlyGlobalApps = {},
                            countlyGlobalAdminApps = {};

                        if (Number.isInteger(member.session_count)) {
                            member.session_count += 1;
                        }
                        else {
                            member.session_count = 1;
                        }

                        countlyDb.collection('members').update(
                            { _id: common.db.ObjectID(member._id) },
                            {
                                $inc: { session_count: 1 },
                                $set: {
                                    last_login: Math.round(new Date().getTime() / 1000),
                                    lu: new Date()
                                }
                            }
                        );

                        if (member.global_admin) {
                            countlyDb.collection('apps').find({}).toArray(function(err2, apps) {
                                adminOfApps = apps;
                                userOfApps = apps;

                                for (let i = 0; i < apps.length; i++) {
                                    if (apps[i].checksum_salt) {
                                        apps[i].salt = apps[i].salt || apps[i].checksum_salt;
                                    }
                                    apps[i].type = apps[i].type || "mobile";
                                    countlyGlobalApps[apps[i]._id] = apps[i];
                                    countlyGlobalApps[apps[i]._id]._id = "" + apps[i]._id;
                                }
                                countlyGlobalAdminApps = countlyGlobalApps;
                                renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps);
                            });
                        }
                        else {
                            var adminOfAppIds = [],
                                userOfAppIds = [];

                            /*
                        We keep this section for backward compatibility.
                        This block will run if member has legacy permission properties like user_of, admin_of.
                        */
                            if (typeof member.permission === "undefined") {
                                if (member.admin_of.length === 1 && member.admin_of[0] === "") {
                                    member.admin_of = [];
                                }

                                for (let i = 0; i < member.admin_of.length; i++) {
                                    if (member.admin_of[i] === "") {
                                        continue;
                                    }
                                    adminOfAppIds[adminOfAppIds.length] = countlyDb.ObjectID(member.admin_of[i]);
                                }

                                for (let i = 0; i < member.user_of.length; i++) {
                                    if (member.user_of[i] === "") {
                                        continue;
                                    }
                                    userOfAppIds[userOfAppIds.length] = countlyDb.ObjectID(member.user_of[i]);
                                }

                                countlyDb.collection('apps').find({ _id: { '$in': adminOfAppIds } }).toArray(function(err2, admin_of) {
                                    for (let i = 0; i < admin_of.length; i++) {
                                        countlyGlobalAdminApps[admin_of[i]._id] = admin_of[i];
                                        countlyGlobalAdminApps[admin_of[i]._id]._id = "" + admin_of[i]._id;
                                    }

                                    countlyDb.collection('apps').find({ _id: { '$in': userOfAppIds } }).toArray(function(err3, user_of) {
                                        adminOfApps = admin_of;
                                        userOfApps = user_of;

                                        for (let i = 0; i < user_of.length; i++) {
                                            countlyGlobalApps[user_of[i]._id] = user_of[i];
                                            countlyGlobalApps[user_of[i]._id]._id = "" + user_of[i]._id;
                                            countlyGlobalApps[user_of[i]._id].type = countlyGlobalApps[user_of[i]._id].type || "mobile";
                                        }

                                        renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps);
                                    });
                                });
                            }
                            else {
                                var writableAppIds = member.permission._.a;
                                var readableAppIds = Object.keys(member.permission.r).filter(readableApp => readableApp !== 'global');
                                var preparedReadableIds = [];
                                var preparedWritableIds = [];

                                for (let i = 0; i < readableAppIds.length; i++) {
                                    if (readableAppIds[i] !== 'undefined' && (member.permission.r[readableAppIds[i]].all || Object.keys(member.permission.r[readableAppIds[i]].allowed).length > 0)) {
                                        preparedReadableIds.push(countlyDb.ObjectID(readableAppIds[i]));
                                    }
                                }

                                for (let i = 0; i < writableAppIds.length; i++) {
                                    preparedWritableIds.push(countlyDb.ObjectID(writableAppIds[i]));
                                }

                                countlyDb.collection('apps').find({ _id: { '$in': preparedReadableIds } }).toArray(function(err4, readableApps) {
                                    countlyDb.collection('apps').find({ _id: { '$in': preparedWritableIds } }).toArray(function(err5, writableApps) {
                                        adminOfApps = writableApps;
                                        userOfApps = readableApps.concat(writableApps);

                                        for (let i = 0; i < userOfApps.length; i++) {
                                            countlyGlobalApps[userOfApps[i]._id] = userOfApps[i];
                                            countlyGlobalApps[userOfApps[i]._id]._id = "" + userOfApps[i]._id;
                                            countlyGlobalApps[userOfApps[i]._id].type = countlyGlobalApps[userOfApps[i]._id].type || "mobile";

                                            if (adminOfApps.indexOf(userOfApps[i]) !== -1) {
                                                countlyGlobalAdminApps[userOfApps[i]._id] = userOfApps[i];
                                                countlyGlobalAdminApps[userOfApps[i]._id]._id = "" + userOfApps[i]._id;
                                                countlyGlobalAdminApps[userOfApps[i]._id].type = userOfApps[i].type || "mobile";
                                            }
                                        }
                                        renderDashboard(req, res, next, member, adminOfApps, userOfApps, countlyGlobalApps, countlyGlobalAdminApps);
                                    });
                                });
                            }
                        }
                    },
                    e => {
                        log.e('Error while checking member login', e);
                    });
                }
                else {
                    membersUtility.clearReqAndRes(req, res);
                    res.redirect(countlyConfig.path + '/login');
                }
            });
        }
    });

    app.get(countlyConfig.path + '/setup', function(req, res) {
        res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
        res.header('Expires', '0');
        res.header('Pragma', 'no-cache');
        countlyDb.collection('members').count(function(err, memberCount) {
            if (!err && memberCount === 0) {
                res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
                res.header('Expires', '0');
                res.header('Pragma', 'no-cache');
                var config = plugins.getConfig("security");
                res.render('setup', {
                    documentationLink: req.countly.documentationLink,
                    helpCenterLink: req.countly.helpCenterLink,
                    feedbackLink: req.countly.feedbackLink,
                    featureRequestLink: req.countly.featureRequestLink,
                    languages: languages,
                    countlyFavicon: req.countly.favicon,
                    countlyTitle: req.countly.title,
                    countlyPage: req.countly.page,
                    "csrf": req.csrfToken(),
                    path: countlyConfig.path || "",
                    cdn: countlyConfig.cdn || "",
                    themeFiles: req.themeFiles,
                    inject_template: req.template,
                    params: {},
                    error: {},
                    security: {
                        password_min: config.password_min,
                        password_char: config.password_char,
                        password_number: config.password_number,
                        password_symbol: config.password_symbol,
                        autocomplete: config.password_autocomplete || false
                    }
                });
            }
            else if (err) {
                res.status(500).send('Server Error');
            }
            else {
                res.redirect(countlyConfig.path + '/login');
            }
        });
    });

    app.get(countlyConfig.path + '/login', function(req, res) {
        res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
        res.header('Expires', '0');
        res.header('Pragma', 'no-cache');
        if (req.session.uid) {
            res.redirect(countlyConfig.path + '/dashboard');
        }
        else {
            countlyDb.collection('members').estimatedDocumentCount(function(err, memberCount) {
                if (memberCount) {
                    if (req.query.message) {
                        req.flash('info', req.query.message);
                    }
                    res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
                    res.header('Expires', '0');
                    res.header('Pragma', 'no-cache');
                    var config = plugins.getConfig("security");
                    res.render('login', {
                        documentationLink: req.countly.documentationLink,
                        helpCenterLink: req.countly.helpCenterLink,
                        feedbackLink: req.countly.feedbackLink,
                        featureRequestLink: req.countly.featureRequestLink,
                        languages: languages,
                        countlyFavicon: req.countly.favicon,
                        countlyTitle: req.countly.title,
                        countlyPage: req.countly.page,
                        "message": req.flash('info'),
                        "csrf": req.csrfToken(),
                        path: countlyConfig.path || "",
                        cdn: countlyConfig.cdn || "",
                        themeFiles: req.themeFiles,
                        inject_template: req.template,
                        security: {autocomplete: config.password_autocomplete || false}
                    });
                }
                else {
                    res.redirect(countlyConfig.path + '/setup');
                }
            });
        }
    });

    app.get(countlyConfig.path + '/forgot', function(req, res) {
        res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
        res.header('Expires', '0');
        res.header('Pragma', 'no-cache');
        if (req.session.uid) {
            res.redirect(countlyConfig.path + '/dashboard');
        }
        else {
            res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
            res.header('Expires', '0');
            res.header('Pragma', 'no-cache');
            res.render('forgot', {
                documentationLink: req.countly.documentationLink,
                helpCenterLink: req.countly.helpCenterLink,
                feedbackLink: req.countly.feedbackLink,
                featureRequestLink: req.countly.featureRequestLink,
                languages: languages,
                countlyFavicon: req.countly.favicon,
                countlyTitle: req.countly.title,
                countlyPage: req.countly.page,
                "csrf": req.csrfToken(),
                "message": req.query.message || "",
                path: countlyConfig.path || "",
                cdn: countlyConfig.cdn || "",
                themeFiles: req.themeFiles,
                inject_template: req.template
            });
        }
    });

    app.get(countlyConfig.path + '/reset/:prid', function(req, res) {
        res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
        res.header('Expires', '0');
        res.header('Pragma', 'no-cache');
        if (req.params.prid) {
            req.params.prid += "";
            countlyDb.collection('password_reset').findOne({prid: req.params.prid}, function(err, passwordReset) {
                var timestamp = Math.round(new Date().getTime() / 1000);

                if (passwordReset && !err) {
                    if (timestamp > (passwordReset.timestamp + 600)) {
                        req.flash('info', 'reset.invalid');
                        res.redirect(countlyConfig.path + '/forgot');
                    }
                    else {
                        res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
                        res.header('Expires', '0');
                        res.header('Pragma', 'no-cache');
                        var config = plugins.getConfig("security");
                        res.render('reset', {
                            documentationLink: req.countly.documentationLink,
                            helpCenterLink: req.countly.helpCenterLink,
                            feedbackLink: req.countly.feedbackLink,
                            featureRequestLink: req.countly.featureRequestLink,
                            languages: languages,
                            countlyFavicon: req.countly.favicon,
                            countlyTitle: req.countly.title,
                            countlyPage: req.countly.page,
                            "csrf": req.csrfToken(),
                            "prid": req.params.prid,
                            "message": req.query.message || "",
                            path: countlyConfig.path || "",
                            cdn: countlyConfig.cdn || "",
                            "newinvite": passwordReset.newInvite,
                            themeFiles: req.themeFiles,
                            inject_template: req.template,
                            security: {autocomplete: config.password_autocomplete || false, password_min: config.password_min}
                        });
                    }
                }
                else {
                    req.flash('info', 'reset.invalid');
                    res.redirect(countlyConfig.path + '/forgot');
                }
            });
        }
        else {
            req.flash('info', 'reset.invalid');
            res.redirect(countlyConfig.path + '/forgot');
        }
    });

    app.post(countlyConfig.path + '/reset', function(req, res/*, next*/) {
        membersUtility.reset(req, function(result, member) {
            if (result === false) {
                if (member) {
                    req.flash('info', 'reset.result');
                    res.redirect(countlyConfig.path + '/login');
                }
                else {
                    res.redirect(countlyConfig.path + '/reset/' + req.body.prid);
                }
            }
            else {
                res.redirect(countlyConfig.path + '/reset/' + req.body.prid + "?message=" + result);
            }
        });
    });

    app.post(countlyConfig.path + '/forgot', function(req, res/*, next*/) {
        if (req.body.email) {
            if (countlyCommon.validateEmail(req.body.email)) {
                membersUtility.forgot(req, function(/*member*/) {
                    preventBruteforce.fail("forgot", req.ip);
                    res.redirect(countlyConfig.path + '/forgot?message=forgot.result');
                });
            }
            else {
                res.redirect(countlyConfig.path + '/forgot?message=forgot.result');
            }
        }
        else {
            res.redirect(countlyConfig.path + '/forgot');
        }
    });

    app.post(countlyConfig.path + '/setup', function(req, res/*, next*/) {
        var params = req.body || {};
        membersUtility.setup(req, function(err) {
            const createDemoApp = !!params.createDemoApp;

            if (!err) {
                res.redirect(countlyConfig.path + '/dashboard' + (createDemoApp ? '?create_demo_app=1' : ''));
            }
            else if (err === "User exists") {
                res.redirect(countlyConfig.path + '/login');
            }
            else if (err && err.message) {
                res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
                res.header('Expires', '0');
                res.header('Pragma', 'no-cache');
                var config = plugins.getConfig("security");
                var data = {
                    documentationLink: req.countly.documentationLink,
                    helpCenterLink: req.countly.helpCenterLink,
                    feedbackLink: req.countly.feedbackLink,
                    featureRequestLink: req.countly.featureRequestLink,
                    languages: languages,
                    countlyFavicon: req.countly.favicon,
                    countlyTitle: req.countly.title,
                    countlyPage: req.countly.page,
                    "csrf": req.csrfToken(),
                    path: countlyConfig.path || "",
                    cdn: countlyConfig.cdn || "",
                    themeFiles: req.themeFiles,
                    inject_template: req.template,
                    params: {},
                    error: err || {},
                    security: {password_min: config.password_min, password_char: config.password_char, password_number: config.password_number, password_symbol: config.password_symbol, autocomplete: config.password_autocomplete || false}
                };
                if (params.email) {
                    data.params.email = params.email;
                }
                if (params.full_name) {
                    data.params.full_name = params.full_name;
                }
                if (params.username) {
                    data.params.username = params.username;
                }
                if (params.password) {
                    data.params.password = params.password;
                }
                res.render('setup', data);
            }
            else {
                res.status(500).send('Server Error');
            }
        });
    });

    app.post(countlyConfig.path + '/login', function(req, res/*, next*/) {
        membersUtility.login(req, res, function(member) {
            if (member) {
                if (member.locked) {
                    res.redirect(countlyConfig.path + '/login?message=login.locked');
                }
                else {
                    res.redirect(countlyConfig.path + '/dashboard');
                    preventBruteforce.reset("login", req.body.username);
                }
            }
            else {
                res.redirect(countlyConfig.path + '/login?message=login.result');
                if (req.body.username) {
                    preventBruteforce.fail("login", req.body.username);
                }
            }
        });
    });

    app.get(countlyConfig.path + '/api-key', function(req, res, next) {
        /**
        * Handles unauthorized access attempt
        * @param {object} response - response object
        * @returns {void} void
        **/
        function unauthorized(response) {
            response.set('WWW-Authenticate', 'Basic realm=Authorization Required');
            return response.status(401).send("-1");
        }
        var user = basicAuth(req);
        if (user && user.name && user.pass) {
            preventBruteforce.isBlocked("login", user.name, function(isBlocked, fails, err) {
                if (isBlocked) {
                    if (err) {
                        res.status(500).send('Server Error');
                    }
                    else {
                        unauthorized(res);
                    }
                }
                else {
                    user.name = (user.name + "").trim();
                    verifyMemberArgon2Hash(user.name, user.pass, (err2, member) => {
                        if (member) {
                            if (member.locked) {
                                plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: user.name}});
                                unauthorized(res);
                            }
                            else {
                                plugins.callMethod("apikeySuccessful", {req: req, res: res, next: next, data: {username: member.username}});
                                preventBruteforce.reset("login", user.name);
                                countlyDb.collection('members').update({_id: member._id}, {$set: {last_login: Math.round(new Date().getTime() / 1000)}}, function() {});
                                res.status(200).send(member.api_key);
                            }
                        }
                        else {
                            plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: user.name}});
                            preventBruteforce.fail("login", user.name);
                            unauthorized(res);
                        }
                    });
                }
            });
        }
        else {
            plugins.callMethod("apikeyFailed", {req: req, res: res, next: next, data: {username: ""}});
            unauthorized(res);
        }
    });

    app.get(countlyConfig.path + '/sdks.js', function(req, res) {
        if (!plugins.getConfig("api").offline_mode) {
            var options = {uri: "https://code.count.ly/js/sdks.js", method: "GET", timeout: 4E3};
            request(options, function(a, c, b) {
                res.set('Content-type', 'application/javascript').status(200).send(b);
            });
        }
        else {
            res.status(403).send("Server is in offline mode, this request cannot be completed.");
        }
    });

    app.post(countlyConfig.path + '/mobile/login', function(req, res, next) {
        if (req.body.username && req.body.password) {
            req.body.username = (req.body.username + "").trim();

            verifyMemberArgon2Hash(req.body.username, req.body.password, (err, member) => {
                if (member) {
                    if (member.locked) {
                        plugins.callMethod("mobileloginFailed", {req: req, res: res, next: next, data: req.body});
                        res.render('mobile/login', { "message": "login.locked", "csrf": req.csrfToken() });
                    }
                    else {
                        plugins.callMethod("mobileloginSuccessful", {req: req, res: res, next: next, data: member});
                        preventBruteforce.reset("login", req.body.username);
                        countlyDb.collection('members').update({_id: member._id}, {$set: {last_login: Math.round(new Date().getTime() / 1000)}}, function() {});
                        res.render('mobile/key', { "key": member.api_key || -1 });
                    }
                }
                else {
                    plugins.callMethod("mobileloginFailed", {req: req, res: res, next: next, data: req.body});
                    preventBruteforce.fail("login", req.body.username);
                    res.render('mobile/login', { "message": "login.result", "csrf": req.csrfToken() });
                }
            });
        }
        else {
            res.render('mobile/login', { "message": "login.result", "csrf": req.csrfToken() });
        }
    });

    app.post(countlyConfig.path + '/dashboard/settings', function(req, res) {
        if (!req.session.uid) {
            res.end();
            return false;
        }

        var newAppOrder = req.body.app_sort_list;

        if (!newAppOrder || newAppOrder.length === 0) {
            res.end();
            return false;
        }

        countlyDb.collection('members').update({_id: countlyDb.ObjectID(req.session.uid + "")}, {'$set': {'appSortList': newAppOrder}}, {'upsert': true}, function() {
            res.end();
            return false;
        });
    });

    app.post(countlyConfig.path + '/apps/icon', function(req, res, next) {
        if (req.body.app_image_id) {
            req.body.app_id = req.body.app_image_id;
        }
        var params = paramsGenerator({req, res});
        validateCreate(params, 'global_upload', async function() {
            if (!req.session.uid && !req.body.app_image_id) {
                res.end();
                return false;
            }

            if (!req.files.app_image || !req.body.app_image_id) {
                res.end();
                return true;
            }

            req.body.app_image_id = common.sanitizeFilename(req.body.app_image_id);

            var tmp_path = req.files.app_image.path,
                target_path = __dirname + '/public/appimages/' + req.body.app_image_id + ".png",
                type = req.files.app_image.type;

            if (type !== "image/png" && type !== "image/gif" && type !== "image/jpeg") {
                fs.unlink(tmp_path, function() {});
                res.send(false);
                return true;
            }
            plugins.callMethod("iconUpload", {req: req, res: res, next: next, data: req.body});
            try {
                const icon = await jimp.Jimp.read(tmp_path);
                const buffer = await icon.cover({h: 72, w: 72}).getBuffer(jimp.JimpMime.png);
                countlyFs.saveData("appimages", target_path, buffer, {id: req.body.app_image_id + ".png", writeMode: "overwrite"}, function() {
                    res.send("appimages/" + req.body.app_image_id + ".png");
                    countlyDb.collection('apps').updateOne({_id: countlyDb.ObjectID(req.body.app_image_id)}, {'$set': {'has_image': true}}, function() {});
                });
            }
            catch (e) {
                console.log("Problem uploading app icon", e);
                res.status(400).send(false);
            }
            fs.unlink(tmp_path, function() {});
        });
    });

    app.post(countlyConfig.path + '/member/icon', async function(req, res, next) {

        var params = paramsGenerator({req, res});
        validateCreate(params, 'global_upload', async function() {
            if (!req.files.member_image || !req.body.member_image_id) {
                res.end();
                return true;
            }

            req.body.member_image_id = common.sanitizeFilename(req.body.member_image_id);
            var tmp_path = req.files.member_image.path,
                target_path = __dirname + '/public/memberimages/' + req.body.member_image_id + ".png",
                type = req.files.member_image.type;

            if (type !== "image/png" && type !== "image/gif" && type !== "image/jpeg") {
                fs.unlink(tmp_path, function() {});
                res.send(false);
                return true;
            }
            try {
                // This is to check that the uploaded image is a real image
                // If jimp cannot read it then it is not a real image
                const image = await jimp.Jimp.read(tmp_path);

                if (!image) {
                    fs.unlink(tmp_path, function() {});
                    res.status(400).send(false);
                    return true;
                }
            }
            catch (err) {
                console.log(err.stack);
                fs.unlink(tmp_path, function() {});
                res.status(400).send(false);
                return true;
            }
            plugins.callMethod("iconUpload", {req: req, res: res, next: next, data: req.body});
            try {
                const icon = await jimp.Jimp.read(tmp_path);
                const buffer = await icon.cover({h: 72, w: 72}).getBuffer(jimp.JimpMime.png);
                countlyFs.saveData("memberimages", target_path, buffer, {id: req.body.member_image_id + ".png", writeMode: "overwrite"}, function() {
                    countlyDb.collection('members').updateOne({_id: countlyDb.ObjectID(req.body.member_image_id + "")}, {'$set': {'member_image': "memberimages/" + req.body.member_image_id + ".png"}}, function() {
                        res.send("memberimages/" + req.body.member_image_id + ".png");
                    });
                });
            }
            catch (e) {
                console.log("Problem uploading member icon", e);
                res.status(400).send(false);
            }
            fs.unlink(tmp_path, function() {});
        });
    });

    app.post(countlyConfig.path + '/user/settings', function(req, res/*, next*/) {
        if (!req.session.uid) {
            res.end();
            return false;
        }
        membersUtility.settings(req, function(result, message) {
            if (result && req.body.member_image === "delete") {
                var target_path = __dirname + '/public/memberimages/' + req.session.uid + ".png";
                countlyFs.deleteFile("memberimages", target_path, {id: req.session.uid + ".png"}, function() { });
            }
            if (message) {
                res.send(message);
            }
            else {
                res.send(result);
            }
            return result;
        });
    });

    app.post(countlyConfig.path + '/user/settings/lang', function(req, res) {
        if (!req.session.uid) {
            res.end();
            return false;
        }

        var updatedUser = {};

        if (req.body.lang) {
            updatedUser.lang = req.body.lang;

            countlyDb.collection('members').update({"_id": countlyDb.ObjectID(req.session.uid + "")}, {'$set': updatedUser}, {safe: true}, function(err, member) {
                if (member && !err) {
                    res.send(true);
                }
                else {
                    res.send(false);
                }
            });
        }
        else {
            res.send(false);
            return false;
        }
    });

    app.post(countlyConfig.path + '/user/settings/active-app', function(req, res) {
        if (!req.session.uid) {
            res.end();
            return false;
        }

        var updatedUser = {};

        if (req.body.appId) {
            updatedUser.active_app_id = req.body.appId;

            countlyDb.collection('members').update({ "_id": countlyDb.ObjectID(req.session.uid + "") }, { '$set': updatedUser }, { safe: true }, function(err, member) {
                if (member && !err) {
                    res.send(true);
                }
                else {
                    res.send(false);
                }
            });
        }
        else {
            res.send(false);
            return false;
        }
    });

    app.post(countlyConfig.path + '/user/settings/column-order', function(req, res) {
        if (!req.session.uid) {
            return res.end();
        }

        if (req.body.columnOrderKey && (req.body.tableSortMap || req.body.reorderSortMap)) {
            let reorderSortMapKey = `columnOrder.${req.body.columnOrderKey}.reorderSortMap`;
            let tableSortMapKey = `columnOrder.${req.body.columnOrderKey}.tableSortMap`;

            if (!req.body.tableSortMap) {
                tableSortMapKey = undefined;
            }
            if (!req.body.reorderSortMap) {
                reorderSortMapKey = undefined;
            }

            countlyDb.collection('members').update({ "_id": countlyDb.ObjectID(req.session.uid + "") }, {
                '$set': {
                    [reorderSortMapKey]: req.body.reorderSortMap,
                    [tableSortMapKey]: req.body.tableSortMap
                }
            }, { safe: true, upsert: true }, function(err, member) {
                if (member && !err) {
                    return res.send(true);
                }
                return res.send(false);
            });
        }
        else {
            return res.send(false);
        }
    });

    app.post(countlyConfig.path + '/users/check/email', function(req, res) {
        if (!req.session.uid || !isGlobalAdmin(req) || !req.body.email) {
            res.send(false);
            return false;
        }
        else {
            membersUtility.checkEmail(req.body.email, function(result) {
                res.send(result);
            });
        }
    });

    app.post(countlyConfig.path + '/users/check/username', function(req, res) {
        if (!req.session.uid || !req.body.username) {
            res.send(false);
            return false;
        }
        else {
            membersUtility.checkUsername(req.body.username, function(result) {
                res.send(result);
            });
        }
    });

    app.get(countlyConfig.path + '/login/token/:token', function(req, res) {
        membersUtility.loginWithToken(req, function(member) {
            if (member) {
                var serverSideRendering = req.query.ssr || false;
                preventBruteforce.reset("login", member.username);
                var options = "";
                if (serverSideRendering) {
                    options += "ssr=" + serverSideRendering;
                }

                if (options && options.length) {
                    options = ("?").concat(options);
                }

                res.redirect(countlyConfig.path + '/dashboard' + options);
            }
            else {
                res.redirect(countlyConfig.path + '/login?message=login.result');
            }
        });
    });

    countlyDb.collection('apps').createIndex({"key": 1}, { unique: true }, function() {});
    countlyDb.collection('members').createIndex({"api_key": 1}, { unique: true }, function() {});
    countlyDb.collection('members').createIndex({ email: 1 }, { unique: true }, function() {});
    countlyDb.collection('jobs').createIndex({ finished: 1 }, function() {});
    countlyDb.collection('jobs').createIndex({ name: 1 }, function() {});
    countlyDb.collection('long_tasks').createIndex({ manually_create: 1, start: -1 }, function() {});

    const serverOptions = {
        port: countlyConfig.web.port,
        host: countlyConfig.web.host || ''
    };

    if (countlyConfig.web.ssl && countlyConfig.web.ssl.enabled) {
        const sslOptions = {
            key: fs.readFileSync(countlyConfig.web.ssl.key),
            cert: fs.readFileSync(countlyConfig.web.ssl.cert)
        };
        if (countlyConfig.web.ssl.ca) {
            sslOptions.ca = fs.readFileSync(countlyConfig.web.ssl.ca);
        }
        https.createServer(sslOptions, app).listen(serverOptions.port, serverOptions.host);
    }
    else {
        app.listen(serverOptions.port, serverOptions.host);
    }
});