Release candidate of basic user management - currently only CRUD on users, no permission assignment.
This commit is contained in:
parent
e7856bfb73
commit
eb2287f6e9
10 changed files with 776 additions and 750 deletions
104
models/users.js
104
models/users.js
|
@ -5,9 +5,11 @@ 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 validators = require('../shared/validators');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
const tools = require('../lib/tools');
|
||||
const tools = require('../lib/tools-async');
|
||||
const Promise = require('bluebird');
|
||||
const bcryptHash = Promise.promisify(require('bcrypt-nodejs').hash);
|
||||
|
||||
const allowedKeys = new Set(['username', 'name', 'email', 'password']);
|
||||
|
||||
|
@ -16,39 +18,89 @@ function hash(user) {
|
|||
}
|
||||
|
||||
async function getById(userId) {
|
||||
const user = await knex('users').select(['id', 'username', 'name', 'email']).where('id', userId).first();
|
||||
const user = await knex('users').select(['id', 'username', 'name', 'email', 'password']).where('id', userId).first();
|
||||
if (!user) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
user.hash = hash(user);
|
||||
|
||||
delete(user.password);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getByUsername(username) {
|
||||
const user = await knex('users').select(['id', 'username', 'name', 'email']).where('username', username).first();
|
||||
if (!user) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
async function serverValidate(data) {
|
||||
const result = {};
|
||||
|
||||
if (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
|
||||
};
|
||||
}
|
||||
|
||||
user.hash = hash(user);
|
||||
if (data.email) {
|
||||
result.email = {};
|
||||
result.email.invalid = await tools.validateEmail(data.email) !== 0;
|
||||
}
|
||||
|
||||
return user;
|
||||
return result;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(params, tx => tx('users'), ['users.id', 'users.username', 'users.name']);
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(user, isCreate) {
|
||||
enforce(validators.usernameValid(user.username), 'Invalid username');
|
||||
|
||||
const otherUserWithSameUsernameQuery = knex('users').where('username', user.username);
|
||||
if (user.id) {
|
||||
otherUserWithSameUsernameQuery.andWhereNot('id', user.id);
|
||||
}
|
||||
|
||||
const otherUserWithSameUsername = await otherUserWithSameUsernameQuery.first();
|
||||
if (otherUserWithSameUsername) {
|
||||
throw new interoperableErrors.DuplicitNameError();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
enforce(await tools.validateEmail(user.email) === 0, 'Invalid email');
|
||||
}
|
||||
|
||||
|
||||
async function create(user) {
|
||||
_validateAndPreprocess(user, true);
|
||||
const userId = await knex('users').insert(filterObject(user, allowedKeys));
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(user) {
|
||||
_validateAndPreprocess(user, false);
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const existingUser = await tx('users').where('id', user.id).first();
|
||||
const existingUser = await tx('users').select(['id', 'username', 'name', 'email', 'password']).where('id', user.id).first();
|
||||
if (!user) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
@ -58,41 +110,13 @@ async function updateWithConsistencyCheck(user) {
|
|||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
const otherUserWithSameUsername = await tx('users').whereNot('id', user.id).andWhere('username', user.username).first();
|
||||
if (otherUserWithSameUsername) {
|
||||
throw new interoperableErrors.DuplicitNameError();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
bcrypt.hash(updates.password, null, null, (err, hash) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
keys.push('password');
|
||||
values.push(hash);
|
||||
|
||||
finalize();
|
||||
});
|
||||
|
||||
tools.validateEmail(updates.email, false)
|
||||
|
||||
} else {
|
||||
delete user.password;
|
||||
}
|
||||
|
||||
await tx('users').where('id', user.id).update(filterObject(user, allowedKeys));
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(userId) {
|
||||
// FIXME: enforce that userId is not the current user
|
||||
enforce(userId !== 1, 'Admin cannot be deleted');
|
||||
await knex('users').where('id', userId).del();
|
||||
}
|
||||
|
||||
|
@ -103,5 +127,5 @@ module.exports = {
|
|||
create,
|
||||
hash,
|
||||
getById,
|
||||
getByUsername
|
||||
serverValidate
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue