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.
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.