Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. GitHub. VS Code Editor. Node. Yarn. TypeScript. TSLint. Next.js. Environmental variables.
  3. Material-UI. Theme. Dark theme. Shared layout. Shared styles. Shared components. Mobile browser.
  4. HTTP. APP server. Next-Express server. Fetch method. API methods. async/await. API server. Express server. Environmental variables. Logs.
  5. User model. Mongoose and MongoDB. MongoDB index. Jest testing. Your Settings page. File upload to AWS S3.
  6. Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.
  7. AWS SES API. Passwordless OAuth API. Mailchimp API.
  8. Application state, App HOC, store and MobX. Data store for User. Toggle theme API. Team. Invitation.
  9. Discussion API. Post API. Websockets.
  10. Stripe API and paid subscription. Email notification for new post. AWS Lambda. AWS API Gateway.
  11. Environmental variables, production/development. Logger. API server. Server-side caching. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. AWS Elastic Beanstalk.

Chapter 5: Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.

Last week to pre-order for $99. The price becomes $199 at midnight on Sunday, August 16th.


The section below is a preview of this book, which is in progress. You can pre-order the book for $99. The price after book's completion will be $199.

If you pre-order the book, you will be emailed about new chapters as they become available.

The book is to be completed by August 16, 2020.


In Chapter 5, you will start with the codebase in the 5-begin folder of our saas repo and end up with the codebase in the 5-end folder.

We will cover the following topics in this chapter:

  • Login page
    - LoginButton

  • Session and cookie
    - Configure session and save/retrieve session from database
    - Mount Express middleware
    - Modify Express route /get-user-by-slug
    - Configure cookie

  • Google OAuth API
    - Express routes for Google OAuth API
    - Configure passport
    - verify function
    - passport and session
    - Static methods publicFields and signInOrSignUpViaGoogle
    - getUserApiMethod API method and '/get-user' Express route
    - Google Cloud Platform
    - Testing Google OAuth API

  • Authentication HOC withAuth
    - getUserApiMethod in withAuth
    - Redirect logic in withAuth
    - Render logic in withAuth HOC
    - Testing withAuth HOC

  • firstGridItem logic in App HOC


We learned many useful topics in Chapter 4. A particularly useful topic was building various API infrastructures. For example, we defined and built "getting user by slug API". For this API, req-res cycles are between APP, API, and a MongoDB server. We built the more complicated "uploading file API" that consists of two sub-APIs. The first sub-API, called "getting signed request API"", involves APP, API, and an AWS S3 server. The second sub-API, called "uploading file using signed request API", involves only APP and an AWS S3 server.

As you progress through this book, we will keep practicing our API-building skills. We will cover more traditional "internal" API infrastructures. These APIs have req-res cycles in between APP, API, and a MongoDB server. We will also cover less-traditional "external" API infrastructures. These APIs involve some external, third-party server other than MongoDB. One example of an external server is the AWS S3 server.

So why are we discussing API infrastructures at the beginning of this chapter?

Currently, our web application has no way to differentiate beetween a guest (non logged-in) user and a logged-in user. Our primary goal in Chapter 5 is to add user authentication to our web application. In Chapter 5, we will implement user authentication using Google OAuth. Then in Chapter 6, we will implement an additional authentication method: passwordless user authentication.

Adding Google OAuth authentication basically amounts to adding a new external API infrastructure to our web app. As you may have concluded at this point, adding any new data exchange to our web app means adding a new internal or external API infrastructure.

In the first section of this chapter, we will discuss and create a Login page. After that, we will discuss details of the "Google OAuth API" API infrastructure. After that, we will implement a new higher-order component called withAuth.

link Login page

In this section, we will create a new page: Login. The page is relatively simply to implement, since you already implemented the Index and YourSettings pages. Check up the code for the Index page at book/5-begin/app/pages/index.tsx and the code for the YourSettings page at book/5-begin/app/pages/your-settings.tsx. The pattern for these pages can be summarized like this:

// some imports go here

type Props = { user: { email: string; displayName: string } };

class Index extends React.Component<Props> {
    public static async getInitialProps() {
        // some JS/TS code goes here
    }

    public render() {
        return (
            <Layout {...this.props}>
                <Head>
                    <title>Index page</title>
                    <meta name="description" content="This is a description of the Index page" />
                </Head>
                // some HTML/React/Material-UI code goes here
            </Layout>
        );
    }
}
export default Index;

The Login page is similar to both Index and YourSettings pages in that:
- We define data types for Props of the page component.
- The Login page contains a Head element and main div element, and the page is wrapped with a Layout component.

The Login page differs from the Index and YourSettings pages in that:
- We don't need to define getInitialProps on the Login page, since we don't any data from page's props
- Instead of having Material-UI's Button component, the Login page will contain an imported custom component called LoginButton.

The Login page is even more straightforward to implement than the Index page:

import Head from 'next/head';
import React from 'react';
import LoginButton from '../components/common/LoginButton';
import Layout from '../components/layout';

class Login extends React.Component {
    public render() {
        return (
            <Layout {...this.props}>
                <div style={{ textAlign: 'center', margin: '0 20px' }}>
                    <Head>
                        <title>Log in to SaaS boilerplate by Async</title>
                        <meta
                            name="description"
                            content="Login and signup page for SaaS boilerplate demo by Async"
                        />
                    </Head>
                    <br />
                    <p style={{ margin: '45px auto', fontSize: '44px', fontWeight: 400 }}>Log in</p>
                    <p>You’ll be logged in for 14 days unless you log out manually.</p>
                    <br />

                    <LoginButton />
                </div>
            </Layout>
        );
    }
}

export default Login;

Place the above code into a new file, book/5-begin/app/pages/login.tsx.

Note how we wrote a descriptive SEO title and description for the Login page. Why do this? It's because we will allow search engine bots to crawl the Login page. We want people who search on Google or other search engines to be able to discover our Login page. When we deploy our web app in Chapter 13, the crawled URL for the Login page will be https://saas-app.builderbook.org/login. We will also set up proper robots.txt and sitemap.xml files for our APP project.

link LoginButton

Unlike other non-page components that we created so far (Notifier, Confirmer, MenuWithLinks), the LoginButton component has no need for state. If you need to remember how we created these non-page components, check up the book/5-begin/app/components/common/ folder.

The LoginButton component has no methods except render, and it has only one prop. The LoginButton gets the value of this prop from the Login page component. Create a new file, book/5-begin/app/components/common/LoginButton.tsx, with following code:

import Button from '@material-ui/core/Button';
import React from 'react';

import { styleLoginButton } from '../../lib/sharedStyles';

class LoginButton extends React.PureComponent {
    public render() {
        const url = `${process.env.URL_API}/auth/google`;

        console.log(url);

        return (
            <React.Fragment>
                <Button variant="contained" style={styleLoginButton} href={url}>
                    <img
                        src="https://storage.googleapis.com/async-await-all/G.svg"
                        alt="Log in with Google"
                    />
                        Log in with Google
                </Button>
                <p />
                <br />
            </React.Fragment>
        );
    }
}

export default LoginButton;

is called a "non-breaking space" HTML entity. On the browser, it renders as one empty space. HTML entities are special (reserved) strings of characters that start with & and render as some difficult-to-type characters:

https://developer.mozilla.org/en-US/docs/Glossary/Entity

We did not discuss the code for the LoginButton component in detail, since we already know how to build even more complicated components. However, there is one important fact to point out. So far in this book, every page we built had a page method that calls an API method. The API method in return sends a request to the API server. In the YourSettings page, the getUserBySlugApiMethod method executes after a user requests the page from the browser, and the updateProfileApiMethod method executes after a user clicks the Update name button. The Login page is similar to the YourSettings page, because a user does indeed need to click a button to send a request to the API server. At the same time, the Login page is different from the YourSettings page, because we defined no page method or API method to send a request to the API server.

Instead of an API method, we use a Button component with an href prop from Material-UI's library. This component renders into an HTML element: anchor <a>. When a user clicks on this anchor element, the browser automatically sends a request with the method GET to the API endpoint ${process.env.URL_API}/auth/google. No need for us to define a page method or API method; however, we do need, as before, to define an Express route with a matching path on the API server.

Alright, it's time to see if our page renders as expected. Since the Login page does not have any methods that send requests to the API server, we can load the Login page without starting our API server.

Start your APP project with yarn dev and navigate to http://localhost:3000/login:

Builder Book

The page looks good, but the grid item on the left looks out of place. Later in this chapter, when we work on the new withAuth higher-order component, we will add proper logic to the code to not display the left grid item. The Login page will ultimately have only one grid instead of two.

The terminal window for your APP project prints:

http://localhost:8000/auth/google

This output is from:

console.log(url);

The API endpoint has a proper value; however, we haven't implemented an Express route at API to process a request with this API endpoint. We will create this Express route later, in section Google OAuth API.

In the next section, we will discuss the concept of session and cookie. After that, we will build our user authentication using Google OAuth.

link Session and cookie

Before we build an infrastructure for user authentication, it is critical to understand the concept of session and cookie.

How does a web application identify an end user who loads a page? How does a web application keep an end user logged-in? You can log in to any web application on the internet, close the tab, reopen the tab, and find that you are still logged in. How does the web application recognize you?

The short answer is session and cookie.

When an end user logs in to web application, the web application saves a so-called cookie object to the end user's browser. When this user later comes back and loads the web application's page, the cookie's name and value are sent to the server with an initial request. The server uses the cookie's name and value to find a session document in the database. The session document will contain the user's id, which the server uses to find and send relevant user data to the browser.

Ok, we understand how a returning end user remains logged in. We will discuss how to keep an end user logged in later in this chapter, after we introduce user authentication via Google OAuth.

For now, our goal is to understand how session and cookie objects are created. We also want to understand how the browser saves cookie in the first place.

Here is a summary of the process:

Builder Book

As we mentioned earlier in this book, code in the API project always runs on the server and never on the browser (client). That's why you always see us referring to API as API server. Contrary to this, code in the APP project can run on the server and on the browser. Why is that? That's because the Next.js framework for React has both server-side and client-side rendered pages. In other words, it is important to understand where an initial request comes from.

Think of getUserBySlugApiMethod. If you load the YourSettings page in a new tab on the browser, YourSettings will be rendered on the server. But if you are on the Index page and click an internal link that leads to the YourSettings page, YourSettings will be rendered on the browser.

Recall from Chapter 4: on the YourSettings page, we found that updateProfileApiMethod produced a CORS-related error, and getUserBySlugApiMethod ran without any error. That's because for updateProfileApiMethod, the request is sent from the browser to the server. For getUserBySlugApiMethod, the request is sent from the server to the server.

Why do we emphasize this distinction? Because later in this section, you will find out that cookie only gets saved to the browser when an initial request to the page comes from the browser.

What is session? As we already mentioned, session is an object. The session object gets created by the server and is not accessible on the browser. This object is a unique object. When an end user loads a page of your web application, the server creates a unique session object.

You, as a developer, can store unique user-related data in the session object. For example, you can save a user's id into session. Why would you that? Because having a persistent login session is a good user experience. You don't want an end user to log in every time he/she loads the page. You could make a log-in session last for 14 days, thus sparing any user from having to re-log in to your web application for 14 days.

When an end user has a cookie object saved to the browser, the API server will receive this cookie with an initial request because of the code we wrote earlier in book/5-begin/app/lib/api/sendRequestAndGetResponse.ts:

if (request && request.headers && request.headers.cookie) {
    headers.cookie = request.headers.cookie;have
}

The API server will use a unique cookie to find a unique session document in the database. From this unique session document, the API server will find a unique user document and save it to req.user. The API server will hold this user document in its memory as req.user.

As you can see from the above diagram, in order create session, we need to mount Express middleware to our Express server API.

Let's create our very first session and cookie in this book. Look at the above diagram again. Let's outline our work:

  1. When the YourSettings page loads, getUserBySlugApiMethod executes, and the browser sends a request to the API server. We don't have to add any code here. We already built the "getting user by slug" API.

  2. API gets a request from the browser and creates a session object, as long as we configured the session's parameters and added session-related Express middleware to the Express server. Ok, here we need to do two things: configure session and mount Express middleware. If we do this, every request to the API server from the browser will result in creation of a session object.

  3. We need to ensure that any session object gets saved to our MongoDB database as a session document. To do so, we have to simply modify the session object. Express middleware and related packages require us to modify a session object if we want it to be saved to the database. We will discuss this requirement in more detail later in this section. Here, we can modify the session object inside the Express route /get-user-by-slug.

  4. The API server sends a response to the browser. This response has a cookie object that gets saved to the browser. Here, we need to add new code as well. We need to configure the cookie's parameters.

  5. We need to ensure that the YourSettings page gets rendered on the browser, not on the server. For that, we need to add a navigational link to the Index page. This link leads to the YourSettings page. When we test our entire implementation, we will load the Index page and then click a navigational link.

There is no additional code to write for Step 1. Below, let's work on steps 2 through 5.


The section above is a preview of this book, which is in progress. You can pre-order the book for $99. The price after book's completion will be $199.

If you pre-order the book, you will be emailed about new chapters as they become available.

The book is to be completed by August 16, 2020.

Last week to pre-order for $99. The price becomes $199 at midnight on Sunday, August 16th.


format_list_bulleted
help_outline
lens