Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
Middleware allows us to pass errors to an error handling function. We should catch all errors that occur in any third party middleware or our own middleware router code and pass them to the error handler function. Use the code below to catch errors that occur in middleware. The code should be the first piece of code within a middleware function.
if(err) { return next(err) }
For example, the mongoDB create function has two parameters (err and data). Note that the error checking occurs as the first piece of code within the create() middleware function.
carsModel.create(carDetails, (err, data) =>
{
if(err)
{
return next(err)
}
return res.json(data)
})
In the above code, the details that are contained in err are provided by the middleware function.
There are also cases where we need to create our own errors. For example, in the middleware below, we are testing if a user is an administrator. When catching our own errors, we should ensure that these errors are dealt with in the same way as errors from third-party middleware. We can use the createError() function from the 'http-errors' package to do this.
... var createError = require('http-errors') ... const checkThatUserIsAnAdministrator = (req, res, next) => { if(req.decodedToken.accessLevel < process.env.ACCESS_LEVEL_ADMIN) { return next(createError(401)) } return next() }
Install the package "http-errors"
The createError() function has two optional parameters: a HTTP status code (i.e. an error code) and/or an error message.
Best practice is to always give an error code.
If we do not provide an error message, then a default error message that matches the error code is automatically created. If we include an error message, then our error message overwrites the default error message. In the example below, the default error message is overwritten with the text `No file was selected to be uploaded` to provide additional information.
const checkThatFileIsUploaded = (req, res, next) => { if(!req.file) { return next(createError(400, `No file was selected to be uploaded`)) } return next() }
Below is a list of some HTTP status codes:
A complete list of HTTP status codes can be found at https://www.restapitutorial.com/httpstatuscodes.html.
Error handling allows us to write cleaner, more maintainable code. In previous examples, we created an errorMessage, which we returned to the client-side in a JSON. The errorMessage code that is highlighted in red below can be replaced with the error handling code in green.
server/routes/users.js WITHOUT middleware Error handling const checkThatUserIsNotAlreadyInUsersCollection = (req, res, next) =>
{
usersModel.findOne({email:req.params.email}, (err, data) =>
{
if(!data)
{
res.json({errorMessage:`User is not logged in`})
}
req.data = data
return next()
})
} WITH middleware Error handling const checkThatUserExistsInUsersCollection = (req, res, next) => { usersModel.findOne({email:req.params.email}, (err, data) => { if(err) { return next(err) } req.data = data return next() }) }
In our previous examples, we have used the value res.data.errorMessage to hold error messages that we wanted to return to the client-side. Using error handling, we can remove the res.data.errorMessage code that we have used in previous examples to handle errors. We replace this code with a catch() error handling function. This results in code that is much shorter and less complex, which is easier to read and understand.
In the example below, the code that is highlighted in red uses res.data.errorMessage. This can be replaced with the catch() code that is highlighted in green.
/client/src/components/Login.js WITHOUT middleware Error handling axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`) .then(res => { if(res.data) { if (res.data.errorMessage) { console.log(res.data.errorMessage) } else // user successfully logged in { console.log("User logged in") localStorage.name = res.data.name localStorage.accessLevel = res.data.accessLevel localStorage.profilePhoto = res.data.profilePhoto localStorage.token = res.data.token this.setState({isLoggedIn:true}) } } else { console.log("Login failed") } }) WITH middleware Error handling axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
.then(res =>
{
localStorage.name = res.data.name
localStorage.accessLevel = res.data.accessLevel
localStorage.profilePhoto = res.data.profilePhoto
localStorage.token = res.data.token
this.setState({isLoggedIn:true})
}) .catch(err =>
{ console.log(err.response.status) // the error message's status code (e.g. 401) console.log(err.response.statusText) // the default error message text that is associated with the status code (e.g. "Unauthorized") console.log(err.response.data) // the error message's text. This can be custom defined on the server-side.
this.setState({wasSubmittedAtLeastOnce: true})
})
Open the middleware project from the previous section in these notes. Change the code to implement proper error handling.
The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.
In this example, we implement error handling. We also remove all non-error console.log() debugging messages from various client-side axios() methods.
import React, {Component} from "react" import {Link} from "react-router-dom" import axios from "axios" import CarTable from "./CarTable" import Logout from "./Logout" import {ACCESS_LEVEL_GUEST, ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants" export default class DisplayAllCars extends Component { constructor(props) { super(props) this.state = { cars:[] } } componentDidMount() { axios.get(`${SERVER_HOST}/cars`) .then(res => { this.setState({cars: res.data}) }) .catch(err => { // do nothing }) } render() { return ( <div className="form-container"> { localStorage.accessLevel > ACCESS_LEVEL_GUEST ? <div className="logout"> { localStorage.profilePhoto !== "null" ? <img id="profilePhoto" src={`data:;base64,${localStorage.profilePhoto}`} alt=""/> : null } <Logout/> </div> : <div> <Link className="green-button" to={"/Login"}>Login</Link> <Link className="blue-button" to={"/Register"}>Register</Link> <Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link> <br/><br/><br/></div> } <div className="table-container"> <CarTable cars={this.state.cars} /> { localStorage.accessLevel >= ACCESS_LEVEL_ADMIN ? <div className="add-new-car"> <Link className="blue-button" to={"/AddCar"}>Add New Car</Link> </div> : null } </div> </div> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
componentDidMount() { axios.get(`${SERVER_HOST}/cars`) .then(res => { this.setState({cars: res.data}) }) .catch(err => { // do nothing }) }
import React, {Component} from "react" import {Redirect, Link} from "react-router-dom" import Form from "react-bootstrap/Form" import axios from "axios" import Button from "../components/Button" import {ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants" export default class AddCar extends Component { constructor(props) { super(props) this.state = { model:"", colour:"", year:"", price:"", selectedFiles:null, redirectToDisplayAllCars:localStorage.accessLevel < ACCESS_LEVEL_ADMIN, wasSubmittedAtLeastOnce:false } } componentDidMount() { this.inputToFocus.focus() } handleChange = (e) => { this.setState({[e.target.name]: e.target.value}) } handleFileChange = (e) => { this.setState({selectedFiles: e.target.files}) } handleSubmit = (e) => { e.preventDefault() let formData = new FormData() formData.append("model", this.state.model) formData.append("colour", this.state.colour) formData.append("year", this.state.year) formData.append("price", this.state.price) if(this.state.selectedFiles) { for(let i = 0; i < this.state.selectedFiles.length; i++) { formData.append("carPhotos", this.state.selectedFiles[i]) } } axios.post(`${SERVER_HOST}/cars`, formData, {headers:{"authorization":localStorage.token, "Content-type": "multipart/form-data"}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) }) } render() { let errorMessage = ""; if(this.state.wasSubmittedAtLeastOnce) { errorMessage = <div className="error">Error: All fields must be filled in<br/></div>; } return ( <div className="form-container"> {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null} {errorMessage} <Form> <Form.Group controlId="model"> <Form.Label>Model</Form.Label> <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="colour"> <Form.Label>Colour</Form.Label> <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="year"> <Form.Label>Year</Form.Label> <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="price"> <Form.Label>Price</Form.Label> <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="photos"> <Form.Label>Photos</Form.Label> <Form.Control type = "file" multiple onChange = {this.handleFileChange} /></Form.Group> <br/><br/> <Button value="Add" className="green-button" onClick={this.handleSubmit}/> <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link> </Form> </div> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.post(`${SERVER_HOST}/cars`, formData, {headers:{"authorization":localStorage.token, "Content-type": "multipart/form-data"}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) })
import React, {Component} from "react" import Form from "react-bootstrap/Form" import {Redirect, Link} from "react-router-dom" import axios from "axios" import Button from "../components/Button" import {ACCESS_LEVEL_NORMAL_USER, SERVER_HOST} from "../config/global_constants" export default class EditCar extends Component { constructor(props) { super(props) this.state = { model: ``, colour: ``, year: ``, price: ``, redirectToDisplayAllCars:localStorage.accessLevel < ACCESS_LEVEL_NORMAL_USER, wasSubmittedAtLeastOnce:false } } componentDidMount() { this.inputToFocus.focus() axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({ model: res.data.model, colour: res.data.colour, year: res.data.year, price: res.data.price }) }) .catch(err => { // do nothing }) } handleChange = (e) => { this.setState({[e.target.name]: e.target.value}) } handleSubmit = (e) => { e.preventDefault() const carObject = { model: this.state.model, colour: this.state.colour, year: this.state.year, price: this.state.price } axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) }) } render() { let errorMessage = ""; if(this.state.wasSubmittedAtLeastOnce) { errorMessage = <div className="error">Error: All fields must be filled in<br/></div>; } return ( <div className="form-container"> {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null} {errorMessage} <Form> <Form.Group controlId="model"> <Form.Label>Model</Form.Label> <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="colour"> <Form.Label>Colour</Form.Label> <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="year"> <Form.Label>Year</Form.Label> <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} /> </Form.Group> <Form.Group controlId="price"> <Form.Label>Price</Form.Label> <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} /> </Form.Group> <Button value="Update" className="green-button" onClick={this.handleSubmit}/> <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link> </Form> </div> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({ model: res.data.model, colour: res.data.colour, year: res.data.year, price: res.data.price }) }) .catch(err => { // do nothing })
axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) })
import React, {Component} from "react" import {Redirect} from "react-router-dom" import axios from "axios" import {SERVER_HOST} from "../config/global_constants" export default class DeleteCar extends Component { constructor(props) { super(props) this.state = { redirectToDisplayAllCars:false } } componentDidMount() { axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { // Do nothing }) } render() { return ( <div> {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null} </div> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}}) .then(res => { this.setState({redirectToDisplayAllCars:true}) }) .catch(err => { // Do nothing })
import React, {Component} from "react" import {Link} from "react-router-dom" import axios from "axios" import {ACCESS_LEVEL_GUEST, ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants" export default class CarTableRow extends Component { componentDidMount() { this.props.car.photos.map(photo => { return axios.get(`${SERVER_HOST}/cars/photo/${photo.filename}`) .then(res => { document.getElementById(photo._id).src = `data:;base64,${res.data.image}` }) .catch(err => { // do nothing }) }) } render() { return ( <tr> <td>{this.props.car.model}</td> <td>{this.props.car.colour}</td> <td>{this.props.car.year}</td> <td>{this.props.car.price}</td> <td className="carPhotos"> {this.props.car.photos.map(photo => <img key={photo._id} id={photo._id} alt=""/>)} </td> <td> {localStorage.accessLevel > ACCESS_LEVEL_GUEST ? <Link className="green-button" to={"/EditCar/" + this.props.car._id}>Edit</Link> : null} {localStorage.accessLevel >= ACCESS_LEVEL_ADMIN ? <Link className="red-button" to={"/DeleteCar/" + this.props.car._id}>Delete</Link> : null} </td> </tr> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
return axios.get(`${SERVER_HOST}/cars/photo/${photo.filename}`) .then(res => { document.getElementById(photo._id).src = `data:;base64,${res.data.image}` }) .catch(err => { // do nothing })
import React, {Component} from "react" import {Redirect, Link} from "react-router-dom" import axios from "axios" import Button from "../components/Button" import {SERVER_HOST} from "../config/global_constants" export default class Login extends Component { constructor(props) { super(props) this.state = { email:"", password:"", isLoggedIn:false, wasSubmittedAtLeastOnce:false } } handleChange = (e) => { this.setState({[e.target.name]: e.target.value}) } handleSubmit = (e) => { axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`) .then(res => { localStorage.name = res.data.name localStorage.accessLevel = res.data.accessLevel localStorage.profilePhoto = res.data.profilePhoto localStorage.token = res.data.token this.setState({isLoggedIn:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) }) } render() { let errorMessage = ""; if(this.state.wasSubmittedAtLeastOnce) { errorMessage = <div className="error">Login Details are incorrect<br/></div>; } return ( <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm"> <h2>Login</h2> {this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null} {errorMessage} <input type = "email" name = "email" placeholder = "Email" autoComplete="email" value={this.state.email} onChange={this.handleChange} /><br/> <input type = "password" name = "password" placeholder = "Password" autoComplete="password" value={this.state.password} onChange={this.handleChange} /><br/><br/> <Button value="Login" className="green-button" onClick={this.handleSubmit}/> <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link> </form> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`) .then(res => { localStorage.name = res.data.name localStorage.accessLevel = res.data.accessLevel localStorage.profilePhoto = res.data.profilePhoto localStorage.token = res.data.token this.setState({isLoggedIn:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) })
import React, {Component} from "react" import {Redirect} from "react-router-dom" import axios from "axios" import Button from "../components/Button" import {SERVER_HOST} from "../config/global_constants" export default class Logout extends Component { constructor(props) { super(props) this.state = { isLoggedIn:true } } handleSubmit = (e) => { e.preventDefault() axios.post(`${SERVER_HOST}/users/logout`) .then(res => { localStorage.clear() this.setState({isLoggedIn:false}) }) .catch(err => { // do nothing }) } render() { return ( <div> {!this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null} <Button value="Log out" className="red-button" onClick={this.handleSubmit}/> </div> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.post(`${SERVER_HOST}/users/logout`) .then(res => { localStorage.clear() this.setState({isLoggedIn:false}) }) .catch(err => { // do nothing })
import React, {Component} from "react" import {Redirect, Link} from "react-router-dom" import axios from "axios" import Button from "../components/Button" import {SERVER_HOST} from "../config/global_constants" export default class Register extends Component { constructor(props) { super(props) this.state = { name:"", email:"", password:"", confirmPassword:"", selectedFile:null, isRegistered:false, wasSubmittedAtLeastOnce:false } } handleChange = (e) => { this.setState({[e.target.name]: e.target.value}) } handleFileChange = (e) => { this.setState({selectedFile: e.target.files[0]}) } handleSubmit = (e) => { e.preventDefault() let formData = new FormData() if(this.state.selectedFile) { formData.append("profilePhoto", this.state.selectedFile, this.state.selectedFile.name) } axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`, formData, {headers: {"Content-type": "multipart/form-data"}}) .then(res => { localStorage.name = res.data.name localStorage.accessLevel = res.data.accessLevel localStorage.profilePhoto = res.data.profilePhoto localStorage.token = res.data.token this.setState({isRegistered:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) }) } render() { let errorMessage = ""; if(this.state.wasSubmittedAtLeastOnce) { errorMessage = <div className="error">Error: All fields must be filled in<br/></div>; } return ( <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm"> {this.state.isRegistered ? <Redirect to="/DisplayAllCars"/> : null} {errorMessage} <h2>New User Registration</h2> <input name = "name" type = "text" placeholder = "Name" autoComplete="name" value = {this.state.name} onChange = {this.handleChange} ref = {(input) => { this.inputToFocus = input }} /><br/> <input name = "email" type = "email" placeholder = "Email" autoComplete="email" value = {this.state.email} onChange = {this.handleChange} /><br/> <input name = "password" type = "password" placeholder = "Password" autoComplete="password" title = "Password must be at least ten-digits long and contains at least one lowercase letter, one uppercase letter, one digit and one of the following characters (£!#€$%^&*)" value = {this.state.password} onChange = {this.handleChange} /><br/> <input name = "confirmPassword" type = "password" placeholder = "Confirm password" autoComplete="confirmPassword" value = {this.state.confirmPassword} onChange = {this.handleChange} /><br/> <input name = "profilePhoto" type = "file" onChange = {this.handleFileChange} /><br/><br/> <Button value="Register New User" className="green-button" onClick={this.handleSubmit} /> <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link> </form> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`, formData, {headers: {"Content-type": "multipart/form-data"}}) .then(res => { localStorage.name = res.data.name localStorage.accessLevel = res.data.accessLevel localStorage.profilePhoto = res.data.profilePhoto localStorage.token = res.data.token this.setState({isRegistered:true}) }) .catch(err => { this.setState({wasSubmittedAtLeastOnce: true}) })
import React, {Component} from "react" import {Redirect, Link} from "react-router-dom" import axios from "axios" import Button from "../components/Button" import {SERVER_HOST} from "../config/global_constants" export default class ResetDatabase extends Component { constructor(props) { super(props) this.state = { isReset:false } } handleChange = (e) => { this.setState({[e.target.name]: e.target.value}) } resetUsersModel = () => { axios.post(`${SERVER_HOST}/users/reset_user_collection`) .then(res => { console.log("User collection reset") localStorage.clear() this.setState({isReset:true}) }) .catch(err => { // do nothing }) } render() { return ( <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm"> {this.state.isReset ? <Redirect to="/DisplayAllCars"/> : null} <p>"Reset User Database" is only for testing purposes.<br/>All code on the client-side and server-side relating to resetting the database should be removed from any development release</p> <Button value="Reset User Database" className="red-button" onClick={this.resetUsersModel}/> <br/><br/> <p>Reset the database and set up an administrator with:<br/> * email <strong>admin@admin.com</strong><br/> * password <strong>123!"£qweQWE</strong></p> <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link> </form> ) } }
The axios() function call does not change.
The .then() function no longer deals with any error handling.
The .catch() function deals with all error handling.
axios.post(`${SERVER_HOST}/users/reset_user_collection`) .then(res => { console.log("User collection reset") localStorage.clear() this.setState({isReset:true}) }) .catch(err => { // do nothing })
// Server-side global variables require(`dotenv`).config({path:`./config/.env`}) // Database require(`./config/db`) // Express const express = require(`express`) const app = express() app.use(require(`body-parser`).json()) app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST})) // Routers app.use(require(`./routes/cars`)) app.use(require(`./routes/users`)) // Port app.listen(process.env.SERVER_PORT, () => { console.log(`Connected to port ` + process.env.SERVER_PORT) }) // Error 404 app.use((req, res, next) => {next(createError(404))}) // Handle errors app.use(function (err, req, res, next) { if (!err.statusCode) { err.statusCode = 500 } // check that all required parameters are not empty in any route if (err instanceof ReferenceError) { err.statusCode = 400 err.message = "Cannot reference a variable that has not been declared. This can be caused in run-time if the user did not input a parameter that is required by a router" } // Server-side error message console.log(err.message + "\nError Details...") // Server-side error details console.log(err) // return error message that will be displayed on client-side console res.status(err.statusCode).send(err.message) })
blah
if (!err.statusCode) { err.statusCode = 500 }
// check that all required parameters are not empty in any route
if (err instanceof ReferenceError)
{
err.statusCode = 400
err.message = "Cannot reference a variable that has not been declared. This can be caused in run-time if the user did not input a parameter that is required by a router"
}
// Server-side error message
console.log(err.message + "\nError Details...")
// Server-side error details
console.log(err)
// return error message that will be displayed on client-side console
res.status(err.statusCode).send(err.message)
const mongoose = require(`mongoose`) let carPhotosSchema = new mongoose.Schema( { filename:{type:String} }) let carsSchema = new mongoose.Schema( { model: {type: String, required:true}, colour: {type: String, required:true}, year: {type: Number, required:true}, price: {type: Number, required:true}, photos:[carPhotosSchema] }, { collection: `cars` }) module.exports = mongoose.model(`cars`, carsSchema)
Marking any required fields will cause mongoDB to return an if we attempt to add a document with a required field that is empty.
const router = require(`express`).Router() var createError = require('http-errors') const usersModel = require(`../models/users`) const bcrypt = require('bcrypt') // needed for password encryption const jwt = require('jsonwebtoken') const fs = require('fs') const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8') const multer = require('multer') const upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`}) const emptyFolder = require('empty-folder') const checkThatUserExistsInUsersCollection = (req, res, next) => { usersModel.findOne({email:req.params.email}, (err, data) => { if(err) { return next(err) } req.data = data return next() }) } const checkThatJWTPasswordIsValid = (req, res, next) => { bcrypt.compare(req.params.password, req.data.password, (err, result) => { if(err) { return next(err) } if(!result) { return next(createError(401)) } return next() }) } const checkThatFileIsUploaded = (req, res, next) => { if(!req.file) { return next(createError(400, `No file was selected to be uploaded`)) } return next() } const checkThatFileIsAnImageFile = (req, res, next) => { if(req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpg" && req.file.mimetype !== "image/jpeg") { fs.unlink(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, (err) => {return next(err)}) } return next() } const checkThatUserIsNotAlreadyInUsersCollection = (req, res, next) => { // If a user with this email does not already exist, then create new user usersModel.findOne({email:req.params.email}, (err, data) => { if(err) { return next(err) } return next(createError(401)) }) return next() } const addNewUserToUsersCollection = (req, res, next) => { bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { if(err) { return next(err) } usersModel.create({name:req.params.name, email:req.params.email, password:hash, profilePhotoFilename:req.file.filename}, (err, data) => { if(err) { return next(err) } const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY}) fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, 'base64', (err, fileData) => { if(err) { return next(err) } return res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:fileData, token:token}) }) }) }) } const emptyUsersCollection = (req, res, next) => { usersModel.deleteMany({}, (err, data) => { if(err) { return next(err) } }) return next() } const addAdminUserToUsersCollection = (req, res, next) => { const adminPassword = `123!"£qweQWE` bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { if(err) { return next(err) } usersModel.create({name:"Administrator", email:"admin@admin.com", password:hash, accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (err, data) => { if(err) { return next(err) } emptyFolder(process.env.UPLOADED_FILES_FOLDER, false, (result) => { return res.json(data) }) }) }) } const returnUsersDetailsAsJSON = (req, res, next) => { const token = jwt.sign({email: req.data.email, accessLevel:req.data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY}) if(req.data.profilePhotoFilename) { fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.data.profilePhotoFilename}`, 'base64', (err, data) => { if(err) { return next(err) } return res.json({name: req.data.name, accessLevel:req.data.accessLevel, profilePhoto:data, token:token}) }) } else { return res.json({name: req.data.name, accessLevel:req.data.accessLevel, profilePhoto:null, token:token}) } } const logout = (req, res, next) => { return res.json({}) } // IMPORTANT // Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection // The code below is for development testing purposes only router.post(`/users/reset_user_collection`, emptyUsersCollection, addAdminUserToUsersCollection) router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), checkThatFileIsUploaded, checkThatFileIsAnImageFile, checkThatUserIsNotAlreadyInUsersCollection, addNewUserToUsersCollection) router.post(`/users/login/:email/:password`, checkThatUserExistsInUsersCollection, checkThatJWTPasswordIsValid, returnUsersDetailsAsJSON) router.post(`/users/logout`, logout) module.exports = router
We capture every error that might occur using the code below.
if (err) { return next(err) }
We use the createError() method to create our own errors. We shall always pass a HTTP error code when we create our own error. In the example below, we create an error with the code 401:
return next(createError(401))
We can also pass an error message to the createError() method. If we do this, our error message will overwrite the default error message for the given HTTP error code. This can help us when we are debugging code.
return next(createError(400, `No file was selected to be uploaded`))
In order to use the createError() method, we need to include the http-errors package.
var createError = require('http-errors')
const router = require(`express`).Router() var createError = require('http-errors') const carsModel = require(`../models/cars`) const jwt = require('jsonwebtoken') const fs = require('fs') const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8') const multer = require('multer') var upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`}) const verifyUsersJWTPassword = (req, res, next) => { jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => { if (err) { return next(err) } req.decodedToken = decodedToken return next() }) } const checkThatUserIsAnAdministrator = (req, res, next) => { if(req.decodedToken.accessLevel < process.env.ACCESS_LEVEL_ADMIN) { return next(createError(401)) } return next() } const createNewCarDocument = (req, res, next) => { // Use the new car details to create a new car document let carDetails = new Object() carDetails.model = req.body.model carDetails.colour = req.body.colour carDetails.year = req.body.year carDetails.price = req.body.price // add the car's photos to the carDetails JSON object carDetails.photos = [] req.files.map((file, index) => { carDetails.photos[index] = {filename:`${file.filename}`} }) carsModel.create(carDetails, (err, data) => { if(err) { return next(err) } return res.json(data) }) } const getAllCarDocuments = (req, res, next) => { //user does not have to be logged in to see car details carsModel.find((err, data) => { if(err) { return next(err) } return res.json(data) }) } const getCarPhotoAsBase64 = (req, res, next) => { fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.params.filename}`, 'base64', (err, data) => { if(err) { return next(err) } return res.json({image:data}) }) } const getCarDocument = (req, res, next) => { carsModel.findById(req.params.id, (err, data) => { if(err) { return next(err) } return res.json(data) }) } const updateCarDocument = (req, res, next) => { carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (err, data) => { if(err) { return next(err) } return res.json(data) }) } const deleteCarDocument = (req, res, next) => { carsModel.findByIdAndRemove(req.params.id, (err, data) => { if(err) { return next(err) } return res.json(data) }) } // read all records router.get(`/cars`, getAllCarDocuments) // get one car photo router.get(`/cars/photo/:filename`, getCarPhotoAsBase64) // Read one record router.get(`/cars/:id`, verifyUsersJWTPassword, getCarDocument) // Add new record router.post(`/cars`, verifyUsersJWTPassword, checkThatUserIsAnAdministrator, upload.array("carPhotos", parseInt(process.env.MAX_NUMBER_OF_UPLOAD_FILES_ALLOWED)), createNewCarDocument) // Update one record router.put(`/cars/:id`, verifyUsersJWTPassword, updateCarDocument) // Delete one record router.delete(`/cars/:id`, verifyUsersJWTPassword, checkThatUserIsAnAdministrator, deleteCarDocument) module.exports = router
Errors are handled in the same way as was described for the in the users router file, server/routes/users.js
Add middleware to perform server-side validation of the client-side form data before the it is used with mongoDB.
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.