From eb972d6b4b32f0a393b91d444407e3c0dbda687f Mon Sep 17 00:00:00 2001 From: Olatunde Garuba Date: Wed, 19 Apr 2017 21:17:24 +0100 Subject: [PATCH 1/7] JWT added to RESTFUL create API --- api/controllers/todoListController.js | 2 +- api/controllers/userController.js | 46 +++++++++++++++++++++++++++ api/models/todoListModel.js | 9 ++---- api/models/userModel.js | 38 ++++++++++++++++++++++ api/routes/todoListRoutes.js | 11 +++++-- package.json | 4 +++ server.js | 23 ++++++++++++-- 7 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 api/controllers/userController.js create mode 100644 api/models/userModel.js diff --git a/api/controllers/todoListController.js b/api/controllers/todoListController.js index f267eef..595b6f5 100644 --- a/api/controllers/todoListController.js +++ b/api/controllers/todoListController.js @@ -32,7 +32,7 @@ exports.read_a_task = function(req, res) { }; exports.update_a_task = function(req, res) { - Task.findOneAndUpdate(req.params.taskId, req.body, {new: true}, function(err, task) { + Task.findOneAndUpdate({_id:req.params.taskId}, req.body, {new: true}, function(err, task) { if (err) res.send(err); res.json(task); diff --git a/api/controllers/userController.js b/api/controllers/userController.js new file mode 100644 index 0000000..6c30fee --- /dev/null +++ b/api/controllers/userController.js @@ -0,0 +1,46 @@ +'use strict'; + +var mongoose = require('mongoose'), + jwt = require('jsonwebtoken'), + bcrypt = require('bcrypt'), + User = mongoose.model('User'); + +exports.register = function(req, res) { + var newUser = new User(req.body); + newUser.hash_password = bcrypt.hashSync(req.body.password, 10); + newUser.save(function(err, user) { + if (err) { + return res.status(400).send({ + message: err + }); + } else { + user.hash_password = undefined; + return res.json(user); + } + }); +}; + +exports.sign_in = function(req, res) { + User.findOne({ + email: req.body.email + }, function(err, user) { + if (err) throw err; + if (!user) { + res.status(401).json({ message: 'Authentication failed. User not found.' }); + } else if (user) { + if (user.comparePassword(req.body.password)) { + res.status(401).json({ message: 'Authentication failed. Wrong password.' }); + } else { + return res.json({token: jwt.sign({ email: user.email, fullName: user.fullName, _id: user._id}, 'RESTFULAPIs')}); + } + } + }); +}; + +exports.loginRequired = function(req, res, next) { + if (req.user) { + next(); + } else { + return res.status(401).json({ message: 'Unauthorized user!' }); + } +}; \ No newline at end of file diff --git a/api/models/todoListModel.js b/api/models/todoListModel.js index 00f45cc..b6dce13 100644 --- a/api/models/todoListModel.js +++ b/api/models/todoListModel.js @@ -9,16 +9,13 @@ var TaskSchema = new Schema({ type: String, Required: 'Kindly enter the name of the task' }, - Created_date: { + created_date: { type: Date, default: Date.now }, status: { - type: [{ - type: String, - enum: ['pending', 'ongoing', 'completed'] - }], - default: ['pending'] + type: String, + default: 'pending' } }); diff --git a/api/models/userModel.js b/api/models/userModel.js new file mode 100644 index 0000000..9f05d34 --- /dev/null +++ b/api/models/userModel.js @@ -0,0 +1,38 @@ +'use strict'; + +var mongoose = require('mongoose'), + bcrypt = require('bcrypt'), + Schema = mongoose.Schema; + +/** + * User Schema + */ +var UserSchema = new Schema({ + fullName: { + type: String, + trim: true, + required: true + }, + email: { + type: String, + unique: true, + lowercase: true, + trim: true, + required: true + }, + hash_password: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now + } +}); + +UserSchema.methods.comparePassword = function(password) { + return bcrypt.compareSync(password, this.hash_password); +}; + + +mongoose.model('User', UserSchema); diff --git a/api/routes/todoListRoutes.js b/api/routes/todoListRoutes.js index d225460..66ff955 100644 --- a/api/routes/todoListRoutes.js +++ b/api/routes/todoListRoutes.js @@ -1,15 +1,22 @@ 'use strict'; module.exports = function(app) { - var todoList = require('../controllers/todoListController'); + var todoList = require('../controllers/todoListController'), + userHandlers = require('../controllers/userController.js'); // todoList Routes app.route('/tasks') .get(todoList.list_all_tasks) - .post(todoList.create_a_task); + .post(userHandlers.loginRequired, todoList.create_a_task); app.route('/tasks/:taskId') .get(todoList.read_a_task) .put(todoList.update_a_task) .delete(todoList.delete_a_task); + + app.route('/auth/register') + .post(userHandlers.register); + + app.route('/auth/sign_in') + .post(userHandlers.sign_in); }; diff --git a/package.json b/package.json index f9032a9..f78ebef 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "keywords": [ "RESTful", "API", + "Authentication", + "JWT", "Tutorial" ], "author": "olatunde garuba", @@ -26,8 +28,10 @@ "nodemon": "^1.11.0" }, "dependencies": { + "bcrypt": "^1.0.2", "body-parser": "^1.15.2", "express": "^4.14.0", + "jsonwebtoken": "^7.3.0", "mongoose": "^4.7.2" } } diff --git a/server.js b/server.js index b0bbb57..91a63df 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,13 @@ +'use strict'; + var express = require('express'), app = express(), port = process.env.PORT || 3000, mongoose = require('mongoose'), Task = require('./api/models/todoListModel'), - bodyParser = require('body-parser'); + User = require('./api/models/userModel'), + bodyParser = require('body-parser'), + jsonwebtoken = require("jsonwebtoken"); mongoose.Promise = global.Promise; mongoose.connect('mongodb://localhost/Tododb'); @@ -12,14 +16,27 @@ mongoose.connect('mongodb://localhost/Tododb'); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); - +app.use(function(req, res, next) { + if (req.headers && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'JWT') { + jsonwebtoken.verify(req.headers.authorization.split(' ')[1], 'RESTFULAPIs', function(err, decode) { + if (err) req.user = undefined; + req.user = decode; + next(); + }); + } else { + req.user = undefined; + next(); + } +}); var routes = require('./api/routes/todoListRoutes'); routes(app); app.use(function(req, res) { - res.status(404).send({url: req.originalUrl + ' not found'}) + res.status(404).send({ url: req.originalUrl + ' not found' }) }); app.listen(port); console.log('todo list RESTful API server started on: ' + port); + +module.exports = app; \ No newline at end of file From 3a097eb54bc255d673a14399fed8c2ba9dec9a65 Mon Sep 17 00:00:00 2001 From: Olatunde Garuba Date: Sat, 20 May 2017 07:12:36 +0100 Subject: [PATCH 2/7] signin modified --- api/controllers/userController.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index 6c30fee..f24f1a2 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -25,15 +25,10 @@ exports.sign_in = function(req, res) { email: req.body.email }, function(err, user) { if (err) throw err; - if (!user) { - res.status(401).json({ message: 'Authentication failed. User not found.' }); - } else if (user) { - if (user.comparePassword(req.body.password)) { - res.status(401).json({ message: 'Authentication failed. Wrong password.' }); - } else { - return res.json({token: jwt.sign({ email: user.email, fullName: user.fullName, _id: user._id}, 'RESTFULAPIs')}); - } + if (!user || !user.comparePassword(req.body.password)) { + return res.status(401).json({ message: 'Authentication failed. Invalid user or password.' }); } + return res.json({ token: jwt.sign({ email: user.email, fullName: user.fullName, _id: user._id }, 'RESTFULAPIs') }); }); }; @@ -43,4 +38,4 @@ exports.loginRequired = function(req, res, next) { } else { return res.status(401).json({ message: 'Unauthorized user!' }); } -}; \ No newline at end of file +}; From dbb95a1488aaf23fc503668b36589a9150f735da Mon Sep 17 00:00:00 2001 From: Olatunde Garuba Date: Sat, 29 Jul 2017 09:43:18 +0100 Subject: [PATCH 3/7] Password reset functionality --- api/controllers/todoListController.js | 2 +- api/controllers/userController.js | 142 ++++++++++++++++++++++- api/models/userModel.js | 6 + api/routes/todoListRoutes.js | 40 ++++--- api/templates/forgot-password-email.html | 18 +++ api/templates/reset-password-email.html | 20 ++++ package.json | 11 +- public/forgot-password.html | 48 ++++++++ public/home.html | 10 ++ public/reset-password.html | 50 ++++++++ server.js | 4 +- 11 files changed, 330 insertions(+), 21 deletions(-) create mode 100644 api/templates/forgot-password-email.html create mode 100644 api/templates/reset-password-email.html create mode 100644 public/forgot-password.html create mode 100644 public/home.html create mode 100644 public/reset-password.html diff --git a/api/controllers/todoListController.js b/api/controllers/todoListController.js index 595b6f5..b020141 100644 --- a/api/controllers/todoListController.js +++ b/api/controllers/todoListController.js @@ -32,7 +32,7 @@ exports.read_a_task = function(req, res) { }; exports.update_a_task = function(req, res) { - Task.findOneAndUpdate({_id:req.params.taskId}, req.body, {new: true}, function(err, task) { + Task.findByIdAndUpdate(req.params.taskId, req.body, {new: true}, function(err, task) { if (err) res.send(err); res.json(task); diff --git a/api/controllers/userController.js b/api/controllers/userController.js index f24f1a2..aa5755c 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -3,7 +3,32 @@ var mongoose = require('mongoose'), jwt = require('jsonwebtoken'), bcrypt = require('bcrypt'), - User = mongoose.model('User'); + User = mongoose.model('User'), + path = require('path'), + async = require('async'), + crypto = require('crypto'), + _ = require('lodash'), + hbs = require('nodemailer-express-handlebars'), + nodemailer = require('nodemailer'); + + +var smtpTransport = nodemailer.createTransport({ + service: process.env.MAILER_SERVICE_PROVIDER || 'Gmail', + auth: { + user: process.env.MAILER_EMAIL_ID || 'auth_email_address', + pass: process.env.MAILER_PASSWORD || 'auth_email_pass' + } +}); + + +var handlebarsOptions = { + viewEngine: 'handlebars', + viewPath: path.resolve('./api/templates/'), + extName: '.html' +}; + +smtpTransport.use('compile', hbs(handlebarsOptions)); + exports.register = function(req, res) { var newUser = new User(req.body); @@ -20,6 +45,18 @@ exports.register = function(req, res) { }); }; +exports.index = function(req, res) { + return res.sendFile(path.resolve('./public/home.html')); +}; + +exports.render_forgot_password_template = function(req, res) { + return res.sendFile(path.resolve('./public/forgot-password.html')); +}; + +exports.render_reset_password_template = function(req, res) { + return res.sendFile(path.resolve('./public/reset-password.html')); +}; + exports.sign_in = function(req, res) { User.findOne({ email: req.body.email @@ -39,3 +76,106 @@ exports.loginRequired = function(req, res, next) { return res.status(401).json({ message: 'Unauthorized user!' }); } }; + +exports.forgot_password = function(req, res) { + async.waterfall([ + function(done) { + User.findOne({ + email: req.body.email + }).exec(function(err, user) { + if (user) { + done(err, user); + } else { + done('User not found.'); + } + }); + }, + function(user, done) { + // create the random token + crypto.randomBytes(20, function(err, buffer) { + var token = buffer.toString('hex'); + done(err, user, token); + }); + }, + function(user, token, done) { + User.findByIdAndUpdate({ _id: user._id }, { reset_password_token: token, reset_password_expires: Date.now() + 86400000 }, { upsert: true, new: true }).exec(function(err, new_user) { + done(err, token, new_user); + }); + }, + function(token, user, done) { + var data = { + to: user.email, + from: 'auth_email_address@gmail.com', + template: 'forgot-password-email', + subject: 'Password help has arrived!', + context: { + url: 'http://localhost:3000/auth/reset_password?token=' + token, + name: user.fullName.split(' ')[0] + } + }; + + smtpTransport.sendMail(data, function(err) { + if (!err) { + return res.json({ message: 'Kindly check your email for further instructions' }); + } else { + return done(err); + } + }); + } + ], function(err) { + return res.status(422).json({ message: err }); + }); +}; + +/** + * Reset password + */ +exports.reset_password = function(req, res, next) { + User.findOne({ + reset_password_token: req.body.token, + reset_password_expires: { + $gt: Date.now() + } + }).exec(function(err, user) { + if (!err && user) { + if (req.body.newPassword === req.body.verifyPassword) { + user.hash_password = bcrypt.hashSync(req.body.newPassword, 10); + user.reset_password_token = undefined; + user.reset_password_expires = undefined; + user.save(function(err) { + if (err) { + return res.status(422).send({ + message: err + }); + } else { + var data = { + to: user.email, + from: 'auth_email_address@gmail.com', + template: 'reset-password-email', + subject: 'Password Reset Confirmation', + context: { + name: user.fullName.split(' ')[0] + } + }; + + smtpTransport.sendMail(data, function(err) { + if (!err) { + return res.json({ message: 'Password reset' }); + } else { + return done(err); + } + }); + } + }); + } else { + return res.status(422).send({ + message: 'Passwords do not match' + }); + } + } else { + return res.status(400).send({ + message: 'Password reset token is invalid or has expired.' + }); + } + }); +}; diff --git a/api/models/userModel.js b/api/models/userModel.js index 9f05d34..5efa971 100644 --- a/api/models/userModel.js +++ b/api/models/userModel.js @@ -27,6 +27,12 @@ var UserSchema = new Schema({ created: { type: Date, default: Date.now + }, + reset_password_token: { + type: String + }, + reset_password_expires: { + type: Date } }); diff --git a/api/routes/todoListRoutes.js b/api/routes/todoListRoutes.js index 66ff955..68404dd 100644 --- a/api/routes/todoListRoutes.js +++ b/api/routes/todoListRoutes.js @@ -1,22 +1,34 @@ 'use strict'; module.exports = function(app) { - var todoList = require('../controllers/todoListController'), - userHandlers = require('../controllers/userController.js'); + var todoList = require('../controllers/todoListController'), + userHandlers = require('../controllers/userController.js'); - // todoList Routes - app.route('/tasks') - .get(todoList.list_all_tasks) - .post(userHandlers.loginRequired, todoList.create_a_task); + // todoList Routes - app.route('/tasks/:taskId') - .get(todoList.read_a_task) - .put(todoList.update_a_task) - .delete(todoList.delete_a_task); + app.route('/') + .get(userHandlers.index); + + app.route('/tasks') + .get(todoList.list_all_tasks) + .post(userHandlers.loginRequired, todoList.create_a_task); - app.route('/auth/register') - .post(userHandlers.register); + app.route('/tasks/:taskId') + .get(todoList.read_a_task) + .put(todoList.update_a_task) + .delete(todoList.delete_a_task); - app.route('/auth/sign_in') - .post(userHandlers.sign_in); + app.route('/auth/register') + .post(userHandlers.register); + + app.route('/auth/sign_in') + .post(userHandlers.sign_in); + + app.route('/auth/forgot_password') + .get(userHandlers.render_forgot_password_template) + .post(userHandlers.forgot_password); + + app.route('/auth/reset_password') + .get(userHandlers.render_reset_password_template) + .post(userHandlers.reset_password); }; diff --git a/api/templates/forgot-password-email.html b/api/templates/forgot-password-email.html new file mode 100644 index 0000000..8e32bc1 --- /dev/null +++ b/api/templates/forgot-password-email.html @@ -0,0 +1,18 @@ + + + + + Forget Password Email + + + +
+

Dear {{name}},

+

You requested for a password reset, kindly use this link to reset your password

+
+

Cheers!

+
+ + + + diff --git a/api/templates/reset-password-email.html b/api/templates/reset-password-email.html new file mode 100644 index 0000000..1e8458f --- /dev/null +++ b/api/templates/reset-password-email.html @@ -0,0 +1,20 @@ + + + + + Password Reset + + + +
+

Dear {{name}},

+

Your password has been successful reset, you can now login with your new password.

+
+
+ Cheers! +
+
+ + + + diff --git a/package.json b/package.json index f78ebef..78f51c8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,10 @@ "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon server.js" }, + "engines": { + "node": "6.11.1", + "npm": ">=3.10.8" + }, "repository": { "type": "git", "url": "git+https://github.com/generalgmt/RESTfulAPITutorial.git" @@ -24,14 +28,13 @@ "url": "https://github.com/generalgmt/RESTfulAPITutorial/issues" }, "homepage": "https://github.com/generalgmt/RESTfulAPITutorial#readme", - "devDependencies": { - "nodemon": "^1.11.0" - }, "dependencies": { "bcrypt": "^1.0.2", "body-parser": "^1.15.2", "express": "^4.14.0", "jsonwebtoken": "^7.3.0", - "mongoose": "^4.7.2" + "mongoose": "^4.7.2", + "nodemailer": "^4.0.1", + "nodemailer-express-handlebars": "^2.0.0" } } diff --git a/public/forgot-password.html b/public/forgot-password.html new file mode 100644 index 0000000..bec4263 --- /dev/null +++ b/public/forgot-password.html @@ -0,0 +1,48 @@ + + + + + Forget Password + + + +
+

Reset Password Form

+
+ + + +
+
+ + + + diff --git a/public/home.html b/public/home.html new file mode 100644 index 0000000..0d9b9b2 --- /dev/null +++ b/public/home.html @@ -0,0 +1,10 @@ + + + + Home Page + + +

Welcome to Reset password with JWT tutorial

+Click here to reset your password + + \ No newline at end of file diff --git a/public/reset-password.html b/public/reset-password.html new file mode 100644 index 0000000..e375adc --- /dev/null +++ b/public/reset-password.html @@ -0,0 +1,50 @@ + + + + + Reset Password + + + +

Password confirmation!

+
+ + + + + +
+
+ + + + diff --git a/server.js b/server.js index 91a63df..d0692a3 100644 --- a/server.js +++ b/server.js @@ -10,12 +10,14 @@ var express = require('express'), jsonwebtoken = require("jsonwebtoken"); mongoose.Promise = global.Promise; -mongoose.connect('mongodb://localhost/Tododb'); +mongoose.connect('mongodb://localhost/Tododb', {useMongoClient: true}); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); +app.use(express.static(__dirname + '/public')); + app.use(function(req, res, next) { if (req.headers && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'JWT') { jsonwebtoken.verify(req.headers.authorization.split(' ')[1], 'RESTFULAPIs', function(err, decode) { From 0b363e86a01f315581bd6529a61dbdf3946302c5 Mon Sep 17 00:00:00 2001 From: Olatunde Garuba Date: Sat, 29 Jul 2017 10:29:21 +0100 Subject: [PATCH 4/7] Password reset functionality --- api/controllers/userController.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index aa5755c..05a3c3c 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -9,14 +9,16 @@ var mongoose = require('mongoose'), crypto = require('crypto'), _ = require('lodash'), hbs = require('nodemailer-express-handlebars'), + email = process.env.MAILER_EMAIL_ID || 'auth_email_address@gmail.com', + pass = process.env.MAILER_PASSWORD || 'auth_email_pass' nodemailer = require('nodemailer'); var smtpTransport = nodemailer.createTransport({ service: process.env.MAILER_SERVICE_PROVIDER || 'Gmail', auth: { - user: process.env.MAILER_EMAIL_ID || 'auth_email_address', - pass: process.env.MAILER_PASSWORD || 'auth_email_pass' + user: email, + pass: pass } }); @@ -105,7 +107,7 @@ exports.forgot_password = function(req, res) { function(token, user, done) { var data = { to: user.email, - from: 'auth_email_address@gmail.com', + from: email, template: 'forgot-password-email', subject: 'Password help has arrived!', context: { @@ -150,7 +152,7 @@ exports.reset_password = function(req, res, next) { } else { var data = { to: user.email, - from: 'auth_email_address@gmail.com', + from: email, template: 'reset-password-email', subject: 'Password Reset Confirmation', context: { From 0cf8bf1de9a1dc9464c64f02011383d812a06b6c Mon Sep 17 00:00:00 2001 From: Olatunde Garuba Date: Sat, 29 Jul 2017 10:31:02 +0100 Subject: [PATCH 5/7] , added --- api/controllers/userController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index 05a3c3c..a3672dc 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -10,7 +10,7 @@ var mongoose = require('mongoose'), _ = require('lodash'), hbs = require('nodemailer-express-handlebars'), email = process.env.MAILER_EMAIL_ID || 'auth_email_address@gmail.com', - pass = process.env.MAILER_PASSWORD || 'auth_email_pass' + pass = process.env.MAILER_PASSWORD || 'auth_email_pass', nodemailer = require('nodemailer'); From 35282c9f6539272784801513d1e4511f63b28ee3 Mon Sep 17 00:00:00 2001 From: Olatunde Michael Garuba Date: Thu, 13 May 2021 21:51:10 +0100 Subject: [PATCH 6/7] Update userController.js --- api/controllers/userController.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index a3672dc..1da4022 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -93,14 +93,17 @@ exports.forgot_password = function(req, res) { }); }, function(user, done) { - // create the random token - crypto.randomBytes(20, function(err, buffer) { - var token = buffer.toString('hex'); - done(err, user, token); - }); + // create a unique token + var tokenObject = { + email: user.email, + id: user._id + }; + var secret = user._id + '_' + user.email + '_' + new Date().getTime(); + var token = jwt.encode(tokenObject, secret); + done(err, user, token); }, function(user, token, done) { - User.findByIdAndUpdate({ _id: user._id }, { reset_password_token: token, reset_password_expires: Date.now() + 86400000 }, { upsert: true, new: true }).exec(function(err, new_user) { + User.findByIdAndUpdate({ _id: user._id }, { reset_password_token: token, reset_password_expires: Date.now() + 86400000 }, { new: true }).exec(function(err, new_user) { done(err, token, new_user); }); }, From 2305947d13eaa317a6f7dece018f792950a40707 Mon Sep 17 00:00:00 2001 From: Olatunde Michael Garuba Date: Thu, 13 May 2021 22:29:00 +0100 Subject: [PATCH 7/7] Update userController.js --- api/controllers/userController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index 1da4022..c242191 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -99,7 +99,7 @@ exports.forgot_password = function(req, res) { id: user._id }; var secret = user._id + '_' + user.email + '_' + new Date().getTime(); - var token = jwt.encode(tokenObject, secret); + var token = jwt.sign(tokenObject, secret); done(err, user, token); }, function(user, token, done) {