mailtrain/models/users.js

379 lines
12 KiB
JavaScript
Raw Normal View History

'use strict';
const config = require('config');
const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
const { enforce, filterObject } = require('../lib/helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const passwordValidator = require('../shared/password-validator')();
const dtHelpers = require('../lib/dt-helpers');
const tools = require('../lib/tools-async');
let crypto = require('crypto');
const settings = require('./settings');
2017-07-08 16:57:41 +00:00
const urllib = require('url');
const _ = require('../lib/translate')._;
const bluebird = require('bluebird');
const bcrypt = require('bcrypt-nodejs');
const bcryptHash = bluebird.promisify(bcrypt.hash);
const bcryptCompare = bluebird.promisify(bcrypt.compare);
const mailer = require('../lib/mailer');
const mailerSendMail = bluebird.promisify(mailer.sendMail);
const passport = require('../lib/passport');
const namespaceHelpers = require('../lib/namespace-helpers');
const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace', 'role']);
const ownAccountAllowedKeys = new Set(['name', 'email', 'password']);
const allowedKeysExternal = new Set(['username', 'namespace', 'role']);
const hashKeys = new Set(['username', 'name', 'email', 'namespace', 'role']);
const shares = require('../../models/shares');
function hash(entity) {
return hasher.hash(filterObject(entity, hashKeys));
}
async function _getBy(context, key, value, extraColumns) {
const columns = ['id', 'username', 'name', 'email', 'namespace', 'role'];
if (extraColumns) {
columns.push(...extraColumns);
}
const user = await knex('users').select(columns).where(key, value).first();
if (!user) {
if (context) {
shares.throwPermissionDenied();
} else {
throw new interoperableErrors.NotFoundError();
}
}
if (context) {
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
}
return user;
}
async function getById(context, id) {
return await _getBy(context, 'id', id);
}
async function getByIdNoPerms(id) {
return await _getBy(null, 'id', id);
}
async function serverValidate(context, data, isOwnAccount) {
const result = {};
if (!isOwnAccount) {
await shares.enforceTypePermission(context, 'namespace', 'manageUsers');
}
if (!isOwnAccount && data.username) {
const query = knex('users').select(['id']).where('username', data.username);
if (data.id) {
// Id is not set in entity creation form
query.andWhereNot('id', data.id);
}
const user = await query.first();
result.username = {
exists: !!user
};
}
if (isOwnAccount && data.currentPassword) {
const user = await knex('users').select(['id', 'password']).where('id', data.id).first();
result.currentPassword = {};
result.currentPassword.incorrect = !await bcryptCompare(data.currentPassword, user.password);
}
if (data.email) {
const query = knex('users').select(['id']).where('email', data.email);
if (data.id) {
// Id is not set in entity creation form
query.andWhereNot('id', data.id);
}
const user = await query.first();
result.email = {};
result.email.invalid = await tools.validateEmail(data.email) !== 0;
result.email.exists = !!user;
}
return result;
}
async function listDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'namespace', requiredOperations: ['manageUsers'] }],
params,
builder => builder.from('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'),
['users.id', 'users.username', 'users.name', 'namespaces.name', 'users.role'],
data => {
const role = data[4];
data[4] = config.roles.global[role] ? config.roles.global[role].name : role;
}
);
}
async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
enforce(await tools.validateEmail(user.email) === 0, 'Invalid email');
await namespaceHelpers.validateEntity(tx, user);
const otherUserWithSameEmailQuery = tx('users').where('email', user.email);
if (user.id) {
otherUserWithSameEmailQuery.andWhereNot('id', user.id);
}
if (await otherUserWithSameEmailQuery.first()) {
throw new interoperableErrors.DuplicitEmailError();
}
if (!isOwnAccount) {
const otherUserWithSameUsernameQuery = tx('users').where('username', user.username);
if (user.id) {
otherUserWithSameUsernameQuery.andWhereNot('id', user.id);
}
if (await otherUserWithSameUsernameQuery.first()) {
throw new interoperableErrors.DuplicitNameError();
}
}
enforce(user.role in config.roles.global, 'Unknown role');
enforce(!isCreate || user.password.length > 0, 'Password not set');
if (user.password) {
const passwordValidatorResults = passwordValidator.test(user.password);
if (passwordValidatorResults.errors.length > 0) {
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
throw new Error('Invalid password');
}
user.password = await bcryptHash(user.password, null, null);
} else {
delete user.password;
}
}
async function create(user) {
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
await knex.transaction(async tx => {
if (passport.isAuthMethodLocal) {
await _validateAndPreprocess(tx, user, true);
const userId = await tx('users').insert(filterObject(user, allowedKeys));
return userId;
} else {
const filteredUser = filterObject(user, allowedKeysExternal);
enforce(user.role in config.roles.global, 'Unknown role');
await namespaceHelpers.validateEntity(tx, user);
const userId = await knex('users').insert(filteredUser);
return userId;
}
});
}
async function updateWithConsistencyCheck(user, isOwnAccount) {
await knex.transaction(async tx => {
const existing = await tx('users').where('id', user.id).first();
if (!existing) {
shares.throwPermissionDenied();
}
const existingHash = hash(existing);
if (existingHash !== user.originalHash) {
throw new interoperableErrors.ChangedError();
}
if (!isOwnAccount) {
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
}
if (passport.isAuthMethodLocal) {
await _validateAndPreprocess(tx, user, false, isOwnAccount);
if (isOwnAccount && user.password) {
if (!await bcryptCompare(user.currentPassword, existing.password)) {
throw new interoperableErrors.IncorrectPasswordError();
}
}
await tx('users').where('id', user.id).update(filterObject(user, isOwnAccount ? ownAccountAllowedKeys : allowedKeys));
} else {
enforce(isOwnAccount, 'Local user management is required');
enforce(user.role in config.roles.global, 'Unknown role');
await namespaceHelpers.validateEntity(tx, user);
await tx('users').where('id', user.id).update(filterObject(user, allowedKeysExternal));
}
});
}
async function remove(context, userId) {
enforce(userId !== 1, 'Admin cannot be deleted');
enforce(context.user.id !== userId, 'User cannot delete himself/herself');
await knex.transaction(async tx => {
const existing = await tx('users').where('id', userId).first();
if (!existing) {
shares.throwPermissionDenied();
}
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
await knex('users').where('id', userId).del();
});
}
async function getByAccessToken(accessToken) {
return await _getBy(null, 'access_token', accessToken);
}
async function getByUsername(username) {
return await _getBy(null, 'username', username);
}
async function getByUsernameIfPasswordMatch(username, password) {
2017-07-08 16:57:41 +00:00
try {
const user = await _getBy(null, 'username', username, ['password']);
2017-07-08 16:57:41 +00:00
if (!await bcryptCompare(password, user.password)) {
throw new interoperableErrors.IncorrectPasswordError();
}
2017-07-08 16:57:41 +00:00
return user;
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
throw new interoperableErrors.IncorrectPasswordError();
}
throw err;
}
}
async function getAccessToken(userId) {
const user = await _getBy(null, 'id', userId, ['access_token']);
return user.access_token;
}
async function resetAccessToken(userId) {
const token = crypto.randomBytes(20).toString('hex').toLowerCase();
await knex('users').where({id: userId}).update({access_token: token});
return token;
}
async function sendPasswordReset(usernameOrEmail) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
await knex.transaction(async tx => {
const user = await tx('users').where('username', usernameOrEmail).orWhere('email', usernameOrEmail).select(['id', 'username', 'email', 'name']).first();
if (user) {
const resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, '');
await tx('users').where('id', user.id).update({
reset_token: resetToken,
reset_expire: new Date(Date.now() + 60 * 60 * 1000)
});
const { serviceUrl, adminEmail } = await settings.get(['serviceUrl', 'adminEmail']);
await mailerSendMail({
from: {
address: adminEmail
},
to: {
address: user.email
},
subject: _('Mailer password change request')
}, {
html: 'emails/password-reset-html.hbs',
text: 'emails/password-reset-text.hbs',
data: {
title: 'Mailtrain',
username: user.username,
name: user.name,
2017-07-08 16:57:41 +00:00
confirmUrl: urllib.resolve(serviceUrl, `/account/reset/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`)
}
});
}
// We intentionally silently ignore the situation when user is not found. This is not to reveal if a user exists in the system.
});
}
async function isPasswordResetTokenValid(username, resetToken) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
const user = await knex('users').select(['id']).where({username, reset_token: resetToken}).andWhere('reset_expire', '>', new Date()).first();
return !!user;
}
2017-07-08 16:57:41 +00:00
async function resetPassword(username, resetToken, password) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
await knex.transaction(async tx => {
const user = await tx('users').select(['id']).where({
username,
reset_token: resetToken
}).andWhere('reset_expire', '>', new Date()).first();
if (user) {
const passwordValidatorResults = passwordValidator.test(password);
if (passwordValidatorResults.errors.length > 0) {
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
throw new Error('Invalid password');
}
password = await bcryptHash(password, null, null);
await tx('users').where({username}).update({
password,
reset_token: null,
reset_expire: null
});
} else {
throw new interoperableErrors.InvalidTokenError();
}
});
}
module.exports = {
listDTAjax,
remove,
updateWithConsistencyCheck,
create,
hash,
getById,
getByIdNoPerms,
serverValidate,
getByAccessToken,
getByUsername,
getByUsernameIfPasswordMatch,
getAccessToken,
resetAccessToken,
sendPasswordReset,
isPasswordResetTokenValid,
resetPassword
};