Building a Basic Node/Express Server (Part 2)

In part one of this series we implemented all the basic building blocks of a node/express server, along with a MongoDB database. In part two we’re going to continue building out the different user controller functions and routes, add in some authentication and validation for added security, and a few other odds and ends that’ll pop up as the app progresses.

To start with, I’m going to go ahead and define all the controller functions and routes we need. We’ll be building them out one at a time throughout this post, but first I want to get them all laid out within the app so we have a roadmap of what things we need to complete. So in the controllers/user.js file:

const User = require('../models/user');

module.exports = {
  create: async (req, res) =>{
    const user = new User(req.body)
    try {
      await user.save()
      res.status(201).send({ user })
    } catch (e) {
      res.status(400).send(e)
    }
  },
  login: async (req, res) => {

  },
  logout: async (req, res) => {

  },
  read: async (req, res) => {

  },
  update: async (req, res) => {

  },
  delete: async (req, res) => {

  }
}

And in the routers/user.js file:

const express = require('express');
const router = new express.Router();
const userController = require('../controllers/user');

router.post('/users', userController.create);
router.post('/users/login', userController.login)
router.post('/users/logout', userController.logout)
router.patch('/users/:id', userController.update)
router.get('/users/me', userController.read)
router.delete('/users/me', userController.delete)

module.exports = router;

With this done, we have the routing and controller structure in place for all the basic User actions we need within the app. But we have a few problems here:

  1. Upon creating an account the user’s password is not encrypted.
  2. When a user wants to log in to an existing account, update their account, get the details of their account, or delete their account, we need a way to verify that they are authorized to do so.
  3. The user currently needs to log in to their account every time they visit the site. We need a way to persist their login session so that, if they log in one a browser, they will remain logged in if they view the site again at a later date.

Let’s start with the first of those challenges, as password encryption occurs upon creation of the user account, and we’ve already written the controller method for that one.

To do this, we’re going to use an npm package built for explicitly for this purpose, bcrypt:

// Terminal
yarn add bcryptjs

Once it’s installed, we need to import it into the models/user.js file, and create a function that automatically runs each time the User object is saved, as this will cover both instances of creating a new User object and updating an existing User object:

// src/models/user.js
const bcrypt = require('bcryptjs');

// [...]

userSchema.pre('save', async (next) => {
  const user = this;
  if (user.isModified('password')) {
    user.password = await bcrypt.hash(user.password, 8)
  }
  next();
})

const User = mongoose.model('User', userSchema);

module.exports = User;

Here we define a function that runs prior to the User object saving, which takes place in the create function we defined in the first part of this series:

create: async (req, res) =>{
    const user = new User(req.body)
    try {
	// ***SAVES HERE***
      await user.save()
      res.status(201).send({ user })
    } catch (e) {
      res.status(400).send(e)
    }
  },

And will take place in the update function once we create it as well. The password encryption function we created accesses the current instance of the User object using the “this” keyword, and if the user’s password has been modified it applies the bcrypt.hash method to it, sending it through 8 hashing rounds. The result?

In the object that’s returned upon creating a new user, our password is encrypted and far more secure!

Now that that’s taken care of we can move on to the next steps in our list: verifying that a user is authorized to take certain actions on their account, and persisting their login across different browser sessions. To persist their login, we’re going to use a JSON web token (JWT, for short) which will be attached to their browser and recognized by our app each time they visit our site.

To do this, first we need to install the jsonwebtoken npm package:

// Terminal
yarn add jsonwebtoken

Which we’ll need to import into the models/user.js file, and create a function that generates the token and attaches it each time a user logs in to the site from a different browser. We also need to add a “tokens” field on the User model to store the different tokens the user will have associated with their account:

// src/models/user.js
const jwt = require('jsonwebtoken');

// [...]

const userSchema = new mongoose.Schema(
		// [...]
    tokens: [
      {
        token: {
          type: String,
          required: true
        }
      }
    ]
  },
  {
    timestamps: true
  }
)

Since each user can have many tokens (one for each browser), the tokens field will be an array containing a number of tokens. With that in place, we can create a User model method that generates the auth token and attaches it to their browser upon successfully logging in:

// src/models/user.js
userSchema.methods.generateAuthToken = async function () {
  const user = this;
  const token = jwt.sign({ _id: user._id.toString() }, 'allhailhypnotoad');
  user.tokens = user.tokens.concat({ token });
  await user.save();
  return token;
}

In this instance we use the explicit function declaration ( function () ) as opposed to an arrow function because we need to use the this keyword, which is inaccessible in arrow functions.

We assign the current instance of the User model to a variable, use it’s ID to generate an auth token, add that token on to the array of tokens we just made part of the user model, save the user model, then return the token as part of the HTTP response.

At the end of the token variable assignment line, you’ll see a string passed in as the second argument to the jwt.sign method: ‘allhailhypnotoad’.

This is what’s called a JWT secret, and as you could probably guess…it’s supposed to be kept secret. Defining it in line here is not recommended, as if we push this code to Github anyone who can view the code will be able to see our JWT secret. That’s no bueno.

So, we’re going to take a quick sidebar here and extract this variable out to another file which we’ll keep hidden. First, we need to install the dotenv npm package which loads environmental variables from an env file:

// Terminal
yarn add dotenv
yarn add env-cmd nodemon --save-dev

We’re also installing two other packages called nodemon and env-cmd, the reasons for which I’ll explain shortly. Then we’ll create a config folder in the root of the directory, with a file in it titled dev.env:

In this file we’ll define our JWT secret:

// config/dev.env
JWT_SECRET=allhailhypnotoad

Again in the root of the directory, we’ll create a file named .gitignore, which we’ll use to specify which files should not be uploaded to github when we push our code up:

// .gitignore
config
node_modules

We’ll include the config folder here so that all variables and other information we need to keep secret can be hidden from view when we push our code to Github. We’ll also include the node_modules folder, since anyone else who wants to work on our code can install the packages on their local device and it’d be a waste of space to upload them all to Github for every project that uses them.

With this in place we now need to adjust the command we use to run our server, as we need to initialize variables within the env file for use in the rest of our code when the server runs. So in package.json:

// package.json
"scripts": {
  "start": "node src/app.js",
  "dev": "env-cmd -f ./config/dev.env nodemon src/app.js"
},

Here we force the env file to run before we run app.js, using the env-cmd package we installed above. We also run app.js using the nodemon package (which we just installed above), which is useful as nodemon automatically restarts the server when it detects changes in the files in the directory. This means that if we normally need to shut down the server and re-run the command to start it based on changes we made, nodemon saves us from having to do that. It’s a small time savings, but every bit counts!

With this in place, we can now swap out the JWT secret in our models/user.js file with a reference to the JWT secret in our hidden .env file, like so:

// src/models/user.js
userSchema.methods.generateAuthToken = async function () {
  const user = this;
  const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET);
  user.tokens = user.tokens.concat({ token });
  await user.save();
  return token;
};

Again, we need the this keyword so we use the explicit function declaration.

And with that, we should be able to restart the server using our new “yarn run dev” command, and have everything up and running:

And there we are! (I’m ignoring the deprecation warning for now, we’ll get to that later)

There’s just one thing we need to do to get our JWT auth token functionality up and running, and that’s to add it to the create new user controller function:

// src/controllers/user.js
create: async (req, res) =>{
  const user = new User(req.body)
  try {
    await user.save()
    const token = await user.generateAuthToken();
    res.status(201).send({ user, token });
  } catch (e) {
    res.status(400).send(e);
  }
},

// And since the generateAuthToken function has a call to user.save() in it, we
// can remove the call from our create function, and write it like so
create: async (req, res) =>{
  const user = new User(req.body)
  try {
    const token = await user.generateAuthToken();
    res.status(201).send({ user, token });
  } catch (e) {
    res.status(400).send(e);
  }
},

Now that that’s all wrapped up, when we create a new user account we should have a JWT token in the tokens array, and upon creating a new user in Postman:

That’s exactly what we get! Another step in this process would be to save the token to the browser of the user in question, but that’s something we’ll tackle later in this project.

Now that that step’s finished, we can move on to verifying that the user is authorized to take different action on the account in question.

For this we’re going to create our own middleware, similar to the pieces we implemented in our app.js file in part 1 of this series. We’ll again create a folder for this in the src folder, and a file named auth.js:

And here’s the code we’ll use for authorization:

const jwt = require('jsonwebtoken');
const User = require('../models/user');

const auth = async (req, res, next) => {
  try {
	// #1
    const token = req.header('Authorization').replace('Bearer ', '');
	// #2
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
	// #3
    const user = await User.findOne({
      _id: decoded._id,
      'tokens.token': token
    })
	// #4
    if (!user) {
      throw new Error()
    }
	// #5
    req.token = token;
    req.user = user;
    next();
  } catch (e) {
    res.status(401).send({ error: 'Please authenticate' });
  }
}

// #6
module.exports = auth;

Since there’s a number of things happening here, I’ll break it out into numbered bullets again:

  1. Here we get the JWT from the request header and assign it to a variable
  2. Then we decode it using the jwt.verify method
  3. We look for a user based upon their id and the token in question
  4. If a user isn’t found based on those credentials, we throw an error (which triggers us immediately moving down to the catch block and executing the code there)
  5. If there’s no error, we attach the token and user to the request body, then call the next function to move on to the next step in the process
  6. Lastly, we export this function for use in another file

With this function created and exported, we can now put it in place and have everything we need to create the rest of our controller functions! In routers/user.js:

// src/routers/user.js

// [...]
const auth = require('../middleware/auth')

router.post('/users', userController.create);
router.post('/users/login', userController.login)
router.post('/users/logout', auth, userController.logout)
router.patch('/users/:id', auth, userController.update)
router.get('/users/me', auth, userController.read)
router.delete('/users/me', auth, userController.delete)

module.exports = router;

We import the auth function, then call it in the relevant routes before the controller functions fire. This way, the user will be prevented from taking any actions that they’re not authorized to take.

I’d planned to knock out the other User controller functions in this piece, but it’s already getting to be the same length as the first piece in this series! So I’ll tie this one up here, and we’ll knock out the other user controller functions in the next piece.

Til the next one!

-Brandon

Click here to read part three!