2017-06-21 00:14:14 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
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')();
|
2017-06-29 21:22:33 +00:00
|
|
|
const validators = require('../shared/validators');
|
2017-06-21 00:14:14 +00:00
|
|
|
const dtHelpers = require('../lib/dt-helpers');
|
2017-06-29 21:22:33 +00:00
|
|
|
const tools = require('../lib/tools-async');
|
|
|
|
const Promise = require('bluebird');
|
2017-06-30 14:11:02 +00:00
|
|
|
const bcrypt = require('bcrypt-nodejs');
|
|
|
|
const bcryptHash = Promise.promisify(bcrypt.hash);
|
|
|
|
const bcryptCompare = Promise.promisify(bcrypt.compare);
|
2017-06-21 00:14:14 +00:00
|
|
|
|
|
|
|
const allowedKeys = new Set(['username', 'name', 'email', 'password']);
|
2017-06-30 14:11:02 +00:00
|
|
|
const ownAccountAllowedKeys = new Set(['name', 'email', 'password']);
|
2017-06-21 00:14:14 +00:00
|
|
|
|
|
|
|
function hash(user) {
|
|
|
|
return hasher.hash(filterObject(user, allowedKeys));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getById(userId) {
|
2017-06-29 21:22:33 +00:00
|
|
|
const user = await knex('users').select(['id', 'username', 'name', 'email', 'password']).where('id', userId).first();
|
2017-06-21 00:14:14 +00:00
|
|
|
if (!user) {
|
|
|
|
throw new interoperableErrors.NotFoundError();
|
|
|
|
}
|
|
|
|
|
|
|
|
user.hash = hash(user);
|
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
delete(user.password);
|
|
|
|
|
2017-06-21 00:14:14 +00:00
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
async function serverValidate(data, isOwnAccount) {
|
2017-06-29 21:22:33 +00:00
|
|
|
const result = {};
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
if (!isOwnAccount && data.username) {
|
2017-06-29 21:22:33 +00:00
|
|
|
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
|
|
|
|
};
|
2017-06-21 00:14:14 +00:00
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
if (data.email) {
|
2017-06-30 14:11:02 +00:00
|
|
|
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();
|
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
result.email = {};
|
|
|
|
result.email.invalid = await tools.validateEmail(data.email) !== 0;
|
2017-06-30 14:11:02 +00:00
|
|
|
result.email.exists = !!user;
|
2017-06-29 21:22:33 +00:00
|
|
|
}
|
2017-06-21 00:14:14 +00:00
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
return result;
|
2017-06-21 00:14:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function listDTAjax(params) {
|
|
|
|
return await dtHelpers.ajaxList(params, tx => tx('users'), ['users.id', 'users.username', 'users.name']);
|
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
|
|
|
enforce(await tools.validateEmail(user.email) === 0, 'Invalid email');
|
2017-06-29 21:22:33 +00:00
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
const otherUserWithSameEmailQuery = tx('users').where('email', user.email);
|
2017-06-29 21:22:33 +00:00
|
|
|
if (user.id) {
|
2017-06-30 14:11:02 +00:00
|
|
|
otherUserWithSameEmailQuery.andWhereNot('id', user.id);
|
2017-06-29 21:22:33 +00:00
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
const otherUserWithSameUsername = await otherUserWithSameEmailQuery.first();
|
2017-06-29 21:22:33 +00:00
|
|
|
if (otherUserWithSameUsername) {
|
2017-06-30 14:11:02 +00:00
|
|
|
throw new interoperableErrors.DuplicitEmailError();
|
2017-06-29 21:22:33 +00:00
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
|
|
|
|
if (!isOwnAccount) {
|
|
|
|
enforce(validators.usernameValid(user.username), 'Invalid username');
|
|
|
|
|
|
|
|
const otherUserWithSameUsernameQuery = tx('users').where('username', user.username);
|
|
|
|
if (user.id) {
|
|
|
|
otherUserWithSameUsernameQuery.andWhereNot('id', user.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
const otherUserWithSameUsername = await otherUserWithSameUsernameQuery.first();
|
|
|
|
if (otherUserWithSameUsername) {
|
|
|
|
throw new interoperableErrors.DuplicitNameError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-06-21 00:14:14 +00:00
|
|
|
async function create(user) {
|
2017-06-30 14:11:02 +00:00
|
|
|
await knex.transaction(async tx => {
|
|
|
|
await _validateAndPreprocess(tx, user, true);
|
|
|
|
const userId = await tx('users').insert(filterObject(user, allowedKeys));
|
|
|
|
return userId;
|
|
|
|
});
|
2017-06-21 00:14:14 +00:00
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
async function updateWithConsistencyCheck(user, isOwnAccount) {
|
2017-06-21 00:14:14 +00:00
|
|
|
await knex.transaction(async tx => {
|
2017-06-30 14:11:02 +00:00
|
|
|
await _validateAndPreprocess(tx, user, false, isOwnAccount);
|
|
|
|
|
2017-06-29 21:22:33 +00:00
|
|
|
const existingUser = await tx('users').select(['id', 'username', 'name', 'email', 'password']).where('id', user.id).first();
|
2017-06-21 00:14:14 +00:00
|
|
|
if (!user) {
|
|
|
|
throw new interoperableErrors.NotFoundError();
|
|
|
|
}
|
|
|
|
|
|
|
|
const existingUserHash = hash(existingUser);
|
|
|
|
if (existingUserHash != user.originalHash) {
|
|
|
|
throw new interoperableErrors.ChangedError();
|
|
|
|
}
|
|
|
|
|
2017-06-30 14:11:02 +00:00
|
|
|
if (isOwnAccount && user.password) {
|
|
|
|
console.log(user.currentPassword);
|
|
|
|
console.log(existingUser.password);
|
|
|
|
|
|
|
|
if (!await bcryptCompare(user.currentPassword, existingUser.password)) {
|
|
|
|
// This is not an interoperable error because current password is verified in account-validate.
|
|
|
|
// A change of password between account-validate and submit would be signalled by ChangedError
|
|
|
|
throw new Error('Incorrect password');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await tx('users').where('id', user.id).update(filterObject(user, isOwnAccount ? ownAccountAllowedKeys : allowedKeys));
|
2017-06-21 00:14:14 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function remove(userId) {
|
|
|
|
// FIXME: enforce that userId is not the current user
|
2017-06-29 21:22:33 +00:00
|
|
|
enforce(userId !== 1, 'Admin cannot be deleted');
|
2017-06-21 00:14:14 +00:00
|
|
|
await knex('users').where('id', userId).del();
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
listDTAjax,
|
|
|
|
remove,
|
|
|
|
updateWithConsistencyCheck,
|
|
|
|
create,
|
|
|
|
hash,
|
|
|
|
getById,
|
2017-06-29 21:22:33 +00:00
|
|
|
serverValidate
|
2017-06-21 00:14:14 +00:00
|
|
|
};
|