parts/mgmt/users.js

/**
* This module is meant handling dashboard user accounts
* @module api/parts/mgmt/users
*/

/** @lends module:api/parts/mgmt/users */
var usersApi = {},
    common = require('./../../utils/common.js'),
    mail = require('./mail.js'),
    countlyConfig = require('./../../../frontend/express/config.js'),
    plugins = require('../../../plugins/pluginManager.js'),
    { hasAdminAccess, getUserApps, getAdminApps, hasReadRight } = require('./../../utils/rights.js');

const countlyCommon = require('../../lib/countly.common.js');
const log = require('../../utils/log.js')('core:mgmt.users');
const _ = require('lodash');

//for password checking when deleting own account. Could be removed after merging with next
var argon2 = require('argon2');
var crypto = require('crypto');
/**
* Get data about current user and output to browser
* @param {params} params - params object
* @returns {boolean} true
**/
usersApi.getCurrentUser = function(params) {
    delete params.member.password;

    common.returnOutput(params, params.member);
    return true;
};

/**
* Get data about specific user by user id, and outputs to browser
* @param {params} params - params object
* @returns {boolean} true if fetched data from db
**/
usersApi.getUserById = function(params) {
    if (!params.qstring.id) {
        common.returnMessage(params, 401, 'Missing user id parameter');
        return false;
    }
    common.db.collection('members').findOne({ _id: common.db.ObjectID(params.qstring.id) }, {
        password: 0,
        appSortList: 0,
        api_key: 0
    }, function(err, member) {

        if (!member || err) {
            common.returnOutput(params, {});
            return false;
        }

        var memberObj = {};

        member.global_admin = (member.global_admin === true);
        member.locked = (member.locked === true);
        member.created_at = member.created_at || 0;
        member.last_login = member.last_login || 0;
        member.is_current_user = (member.api_key === params.member.api_key);
        memberObj[member._id] = member;

        common.returnOutput(params, memberObj);
    });
    return true;
};

/**
* Get list of all users, for global admins only, and outputs to browser
* @param {params} params - params object
* @returns {boolean} true if fetched data from db
**/
usersApi.getAllUsers = function(params) {
    common.db.collection('members').find({}, {
        password: 0,
        appSortList: 0,
        api_key: 0,
    }).toArray(function(err, members) {

        if (!members || err) {
            common.returnOutput(params, {});
            return false;
        }



        common.db.collection('failed_logins').find({}).toArray(function(err2, failedLogins) {
            if (err2) {
                common.returnOutput(params, {});
                return false;
            }
            const bruteforceFails = plugins.getConfig("security").login_tries;
            const bruteforceWait = plugins.getConfig("security").login_wait;

            var membersObj = {};

            for (let i = 0; i < members.length; i++) {
                const result = failedLogins.find(x => (x._id === JSON.stringify(["login", members[i].username]))) || { fails: 0 };

                if (result.fails > 0 && result.fails % bruteforceFails === 0 && Math.floor(new Date().getTime() / 1000) < (((result.fails / bruteforceFails) * bruteforceWait) + result.lastFail)) {
                    members[i].blocked = true;
                }
                else {
                    members[i].blocked = false;
                }

                members[i].global_admin = (members[i].global_admin === true);
                members[i].locked = (members[i].locked === true);
                members[i].created_at = members[i].created_at || 0;
                members[i].last_login = members[i].last_login || 0;
                members[i].is_current_user = (members[i].api_key === params.member.api_key);
                membersObj[members[i]._id] = members[i];
            }

            common.returnOutput(params, membersObj);
        });


        return true;
    });
    return true;
};

/**
* Reset timeban for user and output result to browser
* @param {params} params - params object
* @returns {boolean} true if timeban reseted
**/
usersApi.resetTimeBan = function(params) {
    common.db.collection('failed_logins').remove({_id: JSON.stringify(["login", params.qstring.username])}, (err) => {
        if (err) {
            common.returnMessage(params, 500, 'Remove from collection failed.');
            return false;
        }

        common.returnOutput(params, true);
    });


    return true;
};

/**
* Create new dashboard user and output result to browser
* @param {params} params - params object
* @returns {boolean} true if user created
**/
usersApi.createUser = async function(params) {
    var argProps = {
            'full_name': {
                'required': true,
                'type': 'String'
            },
            'username': {
                'required': true,
                'type': 'String'
            },
            'password': {
                'required': true,
                'type': 'String',
                'min-length': plugins.getConfig("security").password_min,
                'has-number': plugins.getConfig("security").password_number,
                'has-upchar': plugins.getConfig("security").password_char,
                'has-special': plugins.getConfig("security").password_symbol
            },
            'email': {
                'required': true,
                'type': 'String'
            },
            'lang': {
                'required': false,
                'type': 'String'
            },
            'permission': {
                'required': false,
                'type': 'Object'
            },
            'admin_of': {
                'required': false,
                'type': 'Array'
            },
            'user_of': {
                'required': false,
                'type': 'Array'
            },
            'global_admin': {
                'required': false,
                'type': 'Boolean'
            }
        },
        newMember = {};

    await depCheck(params);
    var createUserValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(newMember = createUserValidation.obj)) {
        common.returnMessage(params, 400, createUserValidation.errors);
        return false;
    }

    //adding backwards compatability
    newMember.permission = newMember.permission || {};

    if (!newMember.permission._) {
        newMember.permission._ = {
            a: newMember.admin_of || [],
            u: newMember.user_of ? [newMember.user_of] : []
        };
    }

    if (newMember.admin_of) {
        if (Array.isArray(newMember.admin_of) && newMember.admin_of.length) {
            newMember.permission.c = newMember.permission.c || {};
            newMember.permission.r = newMember.permission.r || {};
            newMember.permission.u = newMember.permission.u || {};
            newMember.permission.d = newMember.permission.d || {};
            for (let i = 0; i < newMember.admin_of.length; i++) {
                newMember.permission.c[newMember.admin_of[i]] = newMember.permission.c[newMember.admin_of[i]] || {all: true, allowed: {}};
                newMember.permission.r[newMember.admin_of[i]] = newMember.permission.r[newMember.admin_of[i]] || {all: true, allowed: {}};
                newMember.permission.u[newMember.admin_of[i]] = newMember.permission.u[newMember.admin_of[i]] || {all: true, allowed: {}};
                newMember.permission.d[newMember.admin_of[i]] = newMember.permission.d[newMember.admin_of[i]] || {all: true, allowed: {}};
            }
        }
        delete newMember.admin_of;
    }

    if (newMember.user_of) {
        if (Array.isArray(newMember.user_of) && newMember.user_of.length) {
            newMember.permission.r = newMember.permission.r || {};
            for (let i = 0; i < newMember.user_of.length; i++) {
                newMember.permission.r[newMember.user_of[i]] = newMember.permission.r[newMember.user_of[i]] || {all: true, allowed: {}};
            }
        }
        delete newMember.user_of;
    }

    common.db.collection('members').findOne({ $or: [{ email: newMember.email }, { username: newMember.username }] }, function(err, member) {
        if (member || err) {
            common.returnMessage(params, 400, ['Email or username already exists']);
            return false;
        }
        else {
            createUser();
            return true;
        }
    });

    /**
    * Creates user document with hashed password
    **/
    async function createUser() {
        //var passwordNoHash = newMember.password;
        var secret = countlyConfig.passwordSecret || "";
        var accessTypes = ["c", "r", "u", "d"];
        newMember.password = await common.argon2Hash(newMember.password + secret);
        newMember.password_changed = 0;
        newMember.created_at = Math.floor(((new Date()).getTime()) / 1000); //TODO: Check if UTC
        for (var type in accessTypes) {
            if (typeof newMember.permission[accessTypes[type]] === "undefined") {
                newMember.permission[accessTypes[type]] = {};
            }
        }
        newMember.locked = false;
        newMember.username = newMember.username.trim();
        newMember.email = newMember.email.trim().toString().toLowerCase();
        crypto.randomBytes(48, function(errorBuff, buffer) {
            newMember.api_key = common.md5Hash(buffer.toString('hex') + Math.random());
            common.db.collection('members').insert(newMember, function(err, member) {
                if (!err && member && member.ops) {
                    member = member.ops;
                }
                else {
                    console.log('Error creating user: ', err);
                }
                if (member && member.length && !err) {
                    var timestamp = Math.round(new Date().getTime() / 1000),
                        prid = sha512Hash(member[0].username + member[0].full_name, timestamp);
                    common.db.collection('password_reset').insert({"prid": prid, "user_id": member[0]._id, "timestamp": timestamp, "newInvite": true}, {safe: true}, function() {
                        mail.sendToNewMemberLink(member[0], prid);
                    });

                    plugins.dispatch("/i/users/create", {
                        params: params,
                        data: member[0]
                    });
                    delete member[0].password;

                    common.returnOutput(params, member[0]);
                }
                else {
                    common.returnMessage(params, 400, ['Error creating user']);
                }
            });
        });
    }

    return true;
};

/**
    * Removes all active sessions for user
    * @param {string} userId - id of the user for which to remove sessions
    **/
function killAllSessionForUser(userId) {
    common.db.collection('sessions_').find({"session": { $regex: userId }}).toArray(function(err, sessions) {

        var delete_us = [];
        sessions = sessions || [];
        for (let i = 0; i < sessions.length; i++) {
            var parsed_data = "";
            try {
                parsed_data = JSON.parse(sessions[i].session);
            }
            catch (SyntaxError) {
                console.log('Parse ' + sessions[i].session + ' JSON failed');
            }
            if (parsed_data && parsed_data.uid === userId) {
                delete_us.push(sessions[i]._id);
            }
        }
        if (delete_us.length > 0) {
            common.db.collection('sessions_').remove({ '_id': { $in: delete_us } });
        }
    });
    //delete auth tokens
    common.db.collection('auth_tokens').remove({
        'owner': userId,
        'purpose': "LoggedInAuth"
    });

}

usersApi.updateHomeSettings = function(params) {
    params = params || {};
    params.qstring = params.qstring || {};
    if (!params.member) {
        common.returnMessage(params, 400, 'Could not get member');
    }
    else if (!params.qstring.homeSettings) {
        common.returnMessage(params, 400, '`homeSettings` should contain stringified object with home settings');
    }
    else if (!params.qstring.app_id) {
        common.returnMessage(params, 400, '`app_id` must be passed');
    }
    else {
        try {
            params.qstring.homeSettings = JSON.parse(params.qstring.homeSettings);
        }
        catch (SyntaxError) {
            params.qstring.homeSettings = {};
        }
        var updateObj = {};
        updateObj["homeSettings." + params.qstring.app_id] = params.qstring.homeSettings;
        common.db.collection('members').update({_id: common.db.ObjectID(params.member._id + "")}, {"$set": updateObj}, function(err3 /* , res1*/) {
            if (err3) {
                console.log(err3);
                common.returnMessage(params, 400, 'Mongo error');
            }
            else {
                common.returnMessage(params, 200, 'Success');
            }
        });
    }
};


/**
 * Checks the permission dependencies of features for each app based on the enabled features, enabling the required permission dependencies if necessary.
 * @param {object} params - params object.
*/
async function depCheck(params) {
    var features = ["core", "events" /* , "global_configurations", "global_applications", "global_users", "global_jobs", "global_upload" */];
    var featuresPermissionDependency = {};
    plugins.dispatch("/permissions/features", { params: params, features: features, featuresPermissionDependency: featuresPermissionDependency }, function() {
        //read permission check, making sure that read is present in every dependency array if any other permission is given
        for (var feature in featuresPermissionDependency) {
            var perms = Object.keys(featuresPermissionDependency[feature]);
            for (var perm of perms) {
                var permFeatures = Object.keys(featuresPermissionDependency[feature][perm]);
                for (var permFeature of permFeatures) {
                    var targetAr = featuresPermissionDependency[feature][perm][permFeature];
                    if (targetAr.length && targetAr.indexOf('r') === -1) {
                        featuresPermissionDependency[feature][perm][permFeature].push('r');
                    }
                }
            }
        }
        //check permission dependency for each app
        const crudTypes = ["c", "r", "u", "d"];
        crudTypes.forEach(function(crudType) {
            let apps = params.qstring.args && params.qstring.args.permission && params.qstring.args.permission[crudType] || {};
            Object.keys(apps).forEach(function(app) {
                let feats = apps[app].allowed || {};
                Object.keys(feats).forEach(function(feat) {
                    let featEnabled = feats[feat];
                    //check if feature is enabled and if it has any dependency
                    if (featEnabled && featuresPermissionDependency[feat] && featuresPermissionDependency[feat][crudType]) {
                        let depFeats = featuresPermissionDependency[feat][crudType];
                        Object.keys(depFeats).forEach(function(depFeat) {
                            depFeats[depFeat].forEach(function(crudPerm) {
                                //add dependency permissions
                                params.qstring.args.permission[crudPerm][app].allowed[depFeat] = true;
                            });
                        });
                    }
                });
            });
        });
    });
}

/**
* Updates dashboard user's data and output result to browser
* @param {params} params - params object
* @returns {boolean} true if user was updated
**/
usersApi.updateUser = async function(params) {
    var argProps = {
            'user_id': {
                'required': true,
                'type': 'String',
                'exclude-from-ret-obj': true
            },
            'full_name': {
                'required': false,
                'type': 'String'
            },
            'username': {
                'required': false,
                'type': 'String'
            },
            'password': {
                'required': false,
                'type': 'String',
                'min-length': plugins.getConfig("security").password_min,
                'has-number': plugins.getConfig("security").password_number,
                'has-upchar': plugins.getConfig("security").password_char,
                'has-special': plugins.getConfig("security").password_symbol
            },
            'email': {
                'required': false,
                'type': 'String'
            },
            'lang': {
                'required': false,
                'type': 'String'
            },
            'admin_of': {
                'required': false,
                'type': 'Array'
            },
            'user_of': {
                'required': false,
                'type': 'Array'
            },
            'global_admin': {
                'required': false,
                'type': 'Boolean'
            },
            'locked': {
                'required': false,
                'type': 'Boolean'
            },
            'send_notification': {
                'required': false,
                'type': 'Boolean',
                'exclude-from-ret-obj': true
            },
            'permission': {
                'required': false,
                'type': 'Object'
            },
            'subscribe_newsletter': {
                'required': false,
                'type': 'Boolean'
            },
        },
        updatedMember = {},
        passwordNoHash = "";

    await depCheck(params);
    var updateUserValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(updatedMember = updateUserValidation.obj)) {
        common.returnMessage(params, 400, updateUserValidation.errors);
        return false;
    }

    if (updatedMember.password) {
        var secret = countlyConfig.passwordSecret || "";
        passwordNoHash = updatedMember.password;
        updatedMember.password = await common.argon2Hash(updatedMember.password + secret);
        if (params.member._id !== params.qstring.args.user_id) {
            updatedMember.password_changed = 0;
        }
    }
    if (updatedMember.username) {
        updatedMember.username = updatedMember.username.trim();
    }
    if (updatedMember.email) {
        updatedMember.email = updatedMember.email.trim().toString().toLowerCase();
    }

    if (params.qstring.args.member_image && params.qstring.args.member_image === 'delete') {
        updatedMember.member_image = "";
    }

    //adding backwards compatability
    if (updatedMember.admin_of) {
        if (Array.isArray(updatedMember.admin_of) && updatedMember.admin_of.length) {
            updatedMember.permission = updatedMember.permission || {};
            if (!updatedMember.permission._) {
                updatedMember.permission._ = {};
            }
            updatedMember.permission._.a = updatedMember.admin_of;
            updatedMember.permission.c = updatedMember.permission.c || {};
            updatedMember.permission.r = updatedMember.permission.r || {};
            updatedMember.permission.u = updatedMember.permission.u || {};
            updatedMember.permission.d = updatedMember.permission.d || {};
            for (let i = 0; i < updatedMember.admin_of.length; i++) {
                updatedMember.permission.c[updatedMember.admin_of[i]] = updatedMember.permission.c[updatedMember.admin_of[i]] || {all: true, allowed: {}};
                updatedMember.permission.r[updatedMember.admin_of[i]] = updatedMember.permission.r[updatedMember.admin_of[i]] || {all: true, allowed: {}};
                updatedMember.permission.u[updatedMember.admin_of[i]] = updatedMember.permission.u[updatedMember.admin_of[i]] || {all: true, allowed: {}};
                updatedMember.permission.d[updatedMember.admin_of[i]] = updatedMember.permission.d[updatedMember.admin_of[i]] || {all: true, allowed: {}};
            }
        }
        delete updatedMember.admin_of;
    }

    if (updatedMember.user_of) {
        if (Array.isArray(updatedMember.user_of) && updatedMember.user_of.length) {
            updatedMember.permission = updatedMember.permission || {};
            if (!updatedMember.permission._) {
                updatedMember.permission._ = {};
            }
            updatedMember.permission._.u = [updatedMember.user_of];
            updatedMember.permission.r = updatedMember.permission.r || {};
            for (let i = 0; i < updatedMember.user_of.length; i++) {
                updatedMember.permission.r[updatedMember.user_of[i]] = updatedMember.permission.r[updatedMember.user_of[i]] || {all: true, allowed: {}};
            }
        }
        delete updatedMember.user_of;
    }


    common.db.collection('members').findOne({ '_id': common.db.ObjectID(params.qstring.args.user_id) }, function(err, memberBefore) {
        common.db.collection('members').update({ '_id': common.db.ObjectID(params.qstring.args.user_id) }, { '$set': updatedMember }, { safe: true }, function(errUpdatingUser) {
            if (errUpdatingUser) {
                common.returnMessage(params, 500, 'Error updating user. Please check api logs.');
                return false;
            }
            common.db.collection('members').findOne({ '_id': common.db.ObjectID(params.qstring.args.user_id) }, function(err2, member) {
                if (member && !err2) {
                    updatedMember._id = params.qstring.args.user_id;
                    plugins.dispatch("/i/users/update", {
                        params: params,
                        data: updatedMember,
                        member: memberBefore
                    });
                    if (params.qstring.args.send_notification && passwordNoHash) {
                        mail.sendToUpdatedMember(member, passwordNoHash);
                    }
                    if (updatedMember.password && params.member._id + "" !== updatedMember._id + "") {
                        killAllSessionForUser(updatedMember._id);
                    }

                    if (updatedMember.email) {
                        //remove password reset e-mail
                        common.db.collection('password_reset').remove({ "user_id": common.db.ObjectID(updatedMember._id + "")}, {multi: true}, function() {});
                    }
                    common.returnMessage(params, 200, 'Success');
                }
                else {
                    common.returnMessage(params, 500, 'Error updating user');
                }
            });
        });
    });

    return true;
};

/**
* Deletes dashboard user and output result to browser
* @param {params} params - params object
* @returns {boolean} true if user was deleted
**/
usersApi.deleteUser = async function(params) {
    var argProps = {
            'user_ids': {
                'required': true,
                'type': 'Array'
            }
        },
        fails = 0,
        userIds = [];

    var deleteUserValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(deleteUserValidation.obj && (userIds = deleteUserValidation.obj.user_ids))) {
        common.returnMessage(params, 400, 'Error: ' + deleteUserValidation.errors);
        return false;
    }

    for (var i = 0; i < userIds.length; i++) {
        //a user can't delete his own account
        //string id can also exist due to cognito, so no check for 24 chars length
        if (!userIds[i] || userIds[i] === params.member._id + "") {
            continue;
        }
        else {
            const user = await common.db.collection('members').findOne({ '_id': common.db.ObjectID(userIds[i]) });
            const promisifiedDispatch = function(prms, data) {
                return new Promise((resolve, reject) => {
                    plugins.dispatch("/i/users/delete", {
                        params: prms,
                        data,
                    }, async(__, otherPluginResults) => {
                        const rejectReasons = otherPluginResults.reduce((acc, result) => {
                            if (result.status === "rejected") {
                                acc.push((result.reason && result.reason.message) || '');
                            }

                            return acc;
                        }, []);

                        if (rejectReasons.length > 0) {
                            log.e("User " + userIds[i] + " deletion failed\n%j", rejectReasons.join("\n"));
                            fails += 1;
                            reject(false);
                        }
                        else {
                            await common.db.collection('auth_tokens').remove({ 'owner': userIds[i] });
                            await usersApi.deleteUserNotes({ member: { _id: userIds[i] } });
                            await common.db.collection('members').remove({_id: common.db.ObjectID(userIds[i])});
                            deleteUserPresets(userIds[i]);
                            resolve(true);
                        }
                    });
                });
            };

            await promisifiedDispatch(params, user);
        }
    }

    if (fails === 0) {
        common.returnMessage(params, 200, 'Success');
        return true;
    }
    else if (fails === userIds.length) {
        common.returnMessage(params, 500, 'User deletion failed, please see logs for more detail');
        return false;
    }
    else {
        common.returnMessage(params, 200, 'Some users cannot be deleted, please see logs for more detail');
        return true;
    }
};

// created functions below are for account deletion. when merging together with next should remove  and include from members utility !!!!!! 

/**
 * 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 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);
}

/**
* 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');
}

/**
* 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) {
    common.db.collection('members').update({ _id: id}, { $set: { password: password}});
}

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


/**
 * 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) {
    common.db.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!");
        }
    });
}

/**
 * Delete user's date presets
 * @param {string} memberId | User id
 */
function deleteUserPresets(memberId) {
    common.db.collection("date_presets").remove({owner: memberId + ""}, function() {
        //handle errors
    });
}

// END of reused functions 


usersApi.deleteOwnAccount = function(params) {
    if (params.qstring.password && params.qstring.password !== "") {
        verifyMemberArgon2Hash(params.member.email, params.qstring.password, (err, member) => {
            const dispatchDeleteCallback = async function(__, otherPluginResults) {
                const rejectReasons = otherPluginResults.reduce((acc, result) => {
                    if (result.status === "rejected") {
                        acc.push((result.reason && result.reason.message) || '');
                    }

                    return acc;
                }, []);

                if (rejectReasons.length > 0) {
                    log.e("User deletion failed\n%j", rejectReasons.join("\n"));
                    common.returnMessage(params, 500, { errorMessage: "User deletion failed. Failed to delete some data related to this user." });
                }
                else {
                    try {
                        await common.db.collection('members').remove({_id: common.db.ObjectID(member._id + "")});
                        killAllSessionForUser(member._id);
                        deleteUserPresets(member._id);
                        common.returnMessage(params, 200, 'Success');
                    }
                    catch (err1) {
                        console.log(err1);
                        common.returnMessage(params, 400, 'Mongo error');
                    }
                }
            };

            if (member) {
                if (member.global_admin) {
                    common.db.collection('members').count({'global_admin': true}, function(err2, count) {
                        if (err2) {
                            console.log(err2);
                            common.returnMessage(params, 400, 'Mongo error');
                        }
                        if (count < 2) {
                            common.returnMessage(params, 400, 'global admin limit');
                        }
                        else {
                            plugins.dispatch("/i/users/delete", {
                                params: params,
                                data: member
                            }, dispatchDeleteCallback);
                        }
                    });
                }
                else {
                    plugins.dispatch("/i/users/delete", {
                        params: params,
                        data: member
                    }, dispatchDeleteCallback);
                }
            }
            else {
                common.returnMessage(params, 400, 'password not valid');
            }
        });
    }
    else {
        common.returnMessage(params, 400, 'password mandatory');
    }
    return true;

};

module.exports = usersApi;
/**
 * Check update or delete note permission.
 *  @param {params} params - params object 
 *  @returns {boolean} true
 */
usersApi.checkNoteEditPermission = async function(params) {
    let noteId = params.qstring.note_id;
    /**
     * get note
     *  @returns {object} promise
     */
    const checkPermission = () => {
        return new Promise((resolve, reject) => {
            common.db.collection('notes').findOne(
                { '_id': common.db.ObjectID(noteId)},
                function(error, note) {
                    if (error) {
                        return reject(false);
                    }
                    const globalAdmin = params.member.global_admin;
                    const isAppAdmin = hasAdminAccess(params.member, params.qstring.app_id);
                    const noteOwner = (note.owner + '' === params.member._id + '');
                    return resolve(noteOwner || (isAppAdmin && note.noteType === 'public') || (globalAdmin && note.noteType === 'public'));
                }
            );
        });
    };
    if (params.qstring.args && params.qstring.args._id) {
        noteId = params.qstring.args._id;
    }
    const permit = await checkPermission();
    return permit;
};

/**
* Create or update note
* @param {params} params - params object
* @returns {boolean} true
**/
usersApi.saveNote = async function(params) {
    var argProps = {
        'note': {
            'required': true,
            'type': 'String'
        },
        'ts': {
            'required': true,
            'type': ''
        },
        'noteType': {
            'required': true,
            'type': 'String',
        },
        'color': {
            'required': true,
            'type': 'String'
        },
        'category': {
            'required': false,
            'type': 'Boolean'
        }
    };
    const args = params.qstring.args;
    const noteValidation = common.validateArgs(args, argProps, true);
    if (noteValidation) {
        const note = {
            app_id: args.app_id,
            note: args.note,
            ts: args.ts,
            noteType: args.noteType,
            emails: args.emails || [],
            color: args.color,
            category: args.category,
            owner: params.member._id + "",
            created_at: new Date().getTime(),
            updated_at: new Date().getTime()
        };

        if (args._id) {
            const editPermission = await usersApi.checkNoteEditPermission(params);
            if (!editPermission) {
                common.returnMessage(params, 403, 'Not allow to edit note');
            }
            else {
                delete note.created_at;
                delete note.owner;
                common.db.collection('notes').update({_id: common.db.ObjectID(args._id)}, {$set: note }, (err) => {
                    if (err) {
                        common.returnMessage(params, 503, 'Save note failed');
                    }
                    else {
                        common.returnMessage(params, 200, 'Success');
                    }
                });
            }
        }
        else {
            common.db.collection('notes').find({ "app_id": args.app_id }).sort({ "created_at": -1 }).limit(1).project({ "indicator": 1 }).toArray(function(err, res) {
                if (err) {
                    common.returnMessage(params, 503, 'Save note failed');
                }
                else {
                    if (res && res.length) {
                        note.indicator = countlyCommon.stringIncrement(res[0].indicator);
                    }
                    else {
                        note.indicator = "A";
                    }
                    common.db.collection('notes').insert(note, (_err) => {
                        if (_err) {
                            common.returnMessage(params, 503, 'Insert Note failed.');
                        }
                        common.returnMessage(params, 200, 'Success');
                    });
                }
            });
        }
    }
    else {
        common.returnMessage(params, 403, 'add notes failed');
    }
    return true;
};

/**
* Delete Note
* @param {params} params - params object
* @returns {boolean} true
**/
usersApi.deleteNote = async function(params) {

    const editPermission = await usersApi.checkNoteEditPermission(params);
    if (!editPermission) {
        common.returnMessage(params, 403, 'Not allow to delete this note');
    }
    else {
        const noteId = params.qstring.note_id;
        const query = {
            '_id': common.db.ObjectID(noteId),
        };
        common.db.collection('notes').remove(query, function(error) {
            if (error) {
                common.returnMessage(params, 503, "Error deleting note");
            }
            common.returnMessage(params, 200, "Success");
        });
    }
    return true;
};

/**
* Delete deleted user note
* @param {params} params - params object
* @returns {boolean} true
**/
usersApi.deleteUserNotes = async function(params) {
    const query = {
        'owner': params.member._id + "",
    };
    common.db.collection('notes').remove(query, function(error) {
        if (error) {
            log.e("Error deleting removed users' note");
        }
    });
    return true;
};
/**
 * fetch apps id for those user can access;
* @param {params} params - params object
* @returns {array} app id array
 */
usersApi.fetchUserAppIds = async function(params) {
    const query = {};
    const appIds = [];
    const adminApps = getAdminApps(params.member);
    const userApps = getUserApps(params.member);
    if (!params.member.global_admin) {
        if (adminApps.length > 0) {
            for (let i = 0; i < adminApps.length ;i++) {
                if (adminApps[i] === "") {
                    continue;
                }
                appIds.push(adminApps[i]);
            }
        }
        if (userApps.length > 0) {
            for (let i = 0; i < userApps.length ;i++) {
                appIds.push(userApps[i]);
            }
        }
    }
    if (appIds.length > 0) {
        query._id = {$in: appIds};
    }
    return appIds;
};
/**
* fetch Notes
* @param {params} params - params object
* @returns {boolean} true
**/
usersApi.fetchNotes = async function(params) {
    countlyCommon.getPeriodObj(params);
    // const timestampRange = countlyCommon.getTimestampRangeQuery(params, false);

    let appIds = [];
    let filteredAppIds = [];
    try {
        appIds = JSON.parse(params.qstring.notes_apps);
        if (!appIds || appIds.length === 0) {
            appIds = await usersApi.fetchUserAppIds(params);
        }
        filteredAppIds = appIds.filter((appId) => {
            if (hasAdminAccess(params.member, appId) || hasReadRight('core', appId, params.member)) {
                return true;
            }
            return false;
        });
    }
    catch (e) {
        log.e(' got error while paring query notes appIds request', e);
    }
    const query = {
        'app_id': {$in: filteredAppIds},
        'ts': {$gte: params.qstring.period[0], $lte: params.qstring.period[1]},
        $or: [
            {'owner': params.member._id + ""},
            {'noteType': 'public'},
            {'emails': {'$in': [params.member.email] }},
        ],
    };

    if (params.qstring.category) {
        query.category = {$in: JSON.parse(params.qstring.category)};
    }

    if (params.qstring.note_type) {
        query.noteType = params.qstring.note_type;
    }

    let skip = params.qstring.iDisplayStart || 0;
    let limit = params.qstring.iDisplayLength || 5000;
    const sEcho = params.qstring.sEcho || 1;
    const orderDirection = {'asc': 1, 'desc': -1};
    const orderByKey = {'3': 'noteType', '2': 'ts'};
    let sortBy = {};
    if (params.qstring.sSearch) {
        /*eslint-disable */
        query.note = {$regex: new RegExp(params.qstring.sSearch, "i")};
        /*eslint-enable */
    }
    if (params.qstring.iSortCol_0 && params.qstring.iSortCol_0 !== '0') {
        Object.assign(sortBy, { [orderByKey[params.qstring.iSortCol_0]]: orderDirection[params.qstring.sSortDir_0]});
    }
    try {
        skip = parseInt(skip, 10);
        limit = parseInt(limit, 10);
    }
    catch (e) {
        log.e(' got error while paring query notes request', e);
    }
    let count = 0;

    common.db.collection('notes').count(query, function(error, noteCount) {
        if (!error && noteCount) {
            count = noteCount;
            common.db.collection('notes').find(query)
                .sort(sortBy)
                .skip(skip)
                .limit(limit)
                .toArray(function(err1, notes) {
                    if (err1) {
                        return common.returnMessage(params, 503, 'fatch notes failed');
                    }
                    let ownerIds = _.uniqBy(notes, 'owner');
                    common.db.collection('members')
                        .find({
                            _id: {
                                $in: ownerIds.map((n) => {
                                    return common.db.ObjectID(n.owner);
                                })
                            }
                        },
                        {full_name: 1})
                        .toArray(function(err2, members) {
                            if (err2) {
                                return common.returnMessage(params, 503, 'fatch countly members for notes failed');
                            }
                            notes = notes.map((n) => {
                                n.owner_name = 'Anonymous';
                                members.forEach((m) => {
                                    if (n.owner === m._id + "") {
                                        n.owner_name = m.full_name;
                                    }
                                });
                                return n;
                            });
                            common.returnOutput(params, {aaData: notes, iTotalDisplayRecords: count, iTotalRecords: count, sEcho});
                        });
                });
        }
        else {
            common.returnOutput(params, {aaData: [], iTotalDisplayRecords: 0, iTotalRecords: 0, sEcho});
        }
    });
    return true;
};

usersApi.ackNotification = function(params) {
    common.db.collection('members').updateOne({"_id": common.db.ObjectID(params.member._id)}, {$set: {['notes.' + params.qstring.path]: true}}, err => {
        if (err) {
            log.e('Error while acking member notification', err);
            return common.returnMessage(params, 500, 'Unknown error');
        }
        else {
            common.returnOutput(params, {['notes.' + params.qstring.path]: true});
        }
    });
};

module.exports = usersApi;