OpenVeo Core server

API Docs for: 7.0.0
Show:

File: app/server/providers/UserProvider.js

'use strict';

/**
 * @module core-providers
 */

var util = require('util');
var path = require('path');
var crypto = require('crypto');
var shortid = require('shortid');
var async = require('async');
var openVeoApi = require('@openveo/api');
var configDir = openVeoApi.fileSystem.getConfDir();
var conf = require(path.join(configDir, 'core/conf.json'));
var ResourceFilter = openVeoApi.storages.ResourceFilter;
var NotFoundError = openVeoApi.errors.NotFoundError;

/**
 * Defines a UserProvider to get and save back end users.
 *
 * @class UserProvider
 * @extends EntityProvider
 * @constructor
 * @param {Database} database The database to interact with
 */
function UserProvider(database) {
  UserProvider.super_.call(this, database, 'core_users');
}

module.exports = UserProvider;
util.inherits(UserProvider, openVeoApi.providers.EntityProvider);

/**
 * Gets an internal user by its credentials.
 *
 * @method getUserByCredentials
 * @async
 * @param {String} email The user email
 * @param {String} password The user clear text password
 * @param {Function} callback Function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Object** The user
 */
UserProvider.prototype.getUserByCredentials = function(email, password, callback) {
  password = crypto.createHmac('sha256', conf.passwordHashKey).update(password).digest('hex');

  this.getOne(
    new ResourceFilter()
      .equal('origin', openVeoApi.passport.STRATEGIES.LOCAL)
      .equal('email', email)
      .equal('password', password),
    {
      exclude: ['password']
    },
    callback
  );
};

/**
 * Gets an internal user by its email.
 *
 * @method getUserByEmail
 * @async
 * @param {String} email The email of the user
 * @param {Function} callback Function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Object** The user
 */
UserProvider.prototype.getUserByEmail = function(email, callback) {
  this.getOne(
    new ResourceFilter()
      .equal('origin', openVeoApi.passport.STRATEGIES.LOCAL)
      .equal('email', email),
    {
      exclude: ['password']
    },
    callback
  );
};

/**
 * Adds users.
 *
 * @method add
 * @async
 * @param {Array} users The list of users to store with for each user:
 *   - **String** name The user name
 *   - **String** email The user email
 *   - **String** password The user password
 *   - **String** passwordValidate The user password validation
 *   - **String** [id] The user id, generated if not specified
 *   - **Array** [roles] The user role ids
 *   - **Boolean** [locked=false] true to lock the user from edition, false otherwise
 * @param {Function} [callback] The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Number** The total amount of users inserted
 *   - **Array** The list of added users
 */
UserProvider.prototype.add = function(users, callback) {
  var self = this;
  var usersToAdd = [];
  var userEmails = [];

  for (var i = 0; i < users.length; i++) {
    var user = users[i];

    if (!user.name || !user.email || !user.password)
      return this.executeCallback(callback, new TypeError('Requires name, email and password to add a user'));

    // Validate password
    if (user.password !== user.passwordValidate)
      return this.executeCallback(callback, new Error('Passwords do not match'));

    // Validate email
    if (!openVeoApi.util.isEmailValid(user.email))
      return this.executeCallback(callback, new TypeError('Invalid email address: ' + user.email));

    userEmails.push(user.email);
  }

  // Find users
  this.getAll(
    new ResourceFilter()
      .equal('origin', openVeoApi.passport.STRATEGIES.LOCAL)
      .in('email', userEmails),
    {
      include: ['email']
    },
    {
      id: 'desc'
    },
    function(getAllError, fetchedUsers) {
      if (getAllError) return self.executeCallback(callback, getAllError);

      for (var i = 0; i < users.length; i++) {
        var user = users[i];

        // Validate email
        for (var j = 0; j < fetchedUsers.length; j++) {
          if (user.email === fetchedUsers[j].email)
            return self.executeCallback(callback, new Error('Email "' + user.email + '" not available'));
        }

        // Encrypt password
        var password = crypto.createHmac('sha256', conf.passwordHashKey).update(user.password).digest('hex');

        usersToAdd.push({
          id: user.id || shortid.generate(),
          name: user.name,
          email: user.email,
          password: password,
          locked: user.locked || false,
          origin: openVeoApi.passport.STRATEGIES.LOCAL,
          roles: user.roles || []
        });
      }

      UserProvider.super_.prototype.add.call(self, usersToAdd, function(addError, total, addedUsers) {
        if (addError) return self.executeCallback(callback, addError);

        if (callback) {
          addedUsers.forEach(function(addedUser) {
            delete addedUser['password'];
          });

          callback(null, total, addedUsers);
        }
      });
    }
  );
};

/**
 * Updates an internal user.
 *
 * @method updateOne
 * @async
 * @param {ResourceFilter} [filter] Rules to filter the user to update
 * @param {Object} data The modifications to perform
 * @param {String} [data.name] The user name
 * @param {String} [data.email] The user email
 * @param {String} [data.password] The user password. Also requires passwordValidate
 * @param {String} [data.passwordValidate] The user password validation. Also requires password
 * @param {Array} [data.roles] The user role ids
 * @param {Boolean} [data.locked] true to lock the user from edition, false otherwise
 * @param {Function} [callback] The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Number** 1 if everything went fine
 */
UserProvider.prototype.updateOne = function(filter, data, callback) {
  var self = this;
  var modifications = {};
  var total;
  var user;

  if (!filter) filter = new ResourceFilter();
  filter.equal('origin', openVeoApi.passport.STRATEGIES.LOCAL);

  // Validate password
  if (data.password) {

    if (data.password !== data.passwordValidate)
      return this.executeCallback(callback, new Error('Passwords does not match'));
    else {

      // Encrypt password
      var password = crypto.createHmac('sha256', conf.passwordHashKey).update(data.password).digest('hex');
      modifications.password = password;

    }
  }

  // Validate email
  if (data.email && !openVeoApi.util.isEmailValid(data.email))
    return this.executeCallback(callback, new TypeError('Invalid email address'));

  // Validate roles
  if (data.roles) modifications.roles = data.roles;

  // Validate name
  if (data.name) modifications.name = data.name;

  // Validate locked
  if (typeof data.locked !== 'undefined') modifications.locked = Boolean(data.locked);

  async.series([

    // Get user corresponding to given filter
    function(callback) {
      self.getOne(
        filter,
        {
          include: ['id']
        },
        function(error, fetchedUser) {
          if (error) return callback(error);
          if (!fetchedUser) return callback(new NotFoundError(JSON.stringify(filter)));

          user = fetchedUser;
          callback();
        }
      );
    },

    // Verify if the email address is not already used
    function(callback) {
      if (!data.email) return callback();
      self.getUserByEmail(data.email, function(error, fetchedUser) {
        if (error) return callback(error);
        if (fetchedUser && fetchedUser.id !== user.id) return callback(new Error('Email not available'));

        modifications.email = data.email;
        callback();
      });
    },

    // Update user
    function(callback) {
      UserProvider.super_.prototype.updateOne.call(
        self,
        new ResourceFilter().equal('id', user.id).equal('origin', openVeoApi.passport.STRATEGIES.LOCAL),
        modifications,
        function(error, totalItems) {
          if (error) return callback(error);
          total = totalItems;
          callback();
        }
      );
    }

  ], function(error) {
    self.executeCallback(callback, error, total);
  });
};

/**
 * Removes users.
 *
 * This will execute core hook "USERS_DELETED" after removing users with:
 * - **Array** The ids of deleted users
 *
 * @method remove
 * @async
 * @param {ResourceFilter} [filter] Rules to filter users to remove
 * @param {Function} [callback] The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Number** The number of removed users
 */
UserProvider.prototype.remove = function(filter, callback) {
  var self = this;

  // Find users
  this.getAll(
    filter,
    {
      include: ['id']
    },
    {
      id: 'desc'
    },
    function(getAllError, users) {
      if (getAllError) return self.executeCallback(callback, getAllError);
      if (!users || !users.length) return self.executeCallback(callback);

      // Remove users
      UserProvider.super_.prototype.remove.call(self, filter, function(removeError, total) {
        if (removeError) return self.executeCallback(callback, removeError);

        // Execute hook
        var api = process.api.getCoreApi();
        api.executeHook(
          api.getHooks().USERS_DELETED,
          users.map(function(user) {
            return user.id;
          }),
          function(hookError) {
            self.executeCallback(callback, hookError, total);
          }
        );
      });
    }
  );
};

/**
 * Adds external users.
 *
 * External users are automatically locked when added.
 *
 * @method addThirdPartyUsers
 * @async
 * @param {Array} users The list of users to add with for each user:
 *   - **String** name The user name
 *   - **String** email The user email
 *   - **String** origin Id of the third party provider system
 *   - **String** originId The user id in third party provider system
 *   - **String** [id] The user id, generated if not specified
 *   - **Array** [originGroups] The user groups in third party provider system
 *   - **Array** [roles] The user role ids
 * @param {Function} [callback] The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Number** The total amount of users inserted
 *   - **Array** The inserted users
 */
UserProvider.prototype.addThirdPartyUsers = function(users, callback) {
  var usersToAdd = [];

  for (var i = 0; i < users.length; i++) {
    var user = users[i];

    if (!user.origin || !user.name || !user.email || !user.originId) {
      return this.executeCallback(
        callback,
        new TypeError('Requires name, email, origin and origin id to add a third party user')
      );
    }

    if (user.origin === openVeoApi.passport.STRATEGIES.LOCAL)
      return this.executeCallback(callback, new Error('Third party user origin can\'t be local'));

    usersToAdd.push({
      id: user.id || shortid.generate(),
      name: user.name,
      email: user.email,
      origin: user.origin,
      originId: user.originId,
      originGroups: user.originGroups || [],
      roles: user.roles || [],
      locked: true
    });
  }

  UserProvider.super_.prototype.add.call(this, usersToAdd, callback);
};

/**
 * Updates an external user.
 *
 * @method updateThirdPartyUser
 * @async
 * @param {ResourceFilter} [filter] Rules to filter users to update
 * @param {Object} data The modifications to perform
 * @param {String} [data.name] The user name
 * @param {String} [data.email] The user email
 * @param {Array} [data.originGroups] The user groups in third party provider system
 * @param {Array} [data.roles] The user role ids
 * @param {Boolean} [data.locked] true to lock the user from edition, false otherwise
 * @param {String} origin The user origin (see openVeoApi.passport.STRATEGIES)
 * @param {Function} callback The function to call when it's done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **Number** 1 if everything went fine
 */
UserProvider.prototype.updateThirdPartyUser = function(filter, data, origin, callback) {
  var modifications = {};

  if (origin === openVeoApi.passport.STRATEGIES.LOCAL)
    return this.executeCallback(callback, new Error('Can\'t update a local user with "updateThirdPartyUser"'));

  if (!filter) filter = new ResourceFilter();
  filter.equal('origin', origin);

  if (data.name) modifications.name = data.name;
  if (data.email) modifications.email = data.email;
  if (data.originGroups) modifications.originGroups = data.originGroups;
  if (data.roles) modifications.roles = data.roles;
  if (typeof data.locked !== 'undefined') modifications.locked = Boolean(data.locked);

  this.storage.updateOne(this.location, filter, modifications, function(error, total) {
    this.executeCallback(callback, error, total);
  }.bind(this));
};

/**
 * Creates users indexes.
 *
 * @method createIndexes
 * @async
 * @param {Function} callback Function to call when it's done with :
 *  - **Error** An error if something went wrong, null otherwise
 */
UserProvider.prototype.createIndexes = function(callback) {
  this.storage.createIndexes(this.location, [
    {key: {name: 1}, name: 'byName'},
    {key: {name: 'text'}, weights: {name: 1}, name: 'querySearch'}
  ], function(error, result) {
    if (result && result.note)
      process.logger.debug('Create users indexes : ' + result.note);

    callback(error);
  });
};