From 0755a0783eefdff9a6bbbb3668878e65313ff03f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Nov 2016 11:59:32 -0700 Subject: [PATCH] Added documentation to the user model + abstracted it --- models/user.js | 152 ++++++++++++++++++++++++++++++--------- routes/api/user/index.js | 34 +++------ 2 files changed, 126 insertions(+), 60 deletions(-) diff --git a/models/user.js b/models/user.js index c6197a220..d841480d8 100644 --- a/models/user.js +++ b/models/user.js @@ -2,32 +2,72 @@ const mongoose = require('../mongoose'); const uuid = require('uuid'); const bcrypt = require('bcrypt'); +// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run +// through during the salting process. const SALT_ROUNDS = 10; +// USER_ROLES is the array of roles that is permissible as a user role. +const USER_ROLES = [ + 'admin', + 'moderator' +]; + +// UserSchema is the mongoose schema defined as the representation of a User in +// MongoDB. const UserSchema = new mongoose.Schema({ + + // This ID represents the most unique identifier for a user, it is generated + // when the user is created as a random uuid. id: { type: String, default: uuid.v4, unique: true, required: true }, + + // This is sourced from the social provider or set manually during user setup + // and simply provides a name to display for the given user. displayName: String, + + // This is true when the user account is disabled, no action should be + // acknowledged when they are disabled. Logins are also prevented. disabled: Boolean, + + // This provides a source of identity proof for users who login using the + // local provider. A local provider will be assumed for users who do not + // have any social profiles. password: String, - profiles: [{ + + // Profiles describes the array of identities for a given user. Any one user + // can have multiple profiles associated with them, including multiple email + // addresses. + profiles: [new mongoose.Schema({ + + // ID provides the identifier for the user profile, in the case of a local + // provider, the id would be an email, in the case of a social provider, + // the id would be the foreign providers identifier. id: { type: String, required: true }, + + // Provider is simply the name attached to the authentication mode. In the + // case of a locally provided profile, this will simply be `local`, or a + // social provider which for Facebook would just be `facebook`. provider: { type: String, required: true } - }], - roles: { - type: [{type: String, enum: ['admin', 'moderator']}] - } + }, { + _id: false + })], + + // Roles provides an array of roles (as strings) that is associated with a + // user. + roles: [String] }, { + + // This will ensure that we have proper timestamps available on this model. timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' @@ -75,6 +115,13 @@ UserSchema.options.toObject.transform = (doc, ret, options) => { return ret; }; +// Create the User model. +const UserModel = mongoose.model('User', UserSchema); + +// UserService is the interface for the application to interact with the +// UserModel through. +const UserService = module.exports = {}; + /** * Finds a user given their email address that we have for them in the system * and ensures that the retuned user matches the password passed in as well. @@ -82,8 +129,8 @@ UserSchema.options.toObject.transform = (doc, ret, options) => { * @param {string} password - password to match against the found user * @param {Function} done [description] */ -UserSchema.statics.findLocalUser = function(email, password) { - return User.findOne({ +UserService.findLocalUser = (email, password) => { + return UserModel.findOne({ profiles: { $elemMatch: { id: email, @@ -120,13 +167,13 @@ UserSchema.statics.findLocalUser = function(email, password) { * @param {String} srcUserID id of the user to which is the source of the merge * @return {Promise} resolves when the users are merged */ -UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) { +UserService.mergeUsers = (dstUserID, srcUserID) => { let srcUser, dstUser; return Promise .all([ - User.findOne({id: dstUserID}).exec(), - User.findOne({id: srcUserID}).exec() + UserModel.findOne({id: dstUserID}).exec(), + UserModel.findOne({id: srcUserID}).exec() ]) .then((users) => { dstUser = users[0]; @@ -147,8 +194,8 @@ UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) { * @param {Object} profile - User social/external profile * @param {Function} done [description] */ -UserSchema.statics.findOrCreateExternalUser = function(profile) { - return User +UserService.findOrCreateExternalUser = (profile) => { + return UserModel .findOne({ profiles: { $elemMatch: { @@ -163,7 +210,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) { } // The user was not found, lets create them! - user = new User({ + user = new UserModel({ displayName: profile.displayName, roles: [], profiles: [ @@ -178,7 +225,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) { }); }; -UserSchema.statics.changePassword = function(id, password) { +UserService.changePassword = (id, password) => { return new Promise((resolve, reject) => { bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { if (err) { @@ -189,7 +236,7 @@ UserSchema.statics.changePassword = function(id, password) { }); }) .then((hashedPassword) => { - return User.update({id}, { + return UserModel.update({id}, { $set: { password: hashedPassword } @@ -202,9 +249,9 @@ UserSchema.statics.changePassword = function(id, password) { * @param {Array} users Users to create * @return {Promise} Resolves with the users that were created */ -UserSchema.statics.createLocalUsers = function(users) { +UserService.createLocalUsers = (users) => { return Promise.all(users.map((user) => { - return User + return UserService .createLocalUser(user.email, user.password, user.displayName); })); }; @@ -216,7 +263,7 @@ UserSchema.statics.createLocalUsers = function(users) { * @param {String} displayName name of the display user * @param {Function} done callback */ -UserSchema.statics.createLocalUser = function(email, password, displayName) { +UserService.createLocalUser = (email, password, displayName) => { if (!email) { return Promise.reject('email is required'); } @@ -235,7 +282,7 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) { return reject(err); } - let user = new User({ + let user = new UserModel({ displayName: displayName, password: hashedPassword, roles: [], @@ -263,8 +310,8 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) { * @param {String} id id of a user * @param {Function} done callback after the operation is complete */ -UserSchema.statics.disableUser = function(id) { - return User.update({ +UserService.disableUser = (id) => { + return UserModel.update({ id: id }, { $set: { @@ -278,8 +325,8 @@ UserSchema.statics.disableUser = function(id) { * @param {String} id id of a user * @param {Function} done callback after the operation is complete */ -UserSchema.statics.enableUser = function(id) { - return User.update({ +UserService.enableUser = (id) => { + return UserModel.update({ id: id }, { $set: { @@ -294,8 +341,16 @@ UserSchema.statics.enableUser = function(id) { * @param {String} role role to add * @param {Function} done callback after the operation is complete */ -UserSchema.statics.addRoleToUser = function(id, role) { - return User.update({ +UserService.addRoleToUser = (id, role) => { + + // Check to see if the user role is in the allowable set of roles. + if (USER_ROLES.indexOf(role) === -1) { + + // User role is not supported! Error out here. + return Promise.reject(new Error(`role ${role} is not supported`)); + } + + return UserModel.update({ id: id }, { $addToSet: { @@ -310,8 +365,8 @@ UserSchema.statics.addRoleToUser = function(id, role) { * @param {String} role role to remove * @param {Function} done callback after the operation is complete */ -UserSchema.statics.removeRoleFromUser = function(id, role) { - return User.update({ +UserService.removeRoleFromUser = (id, role) => { + return UserModel.update({ id: id }, { $pull: { @@ -324,21 +379,50 @@ UserSchema.statics.removeRoleFromUser = function(id, role) { * Finds a user with the id. * @param {String} id user id (uuid) */ -UserSchema.statics.findById = function(id) { - return User.findOne({id}); +UserService.findById = (id) => { + return UserModel.findOne({id}); }; /** * Finds users in an array of idd. * @param {Array} ids array of user identifiers (uuid) */ -UserSchema.statics.findByIdArray = function(ids) { - return User.find({ +UserService.findByIdArray = (ids) => { + return UserModel.find({ 'id': {$in: ids} }); }; -const User = mongoose.model('User', UserSchema); +/** + * Finds a user using a value which gets compared using a prefix match against + * the user's email address and/or their display name. + * @param {String} value value to search by + * @return {Promise} + */ +UserService.search = (value) => { + return UserModel.find({ + $or: [ -module.exports = User; -module.exports.Schema = UserSchema; + // Search by a prefix match on the displayName. + { + 'displayName': { + $regex: new RegExp(`^${value}`), + $options: 'i' + } + }, + + // Search by a prefix match on the email address. + { + 'profiles': { + $elemMatch: { + id: { + $regex: new RegExp(`^${value}`), + $options: 'i' + }, + provider: 'local' + } + } + } + ] + }); +}; diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 4665f4e52..71407300a 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -11,28 +11,9 @@ router.get('/', (req, res, next) => { limit = 50 // Total Per Page } = req.query; - let q = { - $or: [ - { - 'displayName': { - $regex: new RegExp(`^${value}`), - $options: 'i' - }, - 'profiles': { - $elemMatch: { - id: { - $regex: new RegExp(`^${value}`), - $options: 'i' - }, - provider: 'local' - } - } - } - ] - }; - Promise.all([ - User.find(q) + User + .search(value) .sort({[field]: (asc === 'true') ? 1 : -1}) .skip((page - 1) * limit) .limit(limit), @@ -63,11 +44,12 @@ router.get('/', (req, res, next) => { }); router.post('/:user_id/role', (req, res, next) => { - User.addRoleToUser(req.params.user_id, req.body.role) - .then(role => { - res.send(role); - }) - .catch(next); + User + .addRoleToUser(req.params.user_id, req.body.role) + .then(role => { + res.send(role); + }) + .catch(next); }); module.exports = router;