diff --git a/api/auth/auth-router.js b/api/auth/auth-router.js index 47d8e51ae..82c7b4738 100644 --- a/api/auth/auth-router.js +++ b/api/auth/auth-router.js @@ -1,7 +1,35 @@ const router = require('express').Router(); +const db = require('../../data/dbConfig') +const bcrypt = require('bcryptjs') +const { checkFormat, checkNameTaken,} = require('./auth.middleware') +const JWT = require('jsonwebtoken') +const { JWT_SECRET,BCRYPT_ROUNDS } = require('../../config') -router.post('/register', (req, res) => { - res.end('implement register, please!'); +function generateToken(user) { + const payload = { + subject: user.id, + username: user.username + } + const options = { + expiresIn: '1d' + }; + return JWT.sign(payload,JWT_SECRET,options) +} + +router.post('/register', checkFormat, checkNameTaken, async (req, res, next) => { + try { + const { username, password } = req.body; + const newUser = { + username: username, + password: await bcrypt.hash(password, BCRYPT_ROUNDS) + }; + const newID = await db('users').insert(newUser); + const [result] = await db('users').where('id',newID); + + res.status(201).json(result); + } catch (err) { + next(err) + } /* IMPLEMENT You are welcome to build additional middlewares to help with the endpoint's functionality. @@ -29,8 +57,26 @@ router.post('/register', (req, res) => { */ }); -router.post('/login', (req, res) => { - res.end('implement login, please!'); +router.post('/login', checkFormat, async (req, res, next) => { + try { + const { username, password } = req.body; + + db('users').where('username', username).first() + .then(user => { + if (user && bcrypt.compareSync(password, user.password)) { + const token = generateToken(user); + res.status(200).json({ + message: `welcome, ${username}`, + token: token + }); + }else{ + next({ status: 401, message: 'invalid credentials'}); + } + }) + } catch (err) { + next(err) + } + /* IMPLEMENT You are welcome to build additional middlewares to help with the endpoint's functionality. diff --git a/api/auth/auth.middleware.js b/api/auth/auth.middleware.js new file mode 100644 index 000000000..47313f9d2 --- /dev/null +++ b/api/auth/auth.middleware.js @@ -0,0 +1,53 @@ +const db = require('../../data/dbConfig') +const jwt = require('jsonwebtoken') +const { JWT_SECRET } = require('../../config') + +const restricted = (req, res, next) => { + const token = req.headers.authorization + if (token) { + jwt.verify(token, JWT_SECRET, (err, decoded) => { + if (err) { + next({ status: 401, message: `invalid token: ${err.message}` }) + } else { + req.decodedJWT = decoded + next(); + } + }) + } else { + next({ status: 402, message: 'token required' }) + } +} + +const checkFormat = (req, res, next) => { + try { + const { username, password } = req.body + if (username && password) { + next(); + } else { + next({ status: 400, message: "username and password required" }) + } + } catch (err) { + next(err) + } +} + +const checkNameTaken = async (req, res, next) => { + try { + const { username } = req.body + const [user] = await db('users').where('username', username) + .select('username') + if (!user) { + next(); + } else { + next({ status: 400, message: "username taken" }) + } + } catch (err) { + next(err) + } +} + +module.exports = { + checkFormat, + checkNameTaken, + restricted +} \ No newline at end of file diff --git a/api/jokes/jokes-router.js b/api/jokes/jokes-router.js index f663f983c..716cbc53d 100644 --- a/api/jokes/jokes-router.js +++ b/api/jokes/jokes-router.js @@ -1,9 +1,14 @@ // do not make changes to this file const router = require('express').Router(); const jokes = require('./jokes-data'); +const { restricted } = require('../auth/auth.middleware') -router.get('/', (req, res) => { - res.status(200).json(jokes); +router.get('/', restricted, (req, res, next) => { + try { + res.status(200).json(jokes); + } catch (err) { + next(err) + } }); module.exports = router; diff --git a/api/server.js b/api/server.js index 33320b871..f7cb18838 100644 --- a/api/server.js +++ b/api/server.js @@ -16,4 +16,11 @@ server.use(express.json()); server.use('/api/auth', authRouter); server.use('/api/jokes', restrict, jokesRouter); // only logged-in users should have access! +// eslint-disable-next-line no-unused-vars +server.use((err, req, res, next) => { + res.status(err.status || 500).json({ + message: err.message + }) +}) + module.exports = server; diff --git a/api/server.test.js b/api/server.test.js index 96965c559..c2fc16032 100644 --- a/api/server.test.js +++ b/api/server.test.js @@ -1,4 +1,60 @@ // Write your tests here -test('sanity', () => { - expect(true).toBe(false) +const db = require('../data/dbConfig') +const request = require('supertest') +const server = require('./server') + +beforeAll(async () => { + await db.migrate.rollback(); + await db.migrate.latest(); +}) + +beforeEach(async () => { + await db('users').truncate() +}) + + + +describe('[GET] /jokes', () => { + const newUser = { username: "user", password: "1234" } + test('receives an error with no token present', async () => { + await request(server).post('/api/auth/register').send(newUser) + await request(server).post('/api/auth/login').send(newUser) + const data = await request(server).get('/api/jokes') + expect(data.body.message).toBe('token required') + }) + test('returns a list of jokes while authorized', async () => { + await request(server).post('/api/auth/register').send(newUser) + const res = await request(server).post('/api/auth/login').send(newUser) + const data = await request(server).get('/api/jokes').set('Authorization', `${res.body.token}`) + expect(data.body).toHaveLength(3) + }) +}) + +describe('[POST] /auth/register', () => { + const newUser = { username: "user", password: "1234" } + test('new users are listed in the database', async () => { + await request(server).post('/api/auth/register').send(newUser) + const rows = await db('users') + expect(rows).toHaveLength(1) + }) + test('returns username and hashed password', async () => { + const res = await request(server).post('/api/auth/register').send(newUser) + expect(res.body.username).toMatch(newUser.username) + expect(res.body.password).not.toMatch(newUser.password) + }) + +}) + +describe('[POST] /auth/login', () => { + const newUser = { username: "user", password: "1234" } + test('new user obtains a token when logging in', async () => { + await request(server).post('/api/auth/register').send(newUser) + const res = await request(server).post('/api/auth/login').send(newUser) + expect(res.body.token).toBeDefined() + }) + test('incorrect password gives an error', async () => { + await request(server).post('/api/auth/register').send(newUser) + const res = await request(server).post('/api/auth/login').send({ username: newUser.username, password: '123'}) + expect(res.body.message).toBe('invalid credentials') + }) }) diff --git a/config/index.js b/config/index.js new file mode 100644 index 000000000..fc9477d26 --- /dev/null +++ b/config/index.js @@ -0,0 +1,4 @@ +module.exports = { + BCRYPT_ROUNDS: process.env.BCRYPT_ROUNDS || 8, + JWT_SECRET: process.env.JWT_SECRET || 'shh' +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7e4320161..93863c131 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.1", "license": "ISC", "dependencies": { + "bcryptjs": "^2.4.3", "cors": "2.8.5", "express": "4.18.1", "helmet": "5.0.2", + "jsonwebtoken": "^9.0.0", "knex": "2.0.0", "sqlite3": "5.0.8" }, @@ -1586,6 +1588,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1724,6 +1731,11 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2359,6 +2371,14 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4712,6 +4732,54 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -8226,6 +8294,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -8330,6 +8403,11 @@ "node-int64": "^0.4.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8813,6 +8891,14 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -10620,6 +10706,46 @@ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "dev": true }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", diff --git a/package.json b/package.json index 9506c6c20..45c1590ce 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "start": "node index.js", "server": "nodemon index.js", "migrate": "knex migrate:latest", + "rollback": "knex migrate:rollback", + "resetdb": "npm run rollback && npm run migrate", "test": "cross-env NODE_ENV=testing jest --verbose --runInBand --silent" }, "repository": { @@ -14,9 +16,11 @@ }, "license": "ISC", "dependencies": { + "bcryptjs": "^2.4.3", "cors": "2.8.5", "express": "4.18.1", "helmet": "5.0.2", + "jsonwebtoken": "^9.0.0", "knex": "2.0.0", "sqlite3": "5.0.8" },