MEAN Tutorial Part 2 - Adding a user model

Words
466
Reading
3 min
Listen
Play
9y

logo_part2.png

This tutorial series shows how to build web applications with the MEAN stack. The MEAN stack is MongoDB, Express.js, Angular, and Node.js. This stack allows to write applications, where Javascript can be used both for the client and the server part.
In this tutorial a simple webapplication to manage portfolios of cryptocurrencies such as Bitcoin or Ethereum will be built.

What will I learn?

In this part of the tutorial series

  • you will learn to create a mongoose schema and model,
  • you will learn how to create a safe hash from a password with bcryptjs,
  • and you will learn how to use async/await and Promises instead of callback functions,

Requirements

For this part of the tutorial you need the following background knowledge:

  • Previous parts of this series
  • Working with the command line
  • Experience in JavaScript

Difficulty

Intermediate.

Tutorial contents

This part of the tutorial shows how to extend the server framework created in the previous part of this tutorial with user management. It shows how to add new users and how to create and store hashes for their passwords. It also gives a quick overview over using async/await and how to setup testing with mocha.

Add a model for users

In the previous lesson all code fit into the entry point app.js file. Now, we start to modularize and create a new file for our user model. Even though we will only have one model, I'd advise to put all models into a separate folder. Thus, create a new file /models/user.js. Before diving into coding our model, we need to install one additional package: bcryptjs. We will use this package to store hashes of our user's passwords in the database instead of the plain passwords.

npm install bcryptjs@2 --save

While you are at it, I also advise to install nodemon, a little helper that restarts your Node.js application as soon as you change any of its files.

npm install nodemon -g

Since nodemon is not a dependency of our project, it is installed globally (the npm parameter -g does that) on your system. Be sure, that app.js is selected as main in package.json, open a terminal, navigate to the project folder and run nodemon.

Back to our model in /models/user.js. First import the mongoose and bcryptjs packages with:

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
Create the database schema

Before we can create a model with mongoose, it is necessary to create a mongoose.Schema for our model. This schema describes the fields of our model (similar to columns in SQL databases) and their properties. We start with a very simple schema, that only contains the user's email address and a password. At the least, the type for every field must be specified. Possible types are String, Number, Date, Buffer, Boolean, Mixed, ObjectId, and Array. In addition to the type it is possible to set default values, restrict the allowed ranges, or to make a field required (on default every field is optional). Since the user's email will be the identifier of a user, we make this field unique (i.e. no two user instances may share the same email address) and required.

const UserSchema = mongoose.Schema({
  email: {
    type: String,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  }
});

Now, create the user model from this schema and assign it to model.exports, such that it can be referenced when models/user.js is imported with require(). In addition to the schema object, a name for the model must be provided to mongoose.model() as first parameter

const User = (module.exports = mongoose.model("User", UserSchema));
Add users to the database / Excursion on Promise and async/await

We start with a function to add a new user to the database. This function will get a user object, take the password, compute the hash of this password with bcrypt, replace the password with its hash and finally store it in the database. Since each of this steps might be computationally intensive, the bcryptand mongoose packages perform them asynchronously. In an old-school JavaScript application this would be solved by a cascade of callback functions. However, as Node.js supports ES6 async/await constructs since version 8, i will prefer to use these. For those not familiar with these new constructs, I will quickly walk through transforming a callback based implementation of User.addUser to a async/await based implementation. So lets start with the following implementation:

module.exports.addUser = function(newUser, callback) {
    bcrypt.genSalt(10, (err, salt) => {
    bcrypt.hash(newUser.password, salt, (err, hash) => {
      if (err) throw err;
      newUser.password = hash;
      newUser.save(callback);
    });
  });
};

The function parameters are a user object and a callback, which will be called with two parameters. The first parameter is an error (or null, if no error occurred), the second parameter will be the user object, after it has been added to the database. This makes sense, since after newUser has been added to the database, its _id field will be set to its id in the database.

Dealing with passwords is always critical. Imagine the worst case: your database gets hacked. If you stored all your passwords in plain text, the attacker has direct access to your users email/password combination. Since many internet users use the same email/password combinations on different websites, the attacker may try these email/password combinations to steal the users' accounts on other sites. So never ever store plain text passwords of your users in a database. Instead store a hash of the password in your database. A hash function is a functions which maps your password to a different string of a fixed size. An important property of a hash function is, that it cannot be reversed, e.g. given a hash it cannot be computed which password originally mapped to this hash. Only a brute force attack which computes the hashes of all possible passwords until it finds one that matches the hash may be applied. But depending on the complexity of the password these attacks are very expensive. It is also important to use a hash function designed for password hashes. These functions are more expensive to compute, which makes brute force attacks even harder (bcrypt used in this tutorial is designed for such use cases, SHA3 e.g. is not). To make this approach even safer, hashes with salts should be used. How exactly this works and why it is saver is answered here

In the first line this function calls bycrypt.genSalt() to generate a salt for our password hash. After the salt has been computed a callback function is called with the salt. This function passes the password and the salt to bcrypt.hash() to compute the hash. bcrypt.hash() again calls a callback function as soon as the hash has been computed. In this callback the password of the user object is replaced with the hash and the mongoose save() function is called to store the object in the database. save() receives the callback passed to the addUser() function and calls it when the user has been stored successfully. Notice that the indentation of the function gets deeper and deeper, the more callback functions are used. To improve this, rewrite it using Promise objects.

So what is a Promise? A promise is an object that may produce a value at some point in the future. This may be the value resolved by a computation or, if an error occurred, the reason why the computation failed. For these two cases, callback functions are attached to a Promise object. The callback for a successful resolution of the value is attached with then() and receives the resolved value. The callback for the error case is attached with catch() and receives the error that caused the Promise to fail. A big plus of Promises is that they can be chained, i.e. then() and catch() return Promise objects, as well. Since bcryptjs and mongoose already have APIs that return Promise objects, addUser can easily be rewritten to:

module.exports.addUser = function(newUser) {
  return bcrypt
    .genSalt(10)
    .then(salt => bcrypt.hash(newUser.password, salt))
    .then(hash => {
      newUser.password = hash;
      return newUser.save();
    });
};

Notice, that addUser does not receive a callback parameter anymore. Instead, it returns a Promise by chaining Promise objects. If this implementation of addUser is called somewhere else in our code, the return value is not directly a user object but a Promise that may resolve to a user. An example usage could be

    require("./models/user.js").addUser(newUser)
      .then(user => { ... })
      .catch(err => { ... });

All Promise objects must have a handler for rejection installed with catch in Node.js, otherwise a warning will be raised. With future versions, Node.js will abort in this case. Having this, moving to async/await is straight forward. Functions marked with async automatically return a Promise and await is used to wait for Promise objects to resolve. await can be used in async functions, only. The overhauled implementation of addUser is given below:

module.exports.addUser = async function(newUser) {
    let salt = await bcrypt.genSalt(10);
    newUser.password = await bcrypt.hash(newUser.password, salt);
    return newUser.save();
};

All nesting has gone away now and the code doesn't look very different to synchronous code now. Since addUser still returns a Promise on a user object, it may be used the same way as the previous implementation solely based on Promises. But now it is also possible to use the try/catch construct to deal with error handling in functions using async/await. So addUser() could also be invoked the following way

try {
  let user = await require("./models/user.js").addUser(newUser);
} catch (err) {
  // error handling
}

I will use async/await/try/catch throughout this tutorial, so be sure to understand its basics.

Retrieve users from the database

Next, add two small wrapper functions to retrieve user objects from the database if their unique id or email address is known. As save() above, the mongoose functions findById() and findOne() are asynchronous and return Promises, if exec() is called on the object returned by these functions.

module.exports.getUserById = async function(id) {
  return User.findById(id).exec();
};

module.exports.getUserByEmail = async function(email) {
  const query = {
    email: email
  };
  return User.findOne(query).exec();
};
Verify the password for a user

The last function required for the user model is a function that verifies, whether a given user password matches a previously computed hash:

module.exports.passwordMatches = async function(password, hash) {
  return bcrypt.compare(password, hash);
};
Test the user model

None of the code, written for this part of the series has been called so far, since app.js hasn't changed. So let's add some tests for it. A popular testing framework for Node.js is mocha, so let's install it:

npm install mocha@4 --save

mocha expects all tests to be located in a folder named test, thus create this folder now, and also create a new file test/user.js for the tests of our user model. To enable running the tests with npm test, change the test entry in the scripts section of package.json to

  "scripts": {
    "test": "./node_modules/.bin/mocha --reporter spec"
  },

Now, add the following test code to test/user.js:

"use strict";
const User = require("../models/user.js");
const mongoose = require("mongoose");
const assert = require("assert");

const test_db = "mongodb://localhost:27017/mean_stack_test";

before(function(done) {
  mongoose.connect(test_db, () => {
    // drop the User collection on every run, because
    // otherwise the users to add will already be in it
    User.collection.drop();
    done();
  });
});

describe("User", function() {
  it("is added", async function() {
    let newuser = new User({
      email: "[email protected]",
      password: "1234"
    });

    newuser = await User.addUser(newuser);
    assert.notEqual("1234", newuser.password);
  });

  it("is not added twice", async function() {
    let newuser = new User({
      email: "[email protected]",
      password: "1234"
    });

    try {
      newuser = await User.addUser(newuser);
      assert.fail();
    } catch (err) {
      // expect an error when a user is added twice
      assert.ok(true);
    }
  });

  it("password test is correct", async function() {
    let user = await User.getUserByEmail("[email protected]");
    assert.equal(false, await User.passwordMatches("34", user.password));
    assert.equal(true, await User.passwordMatches("1234", user.password));
  });
});

after(function() {
  mongoose.connection.close();
});

The test code should be pretty self-explanatory. One thing I'd like to emphasize is, that it is very important to close the connection to MongoDB at the end of the test. Otherwise mocha will not terminate. Go to the console and enter

npm test

If everything is correct, you will see the following output:

> mean_tutorial@1.0.0 test /Users/nafestw/Projects/mean_tutorial/lesson2
> mocha --reporter spec



  User
    ✓ is added (228ms)
    ✓ is not added twice (102ms)
    ✓ password test is correct (196ms)


  3 passing (586ms)

This concludes the second part of the tutorial series on how to build a web application with the MEAN web application stack.

Example Code on Github

The code for this part of the tutorial can be found on Github.

What's next?

In the next part I will further extend the backend of our application and implement the routes for user registration and authentication.

Curriculum

Previous parts of this series:



Posted on Utopian.io - Rewarding Open Source Contributors

MEAN Tutorial Part 2 - Adding a user model | Ecency