Added support for Datatables

Added support for ajax-based server side validation (useful for validation of emails, duplicate usernames, etc.)
User form more or less ready in the basic version (i.e. without permission management)
This commit is contained in:
Tomas Bures 2017-06-21 02:14:14 +02:00
parent f776170854
commit c81f5544e6
26 changed files with 1097 additions and 167 deletions

80
models/namespaces.js Normal file
View file

@ -0,0 +1,80 @@
'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 allowedKeys = new Set(['name', 'description', 'parent']);
async function list() {
return await knex('namespaces');
}
function hash(ns) {
return hasher.hash(filterObject(ns, allowedKeys));
}
async function getById(nsId) {
const ns = await knex('namespaces').where('id', nsId).first();
if (!ns) {
throw new interoperableErrors.NotFoundError();
}
ns.hash = hash(ns);
return ns;
}
async function create(ns) {
const nsId = await knex('namespaces').insert(filterObject(ns, allowedKeys));
return nsId;
}
async function updateWithConsistencyCheck(ns) {
enforce(ns.id !== 1 || ns.parent === null, 'Cannot assign a parent to the root namespace.');
await knex.transaction(async tx => {
const existingNs = await tx('namespaces').where('id', ns.id).first();
if (!ns) {
throw new interoperableErrors.NotFoundError();
}
const existingNsHash = hash(existingNs);
if (existingNsHash != ns.originalHash) {
throw new interoperableErrors.ChangedError();
}
let iter = ns;
while (iter.parent != null) {
iter = await tx('namespaces').where('id', iter.parent).first();
if (iter.id == ns.id) {
throw new interoperableErrors.LoopDetectedError();
}
}
await tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedKeys));
});
}
async function remove(nsId) {
enforce(nsId !== 1, 'Cannot delete the root namespace.');
await knex.transaction(async tx => {
const childNs = await tx('namespaces').where('parent', nsId).first();
if (childNs) {
throw new interoperableErrors.ChildDetectedError();
}
await tx('namespaces').where('id', nsId).del();
});
}
module.exports = {
hash,
list,
getById,
create,
updateWithConsistencyCheck,
remove
};

107
models/users.js Normal file
View file

@ -0,0 +1,107 @@
'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')();
const dtHelpers = require('../lib/dt-helpers');
const bcrypt = require('bcrypt-nodejs');
const tools = require('../lib/tools');
const allowedKeys = new Set(['username', 'name', 'email', 'password']);
function hash(user) {
return hasher.hash(filterObject(user, allowedKeys));
}
async function getById(userId) {
const user = await knex('users').select(['id', 'username', 'name', 'email']).where('id', userId).first();
if (!user) {
throw new interoperableErrors.NotFoundError();
}
user.hash = hash(user);
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();
}
user.hash = hash(user);
return user;
}
async function listDTAjax(params) {
return await dtHelpers.ajaxList(params, tx => tx('users'), ['users.id', 'users.username', 'users.name']);
}
async function create(user) {
const userId = await knex('users').insert(filterObject(user, allowedKeys));
return userId;
}
async function updateWithConsistencyCheck(user) {
await knex.transaction(async tx => {
const existingUser = await tx('users').where('id', user.id).first();
if (!user) {
throw new interoperableErrors.NotFoundError();
}
const existingUserHash = hash(existingUser);
if (existingUserHash != user.originalHash) {
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
await knex('users').where('id', userId).del();
}
module.exports = {
listDTAjax,
remove,
updateWithConsistencyCheck,
create,
hash,
getById,
getByUsername
};