backend/core/user/index.js
const _ = require('lodash');
const esnConfig = require('../../core')['esn-config'];
const pubsub = require('../../core/pubsub').local;
const logger = require('../logger');
const authToken = require('../auth/token');
const mongoose = require('mongoose');
const User = mongoose.model('User');
const emailAddresses = require('email-addresses');
const CONSTANTS = require('./constants');
const moderation = require('./moderation');
const coreAvailability = require('../availability');
const { getDisplayName } = require('./utils');
const { getOptions } = require('./listener');
const { reindexRegistry } = require('../elasticsearch');
const { TYPE, ELASTICSEARCH, USER_ACTIONS, USER_ACTION_STATES } = CONSTANTS;
module.exports = {
checkEmailsAvailability,
getDisplayName,
TYPE,
recordUser,
provisionUser,
translate,
findByEmail,
findUsersByEmail,
get,
list,
listByCursor,
update,
updateProfile,
updateStates,
removeAccountById,
belongsToCompany,
getCompanies,
getNewToken,
find,
init,
moderation,
metadata: require('./metadata'),
provision: require('./provision'),
domain: require('./domain'),
follow: require('./follow'),
login: require('./login'),
denormalize: require('./denormalize'),
states: require('./states')
};
function getUserTemplate(callback) {
esnConfig('user').get(callback);
}
function recordUser(userData, callback) {
const userAsModel = userData instanceof User ? userData : new User(userData);
checkEmailsAvailability(userAsModel.emails).then(unavailableEmails => {
if (unavailableEmails.length > 0) {
return callback(new Error(`Emails already in use: ${unavailableEmails.join(', ')}`));
}
userAsModel.save((err, resp) => {
if (!err) {
pubsub.topic(CONSTANTS.EVENTS.userCreated).publish(resp);
logger.info('User provisioned in datastore:', userAsModel.emails.join(','));
} else {
logger.warn('Error while trying to provision user in database:', err.message);
}
callback(err, resp);
});
}, callback);
}
function provisionUser(data, callback) {
getUserTemplate((err, template) => {
if (err) {
return callback(err);
}
recordUser({...template, ...data}, callback);
});
}
function findByEmail(email, options, callback) {
if (!callback) {
callback = options;
options = {};
}
User.findOne(buildFindByEmailQuery({ ...options, email }), callback);
}
function findUsersByEmail(email, callback) {
User.find(buildFindByEmailQuery({ email }), callback);
}
function buildFindByEmailQuery(options) {
const query = { $and: [] };
if (options.email) {
const emails = Array.isArray(options.email) ?
options.email.map(item => item.trim().toLowerCase()) :
[options.email.trim().toLowerCase()];
query.$and.push({
accounts: {
$elemMatch: {
emails: {
$in: emails
}
}
}
});
}
if (options.domainId) {
query.$and.push({
domains: {
$elemMatch: {
domain_id: options.domainId
}
}
});
}
return query;
}
function get(uuid, callback) {
User.findOne({_id: uuid}, callback);
}
function list(callback) {
User.find(callback);
}
function listByCursor(options = {}) {
const query = _.isEmpty(options) ? {} : { $and: [] };
if (options.domainIds && options.domainIds.length) {
query.$and.push({
'domains.domain_id': {
$in: options.domainIds
}
});
}
if (options.hasOwnProperty('isSearchable')) {
const searchDisabled = {
$elemMatch: {
name: USER_ACTIONS.searchable,
value: USER_ACTION_STATES.disabled
}
};
query.$and.push({ states: options.isSearchable ? { $not: searchDisabled } : searchDisabled });
}
if (options.hasOwnProperty('canLogin')) {
const loginDisabled = {
$elemMatch: {
name: USER_ACTIONS.login,
value: USER_ACTION_STATES.disabled
}
};
query.$and.push({ states: options.canLogin ? { $not: loginDisabled } : loginDisabled });
}
return User.find(query).cursor();
}
function update(user, callback) {
user.save((err, savedUser) => {
if (!err) {
pubsub.topic(CONSTANTS.EVENTS.userUpdated).publish(savedUser);
}
callback(err, savedUser);
});
}
function updateProfile(user, profile, callback) {
if (!user || !profile) {
return callback(new Error('User and profile are required'));
}
var id = user._id || user;
User.findOneAndUpdate({ _id: id }, { $set: profile || {} }, { new: true }, (err, user) => {
if (!err) {
pubsub.topic(CONSTANTS.EVENTS.userUpdated).publish(user);
}
callback(err, user);
});
}
function removeAccountById(user, accountId, callback) {
let accountIndex = -1;
user.accounts.forEach((account, index) => {
if (account.data && account.data.id === accountId) {
accountIndex = index;
}
if (index === user.accounts.length - 1) {
if (accountIndex === -1) {
return callback(new Error('Invalid account id: ' + accountId));
} else {
user.accounts.splice(accountIndex, 1);
user.markModified('accounts');
return user.save(callback);
}
}
});
}
function belongsToCompany(user, company, callback) {
if (!user || !company) {
return callback(new Error('User and company are required.'));
}
const hasCompany = user.emails.some(email => {
const domain = emailAddresses.parseOneAddress(email).domain.toLowerCase();
const domainWithoutSuffix = domain.split('.')[0].toLowerCase();
return domain === company.toLowerCase() || domainWithoutSuffix === company.toLowerCase();
});
return callback(null, hasCompany);
}
function getCompanies(user, callback) {
if (!user) {
return callback(new Error('User is required.'));
}
const companies = user.emails.map(email => {
const parsedEmail = emailAddresses.parseOneAddress(email);
return parsedEmail.domain.split('.')[0];
});
return callback(null, companies);
}
function getNewToken(user, ttl, callback) {
authToken.getNewToken({ttl: ttl, user: user._id, user_type: TYPE}, callback);
}
function find(query, callback) {
User.findOne(query, callback);
}
function init() {
moderation.init();
coreAvailability.email.addChecker({
name: 'user',
check(email) {
return new Promise((resolve, reject) => {
findByEmail(email, (err, user) => {
if (err) return reject(err);
return resolve(!user);
});
});
}
});
// Register elasticsearch reindex options for users
reindexRegistry.register(ELASTICSEARCH.type, {
name: ELASTICSEARCH.index,
buildReindexOptionsFunction: _buildElasticsearchReindexOptions
});
}
/**
* Translate external payload to OpenPaaS user. This is used by provision modules,
* such as converting LDAP user to OpenPaaS user
*
* @param {Object} baseUser The base user object to be extended
* @param {Object} payload The payload used to convert to OP user
* @return {Object} The OpenPaaS user object
*/
function translate(baseUser, payload) {
const userEmail = payload.username; // we use email as username to authenticate
const domainId = payload.domainId;
const payloadUser = payload.user;
const mapping = payload.mapping;
const outputUser = baseUser || {};
// provision domain
if (!outputUser.domains) {
outputUser.domains = [];
}
if (domainId) {
const domain = _.find(outputUser.domains, domain => String(domain.domain_id) === String(domainId));
if (!domain) {
outputUser.domains.push({ domain_id: domainId });
}
}
// provision email account
if (!outputUser.accounts) {
outputUser.accounts = [];
}
let emailAccount = _.find(outputUser.accounts, { type: 'email' });
if (!emailAccount) {
emailAccount = {
type: 'email',
hosted: true,
emails: []
};
outputUser.accounts.push(emailAccount);
}
if (emailAccount.emails.indexOf(userEmail) === -1) {
emailAccount.emails.push(userEmail);
}
// provision other fields basing on mapping
_.forEach(mapping, (value, key) => {
if (key === 'email') {
const email = payloadUser[value];
if (emailAccount.emails.indexOf(email) === -1) {
emailAccount.emails.push(email);
}
} else {
outputUser[key] = payloadUser[value];
}
});
return outputUser;
}
function checkEmailsAvailability(emails) {
return Promise.all(
emails.map(email =>
coreAvailability.email.isAvailable(email)
.then(result => ({ email, available: result.available }))
))
.then(results =>
results.filter(result => !result.available).map(result => result.email)
);
}
function updateStates(userId, states, callback) {
if (!userId || !states) {
return callback(new Error('User id and states are required'));
}
User.findOneAndUpdate({ _id: userId }, { $set: { states } }, { new: true }, (err, user) => {
if (!err) {
pubsub.topic(CONSTANTS.EVENTS.userUpdated).publish(user);
}
callback(err);
});
}
function _buildElasticsearchReindexOptions() {
const options = getOptions();
const cursor = listByCursor();
options.name = ELASTICSEARCH.index;
options.next = () => cursor.next();
return Promise.resolve(options);
}