commit 834c66b2162a9a67c5eb0a0f6f4b21f2a9a58d9d Author: Mike Fitzpatrick Date: Fri Mar 2 03:00:51 2018 -0500 Initial commit diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..ce27f79 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +}; \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..95d3563 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..8991269 --- /dev/null +++ b/app.js @@ -0,0 +1,31 @@ +var express = require('express'); +var logger = require('morgan'); +var bodyParser = require('body-parser'); + +var auth = require('./routes/auth'); +var geocache = require('./routes/geocache'); +var profiles = require('./routes/profiles'); +var roles = require('./routes/roles'); +var users = require('./routes/users'); + +var app = express(); + +app.use(logger('dev')); +app.use(bodyParser.json({ limit: '5mb' })); +app.use(bodyParser.urlencoded({ extended: false })); + +app.use('/auth', auth); +app.use('/geocache', geocache); +app.use('/profiles', profiles); +app.use('/roles', roles); +app.use('/users', users); + +// catch 404 and forward to error handler +app.use( function(req, res, next) { + console.log('[App::use] 404: Not Found', { args: arguments }) + var err = new Error('Not Found', { args: arguments }); + err.status = 404; + next(err); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..0a28e49 --- /dev/null +++ b/bin/www @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('gcsdb-api:server'); +var http = require('http'); +var mongoose = require('mongoose'); + +/** + * Connect to the Mongo DB + */ +mongoose.connect('mongodb://localhost:27017/gcsdb', (err) => { + if(err) { + throw new Error(err); + } else { + + } +}); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..9b40db0 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,21 @@ +const gulp = require('gulp'); +const mocha = require('gulp-mocha'); +const nodemon = require('gulp-nodemon'); + +gulp.task('default', () => { + nodemon({ + script: './bin/www', + ext: 'js', + tasks: ['mocha'], + env: { 'NODE_ENV': 'development' } + }) +}); + +gulp.task('mocha', () => { + gulp.src(process.cwd() + '/tests/index.js', {read: false}) + .pipe( + mocha({ + reporter: 'nyan' + }) + ); +}); \ No newline at end of file diff --git a/models/detail.js b/models/detail.js new file mode 100644 index 0000000..6693c02 --- /dev/null +++ b/models/detail.js @@ -0,0 +1,22 @@ +const Mongoose = require('mongoose'); + +module.exports = new Mongoose.Schema({ + "about": { type: String }, + "age": { type: Number, index: true }, + "body": { type: String }, + "ethnicity": { type: String }, + "gender": { type: String }, + "height": { type: String }, + "looking": { type: String }, + "name": { type: String, index: true }, + "pic": { + "detail": { type: String, default: "profile/default_detail.png" }, + "thumb": { type: String, default: "profile/default_thumbnail.png" } + }, + "position": { type: String }, + "pronouns": { type: String }, + "weight": { type: Number }, + "status": { type: String }, + "tested": { type: Date }, + "tribe": { type: String } +}); diff --git a/models/geocache.js b/models/geocache.js new file mode 100644 index 0000000..1040aeb --- /dev/null +++ b/models/geocache.js @@ -0,0 +1,425 @@ +const mongoose = require('mongoose'); +const GoogleMaps = require('@google/maps').createClient({ + key: 'AIzaSyCvpBGztvxtRUNigOW9f0GXVRWlukJZsps' +}); + +const Schema = mongoose.Schema; + +const GeocacheSchema = new Schema({ + key: { + type: String, + required: true, + unique: true + }, + formatted: { + type: String, + required: true + }, + loc: { + type: { + type: String, + default: 'Point' + }, + coordinates: [{ + type: Number, + default: [0, 0] + }] + }, + georesult: { + type: Schema.Types.Mixed + } +}); + +const Conversion = { + kilometersToMeters: (distance) => { + return parseFloat(distance * 1000); + }, + + kilometersToMiles: (distance) => { + return parseFloat(distance / 1.60934); + }, + + kilometersToNauticalMiles: (distance) => { + return parseFloat(distance * 0.539957); + }, + + metersToKilometers: (distance) => { + return parseFloat(distance / 1000); + }, + + metersToMiles: (distance) => { + return parseFloat(distance / 1609.34); + }, + + milesToKilometers: (distance) => { + return parseFloat(distance * 1.60934); + }, + + milesToMeters: (distance) => { + return parseFloat(distance * 1609.34); + }, + + milesToNauticalMiles: (distance) => { + return parseFloat(distance * 0.868976); + }, + + nauticalMilesToMeters: (distance) => { + return parseFloat(distance / 0.868976); + }, + + nauticalMilesToMiles: (distance) => { + return parseFloat(distance / 0.868976); + }, + + nauticalMilesToKilometers: (distance) => { + return parseFloat(distance * 1.852000674128); + } +}; + +GeocacheSchema.index({ name: 1, loc: '2dsphere' }); + +const GeocacheModel = mongoose.model('geocache', GeocacheSchema); + +function distanceBetween (geoJSON1, geoJSON2, unit = 'mi') { + var radlat1 = Math.PI * geoJSON1.coordinates[1]/180; + var radlat2 = Math.PI * geoJSON2.coordinates[1]/180; + var theta = geoJSON1.coordinates[0] - geoJSON2.coordinates[0]; + var radtheta = Math.PI * theta/180; + var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); + dist = Math.acos(dist); + dist = dist * 180/Math.PI; + dist = dist * 60 * 1.1515; /* miles between */ + if (unit == "km") { dist = Conversion.metersToKilometers(Conversion.milesToMeters(dist)); } + if (unit == "m") { dist = dist * 1.609344; } + if (unit == "n") { dist = dist * 0.8684; } + return dist; +} + +function queryGeodataApi (query, callback) { + GoogleMaps.geocode({ + address: query + }, function(err, response) { + if (err) { + console.error('[GeocacheModel<>] Address Geocoding Error', { address: query, response: response }); + callback(null, err, null); + } + + if (response.json && Array.isArray(response.json.results)) { + var data = { + key: sanitizeNameForKey(query), + formatted: response.json.results[0].formatted_address, + georesult: response.json.results[0], + loc: { + type: 'Point', + coordinates: [ + response.json.results[0].geometry.location.lng, + response.json.results[0].geometry.location.lat + ] + } + }; + callback(null, null, data); + } + }); +} + +function sanitizeNameForKey (name) { + var key = name.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,''); + key = key.replace(/\s{2,}/g,' '); + key = key.trim(); + key = key.toLowerCase(); + return key; +} + +module.exports = { + + create: (e, geodata) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + var geocacheInstance = new GeocacheModel(geodata); + geocacheInstance.save((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + cb('create', null, result); + }) + .catch((err) => { + cb('create', err, null); + }); + }, + + conversion: Conversion, + + find: (e, searchText) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + GeocacheModel.findOne({ key: sanitizeNameForKey(searchText) }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + cb('find', null, result); + }) + .catch((err) => { + cb('find', err, null); + }); + }, + + findLike: (e, searchText) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + GeocacheModel.find({ key: new RegExp('.*' + sanitizeNameForKey(searchText) + '.*', "i") }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + cb('findLike', null, result); + }) + .catch((err) => { + cb('findLike', err, null); + }); + }, + + findNear: (e, lng, lat, distance) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + var point = { + type: 'Point', + coordinates: [ lng, lat ] + }; + + var opts = { + spherical: true, + maxDistance: Conversion.milesToMeters(distance) + }; + + const promise = new Promise((resolve, reject) => { + GeocacheModel.geoNear(point, opts, (err, results) => { + if (err) { + reject(err); + } + + if (results) { + resolve(results); + } + }); + }); + + promise.then((result) => { + cb('findNear', null, result); + }) + .catch((err) => { + cb('findNear', err, null); + }); + }, + + getGeo: (e, id) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + + const promise = new Promise((resolve, reject) => { + GeocacheModel.findById(id, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + cb('getGeos', null, result); + }) + .catch((err) => { + cb('getGeos', err, null); + }); + }, + + getGeos: (e) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + + const promise = new Promise((resolve, reject) => { + GeocacheModel.find({}, (err, results) => { + if (err) { + reject(err); + } + + if (results) { + resolve(results); + } + }); + }); + + promise.then((result) => { + cb('getGeos', null, result); + }) + .catch((err) => { + cb('getGeos', err, null); + }); + }, + + getGeoJSON: (e, searchText) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + + const promise = new Promise((resolve, reject) => { + queryGeodataApi(searchText, (f, err, geodata) => { + if (err) { + reject(err); + } + + if (geodata) { + resolve(geodata.loc); + } + }); + }); + + promise.then((result) => { + cb('getGeoJSON', null, result); + }) + .catch((err) => { + cb('getGeoJSON', err, null); + }); + }, + + getGeoJSONFromCache: (e, searchText) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + + const promise = new Promise((resolve, reject) => { + GeocacheModel.findOne({ key: sanitizeNameForKey(searchText) }, (err, result) => { + if (err || !result) { + queryGeodataApi(searchText, (f, err, geodata) => { + if (err) { + reject(err); + } + + if (geodata) { + let geocacheInstance = new GeocacheModel(geodata); + geocacheInstance.save((err, result) => { + if (err) { + console.error('[Geocache::getGeoJSON] There was an error creating the GeoJSON entry.', { err: err }); + } + }); + resolve(geodata.loc); + } + }); + } else if (result) { + resolve(result.loc); + } + }); + }); + + promise.then((result) => { + cb('getGeoJSONFromCache', null, result); + }) + .catch((err) => { + cb('getGeoJSONFromCache', err, null); + }); + }, + + populateFormatted: (e) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + GeocacheModel.find({}, (err, results) => { + if (err) { + reject(err); + } + + if (results) { + for (let i = 0; i < results.length; i++) { + if (!results[i].formatted) { + results[i].formatted = results[i].georesult.formatted_address; + GeocacheModel.findByIdAndUpdate(results[i]._id, { $set: results[i] }, (err, result) => { + if (err) console.error('There was an error populating the geocache formatted address.'); + if (result) console.log('The geocache entry was updated'); + }); + } + } + + resolve({ status: 'OK', message: 'The geocache entries have been updated.'}); + } + }); + }); + + promise.then((result) => { + cb('populateFormattedAddresses', null, result); + }) + .catch((err) => { + cb('populateFormattedAddresses', err, null); + }); + }, + + populateKeys: (e) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + GeocacheModel.find({}, (err, results) => { + if (err) { + reject(err); + } + + if (results) { + for (let i = 0; i < results.length; i++) { + if (!results[i].key) { + results[i].key = sanitizeNameForKey(results[i].name); + GeocacheModel.findByIdAndUpdate(results[i]._id, { $set: results[i] }, (err, result) => { + if (err) console.error('There was an error populating the geocache entry key.'); + if (result) console.log('The geocache entry was updated'); + }); + } + } + + resolve({ status: 'OK', message: 'The geocache entries have been updated.'}); + } + }); + }); + + promise.then((result) => { + cb('populateKeys', null, result); + }) + .catch((err) => { + cb('populateKeys', err, null); + }); + }, + + update: (e, id, geodata) => { + var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e; + const promise = new Promise((resolve, reject) => { + GeocacheModel.findByIdAndUpdate(id, { $set: geodata }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + cb('update', null, result); + }) + .catch((err) => { + cb('update', err, null); + }); + } +}; \ No newline at end of file diff --git a/models/message.js b/models/message.js new file mode 100644 index 0000000..d3654ae --- /dev/null +++ b/models/message.js @@ -0,0 +1,8 @@ +const Mongoose = require('mongoose'); + +module.exports = new Mongoose.Schema({ + "order" : { type: Number, default: 0 }, + "text" : { type: String }, + "image" : { type: String }, + "isUser" : { type: boolean, default: false, required: true, index: true } +}); diff --git a/models/profile.js b/models/profile.js new file mode 100644 index 0000000..8080300 --- /dev/null +++ b/models/profile.js @@ -0,0 +1,207 @@ +const fs = require('fs'); +const DetailSchema = require('../models/detail'); +const MessageSchema = require('../models/message'); +const Mongoose = require('mongoose'); +const ShortId = require('shortid'); + +const ATTACHMENT_STORE = '../images'; +const ATTACHMENT_STORE_PROFILE = '/profile'; +const ATTACHMENT_STORE_MESSAGE = '/message'; +const ATTACHMENT_SUFFIX_DETAIL = '_detail'; +const ATTACHMENT_SUFFIX_THUMBNAIL = '_thumbnail'; + +const ProfileSchema = new Mongoose.Schema({ + "order" : { type: Number, default: 0 }, + "details": { type: DetailSchema }, + "messages" : [ { type: ObjectId } ] +}); + +const ProfileModel = Mongoose.model('profiles', ProfileSchema); + +module.exports = { + + all: (e) => { + const promise = new Promise((resolve, reject) => { + var model = VendorModel + .find({}) + .sort({ order: 1 }) + .populate({ + path: 'details' + }) + .populate({ + path: 'messages', + select: 'order text image isUser', + options: { sort: { order: 1 } } + }) + .exec((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('all', null, result); + }) + .catch((err) => { + e.emit('all', err, null); + }); + }, + + create: (e, profiles) => { + var count = profiles.length; + var errors = []; + var results = []; + const promise = new Promise((resolve, reject) => { + for (let i = 0; i < profiles.length; i++) { + var profile = profiles[i]; + var profileInstance = new ProfileModel(profile); + + profileInstance.save((err, result) => { + if (err) { + count -= 1; + errors.push({ + profile: profile, + error: err + }); + if (count === 0) { + reject({ results: results, errors: errors }); + } + } + + if (result) { + count -= 1; + results.push(result); + if (count === 0) { + resolve({ results: results, errors: errors }); + } + } + }); + + } + + }); + + promise.then((result) => { + e.emit('create', null, result); + }) + .catch((err) => { + e.emit('create', err, null); + }); + }, + + delete: (e, id) => { + const promise = new Promise((resolve, reject) => { + ProfileModel.remove({ _id: id }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('delete', null, result); + }) + .catch((err) => { + e.emit('delete', err, null); + }); + }, + + find: (e, find) => { + const promise = new Promise((resolve, reject) => { + var query = ProfileModel.find(find); + + if (!find.select.length || (find.select.length && find.length.indexOf('details'))) { + query.populate({ + path: 'details' + }); + } + + if (!find.select.length || (find.select.length && find.length.indexOf('messages'))) { + query.populate({ + path: 'messages', + select: 'order text image isUser', + options: { sort: { order: 1 } } + }); + } + + query.exec((err, results) => { + if (err) { + reject(err); + } + + if (result) { + resolve(results); + } + }); + }); + + promise.then((result) => { + e.emit('find', null, result); + }) + .catch((err) => { + e.emit('find', err, null); + }); + }, + + get: (e, id) => { + const promise = new Promise((resolve, reject) => { + ProfileModel.find({ _id: id }) + .populate({ + path: 'details' + }) + .populate({ + path: 'messages', + select: 'order text image isUser', + options: { sort: { order: 1 } } + }) + .exec((err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('get', null, result); + }) + .catch((err) => { + e.emit('get', err, null); + }); + }, + + update: (e, id, profile) => { + const promise = new Promise((resolve, reject) => { + ProfileModel.findByIdAndUpdate(id, { $set: profile }, { new: true }, (err, result) => { + if (err) { + reject(err); + } + + if (result) { + resolve(result); + } + }); + }); + + promise.then((result) => { + e.emit('update', null, result); + }) + .catch((err) => { + e.emit('update', err, null); + }); + }, + + updateMessage: (e, profileId, messageId, data) => {} +}; diff --git a/models/reset.js b/models/reset.js new file mode 100644 index 0000000..15cfe5d --- /dev/null +++ b/models/reset.js @@ -0,0 +1,323 @@ +const Authentication = require('../modules/authentication'); +const Crypto = require('crypto'); +const Mongoose = require('mongoose'); +const Mailer = require('nodemailer'); +const Token = require('../modules/token'); + +const secret = 'Creepily hooking the gays up since 2008!'; + +function generateHmac (userId, expires) { + var string = String(userId) + '|' + String(expires); + return Crypto.createHmac('sha1', secret).update(string).digest('hex'); +} + +function sendMail (options, callback) { + // create reusable transporter object using the default SMTP transport + let transporter = Mailer.createTransport({ + host: 'mail.fitz.guru', + port: 587, + secure: false, // secure:true for port 465, secure:false for port 587 + auth: { + user: 'support@fitz.guru', + pass: 'NotSt@ff3d!' + } + }); + + callback = typeof callback === 'function' ? callback : (error, info) => { + if (error) { + return console.log(error); + } + + console.log('Message %s sent: %s', info.messageId, info.response); + }; + + // send mail with defined transport object + transporter.sendMail(options, callback); +} + +const ResetSchema = new Mongoose.Schema({ + user: { type: Schema.Types.ObjectId, required: true }, + expires: { type: Date, default: Date.now }, + used: { type: Boolean, default: false }, + updated_at: { type: Date, default: Date.now } +}); + +const ResetModel = Mongoose.model('resets', ResetSchema); + +module.exports = { + checkReset: (e, id, token, callback) => { + const promise = new Promise((resolve, reject) => { + ResetModel.findOne({ _id: id }, (err, result) => { + if (err) { + reject(err); + } + + if (result && (token === generateHmac(result.user, result.expires)) && (Date.now() <= result.expires)) { + resolve({ user: String(result.user), err: null }); + } else { + resolve({ user: false, err: 'The reset link has expired. Please request a new reset link from the login page.'}); + } + }); + }); + + promise.then((result) => { + if (e) { + e.emit('checkReset', null, result); + } else if (callback) { + callback(null, result.user); + } else { + return true; + } + }) + .catch((err) => { + if (e) { + e.emit('checkReset', err, null); + } else if (callback) { + callback(err, null); + } else { + return false; + } + }); + }, + + forceReset: (e, user, callback) => { + const promise = new Promise((forceResetMailResolve, forceResetMailReject) => { + const forceResetTokenPromise = new Promise((forceResetTokenResolve, forceResetTokenReject) => { + var tokenInstance = new ResetModel({ user: user._id, expires: (Date.now() + (72 * 60 * 60 * 1000)) }); + + tokenInstance.save((err, result) => { + if (err) { + forceResetTokenReject(err); + } + + if (result) { + forceResetTokenResolve(result); + } + }); + }); + + forceResetTokenPromise.then((result) => { + let resetLink = '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" ', + to: user.email, + subject: 'Mandatory Password Reset', + text: 'Mandatory Password Reset\r\r\r\r' + user.name.first + ',\r\rA Timberland GCS Vendor Database Administrator has initiated a password reset on your account.\r\rTo complete the action you will need to reset you password <<' + resetLink + '>>.\r\rIf you have any questions, please contact a system administrator.', + html: '

Mandatory Password Reset

' + user.name.first + ',

A Timberland GCS Vendor Database Administrator has initiated a password reset on your account.

To complete the action you will need to reset your password.

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" ', + to: user.email, + subject: 'New User Account Setup', + text: 'New User Account Setup\r\r\r\r' + user.name.first + ',\r\rA new user has been created for you on the Timberland GCS Vendor Database.\r\rYour username is: <<' + user.userName + '>>.\r\r To complete the setup you will need to create a password <<' + setPasswordLink + '>>.\r\rIf you have any questions, please contact a system administrator.', + html: '

New User Account Setup

' + 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" ', + to: user.email, + subject: 'Password Reset Request', + text: user.name.first + ',\r\rA request has been received to reset your password. If you initiated please visit <<' + resetLink + '>>.\r\rIf you did not initiate this request, you can safely ignore this email.', + html: '

Password Reset Request

' + 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;