DEV Community

loading...
Cover image for Online Food Ordering App (2)

Online Food Ordering App (2)

the22mastermind profile image Bertrand Masabo Updated on ・16 min read

Photo by abillion on Unsplash


Welcome back!

Today we are going to start implementing authentication for the backend of our app "Gourmet".

In this post we will implement the sign up and verify endpoints.


Project steps

  1. Backend - Project Setup
  2. Backend - Authentication
    1. Backend - Authentication - Signup 📌
    2. Backend - Authentication - Login and Logout
  3. Backend - Place order
  4. Backend - View orders list and view a specific order
  5. Backend - Update order
  6. Frontend - Authentication
  7. Frontend - Place order, view orders list, and view order details

2. Backend - Authentication

Sign up

For users to register on our app, we will require their first name, last name, phone number, address, and password. If the values provided are valid, we will send a OTP (One-Time-Password/Passcode) via SMS to their phone number which they can use to confirm their registration.

Following the TDD approach, we are first going to write our tests then we will implement validations, middlewares, routes, controllers and finally we will configure Sequelize to be able to save data in the database.

Before we begin, ensure you have installed and configured Postgres properly as it's the database we are going to be using. Check out this article on how to install it on Ubuntu.

Our signup task is going to be made up of 2 sub-tasks, one for signing up and another for confirming the user's registration. Let's begin with the first one.

  • Ensure you are on your main branch then run git pull origin main to make sure your local branch is up to date with the remote branch
  • Run git checkout -b ft-authentication to create a new branch for today's task

As we build our API there are things that we will need often and to avoid repeating ourselves it's good practice to structure our code for re-usability. That being said, create a new directory called utils inside src. Create two new files statusCodes.js and messages.js inside utils.

  • Open src/utils/statusCodes.js and paste the following inside:

carbon (3)

These are all the HTTP status codes that our API is going to use.

  • Open src/utils/messages.js and paste the following inside:

carbon (1)

This file will contain all the response messages that our API will be returning to the client apps on top of status codes and other data.

Now let's write our tests.

  • Create a file called authentication.test.js in tests directory and paste the following inside:

carbon (1)

In this file, we import our express app along with our assertion libraries (chai and chai-http) and our status codes and messages we defined above. We then define a base URL for our authentication routes and we initialize chai to be able to test http apps. Learn more about chai here.

We then define a SIGN UP suite to hold our 5 test cases. In the first test case, we are testing for when a user submits an empty request (tries to signup without providing any data), what response he/she should get. Notice the use of one of our status code and messages we defined earlier.

In the second test case, we are testing for when a user submits an invalid phone number. Notice the missing + sign on the phone number. The phone number must be in a valid international format since we will use it to send the OTP.

In the third test case, we are testing for when a user submits any other value apart from the required ones (firstName, lastName, phoneNumber, address, and password). Notice the email property.

In the fourth test case, we are testing for when a user submits valid values conforming to validation rules that we will define next. In this case, we expect a successful response that contains a status code of 201, a account created message, a JWT token that the user can use to authenticate for subsequent requests, and a data object containing details of the user. Notice how we expect the user's account status to be false since he/she has not yet verified it. Finally, we retrieve the token in a variable called userToken that we will use in other test cases when verifying the user's account.

In the fifth test case, we are testing for when a user tries to signup more than once using the same phone number.

At this point if you run the tests they will fail apart from Server initialization test which is exactly what we want.

Next up is to write code to make our tests pass.

  • Create the following directories config, controllers, database, helpers, middlewares, routes, services, and validations inside src directory.

  • Create a new file called authentication.js inside validations directory and paste the following code inside:

carbon (4)

We will use this file for authentication validation. In the code above, we start by importing a library called Joi and our response messages we defined in utils. Joi is a powerful data validator for Javascript and I personally like it because it is robust and easy to use. Check out its docs here.

We created a function createErrorMessages to help us in - you guessed it - create validation error messages. The function takes error type and empty, min, max, and pattern custom messages as parameters and depending on the type of the error we assign a custom message. This function returns an object of error types and their messages.

We use the second function signup to define a schema of values that we want users to submit when signing up. Notice the use of Regular Expressions to enforce validation rules. If you're familiar with RegEx, it's pretty straight forward as our use-case is not too complex.

Finally we call Joi's built-in method validate on our schema and pass in a data object i.e. req.body and some options to return all errors at once and to prevent other values not defined in our schema. Check out Joi API for more details and advanced use-cases.

In case of errors, our signup validation function will return an errors object containing a details property. This details property is an array containing all the error messages. We need a way to extract and use the contents of this details property.

  • Create a misc.js file inside helpers directory and paste the following code:

carbon (6)

In this file we define 3 functions:

  • We will use successResponse and errorResponse to return success and error responses respectively.

  • returnErrorMessages checks to see if the parameter errors is present then destructure its details property. We then format each message in our details array to make it more readable and then we use errorResponse defined above to return the result of these formatted messages.

If errors is null it means our validations are passing and we continue with the execution of the request. Think of returnErrorMessages as a middleware.

Let's now use this returnErrorMessages function.

  • Create a file authentication.js in middlewares directory and paste the following code:

carbon (5)

Notice the use of returnErrorMessages by giving it the error object returned by our signup validation function as a parameter.

Before we implement our controller, let's update src/helpers/misc.js with the following:

carbon (7)

Notice the additional functions: generateToken, generateOTP, and generateHashedPassword.

We will use generateToken to generate a JWT token based on the data passed in. Update your .env file and include the JWT_SECRET_KEY like JWT_SECRET_KEY=somesecretkey.

We will use generateOTP to generate a random six digit code that we will send to a user.

Finally, generateHashedPassword will be used to take a plain-text password, encrypt it and return a hash string which we will store in our database. For security reasons, You should never store plain-text passwords in your database.

Okay, let's implement our controller.

  • Create a authentication.js file in controllers directory and paste the following:

carbon (8)

Our controller is where a request that has passed all validations and middlewares will end its journey. This where we will implement saving data in the database and sending OTP to users before returning a response to the user.

Let's implement our routes to see how it looks so far.

  • Create two files authRoutes.js and index.js in routes directory.

  • Paste the following inside src/routes/authRoutes.js:

carbon (9)

If you remember, in our tests we defined our base URL as /api/auth/. This means we will be able to define /api/auth/signup, /api/auth/login, and /api/auth/logout routes respectively.

Let's implement the parent /api/auth/ route handler.

  • Paste the following inside src/routes/index.js:

carbon (10)

Our endpoint is almost complete. We just need to let our express app know about it.

  • Update src/server.js to look look like this:

carbon (11)

  • Run your tests again. This time around, some of them are passing.

Great Job if you managed to reach here! 🎉

Let's now implement sending OTP. When we finish, we will setup Sequelize in order to persist data in the database.

Starting with OTP implementation, we are going to use Twilio. Click here to create a Twilio trial account. After creating your account you should be given some credit you can use to buy numbers and send SMS in trial mode.

Trial accounts have some limitations, namely you cannot send SMS to unverified numbers. So in order to test this functionality, there are 2 options.

Option 1
You can upgrade your account.

Option 2
You can verify numbers you intend to use. Just remember to upgrade your account before going into production to allow everyone to signup.

We are going to use option 2 for now.

  • Login to your Twilio account. Click on the # sign that says Phone numbers on the left panel. On the phone numbers page, click Buy number button and proceed to search for a number you want. Make sure to tick the SMS checkbox.

  • Click on Verified Caller IDs then click on the red plus button to add and verify a number. Make sure to provide a valid phone number that you have access to because Twilio will send a OTP to verify it.

Once done, head back to VS Code and add the following keys in your .env file.

carbon (12)

Let's now install the Twilio library.

  • Open your terminal in your project's root dir and run yarn add twilio

  • Create a twilioConfig.js file in config directory and paste the following:

carbon (13)

In this file we initialize a twilio client instance that we can use throughout our app to send SMS.

Let's now use this client in our code.

  • Update src/heplers/misc.js to look like the following:

carbon (14)

The sendOTP function will take a phone number and a message and it will take care of sending our SMS. Let's now use this function in our controller.

  • Update src/controllers/authentication.js like this:

carbon (15)

Now run your tests again and you should get a OTP delivered to the number you specified in TWILIO_CUSTOMER_NUMBER env variable.

Great! Let's now implement Sequelize and save data in our database.

Since we already installed all the required sequelize library and plugins, let's start using them.

  • In your terminal, navigate to src/database and run npx sequelize-cli init. This command will create the following directories and files: config/config.json, models, migrations, and seeders.

The models directory will contain our models. Think of models as tables in a database.

The migrations directory will contain migrations which are modifications made to our models. We use migrations to change the structure our 'tables'. We can do things like add/remove/rename columns, add/change constraints on columns, etc.

Note that each time we modify the structure of our models we need to run migrations for those changes to take effect. More on this later.

The seeders directory will contain data that we want to inject in the database. Use-case: Imagine you want to test the login functionality. since we have already implemented the signup tests and we know it works well, we can use the seeders to insert in the database valid records of users thus skipping the signup and verify tests which will make our tests run faster. We will use seeders later in this series.

The config.json file will contain credentials to connect to our database. We will need to modify this file and make it dynamic to avoid exposing our database credentials. Let's do it right away.

  • Rename src/database/config/config.json to src/database/config/config.js

  • Replace the contents inside with:

carbon (16)

  • Update your .env file and add the keys for development and test like below:

carbon (17)

Notice the different database names for development and test.

Note that for now we don't need to provide credentials for production in our .env file. The production credentials will be provided to us by heroku when we "provision" (setup) a production database.

  • Replace src/database/models/index.js with the following:

carbon (3)

This file will allow us to import our models dynamically by doing something like: import models from '../database/models' then destructure models to retrieve each model in models directory. This file also creates and exports a sequelize instance that we will be using to interact with the database.

Cool! Let's now use Sequelize to create our first model - User.

  • In your terminal run npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,phoneNumber:string,address:string

This command will create 2 new files: user.js (our user model) and **-create-user.js (our first migration) inside models and migrations directories respectively.

  • Update package.json to include commands to create and drop the database as well as run the migrations like:

carbon (25)

Notice we didn't include the pretest command on the test command since our CI service does this automatically for each build.

If we were to run our migrations right now, our database would be created with just the 4 columns defined when creating our model above.

Let's update our model and add more columns and create a new migration to apply those changes.

  • Update src/database/models/user.js like below:

carbon (19)

  • In your terminal run npx sequelize-cli migration:generate --name add-password-otp-and-status-to-user to create a new migration that will apply the new columns we added to our model.

Tip: As migrations can become many as our app scales, it's good practice to name each migration with what it does. By looking at the name of the new migration, we would know that it add password, otp, and status columns to user model.

  • Replace the contents of src/database/migrations/**-add-password-otp-and-status-to-user.js with the following:

carbon (20)

Check out this link to learn more about creating models and migrations.

If we were to run our 2 migrations now, all the 7 columns would be added to our user table.

One of the things I like about Sequelize is its nice API that allows to interact with the database without writting SQL queries like "INSERT INTO tableName VALUES(....". Oh! This API allows also to write those queries in case you wish to use them. Nice, right!

We are almost done!

  • Create a services.js file inside services directory and paste the following:

carbon (21)

We will be using this file to create functions that will use Sequelize API to CRUD the database.

saveData function receives a model name and obj as parameters then calls the Sequelize built-in method create on the model and returns the data saved in the database.

Similarly we use findByCondition function to find if a record exists in a table given a conditon. Check out this link to learn more about these built-in model methods.

As you might have guessed, we will use findByCondition to check if a user exists in the database and saveData to save the user.

Okay, let's update src/middlewares/authentication.js to look like the following:

carbon (22)

We need to run this function after the validations and before the controller.

  • Update src/routes/authRoutes.js to look like:

carbon (23)

  • Lastly, let's update our controller to use the saveData function we defined in our services. Update src/controllers/authentication.js to look like the following:

In the code above we added the saveData and lodash's omit and pick methods to choose which properties should be in the userData object returned in the response and token respectively.

That's it! Our signup endpoint is done!

Now if you run your tests, they should all pass! Nice, right!

In case you run into a timeout error, make sure to update your script's test command in package.json by adding a timeout flag like below:

carbon (26)

This allows to extend the default Mocha's timeout of 2 seconds for each test case to 8 seconds which will give enough time to our asynchronous functions to finish executing.


Verify

After users have registered and we have sent the OTP, we need a way to verify their phone number thus confirming their account registration.

We are going to implement verify endpoints, the first one will be to check if the OTP submitted by the user is correct. The second will be to re-send the OTP to the user in case there has been an issue and the user didn't receive the first OTP.

  • Open tests/authentication.js and add the following:

carbon (1)

In the code above, we have added test cases for the verify and verify/retry endpoints.

  • In SIGNUP test suite, update Valid signup should return 201 test case like this:

carbon (7)

  • Open src/utils/messages.js and add the following messages:

carbon (2)

  • Open src/validations/authentication.js and add the following:

carbon (3)

  • Open src/middlewares/authentication.js and add the following:

carbon (4)

  • The validateVerifyOTP middleware will help us to use verifyOTP function to validate the otp submitted by the user.
  • The checkUserToken middleware will help us to check if a request contains the Authorization header then will try to decode the token to check if the who made the request exists in our database then returns the user's data or an error. This is how we will be able to link users with their requests.
  • The checkOTP middleware will help us to check if the otp submitted by the user is the same as the one we sent to them via SMS.

    • Open src/services/services.js and add the following:

carbon (5)

  • Open src/controllers/authentication.js and add the following:

carbon (6)

  • Open src/routes/authRoutes.js and add the following:

carbon (8)

Now all our tests should be passing. Let's now update our travis config file and package.json file before we commit our changes to Github.

  • Update .travis.yml file to look like this:

carbon (4)

We added the services option and before_script command which will tell Travis to create a postgres database called gourmet_test before running our tests.

  • Update package.json to include a heroku-postbuild command.

carbon (2)

As the name suggests, this command will run after each build. You can use it to run scripts that you want to execute before your app is deployed. Here we are using it to run our migrations automatically.

The last step is to make sure our CI service and production environments are up to date.

  • Login on Travis then open our gourmet-api repo then click on settings to add environments variables. Make sure to add each env variable with its value.

carbon (1)

  • Head back to VS Code and commit our changes to github. Open a PR on github and wait for Travis to finish building. Both the branch and PR should show a successful build.

Before we merge this PR, let's create a production database on heroku.

  • On your app page on heroku, click on Resources tab then in the Add-ons search field type postgres. Select Heroku Postgres and in the confirmation modal click Submit order form. You should see a confirmation that the add-on heroku-postgresql has been added. Check out the docs for more info.

  • Click on Heroku Postgres to open it in a new tab then click on Settings tab then click the View credentials button.

You should see the credentials of our database. When you provision a database on heroku like this, it adds the DATABASE_URL env variable automatically on your app.

Let's now add the database credentials as env variables. Alternatively, you could use the DATABASE_URL variable in the database/config/config.js and database/models/index.js files.

  • On your main app's settings tab, click on Reveal config vars button and add each credential key and its corresponding value from the database we've just created.

  • Don't forget our Twilio credentials and JWT_SECRET_KEY

Now it's time to merge our PR which will trigger a production build on heroku.

  • Head over to github and merge the PR we created earlier.

Travis should build our merge commit successfully then Heroku should build successfully as well then run our migrations.

Now you could copy the URL of your app from heroku and test the endpoints we implemented with POSTMAN or Insomnia and everything should go smoothly. Check out the links to their docs below.

Today's task was huge because we covered a lot of things. But we have laid the foundation for Sequelize, validations, and middlewares. The next endpoints are going to be fairly straightforward.

In the next post, we will implement the login and logout endpoints.

Tip: To test your API as you build it, you should use a tool like Postman or Insomnia.

They are both great at designing and testing APIs and you can even do things like creating and hosting your API documentation.

Check out the Postman docs and Insomnia docs to learn more.

Note: The endpoints we implemented in this post are a bit naive. For example, we are not checking if a user's account is verified before verifying it. We should also limit requests to the endpoints that uses external resources since the billing of these resources can become a lot. Check out this library to learn how to limit the number of requests. About the other issue of checking if a user's account is verified before verifying it, we can achieve this by using a simple middleware function.

Thanks for reading and or following along!

See you in the next one!


You can find the code in this post here

Discussion (0)

pic
Editor guide