From 26046a715e6faa2dee9284eaffe48870267b8d28 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Thu, 30 Apr 2020 01:37:08 +0600 Subject: [PATCH 01/11] BE#1: Configure and run server --- app.js | 24 ++++++++++++++++++------ configs/app.config.js | 7 +++++++ package.json | 4 ++-- server.js | 7 +++++++ 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 configs/app.config.js create mode 100644 server.js diff --git a/app.js b/app.js index 9bfb81b..85e2c26 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,24 @@ const express = require('express'); const logger = require('morgan'); - +const config = require('./configs/app.config'); const app = express(); -app.get('/', (req, res) => { - res.send('Hello CMS!') +app.use(logger('combined')); +app.use(function (req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Credentials', 'true'); + res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE'); + res.header('Access-Control-Expose-Headers', 'Content-Length'); + res.header('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With, Range'); + if (req.method === 'OPTIONS') { + return res.send(200); + } else { + return next(); + } +}); + +app.get(config.apiEndpointBase, (req, res) => { + res.send(`CMS API ${config.apiVersion}. RUN command for details.`) }); -app.listen(3000, () => { - console.log('Server started and listening on PORT: 3000'); -}); \ No newline at end of file +module.exports = app; \ No newline at end of file diff --git a/configs/app.config.js b/configs/app.config.js new file mode 100644 index 0000000..26d746d --- /dev/null +++ b/configs/app.config.js @@ -0,0 +1,7 @@ +module.exports = { + "port": 3000, + "appEndpoint": "http://localhost:3000", + "apiEndpointBase": "/api/v1", + "apiVersion": "v1", + "environment": "dev" +}; \ No newline at end of file diff --git a/package.json b/package.json index 8c9bc68..e035ecc 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "cms-backend-api", "version": "1.0.0", "description": "Simple CMS backend", - "main": "app.js", + "main": "server.js", "scripts": { - "start": "node app.js", + "start": "node server.js", "test": "mocha" }, "author": "Satyajit Dey", diff --git a/server.js b/server.js new file mode 100644 index 0000000..16ca4a1 --- /dev/null +++ b/server.js @@ -0,0 +1,7 @@ +const app = require('./app'); +const config = require('./configs/app.config'); +const port = config.port || 3000; + +app.listen(port, () => { + console.log(`CMS Engine started and listening port: ${port}`); +}); \ No newline at end of file From 196a7624804515d0515ad528a8a4720fa9ec55fd Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Thu, 30 Apr 2020 18:30:19 +0600 Subject: [PATCH 02/11] BE#1: Add User CRUD operations --- app.js | 12 +++-- configs/auth.config.js | 10 ++++ configs/db.config.js | 5 ++ controllers/users.controller.js | 55 ++++++++++++++++++++ models/users.model.js | 89 +++++++++++++++++++++++++++++++++ package.json | 2 + routes/users.route.js | 20 ++++++++ services/db.service.js | 30 +++++++++++ 8 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 configs/auth.config.js create mode 100644 configs/db.config.js create mode 100644 controllers/users.controller.js create mode 100644 models/users.model.js create mode 100644 routes/users.route.js create mode 100644 services/db.service.js diff --git a/app.js b/app.js index 85e2c26..3bfec30 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,12 @@ const express = require('express'); const logger = require('morgan'); -const config = require('./configs/app.config'); +const bodyParser = require('body-parser'); +const UsersRouter = require('./routes/users.route'); +const appConfig = require('./configs/app.config'); const app = express(); app.use(logger('combined')); + app.use(function (req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Credentials', 'true'); @@ -17,8 +20,11 @@ app.use(function (req, res, next) { } }); -app.get(config.apiEndpointBase, (req, res) => { - res.send(`CMS API ${config.apiVersion}. RUN command for details.`) +app.use(bodyParser.json()); +UsersRouter.routesConfig(app); + +app.get(appConfig.apiEndpointBase, (req, res) => { + res.send(`CMS API ${appConfig.apiVersion}. RUN command for details.`) }); module.exports = app; \ No newline at end of file diff --git a/configs/auth.config.js b/configs/auth.config.js new file mode 100644 index 0000000..cb26d62 --- /dev/null +++ b/configs/auth.config.js @@ -0,0 +1,10 @@ +module.exports = { + "secret": "myS33!!creeeT", + "expiration": 36000,//in SECONDS + "permissionLevels": { + "EDITOR": 1, + "REVIEWER": 4, + "PUBLISHER": 8, + "ADMIN": 512 + } +}; diff --git a/configs/db.config.js b/configs/db.config.js new file mode 100644 index 0000000..b58e23a --- /dev/null +++ b/configs/db.config.js @@ -0,0 +1,5 @@ +module.exports = { + "host": "localhost", + "port": 27017, + "dbName": "cmsdb" +}; \ No newline at end of file diff --git a/controllers/users.controller.js b/controllers/users.controller.js new file mode 100644 index 0000000..404eca4 --- /dev/null +++ b/controllers/users.controller.js @@ -0,0 +1,55 @@ +const UserModel = require('../models/users.model'); +const crypto = require('crypto'); + +exports.insert = (req, res) => { + let salt = crypto.randomBytes(16).toString('base64'); + let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); + req.body.password = salt + "$" + hash; + req.body.permissionLevel = 1; + UserModel.createUser(req.body) + .then((result) => { + res.status(201).send({id: result._id}); + }); +}; + +exports.list = (req, res) => { + let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10; + let page = 0; + if (req.query) { + if (req.query.page) { + req.query.page = parseInt(req.query.page); + page = Number.isInteger(req.query.page) ? req.query.page : 0; + } + } + UserModel.list(limit, page) + .then((result) => { + res.status(200).send(result); + }) +}; + +exports.getById = (req, res) => { + UserModel.findById(req.params.userId) + .then((result) => { + res.status(200).send(result); + }); +}; +exports.patchById = (req, res) => { + if (req.body.password) { + let salt = crypto.randomBytes(16).toString('base64'); + let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); + req.body.password = salt + "$" + hash; + } + + UserModel.patchUser(req.params.userId, req.body) + .then((result) => { + res.status(204).send({}); + }); + +}; + +exports.removeById = (req, res) => { + UserModel.removeById(req.params.userId) + .then((result) => { + res.status(204).send({}); + }); +}; \ No newline at end of file diff --git a/models/users.model.js b/models/users.model.js new file mode 100644 index 0000000..0992af2 --- /dev/null +++ b/models/users.model.js @@ -0,0 +1,89 @@ +const mongoose = require('../services/db.service').mongoose; + +const userSchema = new mongoose.Schema({ + firstName: String, + lastName: String, + email: String, + password: String, + permissionLevel: Number +}); + +// Rather than exposing Document's _id expose virtual id fields that is derived from _id +// Remember you can not query by virtual fields in Mongoose +// Details - https://mongoosejs.com/docs/tutorials/virtuals.html +userSchema.virtual('id').get(function () { + return this._id.toHexString(); +}); + +// Serialize virtual fields +userSchema.set('toJSON', { + virtuals: true +}); + +userSchema.findById = function (cb) { + return this.model('User').find({id: this.id}, cb); +}; + +const UsersModel = mongoose.model('User', userSchema); + + +exports.findByEmail = (email) => { + return UsersModel.find({email: email}); +}; +exports.findById = (id) => { + return UsersModel.findById(id) + .then((result) => { + result = result.toJSON(); + delete result._id; + delete result.__v; + return result; + }); +}; + +exports.createUser = (userData) => { + const user = new UsersModel(userData); + return user.save(); +}; + +exports.list = (perPage, page) => { + return new Promise((resolve, reject) => { + UsersModel.find() + .limit(perPage) + .skip(perPage * page) + .exec(function (err, users) { + if (err) { + reject(err); + } else { + resolve(users); + } + }) + }); +}; + +exports.patchUser = (id, userData) => { + return new Promise((resolve, reject) => { + UsersModel.findById(id, function (err, user) { + if (err) reject(err); + for (let i in userData) { + user[i] = userData[i]; + } + user.save(function (err, updatedUser) { + if (err) return reject(err); + resolve(updatedUser); + }); + }); + }) + +}; + +exports.removeById = (userId) => { + return new Promise((resolve, reject) => { + UsersModel.remove({_id: userId}, (err) => { + if (err) { + reject(err); + } else { + resolve(err); + } + }); + }); +}; diff --git a/package.json b/package.json index e035ecc..4d30327 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ ], "license": "ISC", "dependencies": { + "body-parser": "1.19.0", "express": "4.17.1", + "mongoose": "5.9.10", "morgan": "1.10.0", "passport": "0.4.1" }, diff --git a/routes/users.route.js b/routes/users.route.js new file mode 100644 index 0000000..6044d17 --- /dev/null +++ b/routes/users.route.js @@ -0,0 +1,20 @@ +const UsersController = require('../controllers/users.controller'); +const appConfig = require('../configs/app.config'); + +exports.routesConfig = function (app) { + app.post(`${appConfig.apiEndpointBase}/users`, [ + UsersController.insert + ]); + app.get(`${appConfig.apiEndpointBase}/users`, [ + UsersController.list + ]); + app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ + UsersController.getById + ]); + app.patch(`${appConfig.apiEndpointBase}/users/:userId`, [ + UsersController.patchById + ]); + app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ + UsersController.removeById + ]); +}; diff --git a/services/db.service.js b/services/db.service.js new file mode 100644 index 0000000..40608e5 --- /dev/null +++ b/services/db.service.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose'); +const config = require('../configs/db.config'); + +let count = 0; + +const options = { + autoIndex: false, // Don't build indexes + reconnectTries: 30, // Retry up to 30 times + reconnectInterval: 500, // Reconnect every 500ms + poolSize: 10, // Maintain up to 10 socket connections + // If not connected, return errors immediately rather than waiting for reconnect + bufferMaxEntries: 0, + //get rid off the depreciation errors + useNewUrlParser: true, + useUnifiedTopology: true + +}; +const connectWithRetry = () => { + console.log('MongoDB connection with retry') + mongoose.connect(`mongodb://${config.host}:${config.port}/${config.dbName}`, options).then(() => { + console.log('MongoDB is connected') + }).catch(err => { + console.log('MongoDB connection unsuccessful, retry after 5 seconds. ', ++count); + setTimeout(connectWithRetry, 5000) + }) +}; + +connectWithRetry(); + +exports.mongoose = mongoose; From ff3281a8f29512b0d58080538aae7579cc31a0ff Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Fri, 1 May 2020 21:05:13 +0600 Subject: [PATCH 03/11] BE#1: Add basic User validations --- controllers/users.controller.js | 3 +- middlewares/verify.user.middleware.js | 46 +++++++++++++++++++++++++++ models/users.model.js | 29 +++++++++-------- package.json | 1 + routes/users.route.js | 23 ++++++++++---- 5 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 middlewares/verify.user.middleware.js diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 404eca4..76e409b 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -31,8 +31,9 @@ exports.getById = (req, res) => { UserModel.findById(req.params.userId) .then((result) => { res.status(200).send(result); - }); + }).catch(err => res.status(400).send({'message': err.message})); }; + exports.patchById = (req, res) => { if (req.body.password) { let salt = crypto.randomBytes(16).toString('base64'); diff --git a/middlewares/verify.user.middleware.js b/middlewares/verify.user.middleware.js new file mode 100644 index 0000000..6c82da2 --- /dev/null +++ b/middlewares/verify.user.middleware.js @@ -0,0 +1,46 @@ +const {check, validationResult} = require('express-validator'); + +exports.registrationFieldValidationRules = () => { + return [ + // firstName should not be empty + check('firstName', 'firstName empty').notEmpty(), + // lastName should not be empty + check('lastName', 'lastName empty').notEmpty(), + // email should not be empty + check('email', 'email empty').notEmpty(), + // email must be valid + check('email', 'email is not valid').isEmail(), + // password should not be empty + check('password', 'password empty').notEmpty(), + // password must be at least 8 chars long + check('password', 'password must be at least 8 chars long').isLength({min: 8}) + ] +}; + +exports.updatePasswordValidationRules = () => { + return [ + check('email', 'email empty').notEmpty(), + // email must be valid + check('email', 'email is not valid').isEmail(), + // password should not be empty + check('password', 'password empty').notEmpty(), + // password must be at least 8 chars long + check('password', 'password must be at least 8 chars long').isLength({min: 8}) + ] +}; + +exports.validateRules = (req, res, next) => { + console.log('req.body: ' + req.body.firstName) + const errors = validationResult(req); + if (errors.isEmpty()) { + return next(); + } + + const extractedErrors = []; + errors.array().map(err => extractedErrors.push({[err.param]: err.msg})); + + return res.status(400).json({ + errors: extractedErrors, + }); +}; + diff --git a/models/users.model.js b/models/users.model.js index 0992af2..caaacd5 100644 --- a/models/users.model.js +++ b/models/users.model.js @@ -1,13 +1,17 @@ const mongoose = require('../services/db.service').mongoose; const userSchema = new mongoose.Schema({ - firstName: String, - lastName: String, - email: String, - password: String, - permissionLevel: Number + firstName: {type: String, required: true, trim: true}, + lastName: {type: String, required: true, trim: true}, + email: {type: String, required: true, trim: true}, + password: {type: String, required: true}, + permissionLevel: {type: Number, default: 1}, + createdAt: {type: Date, default: Date.now}, + lastModified: {type: Date, default: Date.now} }); +const Users = mongoose.model('User', userSchema); + // Rather than exposing Document's _id expose virtual id fields that is derived from _id // Remember you can not query by virtual fields in Mongoose // Details - https://mongoosejs.com/docs/tutorials/virtuals.html @@ -24,14 +28,13 @@ userSchema.findById = function (cb) { return this.model('User').find({id: this.id}, cb); }; -const UsersModel = mongoose.model('User', userSchema); - exports.findByEmail = (email) => { - return UsersModel.find({email: email}); + return Users.find({email: email}); }; + exports.findById = (id) => { - return UsersModel.findById(id) + return Users.findById(id) .then((result) => { result = result.toJSON(); delete result._id; @@ -41,13 +44,13 @@ exports.findById = (id) => { }; exports.createUser = (userData) => { - const user = new UsersModel(userData); + const user = new Users(userData); return user.save(); }; exports.list = (perPage, page) => { return new Promise((resolve, reject) => { - UsersModel.find() + Users.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { @@ -62,7 +65,7 @@ exports.list = (perPage, page) => { exports.patchUser = (id, userData) => { return new Promise((resolve, reject) => { - UsersModel.findById(id, function (err, user) { + Users.findById(id, function (err, user) { if (err) reject(err); for (let i in userData) { user[i] = userData[i]; @@ -78,7 +81,7 @@ exports.patchUser = (id, userData) => { exports.removeById = (userId) => { return new Promise((resolve, reject) => { - UsersModel.remove({_id: userId}, (err) => { + Users.remove({_id: userId}, (err) => { if (err) { reject(err); } else { diff --git a/package.json b/package.json index 4d30327..8b63960 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "body-parser": "1.19.0", "express": "4.17.1", + "express-validator": "6.4.0", "mongoose": "5.9.10", "morgan": "1.10.0", "passport": "0.4.1" diff --git a/routes/users.route.js b/routes/users.route.js index 6044d17..4b176f4 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,19 +1,30 @@ const UsersController = require('../controllers/users.controller'); +const VerifyUserMiddleware = require('../middlewares/verify.user.middleware'); const appConfig = require('../configs/app.config'); exports.routesConfig = function (app) { - app.post(`${appConfig.apiEndpointBase}/users`, [ - UsersController.insert - ]); + app.post(`${appConfig.apiEndpointBase}/users`, + //Pass validation rules + VerifyUserMiddleware.registrationFieldValidationRules(), [ + //Validate the rule(s) + VerifyUserMiddleware.validateRules + ], + //Pass the actual operation middleware + UsersController.insert); + app.get(`${appConfig.apiEndpointBase}/users`, [ UsersController.list ]); + app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ UsersController.getById ]); - app.patch(`${appConfig.apiEndpointBase}/users/:userId`, [ - UsersController.patchById - ]); + + app.patch(`${appConfig.apiEndpointBase}/users/:userId`, + VerifyUserMiddleware.updatePasswordValidationRules(), [ + VerifyUserMiddleware.validateRules, + ], UsersController.patchById); + app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ UsersController.removeById ]); From cde810791b28a75487d627148801b8e4f906fb6d Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 01:14:50 +0600 Subject: [PATCH 04/11] BE#1: Check email already exist before creating user and check the userid is valid before sharing user resources --- controllers/users.controller.js | 1 + middlewares/verify.user.middleware.js | 25 ++++++++++++++++++++++--- models/users.model.js | 4 ++-- routes/users.route.js | 13 ++++++++----- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 76e409b..0e3d7ea 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -21,6 +21,7 @@ exports.list = (req, res) => { page = Number.isInteger(req.query.page) ? req.query.page : 0; } } + UserModel.list(limit, page) .then((result) => { res.status(200).send(result); diff --git a/middlewares/verify.user.middleware.js b/middlewares/verify.user.middleware.js index 6c82da2..8527a74 100644 --- a/middlewares/verify.user.middleware.js +++ b/middlewares/verify.user.middleware.js @@ -1,3 +1,6 @@ +const mongoose = require('../services/db.service').mongoose; +const UserModel = require('../models/users.model'); + const {check, validationResult} = require('express-validator'); exports.registrationFieldValidationRules = () => { @@ -9,7 +12,7 @@ exports.registrationFieldValidationRules = () => { // email should not be empty check('email', 'email empty').notEmpty(), // email must be valid - check('email', 'email is not valid').isEmail(), + check('email', 'email is not valid').isEmail().normalizeEmail(), // password should not be empty check('password', 'password empty').notEmpty(), // password must be at least 8 chars long @@ -17,11 +20,28 @@ exports.registrationFieldValidationRules = () => { ] }; +exports.isEmailAlreadyExists = (req, res, next) => { + UserModel.findByEmail(req.body.email) + .then((result) => { + if (result) return res.status(409).json({message: 'Email already in use.'}); + }).catch(err => res.status(500).json({errors: err})); + + return next(); +}; + +exports.verifyUserId = (req, res, next) => { + if (req.params.userId && mongoose.Types.ObjectId.isValid(req.params.userId)) { + return next(); + } + + return res.sendStatus(404); +}; + exports.updatePasswordValidationRules = () => { return [ check('email', 'email empty').notEmpty(), // email must be valid - check('email', 'email is not valid').isEmail(), + check('email', 'email is not valid').isEmail().normalizeEmail(), // password should not be empty check('password', 'password empty').notEmpty(), // password must be at least 8 chars long @@ -30,7 +50,6 @@ exports.updatePasswordValidationRules = () => { }; exports.validateRules = (req, res, next) => { - console.log('req.body: ' + req.body.firstName) const errors = validationResult(req); if (errors.isEmpty()) { return next(); diff --git a/models/users.model.js b/models/users.model.js index caaacd5..da1f7e7 100644 --- a/models/users.model.js +++ b/models/users.model.js @@ -3,7 +3,7 @@ const mongoose = require('../services/db.service').mongoose; const userSchema = new mongoose.Schema({ firstName: {type: String, required: true, trim: true}, lastName: {type: String, required: true, trim: true}, - email: {type: String, required: true, trim: true}, + email: {type: String, unique: true, required: true, trim: true}, password: {type: String, required: true}, permissionLevel: {type: Number, default: 1}, createdAt: {type: Date, default: Date.now}, @@ -30,7 +30,7 @@ userSchema.findById = function (cb) { exports.findByEmail = (email) => { - return Users.find({email: email}); + return Users.findOne({email: email}); }; exports.findById = (id) => { diff --git a/routes/users.route.js b/routes/users.route.js index 4b176f4..0fa6f69 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -7,7 +7,8 @@ exports.routesConfig = function (app) { //Pass validation rules VerifyUserMiddleware.registrationFieldValidationRules(), [ //Validate the rule(s) - VerifyUserMiddleware.validateRules + VerifyUserMiddleware.validateRules, + VerifyUserMiddleware.isEmailAlreadyExists ], //Pass the actual operation middleware UsersController.insert); @@ -17,15 +18,17 @@ exports.routesConfig = function (app) { ]); app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ - UsersController.getById - ]); + VerifyUserMiddleware.verifyUserId + ], UsersController.getById + ); app.patch(`${appConfig.apiEndpointBase}/users/:userId`, VerifyUserMiddleware.updatePasswordValidationRules(), [ VerifyUserMiddleware.validateRules, + VerifyUserMiddleware.verifyUserId ], UsersController.patchById); app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ - UsersController.removeById - ]); + VerifyUserMiddleware.verifyUserId + ], UsersController.removeById); }; From c74babe8e8afcb3ef8954683ffc508cb961dc2ef Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 01:34:43 +0600 Subject: [PATCH 05/11] BE#1: Get access token on login --- app.js | 2 ++ configs/auth.config.js | 11 +++++------ controllers/auth.controller.js | 18 ++++++++++++++++++ package.json | 1 + routes/auth.route.js | 8 ++++++++ 5 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 controllers/auth.controller.js create mode 100644 routes/auth.route.js diff --git a/app.js b/app.js index 3bfec30..b87def8 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,7 @@ const express = require('express'); const logger = require('morgan'); const bodyParser = require('body-parser'); const UsersRouter = require('./routes/users.route'); +const AuthRouter = require('./routes/auth.route'); const appConfig = require('./configs/app.config'); const app = express(); @@ -22,6 +23,7 @@ app.use(function (req, res, next) { app.use(bodyParser.json()); UsersRouter.routesConfig(app); +AuthRouter.routesConfig(app); app.get(appConfig.apiEndpointBase, (req, res) => { res.send(`CMS API ${appConfig.apiVersion}. RUN command for details.`) diff --git a/configs/auth.config.js b/configs/auth.config.js index cb26d62..55bad88 100644 --- a/configs/auth.config.js +++ b/configs/auth.config.js @@ -1,10 +1,9 @@ module.exports = { - "secret": "myS33!!creeeT", - "expiration": 36000,//in SECONDS + "secret": "life is beautiful, when it is beautiful!", + "expiration": 18000,//30 MINUTES in SECONDS "permissionLevels": { - "EDITOR": 1, - "REVIEWER": 4, - "PUBLISHER": 8, - "ADMIN": 512 + "VIEWER": 1, + "EDITOR": 2, + "ADMIN": 128 } }; diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js new file mode 100644 index 0000000..b7eede8 --- /dev/null +++ b/controllers/auth.controller.js @@ -0,0 +1,18 @@ +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); +const {secret} = require('../configs/auth.config'); + +exports.login = (req, res) => { + try { + let refreshId = req.body.userId + secret; + let salt = crypto.randomBytes(16).toString('base64'); + let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); + req.body.refreshKey = salt; + let token = jwt.sign(req.body, secret); + let b = new Buffer(hash); + let refresh_token = b.toString('base64'); + res.status(201).send({accessToken: token, refreshToken: refresh_token}); + } catch (err) { + res.status(500).send({errors: err}); + } +}; diff --git a/package.json b/package.json index 8b63960..f41ab19 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "body-parser": "1.19.0", "express": "4.17.1", "express-validator": "6.4.0", + "jsonwebtoken": "8.5.1", "mongoose": "5.9.10", "morgan": "1.10.0", "passport": "0.4.1" diff --git a/routes/auth.route.js b/routes/auth.route.js new file mode 100644 index 0000000..1ed0a4c --- /dev/null +++ b/routes/auth.route.js @@ -0,0 +1,8 @@ +const AuthorizationController = require('../controllers/auth.controller'); + +exports.routesConfig = function (app) { + + app.post('/auth', [ + AuthorizationController.login + ]); +}; \ No newline at end of file From c2c12ecb15953ac76b877cb43f69416c7126cff3 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 02:23:33 +0600 Subject: [PATCH 06/11] BE#1: Add basic auth field validator --- middlewares/field.validate.middleware.js | 15 +++++++++++++++ middlewares/verify.auth.middleware.js | 12 ++++++++++++ middlewares/verify.user.middleware.js | 16 +--------------- routes/auth.route.js | 6 +++++- routes/users.route.js | 5 +++-- 5 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 middlewares/field.validate.middleware.js create mode 100644 middlewares/verify.auth.middleware.js diff --git a/middlewares/field.validate.middleware.js b/middlewares/field.validate.middleware.js new file mode 100644 index 0000000..d159d93 --- /dev/null +++ b/middlewares/field.validate.middleware.js @@ -0,0 +1,15 @@ +const {validationResult} = require('express-validator'); + +exports.validateRules = (req, res, next) => { + const errors = validationResult(req); + if (errors.isEmpty()) { + return next(); + } + + const extractedErrors = []; + errors.array().map(err => extractedErrors.push({[err.param]: err.msg})); + + return res.status(400).json({ + errors: extractedErrors, + }); +}; \ No newline at end of file diff --git a/middlewares/verify.auth.middleware.js b/middlewares/verify.auth.middleware.js new file mode 100644 index 0000000..1901dcd --- /dev/null +++ b/middlewares/verify.auth.middleware.js @@ -0,0 +1,12 @@ +const {check} = require('express-validator'); + +exports.authFieldValidationRules = () => { + return [ + // email should not be empty + check('email', 'email empty').notEmpty(), + // email must be valid + check('email', 'email is not valid').isEmail().normalizeEmail(), + // password should not be empty + check('password', 'password empty').notEmpty() + ] +}; diff --git a/middlewares/verify.user.middleware.js b/middlewares/verify.user.middleware.js index 8527a74..d58dfac 100644 --- a/middlewares/verify.user.middleware.js +++ b/middlewares/verify.user.middleware.js @@ -1,7 +1,7 @@ const mongoose = require('../services/db.service').mongoose; const UserModel = require('../models/users.model'); -const {check, validationResult} = require('express-validator'); +const {check} = require('express-validator'); exports.registrationFieldValidationRules = () => { return [ @@ -49,17 +49,3 @@ exports.updatePasswordValidationRules = () => { ] }; -exports.validateRules = (req, res, next) => { - const errors = validationResult(req); - if (errors.isEmpty()) { - return next(); - } - - const extractedErrors = []; - errors.array().map(err => extractedErrors.push({[err.param]: err.msg})); - - return res.status(400).json({ - errors: extractedErrors, - }); -}; - diff --git a/routes/auth.route.js b/routes/auth.route.js index 1ed0a4c..22b6cc6 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -1,8 +1,12 @@ const AuthorizationController = require('../controllers/auth.controller'); +const FieldValidateMiddleware = require("../middlewares/field.validate.middleware"); +const VerifyAuthMiddleware = require("../middlewares/verify.auth.middleware"); exports.routesConfig = function (app) { - app.post('/auth', [ + app.post('/auth/token', [ + VerifyAuthMiddleware.authFieldValidationRules(), + [FieldValidateMiddleware.validateRules], AuthorizationController.login ]); }; \ No newline at end of file diff --git a/routes/users.route.js b/routes/users.route.js index 0fa6f69..f7fbc48 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,5 +1,6 @@ const UsersController = require('../controllers/users.controller'); const VerifyUserMiddleware = require('../middlewares/verify.user.middleware'); +const FieldValidateMiddleware = require('../middlewares/field.validate.middleware'); const appConfig = require('../configs/app.config'); exports.routesConfig = function (app) { @@ -7,7 +8,7 @@ exports.routesConfig = function (app) { //Pass validation rules VerifyUserMiddleware.registrationFieldValidationRules(), [ //Validate the rule(s) - VerifyUserMiddleware.validateRules, + FieldValidateMiddleware.validateRules, VerifyUserMiddleware.isEmailAlreadyExists ], //Pass the actual operation middleware @@ -24,7 +25,7 @@ exports.routesConfig = function (app) { app.patch(`${appConfig.apiEndpointBase}/users/:userId`, VerifyUserMiddleware.updatePasswordValidationRules(), [ - VerifyUserMiddleware.validateRules, + FieldValidateMiddleware.validateRules, VerifyUserMiddleware.verifyUserId ], UsersController.patchById); From e53007442a791a37612b76663bbdc1fa3cbf31f3 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 03:21:27 +0600 Subject: [PATCH 07/11] BE#1: Return access-token for valid email and password --- middlewares/verify.auth.middleware.js | 27 +++++++++++++++++++++++++++ models/users.model.js | 1 + routes/auth.route.js | 7 ++++--- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/middlewares/verify.auth.middleware.js b/middlewares/verify.auth.middleware.js index 1901dcd..f0826c6 100644 --- a/middlewares/verify.auth.middleware.js +++ b/middlewares/verify.auth.middleware.js @@ -1,4 +1,6 @@ const {check} = require('express-validator'); +const crypto = require('crypto'); +const UserModel = require('../models/users.model'); exports.authFieldValidationRules = () => { return [ @@ -10,3 +12,28 @@ exports.authFieldValidationRules = () => { check('password', 'password empty').notEmpty() ] }; + +exports.matchEmailAndPassword = (req, res, next) => { + const {email, password} = req.body; + UserModel.findByEmail(email).then(user => { + if (!user) { + return res.status(404).send({errors: `User with email:<${email}> doesn't exist`}); + } else { + let passwordFields = user.password.split('$'); + let salt = passwordFields[0]; + let hash = crypto.createHmac('sha512', salt).update(password).digest("base64"); + if (hash === passwordFields[1]) { + req.body = { + userId: user._id, + email: user.email, + permissionLevel: user.permissionLevel, + provider: 'email', + name: `${user.firstName} ${user.lastName}`, + }; + return next(); + } else { + return res.status(400).send({errors: 'Invalid e-mail or password'}); + } + } + }); +}; \ No newline at end of file diff --git a/models/users.model.js b/models/users.model.js index da1f7e7..515b7f2 100644 --- a/models/users.model.js +++ b/models/users.model.js @@ -45,6 +45,7 @@ exports.findById = (id) => { exports.createUser = (userData) => { const user = new Users(userData); + return user.save(); }; diff --git a/routes/auth.route.js b/routes/auth.route.js index 22b6cc6..19f47a3 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -5,8 +5,9 @@ const VerifyAuthMiddleware = require("../middlewares/verify.auth.middleware"); exports.routesConfig = function (app) { app.post('/auth/token', [ - VerifyAuthMiddleware.authFieldValidationRules(), - [FieldValidateMiddleware.validateRules], - AuthorizationController.login + VerifyAuthMiddleware.authFieldValidationRules(), [ + FieldValidateMiddleware.validateRules, + VerifyAuthMiddleware.matchEmailAndPassword + ], AuthorizationController.login ]); }; \ No newline at end of file From de865d7906aeb07d985bedb53252b8dfd1d3d348 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 03:27:21 +0600 Subject: [PATCH 08/11] BE#1: Rename auth and user middlewares --- ...ddleware.js => auth.validation.middleware.js} | 0 ...dleware.js => field.validation.middleware.js} | 0 ...ddleware.js => user.validation.middleware.js} | 0 routes/auth.route.js | 8 ++++---- routes/users.route.js | 16 ++++++++-------- 5 files changed, 12 insertions(+), 12 deletions(-) rename middlewares/{verify.auth.middleware.js => auth.validation.middleware.js} (100%) rename middlewares/{field.validate.middleware.js => field.validation.middleware.js} (100%) rename middlewares/{verify.user.middleware.js => user.validation.middleware.js} (100%) diff --git a/middlewares/verify.auth.middleware.js b/middlewares/auth.validation.middleware.js similarity index 100% rename from middlewares/verify.auth.middleware.js rename to middlewares/auth.validation.middleware.js diff --git a/middlewares/field.validate.middleware.js b/middlewares/field.validation.middleware.js similarity index 100% rename from middlewares/field.validate.middleware.js rename to middlewares/field.validation.middleware.js diff --git a/middlewares/verify.user.middleware.js b/middlewares/user.validation.middleware.js similarity index 100% rename from middlewares/verify.user.middleware.js rename to middlewares/user.validation.middleware.js diff --git a/routes/auth.route.js b/routes/auth.route.js index 19f47a3..8d94c9b 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -1,13 +1,13 @@ const AuthorizationController = require('../controllers/auth.controller'); -const FieldValidateMiddleware = require("../middlewares/field.validate.middleware"); -const VerifyAuthMiddleware = require("../middlewares/verify.auth.middleware"); +const FieldValidateMiddleware = require("../middlewares/field.validation.middleware"); +const AuthValidationMiddleware = require("../middlewares/auth.validation.middleware"); exports.routesConfig = function (app) { app.post('/auth/token', [ - VerifyAuthMiddleware.authFieldValidationRules(), [ + AuthValidationMiddleware.authFieldValidationRules(), [ FieldValidateMiddleware.validateRules, - VerifyAuthMiddleware.matchEmailAndPassword + AuthValidationMiddleware.matchEmailAndPassword ], AuthorizationController.login ]); }; \ No newline at end of file diff --git a/routes/users.route.js b/routes/users.route.js index f7fbc48..eff9e35 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,15 +1,15 @@ const UsersController = require('../controllers/users.controller'); -const VerifyUserMiddleware = require('../middlewares/verify.user.middleware'); -const FieldValidateMiddleware = require('../middlewares/field.validate.middleware'); +const UserValidationMiddleware = require('../middlewares/user.validation.middleware'); +const FieldValidateMiddleware = require('../middlewares/field.validation.middleware'); const appConfig = require('../configs/app.config'); exports.routesConfig = function (app) { app.post(`${appConfig.apiEndpointBase}/users`, //Pass validation rules - VerifyUserMiddleware.registrationFieldValidationRules(), [ + UserValidationMiddleware.registrationFieldValidationRules(), [ //Validate the rule(s) FieldValidateMiddleware.validateRules, - VerifyUserMiddleware.isEmailAlreadyExists + UserValidationMiddleware.isEmailAlreadyExists ], //Pass the actual operation middleware UsersController.insert); @@ -19,17 +19,17 @@ exports.routesConfig = function (app) { ]); app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ - VerifyUserMiddleware.verifyUserId + UserValidationMiddleware.verifyUserId ], UsersController.getById ); app.patch(`${appConfig.apiEndpointBase}/users/:userId`, - VerifyUserMiddleware.updatePasswordValidationRules(), [ + UserValidationMiddleware.updatePasswordValidationRules(), [ FieldValidateMiddleware.validateRules, - VerifyUserMiddleware.verifyUserId + UserValidationMiddleware.verifyUserId ], UsersController.patchById); app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ - VerifyUserMiddleware.verifyUserId + UserValidationMiddleware.verifyUserId ], UsersController.removeById); }; From 765f049809deafab554c209a5ea198c3e1255610 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 04:00:12 +0600 Subject: [PATCH 09/11] BE#1: Verify JWT token before sharing resource. Add logout res/res. --- controllers/auth.controller.js | 4 ++++ middlewares/auth.validation.middleware.js | 21 +++++++++++++++++++++ routes/auth.route.js | 2 ++ routes/users.route.js | 19 ++++++++++++------- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index b7eede8..5ee7a3b 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -16,3 +16,7 @@ exports.login = (req, res) => { res.status(500).send({errors: err}); } }; + +exports.logout = (req, res) => { + res.status(204).send({accessToken: null, refreshToken: null}); +}; diff --git a/middlewares/auth.validation.middleware.js b/middlewares/auth.validation.middleware.js index f0826c6..4415052 100644 --- a/middlewares/auth.validation.middleware.js +++ b/middlewares/auth.validation.middleware.js @@ -1,5 +1,7 @@ const {check} = require('express-validator'); const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const {secret} = require('../configs/auth.config'); const UserModel = require('../models/users.model'); exports.authFieldValidationRules = () => { @@ -36,4 +38,23 @@ exports.matchEmailAndPassword = (req, res, next) => { } } }); +}; + +exports.verifyJwtToken = (req, res, next) => { + if (req.headers['authorization']) { + try { + let authorization = req.headers['authorization'].split(' '); + if (authorization[0] !== 'Bearer') { + return res.status(401).send(); + } else { + req.jwt = jwt.verify(authorization[1], secret); + return next(); + } + + } catch (err) { + return res.status(403).send(); + } + } else { + return res.status(401).send(); + } }; \ No newline at end of file diff --git a/routes/auth.route.js b/routes/auth.route.js index 8d94c9b..85ca883 100644 --- a/routes/auth.route.js +++ b/routes/auth.route.js @@ -10,4 +10,6 @@ exports.routesConfig = function (app) { AuthValidationMiddleware.matchEmailAndPassword ], AuthorizationController.login ]); + + app.get('/auth/logout', AuthorizationController.logout); }; \ No newline at end of file diff --git a/routes/users.route.js b/routes/users.route.js index eff9e35..a65b90f 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -1,6 +1,9 @@ const UsersController = require('../controllers/users.controller'); + const UserValidationMiddleware = require('../middlewares/user.validation.middleware'); const FieldValidateMiddleware = require('../middlewares/field.validation.middleware'); +const AuthValidationMiddleware = require("../middlewares/auth.validation.middleware"); + const appConfig = require('../configs/app.config'); exports.routesConfig = function (app) { @@ -15,21 +18,23 @@ exports.routesConfig = function (app) { UsersController.insert); app.get(`${appConfig.apiEndpointBase}/users`, [ - UsersController.list - ]); + AuthValidationMiddleware.verifyJwtToken + ], UsersController.list); app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ - UserValidationMiddleware.verifyUserId - ], UsersController.getById - ); + UserValidationMiddleware.verifyUserId, + AuthValidationMiddleware.verifyJwtToken + ], UsersController.getById); app.patch(`${appConfig.apiEndpointBase}/users/:userId`, UserValidationMiddleware.updatePasswordValidationRules(), [ FieldValidateMiddleware.validateRules, - UserValidationMiddleware.verifyUserId + UserValidationMiddleware.verifyUserId, + AuthValidationMiddleware.verifyJwtToken ], UsersController.patchById); app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ - UserValidationMiddleware.verifyUserId + UserValidationMiddleware.verifyUserId, + AuthValidationMiddleware.verifyJwtToken ], UsersController.removeById); }; From 804d61f8c993a793774afb5bfe06f08f73b14f27 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sat, 2 May 2020 19:34:29 +0600 Subject: [PATCH 10/11] BE#1: Add user permissions --- controllers/users.controller.js | 1 - middlewares/auth.permission.middleware.js | 38 +++++++++++++++++++++++ middlewares/user.validation.middleware.js | 8 +++-- routes/users.route.js | 18 ++++++++--- 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 middlewares/auth.permission.middleware.js diff --git a/controllers/users.controller.js b/controllers/users.controller.js index 0e3d7ea..8ae3f3b 100644 --- a/controllers/users.controller.js +++ b/controllers/users.controller.js @@ -5,7 +5,6 @@ exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); req.body.password = salt + "$" + hash; - req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); diff --git a/middlewares/auth.permission.middleware.js b/middlewares/auth.permission.middleware.js new file mode 100644 index 0000000..ebc70fb --- /dev/null +++ b/middlewares/auth.permission.middleware.js @@ -0,0 +1,38 @@ +const authConfig = require('../configs/auth.config'); + +exports.minimumPermissionLevelRequired = (required_permission_level) => { + return (req, res, next) => { + let user_permission_level = parseInt(req.jwt.permissionLevel); + if (user_permission_level & required_permission_level) { + return next(); + } else { + return res.status(403).send(); + } + }; +}; + +exports.onlySameUserOrAdminCanDoThisAction = (req, res, next) => { + + let user_permission_level = parseInt(req.jwt.permissionLevel); + let userId = req.jwt.userId; + if (req.params && req.params.userId && userId === req.params.userId) { + return next(); + } else { + if (user_permission_level & authConfig.permissionLevels.ADMIN) { + return next(); + } else { + return res.status(403).send(); + } + } + +}; + +exports.sameUserCantDoThisAction = (req, res, next) => { + let userId = req.jwt.userId; + + if (req.params.userId !== userId) { + return next(); + } else { + return res.status(400).send(); + } +}; diff --git a/middlewares/user.validation.middleware.js b/middlewares/user.validation.middleware.js index d58dfac..ffc8b8b 100644 --- a/middlewares/user.validation.middleware.js +++ b/middlewares/user.validation.middleware.js @@ -23,10 +23,12 @@ exports.registrationFieldValidationRules = () => { exports.isEmailAlreadyExists = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((result) => { - if (result) return res.status(409).json({message: 'Email already in use.'}); + if (result) { + return res.status(409).json({message: 'Email already in use.'}); + } else { + return next(); + } }).catch(err => res.status(500).json({errors: err})); - - return next(); }; exports.verifyUserId = (req, res, next) => { diff --git a/routes/users.route.js b/routes/users.route.js index a65b90f..c03296e 100644 --- a/routes/users.route.js +++ b/routes/users.route.js @@ -3,8 +3,11 @@ const UsersController = require('../controllers/users.controller'); const UserValidationMiddleware = require('../middlewares/user.validation.middleware'); const FieldValidateMiddleware = require('../middlewares/field.validation.middleware'); const AuthValidationMiddleware = require("../middlewares/auth.validation.middleware"); +const AuthPermissionMiddleware = require("../middlewares/auth.permission.middleware"); const appConfig = require('../configs/app.config'); +const authConfig = require('../configs/auth.config'); + exports.routesConfig = function (app) { app.post(`${appConfig.apiEndpointBase}/users`, @@ -18,23 +21,30 @@ exports.routesConfig = function (app) { UsersController.insert); app.get(`${appConfig.apiEndpointBase}/users`, [ - AuthValidationMiddleware.verifyJwtToken + AuthValidationMiddleware.verifyJwtToken, + AuthPermissionMiddleware.minimumPermissionLevelRequired(authConfig.permissionLevels.ADMIN) ], UsersController.list); app.get(`${appConfig.apiEndpointBase}/users/:userId`, [ UserValidationMiddleware.verifyUserId, - AuthValidationMiddleware.verifyJwtToken + AuthValidationMiddleware.verifyJwtToken, + AuthPermissionMiddleware.minimumPermissionLevelRequired(authConfig.permissionLevels.VIEWER), + AuthPermissionMiddleware.onlySameUserOrAdminCanDoThisAction ], UsersController.getById); app.patch(`${appConfig.apiEndpointBase}/users/:userId`, UserValidationMiddleware.updatePasswordValidationRules(), [ FieldValidateMiddleware.validateRules, UserValidationMiddleware.verifyUserId, - AuthValidationMiddleware.verifyJwtToken + AuthValidationMiddleware.verifyJwtToken, + AuthPermissionMiddleware.minimumPermissionLevelRequired(authConfig.permissionLevels.VIEWER), + AuthPermissionMiddleware.onlySameUserOrAdminCanDoThisAction ], UsersController.patchById); app.delete(`${appConfig.apiEndpointBase}/users/:userId`, [ UserValidationMiddleware.verifyUserId, - AuthValidationMiddleware.verifyJwtToken + AuthValidationMiddleware.verifyJwtToken, + AuthPermissionMiddleware.minimumPermissionLevelRequired(authConfig.permissionLevels.ADMIN), + AuthPermissionMiddleware.sameUserCantDoThisAction ], UsersController.removeById); }; From 083c675dbcb8d946d55574d86a7bc80d9bfe5ca0 Mon Sep 17 00:00:00 2001 From: Satyajit Dey Date: Sun, 3 May 2020 01:34:03 +0600 Subject: [PATCH 11/11] BE#1: Change server listening PORT to 3001 --- configs/app.config.js | 4 ++-- server.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configs/app.config.js b/configs/app.config.js index 26d746d..2b3a47e 100644 --- a/configs/app.config.js +++ b/configs/app.config.js @@ -1,6 +1,6 @@ module.exports = { - "port": 3000, - "appEndpoint": "http://localhost:3000", + "port": 3001, + "appEndpoint": "http://localhost:3001", "apiEndpointBase": "/api/v1", "apiVersion": "v1", "environment": "dev" diff --git a/server.js b/server.js index 16ca4a1..0414d57 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,6 @@ const app = require('./app'); const config = require('./configs/app.config'); -const port = config.port || 3000; +const port = config.port || 3001; app.listen(port, () => { console.log(`CMS Engine started and listening port: ${port}`);