diff --git a/api/controllers/todoListController.js b/api/controllers/todoListController.js index f267eef..8ea82d9 100644 --- a/api/controllers/todoListController.js +++ b/api/controllers/todoListController.js @@ -32,13 +32,13 @@ 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); }); }; -// Task.remove({}).exec(function(){}); + exports.delete_a_task = function(req, res) { Task.remove({ 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/config/development.js b/config/development.js new file mode 100644 index 0000000..4cf8123 --- /dev/null +++ b/config/development.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + env: 'development', + db: 'mongodb://localhost/Tododb', + port: process.env.PORT || 4000, +}; \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..d9f9649 --- /dev/null +++ b/config/index.js @@ -0,0 +1,7 @@ +'use strict'; + +var env = process.env.NODE_ENV || 'development'; +var config = require(`./${env}`); + + +module.exports = config; diff --git a/config/production.js b/config/production.js new file mode 100644 index 0000000..4e40685 --- /dev/null +++ b/config/production.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + env: 'production', + db: process.env.MONGOHQ_URL || process.env.MONGODB_URI, + port: process.env.PORT || 4000, +}; \ No newline at end of file diff --git a/config/test.js b/config/test.js new file mode 100644 index 0000000..71a14bc --- /dev/null +++ b/config/test.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + env: 'test', + db: 'mongodb://localhost/TododbTest', + port: process.env.PORT || 4100, +}; diff --git a/package.json b/package.json index f9032a9..8bcd4e9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "RESTful todoListApi", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "NODE_ENV=test mocha --timeout 10000", + "test-coverage": "NODE_ENV=test istanbul cover _mocha -- -R spec", "start": "nodemon server.js" }, "repository": { @@ -14,6 +15,8 @@ "keywords": [ "RESTful", "API", + "Authentication", + "JWT", "Tutorial" ], "author": "olatunde garuba", @@ -23,11 +26,19 @@ }, "homepage": "https://github.com/generalgmt/RESTfulAPITutorial#readme", "devDependencies": { - "nodemon": "^1.11.0" + "chai": "^3.5.0", + "install": "^0.8.8", + "istanbul": "^0.4.5", + "mocha": "^3.2.0", + "nodemon": "^1.11.0", + "npm": "^4.4.4", + "supertest": "^3.0.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..57d7662 100644 --- a/server.js +++ b/server.js @@ -1,25 +1,44 @@ +'use strict'; + var express = require('express'), app = express(), port = process.env.PORT || 3000, mongoose = require('mongoose'), Task = require('./api/models/todoListModel'), + User = require('./api/models/userModel'), + bodyParser = require('body-parser'), + jsonwebtoken = require("jsonwebtoken"), + config = require('./config/index'), bodyParser = require('body-parser'); mongoose.Promise = global.Promise; -mongoose.connect('mongodb://localhost/Tododb'); +mongoose.connect(config.db); 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); +app.listen(config.port); + +console.log('todo list RESTful API server started on: ' + config.port); -console.log('todo list RESTful API server started on: ' + port); +module.exports = app; \ No newline at end of file diff --git a/test/todos.test.js b/test/todos.test.js new file mode 100644 index 0000000..bc72caf --- /dev/null +++ b/test/todos.test.js @@ -0,0 +1,75 @@ +'use strict'; + +var app = require('../server'); +var chai = require('chai'); +var request = require('supertest'); + +var expect = chai.expect; + +describe('API Tests', function() { + var task = { + name: 'integration test', + }; + + describe('## Create task ', function() { + it('should create a task', function(done) { + request(app) + .post('/tasks') + .send(task) + .end(function(err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.name).to.equal('integration test'); + task = res.body; + done(); + }); + }); + }); + + describe('# Get all tasks', function() { + it('should get all tasks', function(done) { + request(app) + .get('/tasks') + .end(function(err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body).to.be.an('array'); + done(); + }); + }); + }); + describe('Get a task by id', function() { + it('should get a task', function(done) { + request(app) + .get('/tasks/' + task._id) + .end(function(err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.name).to.equal('integration test'); + done(); + }); + }); + }); + + describe('Update a task by id', function() { + it('should modify a task', function(done) { + task.name = 'New Task' + request(app) + .put('/tasks/' + task._id) + .send(task) + .end(function(err, res) { + expect(res.body.name).to.equal('New Task'); + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + describe('Delete a task by id', function() { + it('should delete a task', function(done) { + request(app) + .delete('/tasks/' + task._id) + .end(function(err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.message).to.equal('Task successfully deleted'); + done(); + }); + }); + }); +});