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-many
relationship 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
Here, we have made some couple of changes, these include the following;
- Created a new function
createUserTable
with query to createusers
table. This follows the same format we used in creatingreflections
table in part 2 of this series. What this queryCREATE TABLE IF NOT EXISTS users(...)
does is to create a new table calledusers
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 thatemail 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 dropusers
table. Also follows the same format we used to dropreflections
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
todropReflectionTable
andcreateTables
tocreateReflectionTable
. - We made new changes to
createReflectionTable
SQL query by addingowner_id UUID NOT NULL
andFOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
. We addowner_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 useFOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
to link reflections table by specifying its foreign keyFOREIGN KEY (owner_id)
- the column that is linked to foreign key here isowner_id
.REFERENCES users (id)
is used to reference the main table in this caseusers
table and use(id)
to specified its Primary Key column. We used thisON 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, anddropAllTables()
- 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.
We set up a new Helper
object with fours methods.
hashPassword()
method returns a hashed value of a string usingbcrypt
packaged exposedhashSync
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 anotherbcrypt
exposed methodgenSaltSync
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 argumentspassword
- log in provided password andhashPassword
- hashed password retrieved from the DB based on what password the user used during registration. We usedcompareSync
anotherbcrypt
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 usedjwt.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
Code Steps:
- We created a new User object with
create()
,login()
anddelete()
methods. create()
- We set up a conditionif (!req.body.email || !req.body.password){...}
to validates that bothpassword
andemail
exist in the request body.if(!Helper.isValidEmail(req.body.email)){..}
checks if the users email is valid using the Helper'sisValidEmail
method we created in the previous step. We created a constant variablehashPassword
and assigned the result ofHelper.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 generatedhashPassword
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 calledtoken
and assigned the result ofHelper.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 callingcomparePassword
in ourHelper
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
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
Here we did the following
- Updated
create()
methodINSERT
query to factor inowner_id
. We setowner_id
value toreq.user.id
(remembered we attacheduser
toreq
object in our middleware - previous step). getAll()
- We attached a newWHERE
condition to get all reflections query using owner_id. This will returns all reflections created by the current user.getOne()
- Same as we did forgetAll()
method. We attached a newWHERE
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 newWHERE
condition to retrieve and update reflection only if the reflection's owner_id is the same as the current userIDdelete()
- 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
Here we did the following;
- imported
Auth
andUserWithDb
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 andDELETE /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 throughAuth.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'
Login - POST api/v1/users/login'
Create Reflection - POST /api/v1/reflections
Copy the token gotten from login and set it in the request header with key x-access-token
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()
anddelete()
methods - Set up Auth middleware to verify/validate user's token
- Set up user controller with at least
- 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.
What are reflections??
You just helped me solved a 3 days battle. Thanks a lot for this
this is a great post
please make more of these we really appreciate