Builder Book logo

Book: Builder Book

  1. Introduction
  2. Set up Node.js project. VS code editor and lint. Set up Next.js project. Material-UI integration. Server-side rendering. Custom styles.
  3. HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.
  4. Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.
  5. Testing method with Jest. Transactional email API with AWS SES service. Set up AWS SES service, security credentials. sendEmail method. Export and import syntax for server code. EmailTemplate data model. Update User.signInOrSignUp. Informational success/error messages. Notifier component. notify method.
  6. Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Two improvements. Testing.
  8. Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.
  10. Prepare project for deployment. Environmental variables, production/development. Logger. SEO, robots.txt, sitemap.xml. Compression and security. Deploy project. Heroku. Testing deployed project. AWS Elastic Beanstalk.

Chapter 3: Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.


The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.


In Chapter 3, you'll start with the codebase in the 3-begin folder of our builderbook repo and end up with the codebase in the 3-end folder. We'll cover the following topics in this chapter:

  • Authentication HOC withAuth
    - getInitialProps method
    - Parameters for withAuth HOC
    - Testing withAuth

  • Login page and NProgress

  • Asynchronous execution and callback
    - Promise.then
    - async/await

  • Google Oauth API infrastructure
    - setupGoogle, verify, passport and strategy
    - Express routes /auth/google, /oauth2callback, /logout
    - User.publicFields and User.signInOrSignUp methods
    - generateSlug method
    - this
    - Set up at Google Cloud Platform and testing


In this chapter, we will create a higher-order component (HOC) called withAuth. This HOC, similiar to our App HOC, will wrap our pages. App wraps all pages automatically by default. To wrap a page with withAuth, we have to do so explicitly when exporting a page component. The main purpose of App is to share parts of our application's layout - for example, Material-UI's theme, the Header component, and eventually the Notifier component - between our pages. The main purpose of withAuth is to check if a user is logged in and if so, pass the user prop and its value to page components.

Inside the withAuth HOC, we will use Next.js's getInitialProps method to get a value for the user prop and pass it down to page components. Later in this chapter, we will make a detour to discuss this Next.js method in more detail.

Besides creating the withAuth HOC, we will:
- Discuss the getInitialProps method
- Build our Login page and introduce the Nprogress loading bar.
- Learn about Promise.then, async/await, and the concept of this.
- Add a relatively large external Google OAuth API infrastructure.


Authentication HOC withAuth link

Adding user authentication to our app is overall a big task. Hence, we devote this entire chapter to adding user authentication to our web application. In this chapter, our first goal is to discuss and define a new withAuth HOC that passes the value of user as a prop to the page components that withAuth wraps. We will then have a detour discussion about asynchronous execution, integrate our web app with Google OAuth API service, and test out the entire user authentication flow.

Two main goals for this section are:
- Discuss and define the withAuth HOC in a new file, lib/withAuth.jsx.
- Test our withAuth HOC with a manually-created User MongoDB document in our database.

Important note - from now on, we will start our custom server that we created in Chapter 2 with yarn dev instead of yarn dev-express. In the scripts section of package.json, replace:

"dev": "next"
"dev-express": "nodemon server/server.js --watch server"

With:

"dev": "nodemon server/server.js --watch server"

In Chapter 2, we added user data (as a prop) to our Index page by using Next.js's unique method getInitialProps. That's ok, since it's just one page. However, in our full web application, we will have multiple pages that need the user prop to display their content. Passing the user prop to each page explicitly, by hand, is not productive.

To be more productive, we can create a HOC that will wrap pages that need the user prop and pass the user prop to all of those wrapped pages. So instead of defining PageComponent.getInitialProps directly on the Index page and other pages, we will define withAuth.getInitialProps inside a withAuth HOC that wraps the Index page and other pages.

Let's place this new withAuth HOC inside the lib directory in a new file, lib/withAuth.jsx. Inside our withAuth component, we will specify a few simple boolean parameters - for example, loginRequired to control whether a page requires user authentication (the value of user must be truthy) or not. Then we will wrap a page in our withAuth HOC and pass values to these boolean parameters to specify rules for that particular page. For example, loginRequired: true when passed like this:

export default withAuth(Index, { loginRequired: true })

Means that the Index page requires a user to be authenticated (logged in).

All pages for Admin and Customer users will require a user to be logged in; thus, for those page, we should pass loginRequired: true when wrapping them with the withAuth HOC. In this chapter, you can think of the wrapped Index page as a user's dashboard - the page to which a user is redirected after successfully logging in.

Let's look at the new export code for the Index page again:

export default withAuth(Index, { loginRequired: true })

This export code says to us that the withAuth HOC wraps the Index page. The boolean parameter loginRequired requires a user to be authenticated (logged in) to access this page.

We get the following structure for our withAuth function that takes the page component, BaseComponent, as an argument and also takes loginRequired and logoutRequired parameters as arguments. Ultimately, withAuth returns the App component that, in turn, returns the page component with passed props:

<BaseComponent {...this.props} />

Create a new file, lib/withAuth.jsx, and add the following unfinished construct in it:

import React from 'react';
import PropTypes from 'prop-types';
import Router from 'next/router';

let globalUser = null;

export default function withAuth(
    BaseComponent,
    { loginRequired = true, logoutRequired = false } = {},
) {
    const propTypes = {
        user: PropTypes.shape({
            id: PropTypes.string,
            isAdmin: PropTypes.bool,
        }),
        isFromServer: PropTypes.bool.isRequired,
    };

    const defaultProps = {
        user: null,
    };

    class App extends React.Component {
        static async getInitialProps(ctx) {
            // 1. getInitialProps
        }

        componentDidMount() {
            // 2. componentDidMount
        }

        render() {
            // 3. render

            return (
                <>
                    <BaseComponent {...this.props} />
                </>
            );
        }
    }

    App.propTypes = propTypes;
    App.defaultProps = defaultProps;

    return App;
}

You may wonder why withAuth is a function that returns a component, but App and Document HOCs from the pages/ folder are simply components. That's because App and Document are not exactly HOCs - they are extensions of Next.js's built-in App and Document HOCs. We extend Next.js's HOCs App and Document by defining the classes MyApp and MyDocument, respectively. So App and Document are not technically HOCs but HOC extensions. On the other hand, withAuth is indeed technically a HOC, and here is an example of how to define a simple HOC:
https://blog.jakoblind.no/simple-explanation-of-higher-order-components-hoc/#the-most-simple-hoc

// Take in a component as argument WrappedComponent
function simpleHOC(WrappedComponent) {
    // And return a new anonymous component
    return class extends React.Component{
        render() {
            return <WrappedComponent {...this.props}/>;
        }
    }
}

That's how we defined withAuth earlier. The only difference is that we wrote class App extends and then return App. The above example uses return class extends, which is the same thing. It's important to point out that Next.js internally defines App and Document HOCs in the same way.

You already specified types from props and default values for props in Chapter 1, so we are not discussing it here in detail.

One more note worth mentioning - <> is short syntax for React.Fragment:
https://reactjs.org/docs/fragments.html#short-syntax

In the next few subsections, we will define the following methods and then test our new withAuth HOC with the Index page:
- withAuth.getInitialProps
- withAuth.componentDidMount
- withAuth.render


getInitialProps method link

In this section, we define the withAuth.getInitialProps method. We already used getInitialProps earlier in this book (Index page). Next.js uses the getInitialProps method to populate props of page components. Both HOCs and Next.js pages can use this method to get and pass data, but child components cannot. Child components get props from a parent component.

For example, in Chapter 5, we will introduce the pages/customer/read-chapter.jsx page. As with any page of our web app that is not written as a stateless functional component, we define the ES6 class with class ... extends syntax:

class ReadChapter extends React.Component {
    // some code
}

getInitialProps is a static method of the ReadChapter class:

class ReadChapter extends React.Component {
    static async getInitialProps({ query }) {
        const { bookSlug, chapterSlug } = query;

        const chapter = await getChapterDetail({ bookSlug, chapterSlug });

        return { chapter };
    }
}

Static method means the method defines functions that act on a class instead of a particular object of a class.

In the example above, when a user loads the pages/customer/read-chapter.jsx page, getInitialProps receives two slugs from the request's query part of the URL, passes parameters to the API method getChapterDetail and calls it, and returns the chapter prop. Now, inside the ReadChapter page, we are able to access this.props.chapter.

In this example, we pass ({ query }) to the getInitialProps method, but you can pass other parameters as well, for example (ctx) or ({ req }). Check out the Next.js docs for details:

https://nextjs.org/docs/api-reference/data-fetching/getInitialProps

Typically, since the getInitialProps method can run on both browser and server, you can use getInitialProps when you know that you need page that is accessed by both (1) loading via clicking a Link (client-side rendered page) and (2) loading in a new browser tab (server-side rendered page). If you, as a developer, need a page that is only client-side rendered, you can get data inside componentDidMount instead of getInitialProps. So in our project, ReadChapter will use getInitialProps to get data, but Admin pages (Admin page and AddBook page, discussed in Chapters 5 and 6) will use componentDidMount to get data.

withAuth.getInitialProps needs to calculate the values of user and isFromServer, then pass these values as props to the component App. This is how we define the withAuth.getInitialProps method:

static async getInitialProps(ctx) {
    const isFromServer = typeof window === 'undefined';
    const user = ctx.req ? ctx.req.user && ctx.req.user.    toObject() : globalUser;

    if (isFromServer && user) {
        user._id = user._id.toString();
    }

    const props = { user, isFromServer };

    if (BaseComponent.getInitialProps) {
        Object.assign(props, (await BaseComponent.getInitialProps(ctx)) || {});
    }

    return props;
}

The new boolean prop isFromServer is defined as typeof window === 'undefined'. In other words, if a page is server-side rendered, then the statement typeof window === 'undefined' evaluates to true and isFromServer has a value of true. We use its value twice, once in withAuth.getInitialProps and once in withAuth.componentDidMount. This is how we use the isFromServer boolean parameter inside withAuth.getInitialProps:

if (isFromServer && user) {
    user._id = user._id.toString();
}

On the server, unlike the browser, we need to call the toString method to get a string representation of ObjectId:

https://docs.mongodb.com/manual/reference/method/ObjectId.toString/

https://stackoverflow.com/questions/13104690/nodejs-mongodb-object-id-to-string

To see that user._id from the server is not a string but an object, you can add console.log() statements like the following:


You've reached the end of the Chapter 3 preview. To continue reading, you will need to purchase the book.

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.