Node icon
Building a Production Ready Node.js JSON API

Creating the User Model

PRO
Outline

Our data will be stored in MongoDB. MongoDB is a NoSQL database which stores data in collections and documents (as opposed to tables and rows in SQL). Since MongoDB is non-relational, we don't have certain queries that we would have in a SQL database like the JOIN command, and documents in a collection don't need to have the same schema. We will be using the mongoose.js library for all of our interactions with MongoDB. Mongoose allows us to define schemas and indices for our database, as well as providing callbacks and validations for ensuring our data remains consistent. Mongoose also has a plugin system for reusing code between schemas or importing community libraries to extend the functionality of our models.

We'll be building out each feature in our application in three phases:

  1. Create the mongoose Models
  2. Create any helper methods on our models and route middleware required for the feature
  3. Creating the route to expose the functionality to our users

Creating the User Schema

Mongoose models allow us to access data from MongoDB in an object-oriented fashion. The first step to creating a model is defining the schema for it. Then, we'll need to register the model with Mongoose so that we can use it throughout our application.

Create the schema for the user model

Create the following in models/User.js:

var mongoose = require('mongoose');

var UserSchema = new mongoose.Schema({
  username: String,
  email: String,
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

mongoose.model('User', UserSchema);

The {timestamps: true} option creates a createdAt and updatedAt field on our models that contain timestamps which will get automatically updated when our model changes. The last line mongoose.model('User', UserSchema); registers our schema with mongoose. Our user model can then be accessed anywhere in our application by calling mongoose.model('User').

Now that we have all of our fields for our user created let's add some validations to our model. Validations are checks that get run before our model gets saved to ensure we don't commit any dirty data to our database. You can read more about validations and the built-in validations that come with Mongoose here

Add validations to the User model

In models/User.js, add the following:

var mongoose = require('mongoose');

var UserSchema = new mongoose.Schema({
- username: String,
+ username: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
- email: String,
+ email: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

mongoose.model('User', UserSchema);

We also added the index: true options to username and email to optimize queries that use these fields.

We need our usernames and emails to be unique between users so that users can't sign up with the same information. Mongoose doesn't have a built-in validation for unique fields, but fortunately, we can use the mongoose-unique-validator plugin to get this functionality.

Add a unique validation to the email and username fields

We'll need to require the mongoose-unique-validator library, then, we can use the unique validator on our username and email fields. Finally, we need to register the plugin with our model to enable the unique validator. We're configuring the validator's message to say that the field "is already taken."

var mongoose = require('mongoose');
+var uniqueValidator = require('mongoose-unique-validator');

var UserSchema = new mongoose.Schema({
- username: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
+ username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
- email: {type: String, lowercase: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
+ email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
  bio: String,
  image: String,
  hash: String,
  salt: String
}, {timestamps: true});

+ UserSchema.plugin(uniqueValidator, {message: 'is already taken.'});

mongoose.model('User', UserSchema);

Creating Methods on the User Model

For user authentication to work, we'll need to create some helper methods on our users to set and validate passwords, as well as generate JWT's. To generate and validate hashes, we'll use the pbkdf2 algorithm from the crypto library that comes with Node.

Require the crypto library

In models/User.js, add:

var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
+var crypto = require('crypto');

Next, let's create the method to hash passwords. We'll be generating a random salt for each user. Then we can use crypto.crypto.pbkdf2Sync() to generate hashes using the salt. pbkdf2Sync() takes five parameters: The password to hash, the salt, the iteration (number of times to hash the password), the length (how long the hash should be), and the algorithm.

Create a method for setting User passwords

In models/User.js, add the following before the model gets registered:

+UserSchema.methods.setPassword = function(password){
+  this.salt = crypto.randomBytes(16).toString('hex');
+  this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
+};

mongoose.model('User', UserSchema);

To see if a password is valid for a particular user, we need to run the pbkdf2 with the same number of iterations and key length as our setPassword function with the salt of the user; then we need to check to see if the resulting hash matches the one that's stored in the database.

Create a method to validate passwords
+UserSchema.methods.validPassword = function(password) {
+ var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
+ return this.hash === hash;
+};

mongoose.model('User', UserSchema);

Next, we'll need a method on our model for generating a JWT (JSON Web Token). JWT's are the tokens that will be passed to the front-end that will be used for authentication. The JWT contains a payload (assertions) that is signed by the back-end, so the payload can be read by both the front-end and back-end, but can only be validated by the back-end.

Require the jsonwebtoken and the application secret in the user model.

In models/User.js add the following:

var crypto = require('crypto');
+var jwt = require('jsonwebtoken');
+var secret = require('../config').secret;

We need a secret to sign and validate JWT's. This secret should be a random string that is remembered for your application; it's essentially the password to your JWT's. In config/index.js there's a secret value which is set to "secret" in development and reads from an environment variable in production.

Now, we have everything that's needed to generate a JWT for a user. For the token's payload, we'll be including three fields:

  • id which is the database id of the user
  • username which is the username of the user
  • exp which is a UNIX timestamp in seconds that determines when the token will expire. We'll be setting the token expiration to 60 days in the future.
Create a method on the user model to generate a JWT
+UserSchema.methods.generateJWT = function() {
+  var today = new Date();
+  var exp = new Date(today);
+  exp.setDate(today.getDate() + 60);
+
+  return jwt.sign({
+    id: this._id,
+    username: this.username,
+    exp: parseInt(exp.getTime() / 1000),
+  }, secret);
+};

mongoose.model('User', UserSchema);

Lastly, we'll need a method on the user model to get the JSON representation of the user that will be passed to the front-end during authentication. This JSON format should only be returned to that specific user since it contains sensitive information like the JWT.

Create a method to get the JSON representation of a user for authentication.
+UserSchema.methods.toAuthJSON = function(){
+  return {
+    username: this.username,
+    email: this.email,
+    token: this.generateJWT(),
+    bio: this.bio,
+    image: this.image
+  };
+};

mongoose.model('User', UserSchema);

Now our user model is ready to be used for authentication! But before we can use it, we need to require the file so we can use it throughout our application.

Register our User model with our application

In app.js add:

+ require('./models/User');

app.use(require('./routes'));

Be sure to include models before routes so that our routes will be able to use our models.

 

I finished! On to the next chapter