In the previous part , we learnt how to connect to MongoDB with Mongoose. We also defined our basic User Schema.
In this part , we're going to setup our /login
and our /registration
routes. These API endpoints are going to allow our client-side applications to send POST
requests to register new users as well as to allow existing users to login to their account.
Setting up our Routes β¨
Let's start by creating a new folder in our project directory called routes
.
In this folder , we're going to create our Auth.route.js
file. We're going to define all our Routes in this file and later import it in our server.js
file.
const express = require('express');
const router = express.Router();
const User = require('../models/User.model');
router.post('/register',async(req,res) => {
try {
res.json({message:"This is the register route!"})
}
catch(err) {
console.error(err.message)
}
})
router.post('/login',async(req,res) => {
try {
res.json({message:"This is the login route!"})
}
catch(err) {
console.error(err.message)
}
})
In the code above , we require('express')
and then instantiate Router
which is built into Express.
Router helps us build out our routes. We can handle get
, post
, patch
, delete
and most other HTTP request methods for our routes by chaining the required type of request to our router
object. i.e
router.get('/[our-required-route]',() => {
//This is a callback function
})
We're going to define what should be done when the route is hit, inside the callback function.
In our first code snippet , our callback function is async
. This will make our life easier later when we need to interface with MongoDB to fetch and post data.
For the sake of testing , we will send back json data , with a message key containing a string value
using res.json
.
The callback functions come with a req
and res
parameter which help us interact with a user's request and the response we can send back respectively.
Let's finally add this route to our server.js
file. Add the following code before the require('./helpers/initDB')()
line.
We will require our Auth.route
file and initialise it to AuthRoute
. We will finally use the AuthRoute
by making use of the use
method that Express provides. We'll also define the parent
route to be /api/auth
. This means that if we want to hit our register
route , we'll actually have to hit /api/auth/register
.
...
const AuthRoute = require('./routes/Auth.route');
app.use('/api/auth', AuthRoute);
...
Installing REST Client on VSCode to test our APIs βοΈ
If you want to test the APIs we've just built , you can download the REST Client by going to the Extensions tab on VSCode. You can additionally download Postman or Insomnia as well to test your API.
Let's make a POST
request to our APIs that we defined earlier.
In our routes
folder, create a new file called route.http
. Then write the following line into this file.
POST https://localhost:5000/api/auth/register
You'll see a Send Request
label pop up just over this line now. Click on it.
This will now open a tab on the side with a JSON response.
This response should be
"message" : "This is the register route!"
Make sure your server is running before making the request. You can do this by using npm start
.
Analysing our Login/Registration workflows
Before we can Login or Register users , we need to break down what we need to do step-by-step.
Let's look at our Registration workflow.
- Validate the received registration details
- If there is an error in received registration details , then return
400
status code and the error message. - Check if email exists. (
400
if error) - Check is username exists. (
400
if error) - Salt and then hash the password. (Read Part One)
- Save our user in the database.
Let's break down our Login Workflow next.
- Validate the received login details.
- Check if user with given email exists. (400 if error)
- Check the received user password against the hashed DB password using
bcrypt.compare()
. - Return Success message if password matches , else return invalid details message. (Additionally provide a JWT Token which we will discuss in part 4)
In both the workflows described above , we need to validate the details that we receive from the client side. This involves a lot of string handling which can be tedious work.
However , in this tutorial we're going to use a ready-made package for validation called Joi
.
We're also going to install another package called bcrpyt
. Bcrypt provides ways to salt , hash and compare passwords with its built in methods.
Let's install them both. Exit your server using Ctrl+C
or Cmd+C
and run the following npm command.
npm install @hapi/joi bcrpyt
Writing our Joi validation schemas π
Let's get started on writing our Joi validation schemas. Writing a Joi Validation schema is very easy. We define a Joi
object and define the requirements that our data in this Joi object should have. We can do this by chaining together the built-in methods that Joi provides.
Want to check if a string has at least 6 characters and can only be alphanumerical?
We can achieve this simply with the following code
ourString: Joi.string().min(6).alphanum(),
Joi will return an error message if the ourString
value does not pass the conditions.
Let's now go ahead and build out our validation schemas for the auth-api.
Create a validator.js
file in your /helpers
directory.
Add the following code to this file.
const Joi = require('@hapi/joi');
const registrationValidator = (data) => {
const schema = Joi.object({
username: Joi.string().min(6).required().alphanum(),
email: Joi.string().min(6).required().email(),
password: Joi.string().min(6).required(),
role: Joi.string()
})
return schema.validate(data);
}
const loginValidator = (data) => {
const schema = Joi.object({
email: Joi.string().min(6).required(),
password: Joi.string().min(6).required()
})
return schema.validate(data);
}
module.exports.registrationValidator = registrationValidator;
module.exports.loginValidator = loginValidator;
Finally let's require this file in our Auth.route.js
file.
const { registrationValidator, loginValidator } = require('../helpers/validator');
Building our Register Route π
Inside our try
block , lets start by processing the data that we receive by using req.body
.
try {
const { error } = registrationValidator(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
}
We pass req.body
to our registrationValidator
function that we previously defined our validator.js
file.
If our validator encounters an error in the receiver data , we're going to return the error message with a status code of 400
.
You can test if the API works so far by going to the rest.http
and adding the following
POST https://localhost:5000/api/auth/register
content-type: application/json
{
"email":"test@test.com",
"username":"test",
"password":"test",
}
After hitting the Send Request
button , you'll see that we get an error message with a 400
status code. This is because both our username
and password
are only 4 letters long.
Now that validation is done , We can check if the username or email already exist in the database.
Checking if username and email already exist
Add the following code next ,
//EmailExistCheck
const emailExists = await User.exists({ email: req.body.email });
if (emailExists) return res.status(400).send('Email already exists.');
//UsernameExistCheck
const userNameExists = await User.exists({ username: req.body.username });
if (userNameExists) return res.status(400).send('Username already exists.');
We use the exists
method that MongoDB provides to check if a document containing the given data exists.
We'll return the error message with a 400
status code if either values exist.
Salting and hashing our passwords before storing
Let's make use of the bcrypt
library that we had installed earlier. Make sure you've imported the bcrypt library with the following code.
const bcrypt = require('bcrypt');
Next , let's generate a salt
using the in-built genSalt()
method inside bcrypt.
const salt = await bcrypt.genSalt(10);
If you're unaware about salting or hashing , read the first article of this series.
The bcrypt genSalt()
method generates a salt for us that we'll now use with our password. Let's use the bcrypt.hash()
method to hash our salted password. This method takes the base password and the generated salt as its parameters.
Go ahead and add the following code to your file next.
const hashPassword = await bcrypt.hash(req.body.password, salt);
Now that we have hashed our password , let's go ahead and construct our new user object with the newly hashed password.
const user = new User({
username: req.body.username,
email: req.body.email,
password: hashPassword,
});
Finally , let's save this user into our Database using the save()
method.
const savedUser = await user.save();
res.send(savedUser);
Let's send back the user that we have saved just now as our response with the res.send()
method.
Finally , go back to the rest.http
file and make a POST
request with three valid user credentials defined by our Joi Schema.
If everything went well , you should see the saved user's details containing the hashed password in your response.
You can additionally also go to your Mongo Atlas Client to see if the user's details were registered.
With this , we've finished the process of registering our user.
Let's move on to building out the /login
route next.
Building our Login route π
Building the login system involves the same validation process as registering our users. Go ahead and paste the following code in your file inside the try
block of your login
route.
We're also going to use MongoDB's findOne()
method to extract the credentials of the corresponding email that the user had entered. We will store this inside a user
variable.
//Use Login Values Validator
const { error } = loginValidator(req.body);
if (error) return res.status(400).send(error.details[0].message)
//UserExistCheck
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(400).send('Account does not exist with provided email and password combination.');
Comparing the hashed password with the entered password
To compare our passwords, we're going to make use of bcrypt's .compare()
method. This method takes the user's entered password as its first parameter and the hashed password stored in the DB that we extracted earlier.
const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).send('Incorrect Password');
The code above stores the bool
result of the bcrypt.compare()
method. If the password is invalid , we return a message "Incorrect Password" with a status code of 400
.
Finally , we'll return a success
message back to the user to simulate a successful login attempt using the res.send()
method.
res.send("Login Successful!")
Finally . you can test this out in your rest.http
file by making a POST
request to /api/auth/login
with valid credentials. If all goes well , you should now see the "Login Successful" message!
Congratulations! π
You've just built a login/registration system using Express and MongoDB.
In the next part , we're going to deal with JWTifying our Authentication/Authorisation process. π¨π»βπ»
Top comments (0)