Bozobooks.com: Fullstack k8s application blog series

Book Library React JS Front End with Firebase Authentication Deployed on k8s, with GitOps

A B Vijay Kumar
Cloud Native Daily
Published in
13 min readOct 29, 2022

--

Chapter 7: In this blog, we will build a ReactJS application, that will provide UI to search for books, and add them to the personal library. We will be using Firebase for authentication, and Hashicorp Vault to inject some of the secrets. We will be deploying the ReactJS application on our Kubernetes cluster, using Git Hub actions and ArgoCD.

Architectural and design decisions

The following diagram shows the design of the pages, and the components, that we will be building.

We will be building these three components, which will be rendered in the main page App.js

  • Login page (Login.js) — This component is used for both login and signup/register a user. This integrates with Firebase to create the user credentials
  • Book Search page (BookList.js) — This component is used to search for the book, In the backend, it will call book-info-service-svc Service, to fetch the book information. We will be using GraphQL to query the service.
  • Book Library page( Library.js) — This component will list all the books, that are selected by the user to be added. It will call book-library-service-svc Service, to add and remove books from the library.

The book information interface is componentized so that it can be reused across the pages (Book.js).

Let's now build the page

Step 1: Generate ReactJS boilerplate code

You can create a ReactJS boilerplate code, by following the instructions here. This will create a boilerplate ReactJS project, with all the required node modules, and a sample page. You can test if the page works, before proceeding.

Step 2: Add additional dependencies

We will be using some dependencies for our application, let’s install those node modules.

Material UI

We will be using Material UI to make our interface look neat. To install material UI libraries, run the following commands in the application folder.

npm install @material-ui/core
npm install @material-ui/icons

Firebase

We will need Firebase for authentication. To install firebase dependency, run the following command

npm install firebase

After installing, the package.json should look something like the below screenshot.

Step 3: Build the Template HTML

Build the HTML pages using any WYSIWYG HTML editors, to generate the HTML and the CSS. This will help in making the site look professional. The pages can be moved to a folder with respective CSS, images, ico etc.

Identify and remove the <div> segments of the page, which needs to be rendered dynamically. In this case, We have the following components

  • Book Details Tile
  • Book List as a grid
  • signup and login

These HTML segments need to be moved to the respective .js files so that we can start writing code around them to render those segments dynamically.

Let's now go through the complete code of the ReactJS app, I will be walking through the key segments of the code, you can download the complete code here.

Step 4: Build the application

Let's now walk through the ReactJS components, and page and add logic

App.js

App.js is the main page. The key thing to notice here is the usage of useState() hook. We will be storing the currently logged-in user in this state. it is initialized to null and will be filled when the login is successful.

The user value is stored in the current context using CurrentUserContext, which uses createContext() to create the context object, and it is passed to all the pages, by enclosing <Home/> inside <CurrentUserContext.Provider> tag. This way, All the pages that are rendered will have access to the current user logged in. Think of this as a global variable, implementation in ReactJS.

The following is the source code of CurrentUserContext.js . You can read more about the context here.

Home.js

App.js creates the user context and embeds Home.js inside. Home.js is the main page, which has the core logic of rendering various components.

Line 15 — The page is reading the current user context (that is passed by App.js, in the previous step), and stores into user variable.

Line 19 — We are checking if the user is not null, if not null, we are rendering the page with Navigation tabs for BookList and Library. As seen previously, BookList will help search for the books, and Library lists all the books, that are selected by the user. HashRouter, helps route the call to BookList on / and Library on /library (these are NavLinks we provided in the header).

Line 33 — if the user is null, it redirects to Login.

Login.js

Before we build the Login page, let's first configure Firebase authentication. We will need Firebase-generated authentication details before we can write the code.

Firebase Authentication

To configure firebase authentication, follow the instructions in the link

In my case, I created a bozo-book-library project in Firebase, the following is the screenshot.

In that project, I selected the Authentication provider, and selected email/password sign-in method. The following shows the screenshot of my configuration.

Once that is done, firebase generates the boilerplate code, which we can reuse. This boilerplate code has various configurations including the apiKey, appId, measurementId. These configuration parameters will be stored as Vault secrets and will be injecting them at runtime. We will walk through this later in the blog.

The following is the screenshot where the boilerplate code is provided by Firebase.

Let's copy the boilerplate code into fire.js. Find below the screenshot of fire.js

As you can see the apiKey, appId, measurementId configurations are removed from the source code (as they are secrets) and instead these configuration parameters are replaced with environment variables. The application expects these values to be passed as environment variables. We will be storing these values in Vault and we will be passing this to the application, as an environment variable.

Please refer to Chapter 2, for more details on how to manage secrets.

Let's now write the code for Login and Signup.

Line 10 — We are reading the user details from the context

Line 12–15 — We are defining the email, password, authError and hasAccount variables as states, so that when the value changes the userState() hook, will update the state. We will be using hasAccount as a boolean flag, to check if the user already has the account or not. if not, we will render the signup page

Line 18–26 — we are defining a function, that invokes firebase authentication API to authenticate the user with the passed email and password.

Line 28–33 — We are defining a function, that invokes firebase authentication API to create a new user with the passed email and password.

Line 35–37 — We sign out using firebase authentication API

Line 39–48 — This code registers a callback, to listen to the authentication state changes. We are setting the user state when the authentication state changes (login, logout). The Authentication process is implemented in an async way (as it might take time depending on the network speeds).

The function calls back when there is any change in the authentication status, that is received from the firebase authentication API. When the state changes, and if the user is not null, the user information is stored in the context (that is global).

Line 50–52 — We are using useEffect() hook to call the authListener() We are using useEffect() with an empty array (second parameter) so that we can run this only on the first render, otherwise we will end up calling authListener() on every render, which might end up with too many callbacks or nested callbacks, and freeze the page.

In the above code, we are using the <div> that is generated by our HTML designer tool so that the component looks as per the design.

Line 59 -61— We are getting the email and password, and setting the state of email and password, this will update the state variables.

Line 65–77 — We are checking if the user has the account, if not, we are rendering the Signup otherwise Signin button. On Signin, we are calling the login() function and on signup we are calling signup() function.

Book.js

Book.js implements the Book component, which will be used for Book Search and Book Library pages, to show the book information. The following is the design of the Book component

Let’s walk through the code

Line 37–47 — The Book component expects bookname, bookdescription, imagelink, author, bookId as props (This needs to be passed by the host page). These props are stored in local variables and processed to be rendered.

Line 14–34 — We will be using a Snackbar component to show the messages and errors to the user. this is a pop-up box that will show the message. The methods are used to store the state or change the state of that popup window. below is the code for the Snackbar component. We have two snack bars one to show normal messages/notifications and one to show error messages.

This is the straightforward logic, of opening and closing the message in 3 seconds. Refer to Snackbar documentation for more details

Line 49 — We are once again reading the current user from the context object that is passed on by the parent page

Line 50 — We are setting the relative path for the book-library-service-svc

Let's continue with the rest of the code

Line 52–75 — The above function sends a POST request to the book-library service to add or delete the book for the logged-in user.

Line 79–93 — We are using the HTML code that is generated using the HTML design tool, and writing our code to print the book details.

Booklist.js

Now that we have the Book component ready, we should be able to show the book information on any page.

Let’s now walk through the code

Line 12–15 — We are setting the state variables, which we will be using later in the code

Line 18 — Relative path to fetch book information (book-info-service-svc)

Line 22–24 — We are using useEffect() hook to render the page, whenever there is a change in currentPage state or searchQuery state, the page gets rendered.

currentPage state changes when the user clicks the next page or the previous page links,

searchQuery state changes, when the user sets the search query keyword, and clicks the search button or presses enter key in the search text field.

Line 26–58 — We are implementing the queryService() function to search for the book. This is called when the user provides the search query keyword and clicks the search button or presses enter key.

We are firing a GraphQL query in this code to get the title, authors, description, and links to thumbnail images. We are calling the BookInfo Service that we built in Chapter 3.

When we receive the response, we are converting to json in Line 49 and then stored in resultObject, and calling setSearchResponse() which changes the state of searchResponse state variable, which will trigger a render.

Line 60–65 — We are implementing the onSearch() function which is called when the user provides the search keyword and clicks the search button. we are setting the searchQuery state and calling queryService() the function to fetch the records

Line 67–78 — We are implementing previousPage() and nextPage() functions that update the currentPage state, that will trigger a render.

Line 86–90 — We are rendering the search keyword text field and the search button. when the user clicks the button or presses enter key, it will trigger onSearch() function, that we implemented in the above lines.

Line 93–144 — We are using the HTML code generated by the HTML design tool, to render the Book tiles in a Grid format. We are looping through the resultObject, and passing the book details to render a Book component (Lines 122–136).

We are also providing the Next Page and Previous Page links. We have already implemented pagination on the server side to fetch 10 records at a time. Please refer to Chapter 3 for more details.

Library.js

Library.js also has similar rendering logic as BookList.js (Ideally we should have made this grid as a ReactJS component), except that we are getting the BookIds from the Book library Service and then fetching book information from the Book Information Service.

The following code shows the implementation of getLibraryBooks() which calls Book Library Service and fetches the bookIds for the logged-in user.

The code below shows the implementation of getBooks() which is called to fetch the book information based on bookID, which is similar to what we did in the BookList.js

The rendering of the books as a grid is very similar logic as BookList.js. Ideally, this could have been built as a ReactJS component, which accepts a list of book records, to render.

Now we have all the pages implemented, let's quickly configure our firebase secrets in Vault

Step 5: Inject Secrets from Vault

We can create the key-value pair configuration using the vault kv put command as shown in the screenshot below. Please refer to Chapter 2 for more details on configuring Vault

Login to the vault and provide a read access policy for the new configuration by executing the following command

vault policy write bozobooks-app-policy - <<EOF
path "bozobooks/data/googleapi-config" {
capabilities = ["read"]
}
path "bozobooks/data/postgres-config" {
capabilities = ["read"]
}
path "bozobooks/data/reactjs-firebase-config" {
capabilities = ["read"]
}
EOF

The following is the deployment YAML for our Reactjs application.

As you can see between Lines 18–28 we are providing the vault annotations, to create a secrets config file and inject the secrets into it as export statements.

In Lines 35–40, we are sourcing that exported secrets config file, so that these secrets are available for the application as environment variables. This is what we are accessing in fire.js code using process.env, as shown below

In Line 36, we are using the Docker image of the ReactJS application, lets's now build the docker image and implement GitOps to build and deploy this image to Dockerhub automatically.

Step 6: Build and Deploy the application to Kubernetes cluster with GitOps

The following is the Dockerfile to build our ReactJS application docker image.

We are using node:13.12.0-alpine image as the base image, and creating a /app folder as the working folder, and copying package.json, running npm install, so that it downloads all the dependency modules, we are also installing react-scripts and then copying all the application code, and finally providing the entry point command as npm start

Let's also create a Service YAML in the infrastructure repository

Let's now build the GitOps pipeline code using GitHub actions in the Application repository. This gets triggered when the developer pushes the code.

In the above code

Line 35–57 — We are checking out all the code

Line 38–49 — Sending a slack notification

Line 51–54 — We are generating a new build number

Line 56–57 — We are printing the build number for our debugging purpose

Line 59–68 — We are building the docker image

Line 70–74 — We are logging in to Dockerhub

Line 77–80 — We are tagging the local image, and pushing it to the docker hub

Line 76–93 — We are sending the status as a slack notification

Line 95–102 — We are invoking the infrastructure code to update the Build number in the Deployment YAML. You can see the code below

The following is the screenshot of the app

We still cannot test this app, unless we configure Ingress, as we are using various relative paths to call the respective services, that needs to be configured in the Ingress

In the next chapter, we will be configuring Ingress using cert-manager to implement TLS(HTTPS). Until then have a great time…see you soon

Please provide your feedback and comments.

Further Reading:

--

--

A B Vijay Kumar
Cloud Native Daily

IBM Fellow, Master Inventor, Mobile, RPi & Cloud Architect & Full-Stack Programmer