diff --git a/README.md b/README.md index 71eb93186..5ffc23100 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,22 @@ 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('sessions') which stores user information + that remains peristed until deleted. JWT store user information on generated token + that is a string of data holding user information. 2. What does `bcrypt` do to help us store passwords in a secure manner. + bcrypt secures passwords by hashing them, essentially scrambling the password + and breaking it down into layers of strings, this can be increased depending on the + amount of time the hash is set to happen. For dev. puposes, its around 8 and for + any production hashes, it is increased, varying on servers, and how much it can handle as it slows down the process a lot. 3. How are unit tests different from integration and end-to-end testing. + - unit testing focuses on individual parts and tests those parts alone, + - integration and end to end focuses on 2 or more working together 4. How _Test Driven Development_ changes the way we write applications and tests. + rigorous testing of application as it develops You are expected to be able to answer questions in these areas. Your responses contribute to your Sprint Challenge grade. @@ -38,21 +48,21 @@ You are expected to be able to answer questions in these areas. Your responses c ### Task 1: Project Set Up -- [ ] Create a forked copy of this project -- [ ] Add your team lead as collaborator on Github -- [ ] Clone your OWN version of the repository (Not Lambda's by mistake!) -- [ ] Create a new branch: git checkout -b ``. -- [ ] Implement the project on your newly created `` branch, committing changes regularly -- [ ] Push commits: git push origin `` +- [+] Create a forked copy of this project +- [+] Add your team lead as collaborator on Github +- [+] Clone your OWN version of the repository (Not Lambda's by mistake!) +- [+] Create a new branch: git checkout -b ``. +- [+] Implement the project on your newly created `` branch, committing changes regularly +- [+] Push commits: git push origin `` ### Task 2: Project Requirements Your finished project must include all of the following requirements: -- [ ] An authentication workflow with functionality for account creation and login implemented inside `/auth/auth-router.js`. A `user` has `username` and `password`. Both properties are required. -- [ ] Middleware used to restrict access to resources for non authenticated requests. Use the file: `./auth/authenticate-middleware.js` as a starting point. -- [ ] Configuration for running tests using `Jest`. -- [ ] A **minimum o 2 tests** per API endpoint. +- [+] An authentication workflow with functionality for account creation and login implemented inside `/auth/auth-router.js`. A `user` has `username` and `password`. Both properties are required. +- [+] Middleware used to restrict access to resources for non authenticated requests. Use the file: `./auth/authenticate-middleware.js` as a starting point. +- [+] Configuration for running tests using `Jest`. +- [+] A **minimum o 2 tests** per API endpoint. **Note**: the database already has the users table, but if you run into issues, the migrations are available. diff --git a/api/server.js b/api/server.js index c8acc0eb4..e2043a26f 100644 --- a/api/server.js +++ b/api/server.js @@ -5,6 +5,7 @@ const helmet = require('helmet'); const authenticate = require('../auth/authenticate-middleware.js'); const authRouter = require('../auth/auth-router.js'); const jokesRouter = require('../jokes/jokes-router.js'); +let userRouter = require('../users/users-router'); const server = express(); @@ -14,5 +15,11 @@ server.use(express.json()); server.use('/api/auth', authRouter); server.use('/api/jokes', authenticate, jokesRouter); +server.use('/api/user', authenticate, userRouter); +// server.use('/api/user', userRouter); +server.get('/', (req, res) => { + res.status(200).json({ api: "up" }) +}) + module.exports = server; diff --git a/api/server.spec.js b/api/server.spec.js new file mode 100644 index 000000000..e0f9a8f7e --- /dev/null +++ b/api/server.spec.js @@ -0,0 +1,15 @@ +let supertest = require('supertest'); + +let server = require('./server'); + +describe("read server.js", () => { + describe("GET /", () => { + it("should return 200 OK status", () => { + return supertest(server) + .get("/") + .then(res => { + expect(res.status).toBe(200); + }) + }) + }) +}) \ No newline at end of file diff --git a/auth/auth-router.js b/auth/auth-router.js index 2fa2c9766..984dc7f36 100644 --- a/auth/auth-router.js +++ b/auth/auth-router.js @@ -1,11 +1,59 @@ const router = require('express').Router(); +let Users = require('../users/users-model'); + +let encrypt = require('bcryptjs'); +// let jwt = require('jsonwebtoken'); +let isValid = require('../utils/validate'); +const { createToken } = require('../utils/methods'); + router.post('/register', (req, res) => { // implement registration + let creds = req.body; + + if(isValid.isValid (creds)) { + let rounds = process.env.BCRYPT_ROUNDS || 8; // defaults to 8 on dev env + + let hash = encrypt.hashSync(creds.password, rounds) //takes pw from body and hashes it 8 times(dev) + + creds.password = hash; // sets original pw from body to newly hashed pw + + Users.create(creds) + .then(user => { + res.status(201).json({ data: user }) + }) + .catch(err => { + res.status(500).json({ error: "Could not create user"}) + }) + } else { + res.status(400).json({ message: "Provide a username and password" }); + } + }); router.post('/login', (req, res) => { // implement login + + let { username, password } = req.body; // extracting from body + + if(isValid.isValid(req.body)) { + Users.findBy({ username }) + .then(([ user ]) => { + if(user && encrypt.compareSync(password, user.password)) { + let token = createToken(user); // creates a token with payload + res.status(200).json({ token, message: "Login success" }) + } else { + res.status(401).json({ error: "Either username or password do not match in our records" }); + } + }) + .catch(err => { + res.status(500).json("Could not process request") + }) + } else { + res.status(400).json({ message: "Please provide a valid username and password"}) + } + + }); module.exports = router; diff --git a/auth/auth-router.spec.js b/auth/auth-router.spec.js new file mode 100644 index 000000000..e5882fcaf --- /dev/null +++ b/auth/auth-router.spec.js @@ -0,0 +1,131 @@ +let server = require('../api/server'); + +let supertest = require('supertest'); + +let authRouter = require('./auth-router'); +let db = require('../database/dbConfig'); + + + describe("POST /login flow", () => { + it('should return status 200 OK', async () => { + let user = { + username: "legacy", + password: "mypw" + } + return await supertest(server) + .post('/api/auth/register') + .send(user) + .then(res => { + // console.log(res.body, 'asdfasdf') + // console.log(res.status) + expect(res.status).toBe(201); + // expect(res.body.data.username).toBe(user.username) + }) + }) + + it("Should succesfully give 200 status, succesful message", async() => { + let user = { + username: "legacy", + password: "mypw" + } + + return await supertest(server) + .post('/api/auth/login') + .send(user) + .then(res => { + console.log(res.status, "from login") + console.log(res.body, 'from login flow') + expect(res.status).toBe(200) + expect(res.body.message).toEqual("Login success" ) + + }) + }) + }) + +describe("auth-router.js", () => { + beforeEach(async () => { + await db("users").truncate(); + }) + + describe("POST /register", () => { + it('should return status 201 OK', async () => { + let user = { + username: "legacy", + password: "mypw" + } + return await supertest(server) + .post('/api/auth/register') + .send(user) + .then(res => { + console.log(res.body) + expect(res.status).toBe(201); + expect(res.body.data.username).toBe(user.username) + }) + }) + + it('should give 400 error for no username or password', async () => { + let user = { + username: "Bass", + password: null + } + + return await supertest(server) + .post('/api/auth/register') + .send(user) + .then(res => { + console.log(res.status) + console.log(res.body) + expect(res.status).toBe(400); + expect(res.body).toEqual({ message: 'Provide a username and password' }) + }) + }) + }) + + describe("POST /login", () => { + it("should return status 401", async () => { + let user = { + username: "legacy", + password: "mypw" + } + return await supertest(server) + .post('/api/auth/login') + .send(user) + .then(res => { + console.log(res.status, 'fail login') + expect(res.status).toBe(401) // no record on server + }) + }) + }) + + // describe("POST /login flow", () => { + // it('should return status 200 OK', async () => { + // let user = { + // username: "legacy", + // password: "mypw" + // } + // return await supertest(server) + // .post('/api/auth/register') + // .send(user) + // .then(res => { + // // console.log(res.body, 'asdfasdf') + // // console.log(res.status) + // expect(res.status).toBe(201); + // // expect(res.body.data.username).toBe(user.username) + // }) + // }) + + // it("Should succesfully give 200 status, succesful message", async() => { + // let user = { + // username: "legacy", + // password: "mypw" + // } + + // return await supertest(server) + // .post('/api/auth/login') + // .send(user) + // .then(res => { + // console.log(res.status, "from login") + // }) + // }) + // }) +}) \ No newline at end of file diff --git a/auth/authenticate-middleware.js b/auth/authenticate-middleware.js index 6ca61d0cd..df4df3bf8 100644 --- a/auth/authenticate-middleware.js +++ b/auth/authenticate-middleware.js @@ -1,8 +1,28 @@ +let jwt = require('jsonwebtoken'); +let constants = require('../utils/constants'); +const { jwtSecret } = require('../utils/constants'); + /* 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!' }); + let token = req.headers.authorization; // grabs token from request headers property + let secret = constants.jwtSecret; + + if(token) { + jwt.verify(token, secret, (err, decodedToken) => { + if(err) { + res.status(401).json({ message: "Invalid token" }); + } else { + req.decodedToken = decodedToken; // creates a decoded token inside req + next(); + } + }) + } else { + res.status(401).json({ message: "valid credentials required" }); + } + + // res.status(401).json({ you: 'shall not pass!' }); }; diff --git a/database/auth.db3 b/database/auth.db3 index cc6ee6d93..d74d2b10f 100644 Binary files a/database/auth.db3 and b/database/auth.db3 differ diff --git a/package.json b/package.json index 4feb96236..a00ab45cb 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": "cross-env DB_ENV=testing jest --watch --runInBand" }, "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", "cors": "^2.8.5", + "cross-env": "^7.0.2", "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" + "jest": "^26.0.1", + "nodemon": "^2.0.3", + "supertest": "^4.0.2" + }, + "jest": { + "testEnvironment": "node" } } diff --git a/users/users-model.js b/users/users-model.js new file mode 100644 index 000000000..d6650f34b --- /dev/null +++ b/users/users-model.js @@ -0,0 +1,28 @@ +let db = require('../database/dbConfig'); + +async function create(user) { + try { + const [id] = await db("users").insert(user, "id"); // returns id from new user + + return findById(id);// uses findById to return the user + } catch (error) { + throw error; // gives back error if error is found + } + } + + function findById(id) { + return db("users").where({ id }).first().select("id", "username"); // made it to return only id and username + } + + function findBy(username) { + return db("users as u") + .select("u.id", "u.username", "u.password") + .where(username) + .orderBy("u.id"); + } + + module.exports = { + create, + findById, + findBy, + } \ No newline at end of file diff --git a/users/users-router.js b/users/users-router.js new file mode 100644 index 000000000..f85a76413 --- /dev/null +++ b/users/users-router.js @@ -0,0 +1,7 @@ +let router = require('express').Router(); + +router.get('/', (req, res) => { + res.status(200).json({ message: "Hello user" }) +}) + +module.exports = router; \ No newline at end of file diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 000000000..f17445d7e --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,3 @@ +module.exports = { + jwtSecret: process.env.JWT_SECRET || "real safe, real safe", +} \ No newline at end of file diff --git a/utils/methods.js b/utils/methods.js new file mode 100644 index 000000000..d856c4790 --- /dev/null +++ b/utils/methods.js @@ -0,0 +1,21 @@ +let jwt = require('jsonwebtoken'); +let constants = require('./constants') +module.exports = { + createToken, +} + +function createToken(user) { + let payload = { // returning parameter + subject: user.id, + username: user.username, + }; + + let secret = constants.jwtSecret; // returning parameter + + let options = { // returning parameter + //takes milliseconds but you can do '1d' etc + expiresIn: "1d" + } + + return jwt.sign(payload, secret, options) + } \ No newline at end of file diff --git a/utils/validate.js b/utils/validate.js new file mode 100644 index 000000000..91ce4fab7 --- /dev/null +++ b/utils/validate.js @@ -0,0 +1,7 @@ +module.exports = { + isValid, +} + +function isValid(user) { + return Boolean(user.username && user.password && typeof user.password === "string"); + }// checks to see if username and password exist, and if password is a valid alpha numerical string