July 12, 2019

Build a Serverless HackerNews Clone using React, GraphQL and 8base — Part 2

Richard Umoffia
@richyafro

In the first part of the series, we were able to complete the data part of the application that boasted the following features:

  • Creating posts
  • Listing posts
  • Upvoting posts
  • Displaying post metadata

We utilised a stack consisting of React, GraphQL and 8base as the data layer, we successfully queried the 8base GraphQL endpoints and wrote mutations to create new data with the help of libraries like react-apollo and graphql-tag. In this second article, the focus will be on authenticating users and associating posts with users.

Getting Started


To get started, we’ll switch the current branch. So far, you’ve been working on the starter branch, but for this part, you'll switch to a new branch called auth. Open a terminal in the project directory and run the following command:

{% code-block language="sh" %}
git checkout auth
{% code-block-end %}

In this branch, the features from the first part of the article have been completed, so ensure to finish the first article to avoid spoilers. There are some new files and a couple of "TODO" comments scattered around.

Run {% code-line %}npm install{% code-line-end%} again to install the new dependencies required for this section of the article. Then run {% code-line %}npm start{% code-line-end %} if you haven't already to run the application.

Authentication

In this section, our aim is to implement basic authentication for the application. A user should be able to log in and log out, and a user needs to be authenticated to create a post.

Authentication is the act of confirming the truth of an attribute of a single piece of data claimed true by an entity. — Wikipedia

Before we start making any changes in the code base, we first have to configure the 8base dashboard to be ready for authentication. Everything related to authentication on 8base lies in the SettingsAuthentication page. It should look something like this:

To begin the setup, we have to create an authentication profile. The authentication profile will contain the authentication type, roles etc. To create a new authentication profile, click the button with a plus sign beside the Authentication Profiles text:

So we’ll create an authentication profile with the following values:

  • Name: Choose a descriptive name that helps you or someone else reading your code to understand what this profile does. In our case, you can replace My Auth in the screenshot above with a name like Guest User Auth.
  • Type: The authentication type available for now is 8base authentication. Keep an eye on their documentation to know when using your Auth0 account becomes available.
  • Self Signup: The value for this should be Open to all .
  • Roles: The roles can be either of Guest or Administrator, and it can also be both of them.

A new authentication profile should be generated when you click on "Add Profile", not only that but also the client information was created. The client information is useful for connecting the client application to the 8base authentication settings.

You can copy the client information at the top of the Authentication page, similar to the screenshot below:

Copy the Client ID and the Domain these values will come in handy later in the article. Now that we have completed the first part of the authentication setup, let's head over to the codebase and start making changes.

First, we have to update the provider in the index.js file. The 8base App Provider loads fragments schema and provides it to the Apollo client, along with authentication and table schema. Open the src/index.js and make the following changes:

First, replace the following imports:

{% code-block language="js" %}
import { ApolloProvider } from "react-apollo";
import ApolloClient from "apollo-boost";
{% code-block-end %}

With the client and provider from 8base:

{% code-block language="js" %}
import { AppProvider, WebAuth0AuthClient } from "@8base/react-sdk";
{% code-block-end %}

Below the TODO — 2 is the ApolloClient setup, replace the existing setup with the WebAuth0AuthClient setup. The new setup includes authentication, and we'll be making use of the client credentials that we copied before from the 8base dashboard.

Update the previous setup with the following setup:

{% code-block language="js" %}
const auth0WebClient = new WebAuth0AuthClient({
 /* domain client information from authentication page */
 domain: "<AUTH_DOMAIN>",
 /* client id information */
 clientId: "<AUTH_CLIENT_ID>",
 redirectUri: `${window.location.origin}/auth/callback`,
 logoutRedirectUri: `${window.location.origin}/`
});
{% code-block-end %}

Copy the client information from the 8base authentication page

Then finally, update the render function to make use of the 8base App provider and pass the 8base URI and the auth client configuration as props:

{% code-block language="js" %}
ReactDOM.render(
 <AppProvider uri={URI} authClient={auth0WebClient}>
   {({ loading }) => {
     if (loading) {
       return <p>Please wait...</p>;
     }
     return <App />;
   }}
 </AppProvider>,
 document.getElementById("root")
);
{% code-block-end %}

Next, we’ll head to the header component to set up the login and logout buttons.

Login

We’re going to make changes to the Header component to implement login and logout. The 8base SDK exports a withAuth function to provide components with props for authorizing the user. Open the src/components/header/index.js and update the comments in the file with the snippets for implementing authentication:

The first step is adding the following imports to the top of the file:

{% code-block language="js" %}
import { withAuth } from "@8base/react-sdk";
import { Query } from "react-apollo";
import gql from "graphql-tag";
{% code-block-end %}

Next, we’re going to add a query to fetch the current user. This query will be run if the user has been authorized. The query will come where the TODO — 2 comment is, find the comment and update it with the query below:

{% code-block language="js" %}
const USER_QUERY = gql`
 query UserQuery {
   user {
     id
     email
     t
     firstName
   }
 }
`;
{% code-block-end %}

Next, we’ll wrap the Header export with the withAuth function, giving the component access to the auth state of the current user, and also a function to authenticate the user. Replace the current export within the file with the one below:

{% code-block language="js" %}
export default withAuth(Header);
{% code-block-end %}

We can now update the component to utilize the props provided by the withAuth function. Update the component to be similar to the snippet below:

{% code-block language="js" %}
const Header = ({ auth: { isAuthorized, authorize } }) => {
 ...
};
{% code-block-end %}

The isAuthorized prop holds the authentication state of the current user, can be used to tell if the user is authorized. The next is the authorized function, calling the function will trigger the 8base Auth0 login sequence to login an existing user or sign up a new one. Let's utilize these two variables within the component, locate the "TODO - 5" comment and update the content of the element with the auth className:

{% code-block language="js" %}
<div className="auth">
 {isAuthorized ? (
   <Query query={USER_QUERY}>
     {({ data, loading }) => (
       <div className="user">
         {!loading && <p>{data.user.firstName} </p>} |<LogoutButton />
       </div>
     )}
   </Query>
 ) : (
   <div>
     <button onClick={() => authorize()}>Login</button>
   </div>
 )}
</div>
{% code-block-end %}

In the snippet above, we check if the user isAuthorized, then we query the GraphQL endpoint for the user information and display it alongside a Logout button. If the user isn't authorized, we show a Login button that calls the authorize function when clicked.

Clicking the authorize button will begin the authentication sequence on 8base and Auth0. We need to create a new callback route that the sequence redirects to when authentication is complete. This route will handle updating the authorization state of the user and storing the user's token.

Authorization Callback

Let’s add a new route to the router.js file, update the file to be similar to the snippet below:

{% code-block language="js" %}
import React from "react";
import { Route } from "react-router-dom";
import Home from "./components/home";
import PostForm from "./components/submit";
import AuthCallback from "./authCallback";

const Routes = () => (
 <>
   <Route path="/submit" exact component={PostForm} />
   <Route path="/auth/callback" exact component={AuthCallback} />
   <Route path="/" exact component={Home} />
 </>
);
export default Routes;
{% code-block-end %}

First, we imported a component AuthCallback where the authentication magic happens and then created a new route /auth/callback for the component. This path can be anything you please, but it must be registered in your 8base authentication settings. If we head back to the page, we can see the Custom Domains section where you can enter callback URLs, allowed origins and logout URLs.

So your callback URL in the app must match the one registered on the authentication page. Now to the main task of the day, finishing the authentication flow in the AuthCallback component.

The component lies in the src/authCallback.js file. Open the file and make the following changes. So far the file exports an empty functional component. To fix that, we'll start by adding the following imports to the top of the file:

{% code-block language="js" %}
import { withAuth } from "@8base/react-sdk";
import { withApollo, compose } from "react-apollo";
import gql from "graphql-tag";
{% code-block-end %}

The usual suspects, the withAuth function for checking auth state and authorizing a user; the withApollo function injects the ApolloClient into the function as a single prop client and finally the compose function is useful when working with multiple enhancers. In our case, we'll be using the withAuth and withApollo functions and compose will be useful in bringing them together.

When this component is called with the user data, we’re expecting one of two things, login or signup, so we’re going to check for an existing user with the provided details. When none exists, we proceed to create one and login the user in. This means we’ll need a query and mutation string. Replace the "TODO - 2" comment with the following:

{% code-block language="js" %}
const CURRENT_USER = gql`
 query currentUser {
   user {
     id
   }
 }
`;
const SIGN_UP_USER = gql`
 mutation userSignUp($user: UserCreateInput!) {
   userSignUp(user: $user) {
     id
     email
   }
 }
`;
{% code-block-end %}

The CURRENT_USER query will be useful while checking for an existing user, and if none exists, we move to calling the SIGN_UP_USER mutation. Before we head into the component body, let's inject the props we'll need into the component by wrapping it with the imports above. Update the export line in the file with the following:

{% code-block language="js" %}
export default compose(
 withAuth,
 withApollo
)(AuthCallback);
{% code-block-end %}

The compose function takes several enhancers and combines them thus injecting the AuthCallback component with the props provided the enhancers. Now let's head into the body of the beast, update the body of the component to be similar to the snippet below:

{% code-block language="js" %}
const AuthCallback = ({ auth, history, client }) => {
 useEffect(() => {
   const completeAuth = async () => {
     const { idToken, email } = await auth.getAuthorizedData();
     try {
       /* Check if user exists, if not it'll return an error */
       await client.query({
         query: CURRENT_USER,
         context: { headers: { authorization: `Bearer ${idToken}` } }
       });
     } catch {
       /* If user is not found - sign them up */
       await client.mutate({
         mutation: SIGN_UP_USER,
         variables: { user: { email } },
         context: { headers: { authorization: `Bearer ${idToken}` } }
       });
     }
     /* After succesfull signup store token in local storage */
     await auth.setAuthState({ token: idToken });
     /* Redirect back to home page */
     history.replace("/");
   };
   completeAuth();
 }, []);
 return <p>Please wait...</p>;
};
{% code-block-end %}

Within the component, we utilize the {% code-line %}[useEffect](https://reactjs.org/docs/hooks-effect.html){% code-line-end %} hook. This is useful for running side effect in your component. It is similar to running a side effect within the componentDidMount and componentDidUpdate lifecycles; I'm sure we still remember those classics. Within the useEffect callback function, we have another function that completes the auth flow.

In the completeAuth function, we get the idToken and email returned from calling the {% code-line %}auth.getAuthorizedData(){% code-line-end %} method. These values will be useful for querying the current user/ signing them up.

The idToken will be used as the authorization token when making requests to the GraphQL endpoint. The query method of the client prop is used to run the CURRENT_USER query. When that fails, indicating that there's no existing user with that token, the execution is moved to the catch block where SIGN_UP_USER mutation is done using the email as a variable.

At the end of the completeAuth function, we update the authState with the idToken before redirecting to the /route.

You can head to http://localhost:3000 to try out the new authentication flow. Everything should go well.

After creating a new user, you can head over to the User table on the 8base data page to see if a new record has been created.

Authentication is hereby complete, but we’re yet to implement an exit strategy. Logout. Stay tuned.

The exit strategy — Logout

The user is happily logged in but might want to logout someday so let’s provide them with an option to logout when ready. Head over to the src/components/header/logout.js file and make the following changes. Open the file and add the following import to the top of the file:

{% code-block language="js" %}
import { withLogout } from "@8base/react-sdk";
{% code-block-end %}

The withLogout enhancer is similar to the withAuth but it provides a logout function rather than an authorize function.

Let’s wrap the component with the function. Update the export line with the following:

{% code-block language="js" %}
export default withLogout(LogoutButton);
{% code-block-end %}

And finally the body of the function should look like the following:

{% code-block language="js" %}
const LogoutButton = ({ logout }) => (
 <button onClick={() => logout()}>logout</button>
);
{% code-block-end %}

All put together, the LogoutButton component should look like the following:

{% code-block language="js" %}
import React from "react";
import { withLogout } from "@8base/react-sdk";

const LogoutButton = ({ logout }) => (
 <button onClick={() => logout()}>logout</button>
);

export default withLogout(LogoutButton);
{% code-block-end %}

Seeing as we’ve successfully completed authentication in the application, lets actually utilize this by ensuring that a user is authenticated before creating a post.

Securing post creation

We’re going to make use of the withAuth function here again to enhance the component with the authentication state of the user. Add the following import where you have the "TODO - 1" comment:

{% code-block language="js" %}
import { withAuth } from "@8base/react-sdk";
{% code-block-end %}

Then we’ll update the export line with the following:

{% code-block language="js" %}
export default withAuth(PostForm);
{% code-block-end %}

This injects the authentication state into the component; let’s head into the component body to make use of it. Make the following changes to the component:

{% code-block language="js" %}
const PostForm = ({ history, auth }) => {
 const { isAuthorized, authorize } = auth;
 const [post, setPost] = useState({
   ...
 });
 const onFormSubmit = async createPost => {
   ...
 };

 return !isAuthorized ? (
   <div style={loginAreaStyles}>
     <span>Before you continue please login</span>
     <button onClick={() => authorize()}>Login</button>
   </div>
 ) : (
   <Mutation mutation={POST_MUTATION}>
      ...
    </Mutation>
 );
};
{% code-block-end %}

In the component body, we make two changes. First, we get the isAuthorized variable and the authorized function from the auth object. In the return block of the component, we check if the user isAuthorized. If not, we return a view prompting the user to login using the authorize function.

Conclusion

In this part of the series, we’ve seen how we can easily implement authentication in our application with the help of 8base. We now have a working clone of HackerNews that displays a list of posts, allows post creation, upvoting posts and authenticating users. You can check out the final demo of the application hosted here.

Ready to try 8base?

Sign up for free or simply stay in touch.