Codementor Events

Building a simple API with Nodejs, Expressjs, PostgreSQL DB, and JWT - 3

Published Aug 23, 2018Last updated Feb 19, 2019
Building a simple API with Nodejs, Expressjs, PostgreSQL DB, and JWT - 3

“When you don't create things, you become defined by your tastes rather than ability. Your tastes only narrow & exclude people. So create.”
― Why The Lucky Stiff

We started this series by creating five simple endpoints using basic JS object to persist our data. In part 2, we learned how to connect our app to PostgreSQL DB and save user's data directly on it. We also decided not to use an ORM so that we can have a better understanding of how to use raw SQL to query our data in the DB, which we learned because we were able to create reflections table, insert, update, delete and retrieve data from the table using raw SQL queries.
However, with what we have done so far, when a user USER A creates a reflection USER B can also see that same reflection. Our goal was for reflections created to always be private such that only USER A can see his/her own reflection. USER B should not be able to see USER B reflections or any other user's reflections on the system. How do we solve this? In this part, we will learn how to protect our endpoints with JWT(Json Web Token) such that only authenticated users can access them.

TODO
The following are the steps we will follow in achieving our aim

  • Install the required dependencies. jsonwebtoken - will be used to generate and verify token and bcrypt - will be used to hash user's password before saving it into the DB.
  • Create Users table and set up one-to-many relationship between users' table and reflections' table in the DB
  • Create Helper class - We need to create some helper methods that will help us in generating token, hashing password, validating users password with its corresponding hashed password and validating if input email is valid.
  • Create SignUp, Login, Delete controller methods
  • Create Auth middleware
  • Update reflections controller to queries the DB based on the current user ID
  • Update server.js

Step 1 - Install Dependencies

Run the following command on the terminal to install the requires dependencies

$ npm install --save bcrypt jsonwebtoken

Like I have explained before, we need bcrypt to hash user's password before saving it into the DB and jsonwebtoken will be used to generate and verify token.

Step 2 - Create User Table

We will create a new table users with id, email, password, created_date and modified_date. We also need to set up a one-to-manyrelationship between users table and reflections table - this means a user can have more than one reflections. We will achieve this, by setting up a FOREIGN KEY in reflections table that links to users table through its PRIMARY KEY. The concept of Foreign Key is basically to help in linking two tables together through the Primary Key on the main table. You can read more about this online.
Update db.js with the following code

alt

Here, we have made some couple of changes, these include the following;

  • Created a new function createUserTable with query to create users table. This follows the same format we used in creating reflections table in part 2 of this series. What this query CREATE TABLE IF NOT EXISTS users(...) does is to create a new table called users with the specified column(id, email, password, created_date and modified_date) definitions if it does not exist in the DB. The table requires only an email and password to make it simple. You'll notice that email VARCHAR(128) UNIQUE NOT NULL has unique, this is to make email unique. If a user tries to sign up with an already existing email, it will throw an error.
  • Created a new function dropUserTable to drop users table. Also follows the same format we used to drop reflections table in part 2 of this series. DROP TABLE IF EXISTS users drop/delete the table(users) if it exists.
  • We made some adjustment to the previous functions to make it more specific. We changed dropTables to dropReflectionTable and createTables to createReflectionTable.
  • We made new changes to createReflectionTable SQL query by adding owner_id UUID NOT NULL and FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE. We add owner_id to have a new column on the table that accepts user's id, this will help us in tracking who owns a specific reflection. We use FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE to link reflections table by specifying its foreign key FOREIGN KEY (owner_id) - the column that is linked to foreign key here is owner_id. REFERENCES users (id) is used to reference the main table in this case users table and use (id) to specified its Primary Key column. We used this ON DELETE CASCADE to tell our DB to delete every reflection associated to a particular user when that user has been deleted from the DB - this means when we delete USER A, every reflection created by USER A will automatically be deleted from the DB.
  • Lastly, we created another two new functions createAllTables() - calls to executes the other two functions that create users and reflections tables, and dropAllTables() - calls to executes the other two drop functions. The reason behind these two functions was to avoid running two separate commands in creating or dropping the two tables(users and reflections).

Step 3 - Create Helper class

Create a new js file in src/usingDB/controllers/ and name it Helper.js, add the following code to the file.

alt

We set up a new Helper object with fours methods.

  • hashPassword() method returns a hashed value of a string using bcrypt packaged exposed hashSync method. hashSync takes in two parameters a string (password) and number of rounds/salt - number of rounds defines the number of times the provided string will be hashed. To generate our salt, we used another bcrypt exposed method genSaltSync and we send in 8 as the number of rounds we want the password to be hashed.
  • comparePassword() - this helper method will be needed when a user wants to log in to the system. The method accepts two arguments password - log in provided password and hashPassword - hashed password retrieved from the DB based on what password the user used during registration. We used compareSync another bcrypt module exposed method in comparing the two password and it returns either true or false depending on if the two password matches or not.
  • isValidEmail() method uses regex in validating user's provided email address.
  • generateToken(): we used jwt.sign(..) in signing user's token, by sending userId as the payload and setting token to expire in 7 days.

Step 4 - Create SignUp, Login, Delete controller methods

Now, let's create User's controller with SignUp, Login and Delete methods. Create a new js file in src/usingDB/controllers/ and name it User.js. Add the following to the file

alt

Code Steps:

  • We created a new User object with create(), login() and delete() methods.
  • create() - We set up a condition if (!req.body.email || !req.body.password){...} to validates that both password and email exist in the request body. if(!Helper.isValidEmail(req.body.email)){..} checks if the users email is valid using the Helper's isValidEmail method we created in the previous step. We created a constant variable hashPassword and assigned the result of Helper.hashPassword(req.body.password) to it - (how this works was explained in the previous step.). We set up a query to insert the user's values into the users' table. You will notice we are saving the generated hashPassword into the table as opposed to the plain password text provided by the user, for security reason we don't want to save raw user's password to the DB. We created a new variable called token and assigned the result of Helper.generateToken(rows[0].id) to it. Finally, if everything works okay, we send the generated token back to the user, the user can use the token for every subsequent request to the server.
  • login() - We use this to sign in a user that already have an account. We search the DB user's table to check if the user with the provided email already exist and if the user exists, we check if the user's provided password matches the hash password saved in the DB by calling comparePassword in our Helper object with the hashed password gotten from the DB and user's password from the request body. If the two password matches, then we generate a new token for the user and the token can be used for every subsequent request to the server.
  • delete() - We set up a query to delete the current logged in user using the user Id gotten from the request token. Note: before this will work we need to set up Auth middleware that will check for the validity of user's token.

Step 5 - Create Auth middleware

To validate every request to any of our routes, we need to set up a middleware. In this step, we will set a new middleware that would validate user's token.
Create a new folder inside src/usingDB and name it middleware. Create a new JS file inside middleware folder and name it Auth.js. Add the following code to Auth.js

alt

Here, we create a new Auth object with verifyToken() method. What verifyToken() method does is basically to validate and decode user request token using the same secret key we used in signing the token. We used req.headers['x-access-token'] to get the token from the request header and send it to jwt.verify(..) along with the secret we used in signing the token. If the token is valid, we retrieve the userId from the token payload and query the DB to make sure the user exists in the DB. If the user exists in the DB, we created a new object property in the req object req.user and assigned { id: decoded.userId } to it - We will use this to process other requests handler. Finally, since this method is a middleware, we used next() in moving to the next request handler. If any error occurred in here, we return an error message back to the user without having to move to the next request handler.

Step 6 - Update reflections controller

Now, let's update our Reflections controller to make use of the new changes we made to the reflections table.

Update src/usingDB/controllers/Reflection.js with the following

alt

Here we did the following

  • Updated create() method INSERT query to factor in owner_id. We set owner_id value to req.user.id(remembered we attached user to req object in our middleware - previous step).
  • getAll() - We attached a new WHERE condition to get all reflections query using owner_id. This will returns all reflections created by the current user.
  • getOne() - Same as we did for getAll() method. We attached a new WHERE condition to retrieve the reflection only if its owner_id is the same as the current userID.
  • update() - We did the same thing here also. Added a new WHERE condition to retrieve and update reflection only if the reflection's owner_id is the same as the current userID
  • delete() - Delete a reflection only if its owner_id is the same as the current userID.

Step 7 - Update server.js

Finally, let's update our server.js logic to factor in our new Auth middleware. We also new to create our user's route.

Update server.js with the following code

alt

Here we did the following;

  • imported Auth and UserWithDb from /src/usingDB/middleware/Auth and /src/usingDB/controller/Users respectively.
  • We created a new endpoints to create users - POST /api/v1/users, login user - POST /api/v1/users/login and delete a user - DELETE /api/v1/users/me.
  • We attached a new handler Auth.verifyToken to all our reflections endpoints and DELETE /api/v1/users/me endpoint. This is to make sure only a user with valid token can access those routes. Each request to any of the reflections endpoint will pass through Auth.verifyToken middleware before processing the actual request. With this new setup, a user without a valid token will get an error.

One final thing, we need to create a new environment variable and name it SECRET. You will remember we use SECRET in signing and verifying the user's token.
Add SECRET=justanotherrandomsecretkey to your project .env file. Note - you can give it whatever you like.

RUN

Let's run our server

  • Run the following command from the terminal to create the db tables
$ node db.js createAllTables
  • Run the server
$ npm run dev-start

TEST

Create User - POST api/v1/users'

Screen Shot 2018-08-23 at 11.23.55 AM.png

Login - POST api/v1/users/login'

Screen Shot 2018-08-23 at 11.26.07 AM.png

Create Reflection - POST /api/v1/reflections
Copy the token gotten from login and set it in the request header with key x-access-token

Screen Shot 2018-08-23 at 11.28.59 AM.png

Pending Issues

The following are pending issues for this work, please feel free to work on any of the issues and raise a pull request on the Repo.

usingDB

  • Create an endpoint to Get all Users - GET /api/v1/users
  • Create an endpoint to Get the current user's details - GET /api/v1/users/me
  • Create an endpoint to edit the current user's details - PUT /api/v1/users/me
  • Create an endpoint to get all current user's reflections - GET /api/v1/reflections/me

usingJSObject
Currently, this code will only work when TYPE === db otherwise it will throw an error. To make it work for usingJSObject, we need to set up the following;

  • Set Up User Controller - The requirement for this is the following;
    • Set up user controller with at least create(), login() and delete() methods
    • Set up Auth middleware to verify/validate user's token
  • Update Reflection controller to use owner_id
  • Update server.js to use Auth middleware when TYPE != db
  • Create all the endpoints for users.

In my finally tutorial for this series, I'll compile all the merged PRs and reference the those that worked on them.

Checkout the complete code here

Check out part 1 and part 2 if you haven't read them

As always, drop your questions and comments.

Discover and read more posts from Olawale Aladeusi
get started
post comments13Replies
Frank Simmons
3 years ago

What are reflections??

Ezenwa Ugbomah
5 years ago

You just helped me solved a 3 days battle. Thanks a lot for this

frank cyuzuzo
5 years ago

this is a great post

frank cyuzuzo
5 years ago

please make more of these we really appreciate

Show more replies