From 834c66b2162a9a67c5eb0a0f6f4b21f2a9a58d9d Mon Sep 17 00:00:00 2001
From: Mike Fitzpatrick ' + user.name.first + ', A Timberland GCS Vendor Database Administrator has initiated a password reset on your account.Mandatory Password Reset
If you have any questions, please contact a system administrator.
' + }; + + sendMail(mail, (error, info) => { + if (error) { + console.log('[reset::forceReset] Message Send Error', { error: error }); + forceResetMailResolve({ success: false, message: 'There was an error sending the message', error: error }); + } + + if (info) { + console.log('[reset::forceReset] Message sent', { messageId: info.messageId, response: info.response, resetLink: resetLink }); + forceResetMailResolve({ success: true, message: 'Message ' + info.messageId + ' sent: ' + info.response + '.' }); + } + }); + }) + .catch((err) => { + console.log('[reset::forceReset] There was an error creating the reset token.', { err: err }); + forceResetMailReject(err); + }); + }); + + promise.then((result) => { + if (e) { + e.emit('forceReset', null, result); + } else if (callback) { + callback(result); + } else { + return true; + } + }) + .catch((err) => { + if (e) { + e.emit('forceReset', err, null); + } else if (callback) { + callback(result); + } else { + return false; + } + }); + }, + + getResets: (e) => { + const promise = new Promise((resolve, reject) => { + ResetModel.find({}, (err, results) => { + if (err) { + reject(err); + } + + if (results) { + resolve(results); + } + }); + }); + + promise.then((results) => { + e.emit('getResets', null, results); + }) + .catch((err) => { + e.emit('getResets', err, null); + }); + }, + + markUsed: (e, id) => { + const promise = new Promise((resolve, reject) => { + ResetModel.findByIdAndUpdate(id, { $set: { used: true } }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + if (e) { + e.emit('markUsed', null, result); + } else { + console.log('[ResetModel::markUsed] Password reset token used', { token: result }); + } + }) + .catch((err) => { + if (e) { + e.emit('markUsed', err, null); + } else { + console.error('[ResetModel::markUsed] Error marking password reset token used', { token: result }); + } + }); + }, + + sendNewUser: (e, user) => { + const promise = new Promise((newUserMailResolve, newUserMailReject) => { + const newUserTokenPromise = new Promise((newUserTokenResolve, newUserTokenReject) => { + var tokenInstance = new ResetModel({ user: user._id, expires: (Date.now() + (72 * 60 * 60 * 1000)) }); + + tokenInstance.save((err, result) => { + if (err) { + newUserTokenReject(err); + } + + if (result) { + newUserTokenResolve(result); + } + }); + }); + + newUserTokenPromise.then((result) => { + let setPasswordLink = 'https://timberland.bizdex.cloud/reset/' + encodeURIComponent(result._id) + '/' + encodeURIComponent(generateHmac(result.user, result.expires)); + + // setup email data with unicode symbols + let mail = { + from: '"GCS Vendor Database"' + user.name.first + ',
A new user has been created for you on the Timberland GCS Vendor Database.
Your username is: ' + user.userName + '.
To complete the setup you will need to create a password.If you have any questions, please contact a system administrator.
' + }; + + sendMail(mail, (error, info) => { + if (error) { + console.log('[reset::sendNewUser] Message Send Error', { error: error }); + newUserMailResolve({ success: false, message: 'There was an error sending the message', error: error }); + } + + if (info) { + console.log('[reset::sendNewUser] Message %s sent: %s', info.messageId, info.response); + newUserMailResolve({ success: true, message: 'Message ' + info.messageId + ' sent: ' + info.response + '.' }); + } + }); + }) + .catch((err) => { + console.log('[reset::sendNewUser] There was an error creating the reset token.', { err: err }); + newUserMailReject(err); + }); + }); + + promise.then((result) => { + if (e) { + e.emit('sendNewUser', null, result); + } else { + return true; + } + }) + .catch((err) => { + if (e) { + e.emit('sendNewUser', err, null); + } else { + return false; + } + }); + }, + + sendReset: (e, user) => { + + const resetTokenPromise = new Promise((resetTokenResolve, resetTokenReject) => { + + var tokenInstance = new ResetModel({ user: user._id, expires: (Date.now() + (30 * 60 * 1000)) }); + + tokenInstance.save((err, result) => { + if (err) { + resetTokenReject(err); + } + + if (result) { + resetTokenResolve(result); + } + }); + }); + + resetTokenPromise.then((data) => { + var token = generateHmac(data.user, data.expires); + var tokenId = data._id; + + const sendMailPromise = new Promise((sendMailResolve, sendMailReset) => { + + var resetLink = 'https://timberland.bizdex.cloud/reset/' + encodeURIComponent(tokenId) + '/' + encodeURIComponent(token); + + // setup email data with unicode symbols + var mail = { + from: '"GCS Vendor Database"' + user.name.first + ',
A request has been received to reset your password. If you initiated this request, click here to reset your password.
If you did not initiate this request, you can safely ignore this email.
' + }; + + sendMail(mail, (err, info) => { + if (err) { + var error = { msg: '[reset::sendReset] There was an error sending the reset email.', err: err }; + console.log('[reset::sendReset] Message Send Error', { err: err }); + sendMailResolve({ success: false, message: 'There was an error requesting the password reset.', error: error }); + } + + if (info) { + var message = 'Message ' + info.messageId + ' sent: ' + info.response + '.'; + console.log('[reset::sendReset] ' + message); + sendMailResolve({ success: true, message: 'The password reset request was successfully completed.', response: message }); + } + }); + }); + + sendMailPromise.then((result) => { + e.emit('sendReset', null, result); + }) + .catch((err) => { + e.emit('sendReset', err, null); + }); + }) + .catch((err) => { + var error = { msg: '[reset::sendReset] There was an error creating the reset token.', err: err }; + console.log(error.msg, { err: err }); + e.emit('sendReset', null, { success: false, message: 'There was an error requesting the password reset.', error: error }); + }); + } +}; diff --git a/models/role.js b/models/role.js new file mode 100644 index 0000000..4533106 --- /dev/null +++ b/models/role.js @@ -0,0 +1,153 @@ +const Mongoose = require('mongoose'); + +const RoleSchema = new Mongoose.Schema({ + name: { type: String, required: true, unique: true }, + description: { type: String }, + add: { type: Boolean, default: false }, + delete: { type: Boolean, default: false }, + edit: { type: Boolean, default: false }, + manage: { type: Boolean, default: false }, + super: { type: Boolean, default: false }, + view: { type: Boolean, default: true }, + disabled: { type: Boolean, default: false }, + order: { type: Number, default: 1 }, + updated_at: { type: Date, default: Date.now } +}); + +RoleSchema.pre('findOneAndUpdate', function (next) { + this.update({}, { $set: { updated_at: Date.now() } }); + next(); +}); + +const RoleModel = Mongoose.model('roles', RoleSchema); + +module.exports = { + canRole: (e, roleId, action, callback) => { + [initial, canElevate = false] = Array.isArray(action) ? action : [action]; + + RoleModel.findById(roleId, (err, result) => { + if (err) { + callback('There was an error querying roles', null); + } + + if (result) { + let permissions = result[initial] && canElevate ? { + hasPermission: result[initial], + canElevate: canElevate ? result[canElevate] : null + } : result[initial]; + callback(null, permissions); + } + }); + }, + + createRole: (e, role) => { + const promise = new Promise((resolve, reject) => { + var roleInstance = new RoleModel(role); + + roleInstance.save((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('createRole', null, result); + }) + .catch((err) => { + e.emit('createRole', err, null); + }); + }, + + getRoles: (e, query) => { + query = query || {}; + + const promise = new Promise((resolve, reject) => { + RoleModel + .find(query.find, query.select, query.options) + .exec((err, results) => { + if (err) { + reject(err); + } + + if (results) { + resolve(results); + } + }); + }); + + promise.then((results) => { + e.emit('getRoles', null, results); + }) + .catch((err) => { + e.emit('getRoles', err, null); + }); + }, + + getRole: (e, id) => { + const promise = new Promise((resolve, reject) => { + RoleModel.find({_id: id}, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('getRole', null, result); + }) + .catch((err) => { + e.emit('getRole', err, null); + }); + }, + + updateRole: (e, id, role) => { + const promise = new Promise((resolve, reject) => { + RoleModel.findByIdAndUpdate(id, { $set: role }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('updateRole', null, result); + }) + .catch((err) => { + e.emit('updateRole', err, null); + }); + }, + + deleteRole: (e, id) => { + const promise = new Promise((resolve, reject) => { + RoleModel.remove({ _id: id }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('deleteRole', null, result); + }) + .catch((err) => { + e.emit('deleteRole', err, null); + }); + } +}; diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..87361f7 --- /dev/null +++ b/models/user.js @@ -0,0 +1,703 @@ +const Authentication = require('../modules/authentication'); +const Mongoose = require('mongoose'); +const Reset = require('./reset'); +const Settings = require('./settings'); + +const UserSchema = new Mongoose.Schema({ + userName: { type: String, required: true, unique: true }, + password: { type: String }, + name: { first: { type: String, required: true }, last: { type: String, required: true } }, + title: String, + email: { type: String, required: true, unique: true }, + permission: { type: Schema.Types.ObjectId, ref: 'roles', required: true, default: '59e6e1ab9bd9c04c803a0bc0' }, + avatar: String, + settings: [ Settings.schema ], + forceReset: { type: Boolean, default: true }, + updated_at: { type: Date, default: Date.now } +}); + +UserSchema.pre('findOneAndUpdate', function (next) { + var self = this; + + this.update({}, { $set: { updated_at: Date.now() } }); + + if (this._update.$set.settings && this._update.$set.settings.length) { + + } + + if (this._update.$set.password && this._update.$set.confirmPassword && (this._update.$set.password == this._update.$set.confirmPassword)) { + delete this._update.$set.confirmPassword; + + if (this._update.$set.currentPassword) { + confirmPassword(this._conditions.userName, this._update.$set.currentPassword, (err, valid) => { + if (err || !valid) { + err = new Error({ success: false, message: 'There was an error validating the current password.', err: (err || null) }); + next(err); + } + + if (valid) { + delete this._update.$set.currentPassword; + hashPassword(this._update.$set.password, (err, hashedPassword) => { + self.update({}, { $set: { password: hashedPassword } }); + self.update({}, { $set: { forceReset: false } }); + next(); + }); + } + }); + } else { + hashPassword(this._update.$set.password, (err, hashedPassword) => { + self.update({}, { $set: { password: hashedPassword } }); + self.update({}, { $set: { forceReset: false } }); + next(); + }); + } + } else if (this._update.$set.password && this._update.$set.confirmPassword && (this._update.$set.password != this._update.$set.confirmPassword)) { + let err = new Error({ success: false, message: 'There was an error saving the updated password.'}); + next(err); + } else if (!this._update.$set.password && !this._update.$set.confirmPassword) { + next(); + } +}); + +UserSchema.post('save', function (err, res, next) { + if (err.name === 'MongoError' && err.code === 11000) { + next(new Error('There was a duplicate key error')); + } else if (err) { + next(err); + } else { + next(); + } +}); + +const UserModel = Mongoose.model('users', UserSchema); + +function hashPassword (password, callback) { + callback = callback || false; + + Authentication.hashPassword(password, (err, password) => { + if (err !== null) { + err = new Error({ success: false, message: 'There was an error hashing the updated password.', err: err }); + console.error('[updateUser:hashPassword] ', err); + + if (callback) { + callback(err, null); + } else { + return false; + } + } + + if (password) { + var result = password.toString('hex'); + + if (callback) { + callback(null, result); + } else { + return result; + } + } + }); +} + +function confirmPassword (username, passwordToValidate, callback) { + callback = callback || false; + + UserModel.findById({ userName: username }, (err, user) => { + if (err !== null) { + err = new Error({ success: false, message: 'There was an error locating the user.', err: (err || null) }); + console.error('[updateUser:confirmPassword] ', err); + + if (callback) { + callback(err, null); + } else { + return false; + } + } + + if (user) { + Authentication.verifyPassword(passwordToValidate, Buffer.from(storedUser.password, 'hex'), (err, valid) => { + if (err !== null || !valid) { + err = new Error({ success: false, message: (!err && !valid ? 'The current password was incorrect.' : 'There was an error attempting to validate the password.'), err: (err || null) }); + console.error('[updateUser:confirmPassword] ', { err: err, valid: valid }); + + if (callback) { + callback(err, null); + } else { + return false; + } + } + + if (valid) { + if (callback) { + callback(null, valid); + } else { + return true; + } + } + }); + } + }); +} + +module.exports = { + adminUpdatePassword: (e, username, password) => { + const promise = new Promise((resolve, reject) => { + UserModel.findOneAndUpdate({ userName: username }, { $set: password }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + if (e) { + e.emit('adminUpdatePassword', null, result); + } else { + return result; + } + }) + .catch((err) => { + if (e) { + e.emit('adminUpdatePassword', err, null); + } else { + return err; + } + }); + }, + + authenticateUser: (e, login, headers) => { + const promise = new Promise((resolve, reject) => { + var loginObject, user; + + UserModel + .findOne({ userName: login.userName }, 'userName name title email password permission avatar settings forceReset') + .populate('permission', 'name disabled manageRoles manageUsers manageCategories manageAppPreferences deleteVendor addVendorTag addVendorSample addVendorComment editVendor addNewVendor viewPrivateDetails viewPublicDetails') + .exec((err, result) => { + if (err) { + loginObject = { + status: 200, + authorized: false, + err: { + id: '005', + code: '[UMAU005]', + string: 'There was an error authenticating the user.' + } + } + console.log('[UserModel::authenticateUser] Error finding user', { err: err, username: login.userName }); + resolve(loginObject); + } + + if (result) { + user = result; + + if (user && !user.permission.disabled) { + if (user.forceReset) { + loginObject = { + status: 200, + authorized: false, + err: { + id: '003', + code: '[UMAU003]', + string: 'A password reset has been mandated. Please check your email for a password reset link or request a new one from the login screen.' + } + }; + resolve(loginObject); + } + else { + try { + Authentication.verifyPassword(login.password, Buffer.from(user.password, 'hex'), (err, valid) => { + if (err) { + console.log('[UserModel::authenticateUser] Error validating password', { err: err, user: user }); + reject(err); + } + + loginObject = { + status: 200, + authorized: valid + }; + + if (valid) { + loginObject.user = { + _id: user._id, + uid: user._id, + userName: user.userName, + name: user.name, + title: user.title, + email: user.email, + permission: user.permission, + settings: user.settings, + avatar: user.avatar + }; + + loginObject.timestamp = Date.now(); + + console.log('[UserModel::authenticateUser] User Validated', { user: user, loginObject: loginObject }); + resolve(loginObject); + } else { + loginObject.err = { + id: '002', + code: '[UMAU002]', + string: 'The user id or password you entered was invalid.' + }; + + console.log('[UserModel::authenticateUser] Invalid Password', { user: user, loginObject: loginObject }); + resolve(loginObject); + } + }); + } + catch (err) { + loginObject = { + status: 200, + authorized: false, + err: { + id: '004', + code: '[UMAU004]', + string: 'There was an error authenticating the user, please contact an administrator.' + } + }; + + console.log('[UserModel::authenticateUser] Error verifying password', { err: err, user: user }); + resolve(loginObject); + } + } + } + else if (user && user.permission.disabled) { + loginObject = { + status: 200, + authorized: false, + err: { + id: '000', + code: '[UMAU000]', + string: 'The user is not authorized, please contact an administrator.' + } + }; + console.log('[UserModel::authenticateUser] The user is disabled', { err: err, user: user }); + resolve(loginObject); + } + else { + loginObject = { + status: 200, + authorized: false, + err: { + id: '001', + code: '[UMAU001]', + string: 'The user id or password you entered was invalid.' + } + }; + console.log('[UserModel::authenticateUser] The user does not exist', { err: err, user: user }); + resolve(loginObject); + } + } + }); + }); + + promise.then((result) => { + e.emit('authenticateUser', null, result); + }) + .catch((err) => { + e.emit('authenticateUser', err, null); + }); + }, + + createUser: (e, user) => { + const promise = new Promise((resolve, reject) => { + var userInstance = new UserModel(user); + + userInstance.save((err, result) => { + console.log('createUser', { err: err, result: result, user: userInstance }); + + if (err) { + reject(err); + } + + if (result) { + Reset.sendNewUser(null, result); + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('createUser', null, result); + }) + .catch((err) => { + e.emit('createUser', err, null); + }); + }, + + createUserGod: (e, user) => { + const promise = new Promise((resolve, reject) => { + hashPassword(user.password, (err, hashedPassword) => { + if (err) { + reject(err); + } + + if (hashedPassword) { + user.password = hashedPassword; + + var userInstance = new UserModel(user); + + userInstance.save((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + } + }); + }); + + promise.then((result) => { + e.emit('createUserGod', null, result); + }) + .catch((err) => { + e.emit('createUserGod', err, null); + }); + }, + + createUserSetting: (e, userId, data) => { + const promise = new Promise((resolve, reject) => { + UserModel.findByIdAndUpdate(userId, { $push: { settings: data } }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('createUserSetting', null, result); + }) + .catch((err) => { + e.emit('createUserSetting', err, null); + }); + }, + + deleteUser: (e, id) => { + const promise = new Promise((resolve, reject) => { + UserModel.remove({ _id: id }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('deleteUser', null, result); + }) + .catch((err) => { + e.emit('deleteUser', err, null); + }); + }, + + findUser: (query, callback) => { + UserModel.findOne(query, (err, result) => { + if (err) { + callback(err, null); + } + + if (result) { + callback(null, result); + } + }); + }, + + forcePasswordReset: (e, id) => { + const promise = new Promise((resolve, reject) => { + UserModel.findByIdAndUpdate(id, { $set: { forceReset: true } }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + let resetPromise = new Promise((resetResolve, resetReject) => { + Reset.forceReset(null, result, (result) => { + if (result.success) { + resetResolve({ success: true, message: 'Force password reset initiated on the user.', result: result }); + } else { + resetResolve({ success: false, message: 'There was an error initiating the forced password reset.', result: result }); + } + }); + }); + + resetPromise.then((result) => { + resolve(result); + }) + .catch((err) => { + reject(err); + }); + } + }); + }); + + promise.then((result) => { + e.emit('forcePasswordReset', null, result); + }) + .catch((err) => { + e.emit('forcePasswordReset', err, null); + }); + }, + + getUsers: (e, query, restricted = true) => { + query = query || { find: {}, options: { sort: { 'name.last': 1, 'name.first': 1 }, limit: 0, skip: 0 }}; + + const promise = new Promise((resolve, reject) => { + var projection = 'userName name' + (restricted ? '' : ' title email permission avatar settings forceReset'); + var query = UserModel.find(query.find, projection, query.options); + + if (!restricted) { + query.populate('permission', 'name disabled manageRoles manageUsers manageCategories manageAppPreferences deleteVendor addVendorTag addVendorSample addVendorComment editVendor addNewVendor viewPrivateDetails viewPublicDetails') + } + + query.exec((err, results) => { + if (err) { + reject(err); + } + + if (results) { + resolve(results); + } + }); + }); + + promise.then((results) => { + if (e) { + e.emit('getUsers', null, results); + } else { + return results; + } + }) + .catch((err) => { + if (e) { + e.emit('getUsers', err, null); + } else { + return err; + } + }); + }, + + getUser: (e, id, restricted = true) => { + const promise = new Promise((resolve, reject) => { + var projection = 'userName name' + (restricted ? '' : ' title email permission avatar settings forceReset'); + var query = UserModel + .findById(id) + .projection(projection); + + if (!restricted) { + query.populate('permission', 'name disabled manageRoles manageUsers manageCategories manageAppPreferences deleteVendor addVendorTag addVendorSample addVendorComment editVendor addNewVendor viewPrivateDetails viewPublicDetails'); + } + + query.exec((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + if (e) { + e.emit('getUser', null, result); + } else { + return result; + } + }) + .catch((err) => { + if (e) { + e.emit('getUser', err, null); + } else { + return err; + } + }); + }, + + getUserSetting: (e, id, key) => { + const promise = new Promise((resolve, reject) => { + UserModel.findOne({ _id: id }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + for (let i = 0; i < result.settings.length; i ++) { + if (result.settings[i].key === key) resolve(result.settings[i]); + } + + reject({ method: "getUserSetting", error: "The specified settings key does not exist" }); + } + }); + }); + + promise.then((result) => { + e.emit('getUserSetting', null, result); + }) + .catch((err) => { + e.emit('getUserSetting', err, null); + }); + }, + + getUserSettings: (e, id) => { + const promise = new Promise((resolve, reject) => { + UserModel.findOne({_id: id}, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result.settings); + } + }); + }); + + promise.then((result) => { + e.emit('getUserSettings', null, result); + }) + .catch((err) => { + e.emit('getUserSettings', err, null); + }); + }, + + isUserNameUnique: (e, username) => { + const promise = new Promise((resolve, reject) => { + UserModel.findOne({ userName: username }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve({ unique: false, length: true }); + } else { + resolve({ unique: true, length: true }); + } + }); + }); + + promise.then((result) => { + e.emit('isUserNameUnique', null, result); + }) + .catch((err) => { + e.emit('isUserNameUnique', err, null); + }); + }, + + updateUser: (e, id, user) => { + const promise = new Promise((resolve, reject) => { + UserModel + .findByIdAndUpdate(id, { $set: user }, { new: true }) + .populate('permission', 'name disabled manageRoles manageUsers manageCategories manageAppPreferences deleteVendor addVendorTag addVendorSample addVendorComment editVendor addNewVendor viewPrivateDetails viewPublicDetails') + .exec((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('updateUser', null, result); + }) + .catch((err) => { + e.emit('updateUser', err, null); + }); + }, + + updateUserByUserName: (e, username, user) => { + const promise = new Promise((resolve, reject) => { + UserModel.update({ userName: username }, { $set: user }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('updateUserByUserName', null, result); + }) + .catch((err) => { + e.emit('updateUserByUserName', err, null); + }); + }, + + updateUserSetting: (e, userId, settingsId, data) => { + const promise = new Promise((resolve, reject) => { + var changed = {}; + + for (let property in data) { + changed['settings.$.' + property] = data[property]; + } + + UserModel.update({ _id: userId, 'settings._id': settingsId }, { $set: changed }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('updateUserSetting', null, result); + }) + .catch((err) => { + e.emit('updateUserSetting', err, null); + }); + }, + + updatePassword: (e, id, token, data) => { + const promise = new Promise((resolve, reject) => { + Reset.checkReset(null, id, token, (err, validatedId) => { + if (data.userId === validatedId) { + UserModel.findByIdAndUpdate(data.userId, { $set: { password: data.password, confirmPassword: data.confirmPassword, forceReset: false } }, { new: true }, (err, updated) => { + if (err) { + if (err.success === false) { + resolve(err); + } else { + reject(err); + } + } + + if (updated) { + Reset.markUsed(null, id); + resolve({ success: true, updated: updated }); + } + }); + } else { + resolve({ success: false, message: 'The password reset link is not valid. Please request a new link.'}); + } + }); + + }); + + promise.then((result) => { + e.emit('updatePassword', null, result); + }) + .catch((err) => { + e.emit('updatePassword', err, null); + }); + } +}; diff --git a/modules/authentication.js b/modules/authentication.js new file mode 100644 index 0000000..57514ce --- /dev/null +++ b/modules/authentication.js @@ -0,0 +1,92 @@ +var crypto = require('crypto'); + +// larger numbers mean better security, less +var config = { + digest: 'sha512', + // size of the generated hash + hashBytes: 32, + // larger salt means hashed passwords are more resistant to rainbow table, but + // you get diminishing returns pretty fast + saltBytes: 24, + // more iterations means an attacker has to take longer to brute force an + // individual password, so larger is better. however, larger also means longer + // to hash the password. tune so that hashing the password takes about a + // second + iterations: 233335 +}; + +/** + * Hash a password using Node's asynchronous pbkdf2 (key derivation) function. + * + * Returns a self-contained buffer which can be arbitrarily encoded for storage + * that contains all the data needed to verify a password. + * + * @param {!String} password + * @param {!function(?Error, ?Buffer=)} callback + */ +function hashPassword (password, callback) { + // generate a salt for pbkdf2 + crypto.randomBytes(config.saltBytes, function (err, salt) { + if (err) { + return callback(err); + } + + crypto.pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest, + function (err, hash) { + if (err) { + return callback(err); + } + + var combined = Buffer.alloc((hash.length + salt.length + 8)); + + // include the size of the salt so that we can, during verification, + // figure out how much of the hash is salt + combined.writeUInt32BE(salt.length, 0, true); + // similarly, include the iteration count + combined.writeUInt32BE(config.iterations, 4, true); + + salt.copy(combined, 8); + hash.copy(combined, salt.length + 8); + callback(null, combined); + } + ); + }); +} + +/** + * Verify a password using Node's asynchronous pbkdf2 (key derivation) function. + * + * Accepts a hash and salt generated by hashPassword, and returns whether the + * hash matched the password (as a boolean). + * + * @param {!String} password + * @param {!Buffer} combined Buffer containing hash and salt as generated by + * hashPassword. + * @param {!function(?Error, !boolean)} + */ +function verifyPassword (password, combined, callback) { + // extract the salt and hash from the combined buffer + var saltBytes = combined.readUInt32BE(0); + var hashBytes = combined.length - saltBytes - 8; + var iterations = combined.readUInt32BE(4); + var salt = combined.slice(8, saltBytes + 8); + var hash = combined.toString('binary', saltBytes + 8); + + // verify the salt and hash against the password + crypto.pbkdf2(password, salt, iterations, hashBytes, config.digest, function(err, verify) { + if (err && typeof callback === 'function') { + return callback(err, false); + } else if (err) { + return false; + } + + if (typeof callback === 'function') { + callback(null, verify.toString('binary') === hash); + } else { + return true; + } + }); +} + +exports.hashPassword = hashPassword; +exports.verifyPassword = verifyPassword; \ No newline at end of file diff --git a/modules/geocoder.js b/modules/geocoder.js new file mode 100644 index 0000000..4c74e8b --- /dev/null +++ b/modules/geocoder.js @@ -0,0 +1,49 @@ +var NodeGeocoder = require('node-geocoder'); + +var options = { + provider: 'google', + + // Optional depending on the providers + httpAdapter: 'https', // Default + apiKey: 'AIzaSyCvpBGztvxtRUNigOW9f0GXVRWlukJZsps', // for Mapquest, OpenCage, Google Premier + formatter: null // 'gpx', 'string', ... +}; + +var geocoder = NodeGeocoder(options); + + +exports.geocoder = geocoder; + + +// Using callback +// geocoder.geocode('29 champs elysée paris', function(err, res) { +// console.log(res); +// }); +// +// // Or using Promise +// geocoder.geocode('29 champs elysée paris') +// .then(function(res) { +// console.log(res); +// }) +// .catch(function(err) { +// console.log(err); +// }); + +// output : +// [{ +// latitude: 48.8698679, +// longitude: 2.3072976, +// country: 'France', +// countryCode: 'FR', +// city: 'Paris', +// zipcode: '75008', +// streetName: 'Champs-Élysées', +// streetNumber: '29', +// administrativeLevels: { +// level1long: 'Île-de-France', +// level1short: 'IDF', +// level2long: 'Paris', +// level2short: '75' +// }, +// provider: 'google' +// }] \ No newline at end of file diff --git a/modules/token.js b/modules/token.js new file mode 100644 index 0000000..479a6f2 --- /dev/null +++ b/modules/token.js @@ -0,0 +1,147 @@ +const JWT = require('jsonwebtoken'); +const Roles = require('../models/role'); + +const KEY = 'Th1s is THE s3cr3t kEy. It secures the t0ken!'; + +const Token = { + create: (payload, callback) => { + JWT.sign(payload, KEY, { expiresIn: '1h' }, callback); + }, + verify: (token, callback) => { + JWT.verify(token, KEY, callback); + } +}; + +function createAnonymousToken (e) { + Token.create({ user: null, permission: 0 }, (err, token) => { + if (err) { + e.emit('token:create', err, null); + } + + if (token) { + e.emit('token:create', null, token); + } + }); +} + +function createHmac (e, options) { + +} + +function createAuthenticatedToken (e, user, event = 'token:create') { + Token.create({ user: user.userName, permission: user.permission._id, uid: user.uid }, (err, token) => { + if (err) { + e.emit(event, err, null); + } + + if (token) { + e.emit(event, null, token); + } + }); +} + +function refreshToken (e, token) { + Token.verify(e, token, (err, decoded) => { + if (err) { + e.emit('token:refresh', err, null); + } + + if (decoded) { + createAuthenticatedToken( + e, + { user: decoded.user, permission: decoded.permission }, + 'token:refresh' + ); + } + }); +} + +function validateToken (e, token, callback) { + if (token) { + token = token.replace(/(bearer|basic)\s/i, ''); + + Token.verify(token, (err, decoded) => { + var result = { valid: !!decoded, data: decoded }; + + if (e) { + if (err || !result.valid) { + e.emit('token:validate', (err || result), null); + } + + e.emit('token:validate', null, result); + } + + else if (typeof callback === 'function') { + if (err) { + callback(err, null); + } + + callback(null, result); + } + }); + } else { + if (e) { + e.emit('token:validate', 'No session token passed.', null); + } + + else if (typeof callback === 'function') { + callback('No session token passed.', null); + } + } +} + +function verifyTokenAndUserThen (token, minimumPermission, callback) { + validateToken(null, token, (err, decoded) => { + if (err) { + callback(err, null); + } + + if (decoded && decoded.valid && decoded.data.permission >= minimumPermission) { + callback(null, decoded); + } else { + callback('User role does not have permission', null); + } + }); +} + +function verifyTokenAndRoleThen (token, action, callback, log = false) { + if (log) console.log('verifyTokenAndRoleThen', { token: token, action: action }); + validateToken(null, token, (err, decoded) => { + if (log) console.log('verifyTokenAndRoleThen::validateToken', { err: err, decoded: decoded.data }); + + if (err) { + callback('Session could not be validated.', null); + } + + let [initial, canElevateTo = false] = Array.isArray(action) ? action : [ action ]; + + if (log) { + console.log('Roles.canRole[' + initial + ']', Roles.canRole(null, decoded.data.permission, initial)); + console.log('Roles.canRole[' + canElevateTo + ']', Roles.canRole(null, decoded.data.permission, canElevateTo)); + } + + if (decoded && decoded.valid) { + Roles.canRole(null, decoded.data.permission, action, (err, result) => { + if (err) { + callback('There was an error verifying the role permissions.', null); + } + + if (result) { + decoded.hasPermission = result.hasPermission; + decoded.canElevate = result.canElevate; + callback(null, decoded); + } + }); + } + }); +} + + +module.exports = { + anonymous: createAnonymousToken, + create: createAuthenticatedToken, + refresh: refreshToken, + validate: validateToken, + verifyRoleThen: verifyTokenAndRoleThen, + verifyThen: verifyTokenAndRoleThen +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c3cc273 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "urge-api", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "@google/maps": "^0.4.5", + "body-parser": "~1.15.2", + "crypto": "0.0.3", + "debug": "~2.2.0", + "express": "~4.14.0", + "jsonwebtoken": "^7.3.0", + "moment": "^2.17.1", + "mongoose": "^4.7.4", + "morgan": "~1.7.0", + "multer": "^1.2.0", + "nodemailer": "~4.0.1", + "shortid": "~2.2.8", + "vcard-js": "^1.2.2" + }, + "devDependencies": { + "chai": "^3.5.0", + "gulp": "^3.9.1", + "gulp-mocha": "^3.0.1", + "gulp-nodemon": "^2.2.1", + "nodemon": "^1.11.0", + "request": "^2.79.0" + }, + "description": "Urge App API", + "main": "app.js", + "directories": { + "test": "tests" + }, + "repository": { + "type": "git", + "url": "gitolite@honey.fitz.guru:gcsdb-api.git" + }, + "keywords": [ + "Timberland", + "TBL", + "GCS", + "database" + ], + "author": "Mike Fitzpatrick (mike@fitz.guru)", + "license": "ISC" +} diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..bf26ecd --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,208 @@ +const EventEmitter = require('events'); +const Express = require('express'); +const ResetModel = require('../models/reset'); +const Router = Express.Router(); +const Token = require('../modules/token'); +const UserModel = require('../models/user'); + +Router.route('/login') + .post((req, res, next) => { + var AuthEvents = new EventEmitter(); + var data = req.body; + var headers = req.headers; + + AuthEvents.once('authenticateUser', (err, result) => { + console.log('[AuthRoute::POST::/auth/login] User Authenticated', { err: err, result: result }); + + login = result || {}; + login.status = result.status || 500; + + + if (err) { + login.err = err; + res.status(login.status).json(login); + } + + if (login) { + // Authenticated - create session + if (login.authorized) { + var TokenEvents = new EventEmitter(); + + TokenEvents.once('token:create', (err, token) => { + if (err) { + login = { + status: 500, + authorized: false, + err: err + }; + } + + if (token) { + login.token = token; + res.status(login.status).json(login); + } + }); + + Token.create(TokenEvents, login.user); + } + // Authentication failed + else { + res.status(login.status).json(login); + } + } + }); + + UserModel.authenticateUser(AuthEvents, data, headers); + }); + +Router.route('/reset/godmode') + .get((req, res) => { + var ResetEvents = new EventEmitter(); + + ResetEvents.once('getResets', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was a problem executing your request', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + ResetModel.getResets(ResetEvents); + }); + +Router.route('/reset/:id?/:token?') + .get((req, res) => { + var id = req.params.id ? decodeURIComponent(req.params.id) : false; + var token = req.params.token ? decodeURIComponent(req.params.token) : false; + var ResetEvents = new EventEmitter(); + + ResetEvents.once('checkReset', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error validating the password reset', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + ResetModel.checkReset(ResetEvents, id, token); + }) + .post((req, res) => { + var username = req.body.username; + var ResetEvents = new EventEmitter(); + + ResetEvents.once('sendReset', (err, result) => { + if (err) { + console.log('[routes/auth::sendReset] Error: ', { err: err }); + res.status(500).json({ message: 'There was an error requesting the password reset', err: err }); + } + + if (result) { + console.log('[routes/auth::sendReset] Success: ', { result: result }); + res.status(200).json(result); + } + }); + + UserModel.findUser({ userName: username }, (err, user) => { + ResetModel.sendReset(ResetEvents, user); + }); + }) + .put((req, res) => { + var id = req.params.id ? decodeURIComponent(req.params.id) : false; + var token = req.params.token ? decodeURIComponent(req.params.token) : false; + var data = req.body; + var UserEvents = new EventEmitter(); + + UserEvents.once('updatePassword', (err, result) => { + if (err) { + res.status(500).json({ message: err.message, err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.updatePassword(UserEvents, id, token, data); + }); + +Router.route('/godmode/:username?') + .patch((req, res) => { + var username = req.params.username ? decodeURIComponent(req.params.username) : false; + var password = req.body; + var UserEvents = new EventEmitter(); + + UserEvents.once('adminUpdatePassword', (err, result) => { + if (err) { + res.status(500).json({ message: err.message, err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.adminUpdatePassword(UserEvents, username, password); + }); + +Router.route('/session') + .get((req, res) => { + var AuthEvents = new EventEmitter(); + var token = req.get('authorization'); + + AuthEvents.once('token:validate', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error validating the token', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + Token.validate(AuthEvents, token); + }) + .post((req, res) => { + var AuthEvents = new EventEmitter(); + + AuthEvents.once('token:create', (err, token) => { + if (err) { + res.status(500).json({ + status: 500, + authorized: false, + err: err + }); + } + + if (token) { + res.status(200).json({ + status: 200, + authorized: false, + token: token + }); + } + }); + + Token.anonymous(AuthEvents); + }) + .put((req, res) => { + var AuthEvents = new EventEmitter(); + var token = req.get('authorization'); + + AuthEvents.once('token:refresh', (err, token) => { + if (err) { + res.status(500).json({ message: 'There was an error refreshing the token', err: err }); + } + + if (token) { + res.status(200).json(token); + } + }); + + Token.refresh(AuthEvents, token); + }); + +module.exports = Router; diff --git a/routes/geocache.js b/routes/geocache.js new file mode 100644 index 0000000..ee4775b --- /dev/null +++ b/routes/geocache.js @@ -0,0 +1,86 @@ +var EventEmitter = require('events'); +var Express = require('express'); +var GeocacheModel = require('../models/geocache'); +var Router = Express.Router(); +var Token = require('../modules/token'); + +function updateGeocache (req, res, next) { + Token.verifyThen(req.get('authorization'), 'manageAppPreferences', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var GeoEvents = new EventEmitter(); + var id = req.params.id; + var data = req.body; + + GeoEvents.once('update', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get geodata' + (id ? ' for id: ' + id : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + GeocacheModel.update(GeoEvents, id, geodata); + }); +} + +Router.route('/populate/:field') + .get((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'manageAppPreferences', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var GeoEvents = new EventEmitter(); + var method = 'populate' + (req.params.field[0].toUpperCase() + req.params.field.substring(1)); + + GeoEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get geodata' + (id ? ' for id: ' + id : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + GeocacheModel[method](GeoEvents); + }); + }); + +Router.route('/:id?') + .get((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'viewPublicDetails', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var GeoEvents = new EventEmitter(); + var id = req.params.id || false; + var method = id ? 'getGeo' : 'getGeos'; + + GeoEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get geodata' + (id ? ' for id: ' + id : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + GeocacheModel[method](GeoEvents, id || null); + }); + }) + .patch( updateGeocache ) + .post( updateGeocache ) + .put( updateGeocache ); + +module.exports = Router; diff --git a/routes/profiles.js b/routes/profiles.js new file mode 100644 index 0000000..53eb3de --- /dev/null +++ b/routes/profiles.js @@ -0,0 +1,224 @@ +var EventEmitter = require('events'); +var Express = require('express'); +var Profiles = require('../models/profile'); +var Router = Express.Router(); +var Token = require('../modules/token'); + +function update (req, res, next) { + Token.verifyThen(req.get('authorization'), 'edit', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var id = req.params.id; + var data = req.body; + + if (!id || !data) { + res.status(500).json({ message: 'No profile id or data specified.', err: err }); + return; + } + + ProfileEvents.once('update', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not update profile id: ' + id, err: err}); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.update(ProfileEvents, id, data); + }); +} + +function updateMessage (req, res, next) { + Token.verifyThen(req.get('authorization'), 'edit', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var profileId = req.params.profileId; + var messageId = req.params.messageId; + var data = req.body; + + if (!id || !data) { + res.status(500).json({ message: 'No profile id or data specified.', err: err }); + return; + } + + ProfileEvents.once('updateMessage', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not update profile id: ' + id, err: err}); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.updateMessage(ProfileEvents, profileId, messageId, data); + }); +} + +Router.route('/') + .post((req, res) => { +// Token.verifyThen(req.get('authorization'), 'add', (err, decoded) => { +// if (err) { +// res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); +// return; +// } + + var ProfileEvents = new EventEmitter(); + var profile = Array.isArray(req.body) ? req.body : [ req.body ]; + var multi = profile.length > 1; + + ProfileEvents.once('create', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not create profile' + (multi ? 's' : ''), err: err, profile: profile }); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.create(ProfileEvents, profile); +// }); + }); + +Router.route('/find/:limit?/:skip?/:min?/:max?/:pos?/:lkng?/:tribes?/:ethnos?') + .get((req, res) => { + Token.verifyThen(req.get('authorization'), 'view', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var find = processQueryParams(req.params); + + var query = { + find: find, + select: null, + options: { + limit: 0, + skip: 0, + sort: { 'order': 1 } + } + }; + + ProfileEvents.once('find', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error getting the vendor list', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.find(ProfileEvents, query); + }); + }); + +Router.route('/list/:limit?/:skip?/:min?/:max?/:pos?/:lkng?/:tribes?/:ethnos?') + .get((req, res) => { + Token.verifyThen(req.get('authorization'), 'view', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var find = processQueryParams(req.params); + + var query = { + find: find, + select: '_id order details.name details.pics.thumbnail', + options: { + limit: 0, + skip: 0, + sort: { 'order': 1 } + } + }; + + ProfileEvents.once('find', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error getting the vendor list', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.find(ProfileEvents, query); + }); + }); + +Router.route('/:profileId?/messages/:messageId?') + .delete((req, res) => { + + }) + .get((req, res) => { + + }) + .patch( updateMessage ) + .put( updateMessage ); + +Router.route('/:id?') + .delete( (req, res) => { + Token.verifyThen(req.get('authorization'), 'delete', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var id = req.params.id; + + ProfileEvents.once('delete', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not delete profile id: ' + id, err: err}); + } + + if (result) { + res.status(204).json({}); + } + }); + + Profiles.delete(ProfileEvents, id); + }); + }) + .get( (req, res) => { + Token.verifyThen(req.get('authorization'), 'view', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var ProfileEvents = new EventEmitter(); + var id = req.params.id || null; + + ProfileEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get profile' + (id ? '' : 's'), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + Profiles.get(ProfileEvents, id || null); + }); + }) + .patch( update ) + .put( update ); + +module.exports = Router; diff --git a/routes/roles.js b/routes/roles.js new file mode 100644 index 0000000..8d4c67b --- /dev/null +++ b/routes/roles.js @@ -0,0 +1,152 @@ +var Express = require('express'); +var Router = Express.Router(); +var EventEmitter = require('events'); +var RoleModel = require('../models/role'); +var Token = require('../modules/token'); + +function updateRole (req, res, next) { + Token.verifyThen(req.get('authorization'), 'super', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var RoleEvents = new EventEmitter(); + var id = req.params.id; + var data = req.body; + + RoleEvents.once('updateRole', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not update role id ' + id, err: err}); + } + + if (result) { + res.status(200).json(result); + } + }); + + RoleModel.updateRole(RoleEvents, id, data); + }); +} + +Router.route('/') + .post((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'super', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var RoleEvents = new EventEmitter(); + var role = req.body; + + RoleEvents.once('createRole', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not create role', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + RoleModel.createRole(RoleEvents, role); + }); + }); + +Router.route('/search/:find?') + .get((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'super', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var RoleEvents = new EventEmitter(); + + // Process parameters + var find = req.params.find ? decodeURIComponent(req.params.find) : false; + + if (find) { + find = { + 'name': new RegExp(find, 'i') + }; + } + + // Setup query object + var query = { + find: find || (req.query.find ? JSON.parse(decodeURIComponent(req.query.find)) : {}), + select: req.query.select ? decodeURIComponent(req.query.select) : null, + options: { + limit: req.query.limit ? parseInt(req.query.limit) : 0, + skip: req.query.ski ? parseInt(req.query.skip) : 0, + sort: req.query.sort ? JSON.parse(decodeURIComponent(req.query.sort)) : { 'value': 1 } + } + }; + + RoleEvents.once('getRoles', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error performing the role search', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + RoleModel.getRoles(RoleEvents, query); + }); + }); + +Router.route('/:id?') + .get( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'view', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var RoleEvents = new EventEmitter(); + var id = req.params.id || false; + var method = id ? 'getRole' : 'getRoles'; + + RoleEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get role' + (id ? '' : 's'), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + RoleModel[method](RoleEvents, id || null); + }); + }) + .put( updateRole ) + .patch( updateRole ) + .delete( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'super', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var RoleEvents = new EventEmitter(); + var id = req.params.id; + + RoleEvents.once('deleteRole', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not delete role id ' + id, err: err}); + } + + if (result) { + res.status(204).json({}); + } + }); + + RoleModel.deleteRole(RoleEvents, id); + }); + }); + +module.exports = Router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..0a36a9c --- /dev/null +++ b/routes/users.js @@ -0,0 +1,312 @@ +var EventEmitter = require('events'); +var Express = require('express'); +var Router = Express.Router(); +var Token = require('../modules/token'); +var UserModel = require('../models/user'); + +function updateUser (req, res, next) { + Token.verifyThen(req.get('authorization'), ['view', 'super'], (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var id = req.params.id; + var data = req.body; + + if (id === decoded.data.uid || decoded.canElevate) { + if (!decoded.canElevate) { + delete data.permission; + } + + UserEvents.once('updateUser', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not update user id ' + id, err: err}); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.updateUser(UserEvents, id, data); + } else { + res.status(403).json({ message: 'User not authorized to perform this action.' }); + } + }); +} + +function updateUserSetting (req, res, next) { + console.log('[UsersRoute::updateUserSetting]'); + console.log('req.params: ', req.params); + console.log('req.body: ', req.body); + + Token.verifyThen(req.get('authorization'), 'viewPublicDetails', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var userId = req.params.userId; + var settingsId = req.params.settingsId; + var data = req.body; + + + UserEvents.once('updateUserSetting', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not update setting' + (data.key ? ' key:' + data.key : 's') + ' for user ' + (userId ? userId : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.updateUserSetting(UserEvents, userId, settingsId, data); + }); +} + +Router.route('/') + .post((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'manageUsers', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var user = req.body; + + UserEvents.once('createUser', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not create user', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.createUser(UserEvents, user); + }); + }); + +Router.route('/search/:find?') + .get((req, res, next) => { + Token.verifyThen(req.get('authorization'), 'manageUsers', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + + // Process parameters + var find = req.params.find ? decodeURIComponent(req.params.find) : false; + + if (find) { + find = { + 'userName': new RegExp(find, 'i'), + 'name.last': new RegExp(find, 'i'), + 'name.first': new RegExp(find, 'i'), + 'email': new RegExp(find, 'i') + }; + } + + // Setup query object + var query = { + find: find || (req.query.find ? JSON.parse(decodeURIComponent(req.query.find)) : {}), + select: req.query.select ? decodeURIComponent(req.query.select) : null, + options: { + limit: req.query.limit ? parseInt(req.query.limit) : 0, + skip: req.query.ski ? parseInt(req.query.skip) : 0, + sort: req.query.sort ? JSON.parse(decodeURIComponent(req.query.sort)) : { 'userName': 1 } + } + }; + + UserEvents.once('getUsers', (err, result) => { + if (err) { + res.status(500).json({ message: 'There was an error performing the user search', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.getUsers(UserEvents, query); + }); + }); + +Router.route('/validate/:username?') + .get((req, res) => { + Token.verifyThen(req.get('authorization'), 'viewPublicDetails', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var username = req.params.username || ''; + + if (username && username.length >= 4) { + UserEvents.once('isUserNameUnique', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not validate username: ' + username, err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.isUserNameUnique(UserEvents, username); + } else { + res.status(200).json({ unique: null, length: false }); + } + }); + }); + +Router.route('/force-password-reset/:id') + .post( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'manageUsers', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var id = req.params.id; + + UserEvents.once('forcePasswordReset', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not force password reset for the user', err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.forcePasswordReset(UserEvents, id); + }); + }); + +Router.route('/:id/settings/:key?') + .get( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'viewPublicDetails', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var id = req.params.id; + var key = req.params.key || false; + var method = key ? 'getUserSetting' : 'getUserSettings'; + + UserEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get setting' + (key ? ' key:' + key : 's') + ' for user ' + (id ? id : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel[method](UserEvents, id, key); + }); + }); + +Router.route('/:userId/settings/:settingsId?') + .patch( updateUserSetting ) + .post( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'viewPublicDetails', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var userId = req.params.userId; + var data = req.body; + + UserEvents.once('createUserSetting', (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not create setting' + (data.key ? ' key:' + data.key : 's') + ' for user ' + (userId ? userId : ''), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel.createUserSetting(UserEvents, userId, data); + }); + }) + .put( updateUserSetting ); + +Router.route('/:id?') + .get( (req, res, next) => { + Token.verifyThen(req.get('authorization'), ['viewPublicDetails', 'manageUsers'], (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action. ' + err, err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var id = req.params.id || false; + var method = id ? 'getUser' : 'getUsers'; + + if ((id === decoded.data.uid && method === 'getUser') || decoded.canElevate) { + UserEvents.once(method, (err, result) => { + if (err) { + res.status(500).json({ message: 'Could not get user' + (id ? '' : 's'), err: err }); + } + + if (result) { + res.status(200).json(result); + } + }); + + UserModel[method](UserEvents, id || false, !decoded.canElevate); + } else { + res.status(403).json({ message: 'User not authorized to perform this action.' }); + } + }); + }) + .put( updateUser ) + .patch( updateUser ) + .delete( (req, res, next) => { + Token.verifyThen(req.get('authorization'), 'manageUsers', (err, decoded) => { + if (err) { + res.status(403).json({ message: 'User not authorized to perform this action.', err: err }); + return; + } + + var UserEvents = new EventEmitter(); + var id = req.params.id; + + if (id === decoded.data.uid) { + res.status(403).json({ message: 'You cannot delete yourself. Surely it isn\'t that bad?!' }); + return; + } + + UserEvents.once('deleteUser', (err, result) => { + if (err) { + res.status(500).json({message: 'Could not delete user id ' + id, err: err}); + } + + if (result) { + res.status(204).json({}); + } + }); + + UserModel.deleteUser(UserEvents, id); + }); + }); + +module.exports = Router;