From 32a28810f47de7b75d112c67eb462b3f4d19d687 Mon Sep 17 00:00:00 2001 From: Evan Moore Date: Tue, 2 May 2017 22:06:58 -0600 Subject: [PATCH] Add /users/:id route * includes tests * user must be owner to access information * server side for issue #60 --- server/app.js | 2 + server/db/seeds/01_users.js | 23 ++++--- server/helpers/auth.js | 20 ++++--- server/routes/asks.js | 8 +-- server/routes/auth.js | 6 -- server/routes/users.js | 30 ++++++++++ server/test/ask_routes.spec.js | 8 +-- server/test/auth_routes.spec.js | 38 +----------- server/test/spec_helpers.js | 18 ++++++ server/test/user_routes.spec.js | 102 ++++++++++++++++++++++++++++++++ 10 files changed, 189 insertions(+), 66 deletions(-) create mode 100644 server/routes/users.js create mode 100644 server/test/spec_helpers.js create mode 100644 server/test/user_routes.spec.js diff --git a/server/app.js b/server/app.js index 882f8d9..52785f5 100644 --- a/server/app.js +++ b/server/app.js @@ -5,6 +5,7 @@ const cookieParser = require('cookie-parser') const bodyParser = require('body-parser') const asksRoutes = require('./routes/asks') +const usersRoutes = require('./routes/users') const authRoutes = require('./routes/auth') const app = express() @@ -26,6 +27,7 @@ app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) app.use('/api/v1/asks', asksRoutes) +app.use('/api/v1/users', usersRoutes) app.use('/api/v1/auth', authRoutes) // catch 404 and forward to error handler diff --git a/server/db/seeds/01_users.js b/server/db/seeds/01_users.js index 44db082..27e9c4e 100644 --- a/server/db/seeds/01_users.js +++ b/server/db/seeds/01_users.js @@ -1,15 +1,22 @@ const authHelpers = require('../../helpers/auth') -exports.seed = (knex, Promise) => { +exports.seed = knex => { return knex('asks').del() .then(() => knex('users').del()) .then(() => { - const pwHash = authHelpers.hashPassword('testuser123') - return knex('users').insert({ - firstName: 'Test', - lastName: 'User', - email: 'testuser@example.com', - password: pwHash - }) + return knex('users').insert([ + { + firstName: 'Test', + lastName: 'User', + email: 'testuser@example.com', + password: authHelpers.hashPassword('testuser123') + }, + { + firstName: 'Another', + lastName: 'User', + email: 'anotheruser@example.com', + password: authHelpers.hashPassword('anotheruser123') + } + ]) }) } diff --git a/server/helpers/auth.js b/server/helpers/auth.js index 871aeed..4e01915 100644 --- a/server/helpers/auth.js +++ b/server/helpers/auth.js @@ -22,20 +22,25 @@ function encodeToken (user, exp) { } function decodeToken (token, callback) { - jwt.verify(token, process.env.TOKEN_SECRET, callback) + return jwt.verify(token, process.env.TOKEN_SECRET, callback) } -function protect (req, res, next) { +function decodeTokenSync (token) { + return jwt.verify(token, process.env.TOKEN_SECRET) +} + +function requireLogin (req, res, next) { if (!req.headers.authorization) { - return next(new Error('Please log in')) + const error = new Error('Please log in') + error.status = 401 + return next(error) } const header = req.headers.authorization.split(' ') const token = header[1] decodeToken(token, (err, payload) => { if (err) { - res.status(401).json({ - status: err.message - }) + err.status = 401 + next(err) } else { const userID = parseInt(payload.sub, 10) // confirm that the user is still in the DB @@ -51,5 +56,6 @@ module.exports = { hashPassword, encodeToken, decodeToken, - protect + decodeTokenSync, + requireLogin } diff --git a/server/routes/asks.js b/server/routes/asks.js index facf17c..46ae946 100644 --- a/server/routes/asks.js +++ b/server/routes/asks.js @@ -1,6 +1,6 @@ const express = require('express') const Ask = require('../db/models/Ask') -const protect = require('../helpers/auth').protect +const requireLogin = require('../helpers/auth').requireLogin const router = express.Router() @@ -16,21 +16,21 @@ router.get('/:id', (req, res, next) => { .catch(err => next(err)) }) -router.post('/', protect, (req, res, next) => { +router.post('/', requireLogin, (req, res, next) => { Ask.createAsk(req.body) .then(askID => Ask.getAsk(askID)) .then(ask => res.json(ask)) .catch(err => next(err)) }) -router.put('/:id', protect, (req, res, next) => { +router.put('/:id', requireLogin, (req, res, next) => { Ask.updateAsk(req.params.id, req.body) .then(id => Ask.getAsk(id)) .then(updatedJob => res.json(updatedJob)) .catch(err => next(err)) }) -router.delete('/:id', protect, (req, res, next) => { +router.delete('/:id', requireLogin, (req, res, next) => { Ask.deleteAsk(req.params.id) .then(() => res.json({ status: 'success' })) .catch(err => next(err)) diff --git a/server/routes/auth.js b/server/routes/auth.js index 2b381cd..5ce54f4 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -41,10 +41,4 @@ router.post('/login', (req, res, next) => { .catch(err => next(err)) }) -router.get('/user', authHelpers.protect, (req, res, next) => { - res.status(200).json({ - status: 'success' - }) -}) - module.exports = router diff --git a/server/routes/users.js b/server/routes/users.js new file mode 100644 index 0000000..5f04cbe --- /dev/null +++ b/server/routes/users.js @@ -0,0 +1,30 @@ +const express = require('express') +const _ = require('lodash') +const User = require('../db/models/User') +const authHelpers = require('../helpers/auth') + +const router = express.Router() + +router.get('/:id', authHelpers.requireLogin, (req, res, next) => { + const requestedUserID = parseInt(req.params.id, 10) + const authToken = req.headers.authorization.split(' ')[1] + const currentUserID = authHelpers.decodeTokenSync(authToken).sub + User.getUserByID(requestedUserID) + .then((user) => { + if (!user) { + const error = new Error('User not found') + error.status = 404 + return next(error) + } + if (requestedUserID !== currentUserID) { + const error = new Error('You are not authorized to access this page') + error.status = 403 + return next(error) + } + user = _.pick(user, ['firstName', 'lastName', 'email']) + return res.json(user) + }) + .catch(err => next(err)) +}) + +module.exports = router diff --git a/server/test/ask_routes.spec.js b/server/test/ask_routes.spec.js index 95ebf11..ef5bff9 100644 --- a/server/test/ask_routes.spec.js +++ b/server/test/ask_routes.spec.js @@ -12,7 +12,7 @@ const authHelpers = require('../helpers/auth') const should = chai.should() chai.use(chaiHttp) -describe('API Routes', function () { +describe('Ask Routes', function () { beforeEach(() => { return knex.migrate.rollback() .then(() => knex.migrate.latest()) @@ -114,7 +114,7 @@ describe('API Routes', function () { }) .end((err, res) => { should.exist(err) - res.should.have.status(500) + res.should.have.status(401) res.should.be.json res.body.status.should.eql('error') res.body.status.should.eql('error') @@ -182,7 +182,7 @@ describe('API Routes', function () { .end((err, res) => { should.exist(err) res.should.be.json - res.should.have.status(500) + res.should.have.status(401) res.body.status.should.eql('error') res.body.error.should.eql('Please log in') done() @@ -228,7 +228,7 @@ describe('API Routes', function () { }) .catch((err) => { should.exist(err) - err.status.should.eql(500) + err.status.should.eql(401) }) }) }) diff --git a/server/test/auth_routes.spec.js b/server/test/auth_routes.spec.js index 2c44a5d..b8fca6e 100644 --- a/server/test/auth_routes.spec.js +++ b/server/test/auth_routes.spec.js @@ -9,7 +9,7 @@ const should = chai.should() chai.use(chaiHttp) -describe('API Routes', function () { +describe('Auth Routes', function () { beforeEach(() => { return knex.migrate.rollback() .then(() => knex.migrate.latest()) @@ -94,40 +94,4 @@ describe('API Routes', function () { }) }) }) - - describe('GET api/v1/auth/user', () => { - it('should return with status: success', (done) => { - chai.request(server) - .post('/api/v1/auth/login') - .send({ - email: 'testuser@example.com', - password: 'testuser123' - }) - .end((error, response) => { - should.not.exist(error) - const token = response.body.token - chai.request(server) - .get('/api/v1/auth/user') - .set('authorization', `Bearer ${token}`) - .end((err, res) => { - should.not.exist(err) - res.status.should.eql(200) - res.should.be.json - res.body.status.should.eql('success') - done() - }) - }) - }) - it('should throw an error if a user is not logged in', (done) => { - chai.request(server) - .get('/api/v1/auth/user') - .end((err, res) => { - should.exist(err) - res.status.should.eql(500) - res.should.be.json - res.body.status.should.eql('error') - done() - }) - }) - }) }) diff --git a/server/test/spec_helpers.js b/server/test/spec_helpers.js new file mode 100644 index 0000000..2c57a11 --- /dev/null +++ b/server/test/spec_helpers.js @@ -0,0 +1,18 @@ +const chai = require('chai') +const chaiHttp = require('chai-http') +const server = require('../app') + +chai.use(chaiHttp) + +function loginUser () { + return chai.request(server) + .post('/api/v1/auth/login') + .send({ + email: 'testuser@example.com', + password: 'testuser123' + }) +} + +module.exports = { + loginUser +} diff --git a/server/test/user_routes.spec.js b/server/test/user_routes.spec.js new file mode 100644 index 0000000..149a66b --- /dev/null +++ b/server/test/user_routes.spec.js @@ -0,0 +1,102 @@ +/* eslint-disable no-undef, no-unused-expressions, handle-callback-err */ +process.env.NODE_ENV = 'test' + +const chai = require('chai') +const chaiHttp = require('chai-http') +const server = require('../app') +const knex = require('../db/knex') +const User = require('../db/models/User') +const should = chai.should() +const authHelpers = require('../helpers/auth') +const specHelpers = require('./spec_helpers') +chai.use(chaiHttp) + +describe('User Routes', function () { + beforeEach(() => { + return knex.migrate.rollback() + .then(() => knex.migrate.latest()) + .then(() => knex.seed.run()) + }) + + afterEach(() => { + return knex.migrate.rollback() + }) + + describe('GET /api/v1/user/:id', () => { + it('provides user data to authenticated user', (done) => { + specHelpers.loginUser() + .end((err, res) => { + const authToken = res.body.token + const userID = authHelpers.decodeTokenSync(authToken).sub + chai.request(server) + .get(`/api/v1/users/${userID}`) + .set('authorization', `Bearer ${authToken}`) + .end((err, res) => { + should.not.exist(err) + res.should.be.json + res.status.should.eql(200) + res.body.should.include.keys('firstName', 'lastName', 'email') + res.body.should.not.include.keys('password') + done() + }) + }) + }) + it('returns 404 error if the user does not exist', (done) => { + specHelpers.loginUser() + .end((err, res) => { + const authToken = res.body.token + const userID = authHelpers.decodeTokenSync(authToken).sub + chai.request(server) + .get('/api/v1/users/0') + .set('authorization', `Bearer ${authToken}`) + .end((err, res) => { + should.exist(err) + res.should.be.json + res.status.should.equal(404) + res.body.status.should.equal('error') + res.body.error.should.equal('User not found') + done() + }) + }) + }) + it('returns 401 error if the user provides no authentication', (done) => { + specHelpers.loginUser() + .end((err, res) => { + const authToken = res.body.token + const userID = authHelpers.decodeTokenSync(authToken).sub + chai.request(server) + .get(`/api/v1/users/${userID}`) + // NOT setting authentication header + .end((err, res) => { + should.exist(err) + res.should.be.json + res.status.should.equal(401) + res.body.status.should.equal('error') + res.body.error.should.equal('Please log in') + done() + }) + }) + }) + it('returns 403 error if the user does not own the profile', (done) => { + specHelpers.loginUser() + .end((err, res) => { + const authToken = res.body.token + User.getUserByEmail('anotheruser@example.com') + .then((anotherUser) => { + chai.request(server) + .get(`/api/v1/users/${anotherUser.id}`) + .set('authorization', `Bearer ${authToken}`) + .end((err, res) => { + should.exist(err) + res.should.be.json + res.status.should.equal(403) + res.body.status.should.equal('error') + res.body.error.should.equal('You are not authorized to access this page') + done() + }) + }) + .catch(err => console.log(err)) + }) + }) + }) +})