fh-wfm-user
Version:
168 lines (152 loc) • 5.48 kB
JavaScript
var _ = require('lodash');
var hash = require('./hash');
var q = require('q');
q.longStackSupport = process.env.NODE_ENV === 'development';
var verifyErrMessage = 'User not found with supplied credentials';
var simpleStore = require('fh-wfm-simple-store');
function stripPassword(user) {
user = _.cloneDeep(user);
if (user.password) {
delete user.password;
}
return user;
}
function hashUserPassword(user, pwd) {
return q.nfcall(hash.saltAndHash, pwd).then(function(hashed) {
user.password = hashed;
return user;
});
}
function decorateStore(Store) {
const origCreate = Store.prototype.create;
const origUpdate = Store.prototype.update;
const origRead = Store.prototype.read;
const origListen = Store.prototype.listen;
Store.prototype.create = function(user) {
user = _.cloneDeep(user);
return hashUserPassword(user, user.password)
.then(origCreate.bind(this))
.then(stripPassword);
};
Store.prototype.read = function(id) {
return origRead.call(this, id).then(function(user) {
if (!user) {
throw new Error('User not found');
}
return user;
});
};
Store.prototype.findByUsername = function(username) {
return this.list({
key: 'username',
value: username
}).then(_.first);
};
/**
* Finds a user by username.
*/
Store.prototype.byUsername = function(username) {
return this.findByUsername(username, this.data)
.then(stripPassword);
};
/**
* Updates an user's data, searching by the ID field. The ID field is read-only.
* Also updates the user's password unconditionally,
* so this function should only be acessible from a trusted application.
* @param {Object} user User data to update
* @return {Promise} Promise that resolves to the updated user
*/
Store.prototype.update = function(user) {
var passwordPromise = user.password ? hashUserPassword(user, user.password) : q(user);
return passwordPromise
.then(origUpdate.bind(this))
.then(stripPassword);
};
/**
* Update's a user's password when also given the current password
* @param {String} username
* @param {String} oldPwd The user's current password
* @param {String} newPwd The desired new password for the user
* @return {Promise} Promise that resolves to the updated user when successful
* or rejects if the user is not found or the current password does not match.
* Note that the password field is not readable
*/
Store.prototype.updatePassword = function(username, oldPwd, newPwd) {
var user;
return this.findByUsername(username).then(function(found) {
return user = found;
})
.then(_.partial(doVerify, oldPwd))
.then(function(match) {
if (!match) {
return q.reject(new Error());
}
// old password matches, update to new password
user.password = newPwd;
return user;
})
.then(this.update.bind(this))
.then(stripPassword)
.catch(function() {
return q.reject(new Error(verifyErrMessage));
});
};
function doVerify(password, user) {
// user.password here should be the stored hash
return q.nfcall(hash.verify, password, user.password).then(function(match) {
if (!match) {
throw new Error(verifyErrMessage);
} else {
return true;
}
});
}
/**
* Verifies if the user with the given username has the supplied password.
* @param {String} username
* @param {String} password
* @return {Promise} A promise that resolves to a boolean of whether the password matches,
* the promise is also rejected if the user is not found or the password does not match
*/
Store.prototype.verifyPassword = function(username, password) {
return this.findByUsername(username)
.then(function(user) {
return q.delay(user, 500);
})
.then(_.partial(doVerify, password))
.catch(function() {
return q.reject(new Error(verifyErrMessage));
});
};
Store.prototype.listen = function(topicPrefix, mediator) {
var self = this;
origListen.call(this, topicPrefix, mediator);
this.topics
.on('username:read', function(username) {
self.byUsername(username).then(function(user) {
mediator.publish([self.topics.getTopic('username:read', 'done'), username].join(':'), user);
}).catch(function(err) {
mediator.publish([self.topics.getTopic('username:read', 'error'), username].join(':'), new Error(err.message));
});
})
.on('auth', function(data) {
return self.verifyPassword(data.username, data.password).then(function(passwordCorrect) {
mediator.publish([self.topics.getTopic('auth', 'done'), data.username].join(':'), passwordCorrect);
}).catch(function(err) {
mediator.publish([self.topics.getTopic('auth', 'error'), data.username].join(':'), new Error(err.message));
});
})
.on('password', function(data) {
self.updatePassword(data.username, data.oldPwd, data.newPwd).then(function(user) {
mediator.publish([self.topics.getTopic('password', 'done'), data.username].join(':'), user);
}).catch(function(err) {
mediator.publish([self.topics.getTopic('password', 'error'), data.username].join(':'), new Error(err.message));
});
});
};
}
module.exports = function initializeUserStoreClass(config) {
var Store = simpleStore(config);
decorateStore(Store);
return Store;
};