Source: service/UsersService.js

'use strict';

/**
 * Plain user functionality in service methods.
 * @module service/users
 * @license MIT
 * @author Kai KRETSCHMANN <kai@kretschmann.consulting>
 */

require('datejs');
const jsonpatch = require('json-patch');
const LoginModel = require('../models/login.js');
const DomainModel = require('../models/domain.js');
const LogauthModel = require('../models/logauth.js');
const bcrypt = require('bcrypt');

const saltRounds = 10;

const log4js = require('log4js');
const logger = log4js.getLogger();
logger.error(`env LogLevel is ${process.env.LOGLEVEL}!`);
logger.level = process.env.LOGLEVEL || /* istanbul ignore next */ 'WARN'; // LCOV_EXCL_LINE
logger.error(`active LogLevel ${logger.level}`);

/**
 * Helper function getUserObject.
 * @method getUserObject
 * @private
 * @param {string} username The textual user name
 * @returns {Promise} user object or error
 */
function getUserObject (username) {
  return new Promise(function (resolve, reject) {
    LoginModel.find({
      name: username
    })
      .then(itemFound => {
        if (itemFound.length === 1) {
          logger.info('Found user');
          resolve(itemFound[0]);
        } else {
          reject(Error('Not found'));
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`getUser failed: ${err}`);
        reject(err);
      });
  });
}

/**
 * List all registered blacklisted domain.
 * @method listDomains
 * @public
 * @returns {Promise} array of domain entries or error
 **/
exports.listDomains = function () {
  return new Promise(function (resolve, reject) {
    logger.debug('In list domains service');

    DomainModel.find({})
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Add blacklisted domain.
 * @method addDomain
 * @public
 * @param {string} domainname - Domain name
 * @returns {Promise} array with domain added or error
 **/
exports.addDomain = function (domainname) {
  return new Promise(function (resolve, reject) {
    logger.debug('In add domain service');

    const tsnow = new Date();
    const d = new DomainModel({
      name: domainname,
      tscreated: tsnow
    });

    d.save()
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Delete one registered blacklisted domain.
 * @method deleteDomain
 * @public
 * @param {string} domainname - Domain name
 * @returns {Promise} OK or error
 **/
exports.deleteDomain = function (domainname) {
  return new Promise(function (resolve, reject) {
    logger.debug('In delete domain service');

    DomainModel.deleteOne({
      name: domainname
    })
      .then(item => {
        if (item.deletedCount !== 1) {
          logger.error('not found, not deleted');
          const e = new Error('not found');
          e.code = 404;
          e.msg = 'not found';
          reject(e);
        } else {
          resolve('OK');
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Check for registered blacklisted domain.
 * @method checkDomain
 * @public
 * @param {string} domainname - Domain name
 * @returns {Promise} domain object or null or error
 **/
exports.checkDomain = function (domainname) {
  return new Promise(function (resolve, reject) {
    logger.debug('In check domain service');

    DomainModel.find({
      name: domainname
    })
      .then(item => {
        if (item.length === 1) {
          resolve(item[0]);
        } else {
          resolve(null);
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * List all registered users.
 * @method listUsers
 * @public
 * @returns {Promise} array of user entries or error
 **/
exports.listUsers = function () {
  return new Promise(function (resolve, reject) {
    logger.debug('In list users service');

    LoginModel.find({}, {
      role: 1,
      status: 1,
      name: 1,
      email: 1,
      tscreated: 1
    })
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Update user entry with new status
 * @method putUserStatus
 * @public
 * @param {string} id - user id in database
 * @param {string} newStatus - user status to change to
 * @returns {Promise} updated user object or error
 **/
exports.putUserStatus = function (id, newStatus) {
  return new Promise(function (resolve, reject) {
    logger.debug(`In put user status service ${id} ${newStatus}`);

    LoginModel.findOneAndUpdate({
      _id: id
    }, {
      status: newStatus
    })
      .then(item => {
        logger.info(item._id);
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Run query over users.
 * @method checkGetUserStatus
 * @private
 * @param {object} resolve
 * @param {object} reject
 * @param {object} query
 */
function checkGetUserStatus (resolve, reject, query) {
  LoginModel.find(query, {
    role: 1,
    status: 1,
    name: 1,
    email: 1,
    tscreated: 1
  })
    .then(item => {
      if (item.length > 0) {
        resolve(item[0]);
      } else {
        const e = new Error('not found');
        e.code = 404;
        e.msg = 'not found';
        reject(e);
      }
    })
    .catch(err => /* istanbul ignore next */ {
      logger.error(`Not OK: ${err}`);
      reject(err);
    });
}

/**
 * Show userdata of given id.
 * @method listUser
 * @public
 * @param {string} id - user id in database
 * @returns {Promise} object with user data or error
 **/
exports.listUser = function (idUser) {
  return new Promise(function (resolve, reject) {
    logger.debug('In list user service');

    checkGetUserStatus(resolve, reject, {
      _id: idUser
    });
  });
};

/**
 * Show userdata of given name.
 * @method getUser
 * @public
 * @param {string} name - user name in database
 * @returns {Promise} object with user data or error
 **/
exports.getUser = function (name) {
  return new Promise(function (resolve, reject) {
    logger.debug('In get user service');

    checkGetUserStatus(resolve, reject, { name });
  });
};

/**
 * Change user data by json patch request.
 * @method patchUser
 * @public
 * @param {string} idUser - id of user in database
 * @param {string} jpatch - json patch data
 * @returns {Promise} object of updated user data or error
 **/
exports.patchUser = function (idUser, jpatch) {
  return new Promise(function (resolve, reject) {
    logger.debug('In patch user service');

    LoginModel.find({
      _id: idUser
    }, {
      role: 1,
      status: 1,
      name: 1,
      email: 1,
      tscreated: 1
    })
      .then(item => {
        if (item.length > 0) {
          const userDoc = item[0];
          const patchedUser = jsonpatch.apply(userDoc, jpatch);
          patchedUser.save();
          resolve(patchedUser);
        } else {
          const e = new Error('not found');
          e.code = 404;
          e.msg = 'not found';
          reject(e);
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Delete one user from database.
 * @method deleteUser
 * @public
 * @param {string} idUser - the id of the user object
 * @returns {Promise} object of user entry for the very last time
 **/
exports.deleteUser = function (idUser) {
  return new Promise(function (resolve, reject) {
    logger.debug('In delete user service');
    let e;
    let userDoc;

    LoginModel.find({
      _id: idUser
    }, {
      role: 1,
      status: 1,
      name: 1,
      email: 1,
      tscreated: 1
    })
      .then(item => {
        switch (item.length) {
          case 0:
            e = new Error('not found');
            e.code = 404;
            e.msg = 'not found';
            reject(e);
            break;
          case 1:
            logger.info('Item found');
            userDoc = item[0];
            userDoc.status = 'deleted';
            userDoc.save()
              .then(itemSave => {
                logger.info('Updated item saved');
                resolve(userDoc);
              })
              .catch(err => /* istanbul ignore next */ {
                logger.error(`Not OK: ${err}`);
                reject(err);
              });
            break;
          default:
            e = new Error(`More than one user found ${item.length}`);
            logger.error(`Not OK: ${e}`);
            reject(e);
            break;
        } // switch
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Creates an user entry in database.
 * @method createUser
 * @public
 * @param {string} user - json user object
 * @returns {Promise} object of created user
 **/
exports.createUser = function (user) {
  return new Promise(function (resolve, reject) {
    logger.debug('In create user service');

    const domain = user.email.split('@')[1];
    logger.info(`Check domain ${domain}`);
    DomainModel.find({
      name: domain
    })
      .then(item => {
        if (item.length === 1) {
          logger.error(`Domain black listed: ${domain}`);
          const e = new Error('not that domain');
          e.code = 400;
          e.msg = 'not that domain';
          reject(e);
        } else {
          const tsnow = new Date();
          bcrypt.hash(user.password, saltRounds, function (err, hash) {
            if (err) {
              reject(err);
            }
            const u = new LoginModel({
              name: user.username,
              passwd: hash,
              email: user.email,
              role: 'user',
              status: 'register',
              tscreated: tsnow
            });

            u.save()
              .then(_item2 => {
                resolve(u);
              })
              .catch(errSave => /* istanbul ignore next */ {
                logger.error(`Not OK: ${err}`);
                reject(errSave);
              });
          });
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Check login data for user.
 * @method checkUser
 * @public
 * @param {string} name - user name
 * @param {string} passwd - clear text password from web form
 * @returns {Promise} object of athenticated user or error
 **/
exports.checkUser = function (name, passwd) {
  return new Promise(function (resolve, reject) {
    logger.debug('In check users service');

    LoginModel.find({
      name,
      status: 'active'
    })
      .then(item => {
        if (item.length > 0) {
          const pwhash = item[0].passwd;
          bcrypt.compare(passwd, pwhash, function (err, result) {
            if (err) {
              logger.error(`Some error during compare: ${err}`);
              reject(err);
            }
            if (result) {
              logger.info('matched');
              resolve(item[0]);
            } else {
              logger.error('Pwd mismatch');
              reject(Error('pwd mismatch')); // do not show that detail to the end user, just log it
            }
          });
        } else {
          logger.error('No entry found or perhaps not yet activated');
          reject(Error('not found')); // do not show that detail to the end user, just log it
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Check if user is activated.
 * @method isActiveUser
 * @public
 * @param {string} uname - user login name
 * @returns {Promise} true or error
 **/
exports.isActiveUser = function (uname) {
  return new Promise(function (resolve, reject) {
    logger.debug('In isActiveUser service');

    LoginModel.find({
      name: uname,
      status: 'active'
    })
      .then(item => {
        if (item.length > 0) {
          logger.debug('Was found OK');
          resolve(true);
        } else {
          logger.error('No match found');
          reject(Error('not found'));
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Helper function roleCheck.
 * @method roleCheck
 * @private
 * @param {string} uname - user login name
 * @param {object} item The item found in db
 * @param {string} aRoles - Array of strings
 * @param {object} resolve callback OK
 * @param {object} reject callback bad
 * @returns {Promise} true or error
 */
function roleCheck (uname, item, aRoles, resolve, reject) {
  logger.info(`Check user ${uname} for roles`);
  if (item.length > 0) {
    const uRole = item[0].role;
    logger.debug(`Check user ${uname} has role ${uRole}`);

    if (aRoles.indexOf(uRole) === -1) {
      logger.error(`User ${uname} does not have one of the wanted roles ${uRole}`);
      reject(Error('not in role'));
    }

    logger.debug(`OK: User ${uname} has role ${uRole}`);
    resolve(true);
  } else {
    logger.error(`No match for ${uname} found`);
    reject(Error('not found'));
  }
}

/**
 * Check if user has one of the wanted roles.
 * @method hasRole
 * @public
 * @param {string} uname - user login name
 * @param {string} aRoles - Array of strings
 * @returns {Promise} true or error
 **/
exports.hasRole = function (uname, aRoles) {
  return new Promise(function (resolve, reject) {
    logger.debug('In hasRole service');

    LoginModel.find({
      name: uname
    })
      .then(item => {
        if (item.length > 0) {
          logger.debug('Was found OK');
          const uRole = item[0].role;
          logger.info(`User ${uname} has role ${uRole}`);

          if (aRoles.indexOf(uRole) === -1) {
            logger.error('User does not have one of the wanted roles');
            reject(Error('not in role'));
          } else {
            logger.debug(`User ${uname} has wanted role ${uRole}`);
            resolve(true);
          }
        } else {
          logger.error('No match found');
          reject(Error('not found'));
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Check if active user has one of the wanted roles.
 * @method isActiveHasRole
 * @public
 * @param {string} uname - user login name
 * @param {string} aRoles - Array of strings
 * @returns {Promise} boolean or error
 **/
exports.isActiveHasRole = function (uname, aRoles) {
  return new Promise(function (resolve, reject) {
    logger.debug('In isActiveHasRole service');

    LoginModel.find({
      name: uname,
      status: 'active'
    })
      .then(item => {
        roleCheck(uname, item, aRoles, resolve, reject);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Store valid auth via JWT.
 * @method logAuthSuccess
 * @public
 * @param {string} uname - user login name
 * @param {string} myremoteip - Remote IP in IPv4 or IPv6 format, who knows
 * @param {number} myvalidity - Timestamp in epoch seconds until when JWT is valid
 * @returns {Promise} true or error
 **/
exports.logAuthSuccess = function (uname, myremoteip, myvalidity) {
  return new Promise(function (resolve, reject) {
    logger.debug('In logAuthSuccess service');

    getUserObject(uname)
      .then(userObject => {
        const tsnow = new Date();
        const query = {
          username: userObject,
          remoteip: myremoteip
        };
        const newdata = {
          tssuccess: tsnow,
          tsexpire: null,
          countexpire: 0,
          validity: myvalidity
        };

        const updatequery = LogauthModel.findOneAndUpdate(query, newdata, { new: true, upsert: true });
        updatequery.exec()
          .then(_doc => {
            logger.info('stored logAuthSuccess');
            resolve(true);
          })
          .catch(err => /* istanbul ignore next */ {
            logger.error(err);
            reject(err);
          });
      });
  });
};

/**
 * Store invalid auth via JWT.
 * @method logAuthFailure
 * @public
 * @param {string} uname - user login name
 * @param {string} remoteip - Array of strings
 * @returns {Promise} true
 **/
exports.logAuthFailure = function (uname, remoteip) {
  return new Promise(function (resolve, _reject) {
    logger.debug('In logAuthFailure service');
    resolve(true);
  });
}

/**
 * Store expired auth via JWT.
 * @method logAuthExpired
 * @public
 * @param {string} uname - user login name
 * @param {string} myremoteip - Array of strings
 * @param {number} myvalidity - Timestamp in epoch seconds until when JWT is valid
 * @returns {Promise} true or error
 **/
exports.logAuthExpired = function (uname, myremoteip, myvalidity) {
  return new Promise(function (resolve, reject) {
    logger.debug('In logAuthExpired service');

    getUserObject(uname)
      .then(userObject => {
        const tsnow = new Date();
        const query = {
          username: userObject,
          remoteip: myremoteip
        };
        const newdata = {
          tssuccess: null,
          tsexpire: tsnow,
          validity: myvalidity,
          $inc: { countexpire: 1 }
        };

        const updatequery = LogauthModel.findOneAndUpdate(query, newdata, { new: true, upsert: true });
        updatequery.exec()
          .then(_doc => {
            logger.info('stored logAuthExpired');
            resolve(true);
          })
          .catch(err => /* istanbul ignore next */ {
            logger.error(err);
            reject(err);
          });
      });
  });
};

/**
 * List logauth.
 * @method getLogauth
 * @public
 * @returns {Promise} array of logauth entries or error
 **/
exports.getLogauth = function () {
  return new Promise(function (resolve, reject) {
    logger.debug('In getLogauth service');

    LogauthModel.find({}, { _id: 0, __v: 0 })
      .populate({ path: 'username', select: '-_id -__v -passwd' })
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * export unit testing for test env only
 */
if (process.env.NODE_ENV === 'test') {
  exports.roleCheck = roleCheck;
}