Builder Book

  1. Introduction
  2. App structure. Next.js. HOC. Material-UI. Server-side rendering. Styles.
  3. Server. Database. Session. Header and MenuDrop components.
  4. Authentication HOC. Promise. Async/await. Static method for User model. Google OAuth.
  5. Testing with Jest. Debugging with Winston. Transactional emails. In-app notifications.
  6. Book and Chapter models. Internal API. Render chapter.
  7. Github integration. Admin dashboard. Testing Admin UX and Github integration.
  8. Table of Contents. Highlight for section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book logic. ReadChapter page. Checkout flow. MyBooks page. Mailchimp API. Deploy app.

Chapter 3: Authentication HOC. Promise. Async/await. Static method for User model. Google OAuth.

We keep the book up-to-date with the latest frameworks and packages.


In Chapter 3, you'll start with the codebase in the 3-start 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
    - Parameters for withAuth HOC

  • getInitialProps() method

  • Login page and Nprogress

  • Promise.then()

  • Async/await

  • Static method signInOrSignUp()

  • Generate slug

  • Google OAuth: auth() function

  • Express routes for auth()

  • Initialize auth() on server.

  • Testing


In this chapter, we will create a higher-order component (HOC) called withAuth.js (or simply withAuth). This component, similiar to our withLayout HOC, will wrap our pages. The main purpose of withLayout is to server-side render and add Header and Notifier components to our pages. The main purpose of withAuth is to check a user's session and send that user's data to our pages.

Inside the withAuth HOC, we will use Next.js's getInitialProps() method to fetch a user's data and pass it to our wrapped pages. Later in this chapter, we will make a detour to discuss this method in more detail.

Besides creating the withAuth HOC, we will:

  • add a Login page
  • learn more about Promise() and then(), async/await, and the concept of this
  • add basic static methods to our User model

link Authentication HOC

Adding user authentication to our app is a big task. In this chapter, our main goal is to write a withAuth HOC that passes user data to wraped pages. In the next chapter (Chapter 4), we will integrate our app with Google OAuth and test out the entire authentication flow.

Two main goals for this section are:

  1. set up our withAuth HOC at lib/withAuth.js - this component passes a user object to pages and redirects a user according to his/her login status
  2. test our withAuth HOC with a manually-added user 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"

and

"dev-express": "nodemon server/app.js --watch server --exec babel-node --presets=@babel/preset-env"

with:

"dev": "nodemon server/app.js --watch server --exec babel-node --presets=@babel/preset-env"

In Chapter 2, we passed user data from our server to our Index page by using Next's getInitialProps() method. That's ok since it's just one page. However, in our app, we will have multiple pages that require a user's data. Passing user data to each page is not productive.

To be more productive, we create a HOC that will wrap pages and pass user data to those wrapped pages. So instead of using getInitialProps() directly on the Index page, we will use getInitialProps() inside a withAuth HOC that wraps the Index page.

Let's place this new HOC in the same place as our withLayout HOC - inside the lib directory at ./lib/withAuth.js. Inside our withAuth component, we will specify simple boolean parameters - for example, loginRequired: true to control when an individual page requires user authorization. Then we will wrap a page in our withAuth HOC with these parameters to specify rules for that particular page.

The Index page (as you may have guessed) becomes a dashboard page for logged-in users. Once authenticated, we send a user's data to this page.

The export code for our Index page will look like this:

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

It says:

  • withLayout HOC wraps the Index page (as a result, this page gets the Header component)
  • withAuth HOC wraps the Index page as well. The boolean parameter loginRequired requires a user to be authenticated to access this page.

The structure of withAuth will be very similar to that of withLayout. The main purpose of withAuth is to receive a user prop with the help of the getInitialProps() method and then pass this user prop to a child component, which is any page that withAuth wraps.

In fact, our withLayout HOC does exactly that using Next.js's getIntitialProps() method.
lib/withLayout.js :

import React from 'react';
import PropTypes from 'prop-types';
import { MuiThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';

import getContext from './context';
import Header from '../components/Header';

function withLayout(BaseComponent) {
    class App extends React.Component {
        constructor(props) {
            super(props);
            const { pageContext } = this.props;
            this.pageContext = pageContext || getContext();
        }

        componentDidMount() {
            const jssStyles = document.querySelector('#jss-server-side');
            if (jssStyles && jssStyles.parentNode) {
                jssStyles.parentNode.removeChild(jssStyles);
            }
        }

        render() {
            return (
                <MuiThemeProvider
                    theme={this.pageContext.theme}
                    sheetsManager={this.pageContext.sheetsManager}
                >
                    <CssBaseline />
                    <div>
                        <Header {...this.props} />
                        <BaseComponent {...this.props} />
                    </div>
                </MuiThemeProvider>
            );
        }
    }

    App.propTypes = {
        pageContext: PropTypes.object, // eslint-disable-line
    };

    App.defaultProps = {
        pageContext: null,
    };

    App.getInitialProps = (ctx) => {
        if (BaseComponent.getInitialProps) {
            return BaseComponent.getInitialProps(ctx);
        }

        return {};
    };

    return App;
}

export default withLayout;

Following a similar structure as withLayout, we get this code for withAuth:
lib/withAuth.js :

import React from 'react';
import PropTypes from 'prop-types';

let globalUser = null;

function withAuth(BaseComponent) {
    class App extends React.Component {
        // specify propTypes and defaultProps

        static async getInitialProps(ctx) {
            const isFromServer = !!ctx.req;
            const user = ctx.req ? ctx.req.user && ctx.req.user.toObject() : globalUser;

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

            const props = { user, isFromServer };

            // Call child component's getInitialProps, if it is defined
            if (BaseComponent.getInitialProps) {
                Object.assign(props, (await BaseComponent.getInitialProps(ctx)) || {});
            }

            return props;
        }

        componentDidMount() {
            const { user, isFromServer } = this.props;

            if (isFromServer) {
                globalUser = user;
            }
        }

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

    return App;
}

export default withAuth;

You may notice a few differences.

  • The first difference is // specify propTypes and defaultProps. Validation of types is optional. However, validating props via propTypes and specifying defaultProps may become handy in situations when you pass the wrong data type to a prop, or when you need to specify a default value for a prop. Take a look at how we did it at ./pages/index.js in Chapter 1.

Go ahead and replace the line // specify propTypes and defaultProps in the withAuth carcass above with:

static propTypes = {
    user: PropTypes.shape({
        displayName: PropTypes.string,
        email: PropTypes.string.isRequired
    }),

    isFromServer: PropTypes.bool.isRequired
}

static defaultProps = {
    user: null
}
  • The second difference is the new boolean prop isFromServer defined as !!ctx.req. This parameter ensures that context (ctx) is rendered on the server.
    Request req and thus ctx.req both exist on the server.
  • In case ctx.req is not rendered on the server - it's undefined on the client and we get !!undefined is false.
  • If ctx.req is rendered on the server and exists on the client, we get !!value is true.
    Remember that the first ! converts an object to boolean and negates it. The second ! negates that boolean. In JavaScript, undefined is falsy, meaning !undefined is true, and !!undefined is false.
  • Third, user._id is not a string, but on the client, we need it to be a string. Thus, we stringify MongoDB's _id with: user._id = user._id.toString()
  • Fourth, withAuth has no parameters.
    We will add two parameters to our withAuth HOC, but we'll do that later in this chapter. For now, let's go back to our main goal - fetch user data via withAuth instead of sending this data directly to our Index page.

We mentioned the main property of the getInitialProps() method in Chapter 1 when we discussed our withLayout HOC. This method fetches data and populates props with that data. If you'd like to know more, we made a detour and wrote a getInitialProps() section right after this one.

At this point, we have a basic version of our withAuth HOC. Let's make necessary changes to our Index page. Recall this page's code:

  • We wrote the page's component as a stateless functional component in Chapter 1
  • The page gets user data directly from the server via Index.getInitialProps, which we set up in Chapter 2

Here is the page's code:
pages/index.js :

import PropTypes from 'prop-types';

import Head from 'next/head';

import withLayout from '../lib/withLayout';

const Index = ({ user }) => (
    <div style={{ padding: '10px 45px' }}>
        <Head>
            <title>Index page</title>
            <meta name="description" content="This is the description of the Index page" />
        </Head>
        <p>Content on Index page</p>
        <p>Email: {user.email}</p>
    </div>
);

Index.getInitialProps = async ({ query }) => ({ user: query.user });

Index.propTypes = {
    user: PropTypes.shape({
        displayName: PropTypes.string,
        email: PropTypes.string.isRequired,
    }),
};

Index.defaultProps = {
    user: null,
};

export default withLayout(Index);

Let's make three changes to the Index page:

  1. re-write the page's component as a normal component (class Index extends React.Component),
  2. remove the Index.getInitialProps method so that user data is not directly sent to the Index page
  3. import withAuth and wrap this HOC around the Index component (we do so with export default withAuth(withLayout(Index))):

After you make these changes, you should get:
pages/index.js :

import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';

import withAuth from '../lib/withAuth';
import withLayout from '../lib/withLayout';

class Index extends React.Component {
    static propTypes = {
        user: PropTypes.shape({
            displayName: PropTypes.string,
            email: PropTypes.string.isRequired,
        })
    };

    static defaultProps = {
        user: null,
    };

    render() {
        const { user } = this.props;
        return (
            <div style={{ padding: '10px 45px' }}>
                <Head>
                    <title>Dashboard</title>
                    <meta 
                        name="description"
                        content="List of purchased books."
                    >
                </Head>
                <p>Dashboard</p>
                <p>Email: {user.email}</p>
                </div>
        )
    }
}

export default withAuth(withLayout(Index));

To continue reading, buy this book.

We keep the book up-to-date with the latest frameworks and packages.


format_list_bulleted
help_outline