diff --git a/README.md b/README.md index 71eb93186..3dd1d4c9e 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,16 @@ Commit your code regularly and meaningfully. This helps both you (in case you ev Be prepared to demonstrate your understanding of this week's concepts by answering questions on the following topics. You might prepare by writing down your own answers before hand. 1. Differences between using _sessions_ or _JSON Web Tokens_ for authentication. + Sessions use cookies to create instances that store the user's data and remains in memory until the instance is deleted. With tokens the data is stored and passed back and forth. This is best suited for communication between trusted devices. 2. What does `bcrypt` do to help us store passwords in a secure manner. + bcrypt provides an efficent way for us to hash our passwords. 3. How are unit tests different from integration and end-to-end testing. + Unit test only focus on a relativley small piece of the code. Intergration tests focus on two "layers" of code and how well they communicate with each other. End-to-end testing focuses on the functionality of the entire program. 4. How _Test Driven Development_ changes the way we write applications and tests. + TDD makes you more mindful of how clean code should be written and how each piece works together before you begin writing the program. You are expected to be able to answer questions in these areas. Your responses contribute to your Sprint Challenge grade. diff --git a/api/server..spec.js b/api/server..spec.js new file mode 100644 index 000000000..14fa44fe8 --- /dev/null +++ b/api/server..spec.js @@ -0,0 +1,86 @@ +const request = require('supertest') +const db = require('../database/dbConfig'); +const server = require('./server'); + +describe('end point tests', function() { + describe('POST /register and POST /login', function() { + beforeAll(async() => { + await db('users').truncate(); + }) + + //#1 should return status 201 + it('POST /auth/register', function() { + return request(server) + .post('/api/auth/register') + .send({ username: "test", password: "1234567" }) + .then(res => { + console.log(res.body) + expect(res.status).toBe(201) + expect(res.body.data.username).toBe(user.username) + }) + }) + + //#2 should give 400 error for invalid credentials' + it(' POST /auth/register', function() { + return request(server) + .post('/api/auth/register') + .send({ username: "nope" , password: "nope" }) + .then(res => { + console.log(res.status) + console.log(res.body) + expect(res.status).toBe(400); + expect(res.body).toEqual({ message: 'incorrect username/password' }) + }) + }) + + //#3 should return status 200 + it('POST /auth/login', function() { + return request(server) + .post('/api/auth/login') + .send({ username: 'test', password: '1234567' }) + .then(res => { + const token = res.data.token + return request(server) + .get('/api/jokes/') + .set({token}) + .then(res => { + expect(res.status).toBe(200); + }) + }) + }) + + //#4 res.type should match json + it(' POST /auth/login"', function() { + return request(server) + .post('/api/auth/login') + .send({ username: "test", password: "1234567" }) + .then(res => { + const token = res.data.token + return request(server) + .get('/api/jokes/') + .set({token}) + .then(res => { + expect(res.type).toMatch(/json/i); + }) + }) + }) + + //#5 res.type should match json + it(' GET /jokes/', function() { + return request(server) + .get('/api/jokes/') + .then(res => { + expect(res.type).toMatch(/json/i); + }) + }) + + //#6 should return a response + it(' GET /jokes/', function() { + return request(server) + .get('/api/jokes/') + .then(res => { + expect(res.body).toBeTruthy(); + }) + }) + }) +}) \ No newline at end of file diff --git a/api/server.js b/api/server.js index c8acc0eb4..bc2523674 100644 --- a/api/server.js +++ b/api/server.js @@ -16,3 +16,17 @@ server.use('/api/auth', authRouter); server.use('/api/jokes', authenticate, jokesRouter); module.exports = server; + +// function checkRole(user) { +// return (req, res, next) => { +// if ( +// req.decodedToken && +// req.decodedToken.role && +// req.decodedToken.role.toLowerCase() === user +// ) { +// next() +// } else { +// res.status(403).json({ message: 'Must be logged in' }) +// } +// } +// } \ No newline at end of file diff --git a/auth/Token.js b/auth/Token.js new file mode 100644 index 000000000..a31e7fb8d --- /dev/null +++ b/auth/Token.js @@ -0,0 +1,14 @@ +const jwt = require('jsonwebtoken'); +const { jwtSecret } = require('../secrets/authSecret'); + +function Token(username) { + const payload = { + subject: username.id, + username: username.username, + role: username.role || 'user', + }; + + return jwt.sign(payload, jwtSecret); +} + +module.exports = Token; \ No newline at end of file diff --git a/auth/auth-router.js b/auth/auth-router.js index 2fa2c9766..5fbcc47b9 100644 --- a/auth/auth-router.js +++ b/auth/auth-router.js @@ -1,11 +1,45 @@ const router = require('express').Router(); +const bcrypt = require('bcryptjs'); + +const Users = require('../users/model'); +const newToken = require('./Token'); +const { validateUser } = require('../users/validation'); router.post('/register', (req, res) => { - // implement registration + let user = req.body + const validateResult = validateUser(user); + if (validateResult.isSuccessful === true) { + const hash = bcrypt.hashSync(user.password, 10); + user.password = hash; + Users.add(user) + .then(saved => { + const token = newToken(saved); + res.status(201).json(token); + }) + .catch(err => { + res.status(500).json({ message: 'Error', err }) + }) + } else { + res.status(400).json({ Message: 'invalid', errors: validateUser(user) }) + } }); router.post('/login', (req, res) => { - // implement login + let { username, password } = req.body; + Users.findBy({ username }) + .first() + .then(user => { + if (user && bcrypt.compareSync(password, user.password)) { + const token = newToken(user); + res.status(200).json({ Message: `Welcome ${user.username}`, token }); + } else { + res.status(401).json({ Message: 'Credentials invalid' }); + } + }) + .catch(err => { + res.status(500).json({ Message: 'Error' }) + }) }); + module.exports = router; diff --git a/auth/auth.spec.js b/auth/auth.spec.js new file mode 100644 index 000000000..54574584b --- /dev/null +++ b/auth/auth.spec.js @@ -0,0 +1,35 @@ +let supertest = require('supertest'); +let server = require('../api/server'); + +let authRouter = require('./auth-router'); +let db = require('../database/dbConfig'); + + + describe("create user", () => { + it('should register new user', async () => { + const res = await supertest(server) + .post("/api/auth/register") + .send({ + username: "Reggie", + password: "321cba" + }) + expect(res.statusCode).toBe(201) + expect(res.type).toBe("application/json") + }) + + it("Should return 200 status", async() => { + let user = { + username: "Reggie", + password: "321cba" + } + + return await supertest(server) + .post('/api/auth/login') + .send(user) + .then(res => { + expect(res.status).toBe(200) + expect(res.body.message).toEqual("Welcome Reggie!" ) + + }) + }) + }) \ No newline at end of file diff --git a/auth/authenticate-middleware.js b/auth/authenticate-middleware.js index 6ca61d0cd..cd870a187 100644 --- a/auth/authenticate-middleware.js +++ b/auth/authenticate-middleware.js @@ -1,8 +1,34 @@ -/* - complete the middleware code to check if the user is logged in - before granting access to the next middleware/route handler -*/ - -module.exports = (req, res, next) => { - res.status(401).json({ you: 'shall not pass!' }); -}; +const jwt = require('jsonwebtoken'); +// const { jwtSecret } = require('../secrets/authSecret'); + + +function restrict() { + return async (req, res, next) => { + const authError = {err: 'invalid credentials'} + try{ + const token = req.headers.authorization + if(!token) {return res.status(401).json(authError)} + jwt.verify(token, 'safe', (err, decoded) => { + if(err) {return res.status(401).json(authError)} + next() + }) + } + catch(err) {next(err)} + } + } + + +module.exports = restrict + + // = req.headers; + // if (authorization) { + // jwt.verify(authorization, jwtSecret, (err, decodedToken) => { + // if (err) { + // res.status(401).json({ message: "Invalid credentials" }); + // } else { + // req.decodedToken = decodedToken; + // next(); + // } + // }); + // } else { + // res.status(400).json({ message: "Please enter credentials" }); diff --git a/index.js b/index.js index fd80bbe6d..f20e7d3c9 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const server = require('./api/server.js'); -const PORT = process.env.PORT || 3300; +const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`\n=== Server listening on port ${PORT} ===\n`); }); diff --git a/jokes/jokes-router.js b/jokes/jokes-router.js index aa93d0fa9..73dc8c91d 100644 --- a/jokes/jokes-router.js +++ b/jokes/jokes-router.js @@ -1,8 +1,10 @@ const axios = require('axios'); +const Users = require('../users/model') +const restricted = require('../auth/authenticate-middleware') const router = require('express').Router(); -router.get('/', (req, res) => { +router.get('/', restricted(), (req, res) => { const requestOptions = { headers: { accept: 'application/json' }, }; @@ -17,4 +19,14 @@ router.get('/', (req, res) => { }); }); +router.get('/user', restricted(), (req, res) => { + Users.find() + .then(param => { + res.status(200).json(param) + }) + .catch(err => { + res.status(500).json(err) + }) +}) + module.exports = router; diff --git a/package.json b/package.json index 4feb96236..a59d1cf95 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Authentication Sprint Challenge", "main": "index.js", "scripts": { - "server": "nodemon index.js" + "server": "nodemon index.js", + "test": "jest --watchAll" }, "repository": { "type": "git", @@ -19,13 +20,21 @@ "homepage": "https://github.com/LambdaSchool/Sprint-Challenge-Authentication#readme", "dependencies": { "axios": "^0.19.2", + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", + "dotenv": "^8.2.0", "express": "^4.17.1", "helmet": "^3.22.0", + "jsonwebtoken": "^8.5.1", "knex": "^0.21.0", "sqlite3": "^4.1.1" }, "devDependencies": { - "nodemon": "^2.0.3" + "cross-env": "^7.0.2", + "jest": "^26.4.2", + "knex-cleaner": "^1.3.1", + "nodemon": "^2.0.3", + "supertest": "^4.0.2" } } diff --git a/secrets/authSecret.js b/secrets/authSecret.js new file mode 100644 index 000000000..35c85386d --- /dev/null +++ b/secrets/authSecret.js @@ -0,0 +1,5 @@ +module.exports = { + + jwtSecret: process.env.JWT_SECRET || 'secret', + +} \ No newline at end of file diff --git a/users/model.js b/users/model.js new file mode 100644 index 000000000..97a3fd48f --- /dev/null +++ b/users/model.js @@ -0,0 +1,24 @@ +const db = require('../database/dbConfig'); +module.exports = { + add, + find, + findBy, + findById +} + +function find() { + return db('users').select('id', 'username', 'password'); +} + +function findBy(filter) { + return db('users').where(filter); +} + +function findById(id) { + return db('users').where({ id }).first(); +} + +async function add(user) { + const [id] = await db('users').insert(user); + return findById(id); +} \ No newline at end of file diff --git a/users/validation.js b/users/validation.js new file mode 100644 index 000000000..7f09c6532 --- /dev/null +++ b/users/validation.js @@ -0,0 +1,16 @@ +function validateUser(user) { + let errors = []; + + if (!user.username || user.username.length >= 7) { + errors.push('Username must have at least seven characters'); + } + if (!user.password || user.password.length >= 7) { + errors.push('Password must be at least seven characters') + } + return { + isSuccessful: errors.length > 0 ? false : true, + errors, + } +} + +module.exports = { validateUser } \ No newline at end of file