Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/models/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const mongoose = require('mongoose')

const schema = new mongoose.Schema({
username : {
type : String,
required : true
},
password : {
type : String,
required : true
},
admin :{
type : Boolean,
default : false
}

}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } })

module.exports = mongoose.model('User', schema)
135 changes: 135 additions & 0 deletions api/routes/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const router = require('express').Router()
const User = require('../models/user');
const bcrypt = require('bcrypt');
const jsonwebtoken = require('jsonwebtoken');

const {SECRET_KEY} = process.env

const saltRounds = 10;

// Add a post route for sign up
router.post('/signup', async (req, res, next) =>{
var status = 201;
try {
const {username, password} = req.body;
if (!username)
{
status = 400;
throw new Error('Please provide a valid username');
}
if (!password)
{
status = 400;
throw new Error('Please provide a valid password');
}
if(password.length < 8)
{
status = 400;
throw new Error('Your password should be atleast 8 characters');
}
var user = await User.findOne({username});
if (user)
{
status = 400;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are all the same, you could probably just define this at the top!

throw new Error('User already exists');
}
var hashedPassword = await bcrypt.hash(req.body.password, saltRounds);
const response = await User.create({username : username, password: hashedPassword});
res.status(status).json({status, response})
}
catch(err){
console.error(err);
const error = new Error(err.message)
error.status = 400
next(error)
}
})

// Add a post route for login
router.post('/login', async (req, res, next) => {
var status = 201;

try{
const {username, password} = req.body;
var guest = await User.findOne({username})
if (!guest)
{
throw new Error('User not found')
}

const isValid = await bcrypt.compare(password, guest.password)
if (!isValid) throw new Error('Password is invalid');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to use the same error message for both of the above errors. Otherwise, it's easier for someone to "guess" another user's username and is more insecure.


const payload = { id: guest._id } // Set up payload
const options = { expiresIn: '1 day' } // Set up expiration
const token = jsonwebtoken.sign(payload, SECRET_KEY , options)
res.status(status).json({status, token})
}
catch(err){
console.error(err);
const error = new Error('Login creds incorrect.')
error.status = 400
next(error)
}
})

// Add a patch route for an admin to update another users admin privileges
// I am assuming here that the param that is passed in for :id is the username
router.patch('/users/:id/permissions', async (req, res, next) => {
const status = 200
try {
// Check if A valid JWT token is not provided (status 401)
const token = req.headers.authorization.split('Bearer ')[1]
// Check if a token is provided
if (!token)
{
status = 401;
throw new Error(' No token present')
}
// Check if the token is valid
const payload = jsonwebtoken.verify(token, SECRET_KEY)
if (!payload)
{
status = 401;
throw new Error('Invalid token')
}

// Check if request body has admin
if(!req.body.admin)
{
status = 400;
throw new Error('Body does not contain admin')
}

// Authentication
const adminUser = await User.findOne({ _id: payload.id })
if(!adminUser)
{
status = 401;
throw new Error(' No admin user found')
}

if(!(adminUser.admin))
{
status = 401;
throw new Error('User is not an admin')
}

const user = await User.findOneAndUpdate({ username : req.params.id}, { admin: req.body.admin }, { new: true })

if(!user)
{
status = 404;
throw new Error(' The requested user cannot be found');
}

res.json({ status, message:"Successfully updated admin privileges" });
} catch (e) {
console.error(e)
const error = new Error(`Unable to update user permissions`)
error.status = 404
next(error)
}
})

module.exports = router
110 changes: 101 additions & 9 deletions api/routes/books.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const router = require('express').Router()
const Book = require('../models/book')
const User = require('../models/user')
const jsonwebtoken = require('jsonwebtoken');

const {SECRET_KEY} = process.env

router.get('/', async (req, res, next) => {
const status = 200
const response = await Book.find().select('-__v')

res.json({ status, response })
})

Expand All @@ -14,8 +18,8 @@ router.get('/:id', async (req, res, next) => {
try {
const response = await Book.findById(id).select('-__v')
if (!response) throw new Error(`Invalid Book _id: ${id}`)
res.json({ status, response })

res.json({ status, response })
} catch (e) {
console.error(e)
const error = new Error(`Cannot find book with id ${id}.`)
Expand All @@ -28,9 +32,27 @@ router.get('/:id', async (req, res, next) => {
router.post('/', async (req, res, next) => {
const status = 200
try {

// Check if the user is an admin
const token = req.headers.authorization.split('Bearer ')[1]
const payload = jsonwebtoken.verify(token, SECRET_KEY)
const user = await User.findOne({ _id: payload.id }).select('-__v -password')
if(!user)
{
const error = new Error('Cannot find user')
error.status = 404
return next(error)
}
if(!(user.admin))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need the ()s here. Just !user.admin is fine.

{
const error = new Error('User not an admin')
error.status = 404
return next(error)
}

const book = await Book.create(req.body)
if (!book) throw new Error(`Request body failed: ${JSON.stringify(req.body)}`)

const response = await Book.findById(book._id).select('-__v')
res.json({ status, response })
} catch (e) {
Expand All @@ -45,34 +67,104 @@ router.post('/', async (req, res, next) => {
// You should only be able to reserve a book if a user is logged in
router.patch('/:id/reserve', async (req, res, next) => {
const { id } = req.params
var status;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use let instead!

try {
// Check if its valid user/token
const token = req.headers.authorization.split('Bearer ')[1]
const payload = jsonwebtoken.verify(token, SECRET_KEY)
const user = await User.findOne({ _id: payload.id }).select('-__v -password')
if(!user)
{
const error = new Error('Cannot find user')
error.status = 404
return next(error)
}

const book = await Book.findById(id)
if (!book) {
const error = new Error(`Invalid Book _id: ${id}`)
error.message = 404
return next(error)
}

if (book.reserved.status === true)
{
const error = new Error('Book already reserved')
error.message = 400
return next(error)
}

book.reserved.status = true
// Set the reserved memberId to the current user
book.reserved.memberId = user._id
await book.save()

const response = await Book.findById(book._id).select('-__v')
const status = 200
res.json({ status, response })
} catch (e) {
console.error(e)
const message = 'Failed to set the reservation'
const error = new Error(message)
error.status = status
next(error)
}
})

// You should only be able to return a book if the user is logged in
// and that user is the one who reserved the book
router.patch('/:id/return', async (req, res, next) => {

try{
// Check if its valid user/token
const token = req.headers.authorization.split('Bearer ')[1]
const payload = jsonwebtoken.verify(token, SECRET_KEY)
const user = await User.findOne({ _id: payload.id }).select('-__v -password')
if(!user)
{
const error = new Error('Cannot find user')
error.status = 404
return next(error)
}

// Find the book
const book = await Book.findById(id)
if (!book) {
const error = new Error(`Invalid Book _id: ${id}`)
error.status = 404
return next(error)
}
if(book.reserved.status === false)
{
const error = new Error('You cannot return an unreserved book')
error.status = 400
return next(error)
}
if(book.reserved.memberId != user._id)
{
const error = new Error('The user id does not match')
error.status = 401
return next(error)
}

/// If able to return successfully, set reserved status to false and clear the memberId
book.reserved.status = false
book.reserved.memberId = ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null might be a better value here.

await book.save()

const response = await Book.findById(book._id).select('-__v')
const status = 200
const message = 'You must implement this route!'

console.log(message)
res.status(status).json({ status, message })
res.json({ status, response })

res.status(status).json({ status, response })
}
catch (e) {
console.error(e)
const message = 'Failed to set the reservation'
const error = new Error(message)
error.status = status
next(error)
}
})

module.exports = router
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ app.use(require('body-parser').json())

// Routes
app.use('/api/books', require('./api/routes/books'))
app.use('/api', require('./api/routes/auth'))

// Not Found Handler
app.use((req, res, next) => {
Expand Down
33 changes: 31 additions & 2 deletions db/seeds.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
const mongoose = require('mongoose')
const Book = require('../api/models/book')
const User = require('../api/models/user')
const config = require('../nodemon.json')
const bcrypt = require('bcrypt')

const reset = async () => {
mongoose.connect(config.env.MONGO_DB_CONNECTION, { useNewUrlParser: true })

// Reset the books collection

await Book.deleteMany() // Deletes all records
return await Book.create([
const books = await Book.create([
{
title: 'The Colour of Magic',
published: 1983,
Expand Down Expand Up @@ -41,9 +46,33 @@ const reset = async () => {
]
}
])


// Reset the user collection
await User.deleteMany() // Deletes all records
// Create 2 users with hashed passwords
const saltRounds = 10;

const adminPassword = await bcrypt.hash('CourseInstructor', saltRounds);
const userPassword = await bcrypt.hash('CourseStudent', saltRounds);
const users = User.create([
{
username : 'WesReid',
password : adminPassword,
admin : true
},
{
username : 'Soma',
password : userPassword
}
])

// Return both books and users
return (books, users);
}

// response here returns only the last await response which in this case happens to be users
reset().catch(console.error).then((response) => {
console.log(`Seeds successful! ${response.length} records created.`)
console.log(`Seeds successful! , Books and user records created`)
return mongoose.disconnect()
})
Loading