Creating the Authentication Frontend

Learn how to fetch user details and send requests to the Strapi backend.

In this lesson, we’ll develop a frontend that’s capable of fetching users’ details and sending them to the Strapi API. The frontend will also perform actions based on the response it gets from the Strapi API.

For our application, we want to allow users to save recipes to their favorites so they can look up recipes that they want quickly and without having to search for them again. To associate users with their saved recipes, we’ll need to authenticate them and fetch their details.

Because we have the backend set up already, we need to create a front-end application capable of consuming the back-end APIs and authenticating users.

Creating the authentication context

Before we can implement our application’s authentication functionality, we need to set up a few things. We’ll need a few helpers, as well as an authentication context and context provider. Let’s get started with those implementations.

The helpers.js and constants.js files will go in the /utils folder. Both of these files contain a few utilities and helper functions that will be used for multiple pages. We’re keeping them in a separate file to keep the code modular and to avoid redundancy.

Press + to interact
helpers.js
constants.js
import { AUTH_TOKEN } from "./constants";
export const getToken = () => {
if (typeof window !== 'undefined') return localStorage.getItem(AUTH_TOKEN);
return null;
};
export const setToken = (token) => {
if (token) {
if (typeof window !== 'undefined') localStorage.setItem(AUTH_TOKEN, token);
}
};
export const removeToken = () => {
if (typeof window !== 'undefined') localStorage.removeItem(AUTH_TOKEN);
};
export const checkLogin = () => {
if (getToken) return true;
return false;
}

Here, we’ve defined a few functions that will help with our authentication. We use the getToken() and the setToken() functions to get and set the authentication token for the current user. The removeToken() function removes the token when the user logs out. Finally, the checkLogin() function checks if a user is logged in or not.

Note: The if (typeof window !== 'undefined') condition is used because Next.js pre-renders content, and at the point in time when we’re pre-rendering, the localStorage of the browser can’t be accessed because the code isn’t running on the browser. So, we have to add this condition to ensure that it does gets the token after the page is loaded on the browser.

Once this is done, we can start setting up the AuthContext to store the details of the logged-in user. For this, we’ll create a directory named context and use the AuthContext.js file to create a React context.

Press + to interact
import { createContext, useContext } from "react";
export const AuthContext = createContext({
user: undefined,
isLoading: false,
setUser: () => {},
});
export const useAuthContext = () => useContext(AuthContext);

In the AuthContext.js file, we create a React context that’s accessible to the complete application. The context includes the user attribute, which saves the user’s details. The isLoading boolean checks if the authentication process is taking place. The setUser function allows us to set the user when the user logs in.

There’s just one more thing to do before we can implement our authentication functionality. We need to make an AuthProvider component that provides the authentication context to the whole application. This is what the AuthProvider component looks like:

Press + to interact
import React, { useState } from "react";
import { AuthContext } from "../context/AuthContext";
import { API, BEARER } from "../utils/constants";
import { useEffect } from "react";
import { getToken } from "../utils/helpers";
const AuthProvider = ({ children }) => {
const [userData, setUserData] = useState();
const [isLoading, setIsLoading] = useState(false);
const authToken = getToken();
const fetchLoggedInUser = async (token) => {
setIsLoading(true);
try {
const response = await fetch(`${API}/users/me`, {
headers: { Authorization: `${BEARER} ${token}` },
});
const data = await response.json();
setUserData(data);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
const handleUser = (user) => {
setUserData(user);
};
useEffect(() => {
if (authToken) {
fetchLoggedInUser(authToken);
}
}, [authToken]);
return (
<AuthContext.Provider
value={{ user: userData, setUser: handleUser, isLoading }}
>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;

We send an API call to /users/me. This is the default route to get the current authorized user. For this, we need to pass the authentication token in the headers of the GET request in line 17.

The AuthProvider component is the component where we have the logic of authentication. In lines 13–27, we have the fetchedLoggedInUser function that fetches the user currently logged in and sets the user.

Finally, we have to provide the context to the application using this AuthProvider component. For that, we need to update our _app.js file, which is the first access point of our Next.js application. Our _app.js file now looks like this:

Press + to interact
import '../styles/globals.css'
import AuthProvider from '../components/AuthProvider'
function MyApp({ Component, pageProps }) {
return <AuthProvider><Component {...pageProps} /></AuthProvider>
}
export default MyApp

AuthProvider is the parent component of our application. This way, all the pages of our application have access to the current user. Now, we’re ready to implement our authentication functionality.

Registration

Let’s learn how to register new users to our application. Because we’ve already created the backend to handle the requests, we need to create a frontend for the application that the user can interact with. First, let’s take a look at what our markup looks like:

Press + to interact
register.js
AuthHeader.js
import Head from 'next/head';
import React, {useState} from 'react'
import styles from '../../styles/Home.module.css'
import AuthHeader from '../../components/AuthHeader';
import { Button, TextField } from '@mui/material';
const Register = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const handleRegister = () => {
console.log('User email:' + email + 'username:' + username)
}
return (
<>
<Head>
<title>Register</title>
</Head>
<AuthHeader page='register' />
<main className={styles.main}>
<h3>Welcome back, please login to your account</h3>
<div className={styles.searchbarDiv}>
<TextField fullWidth
placeholder='Username'
onChange={(e)=> {setUsername(e.target.value)}}
/> <br /> <br />
<TextField fullWidth
placeholder='Email'
onChange={(e) => {setEmail(e.target.value)}}
/> <br /> <br />
<TextField fullWidth
placeholder='Password'
type='password'
onChange={(e) => {setPassword(e.target.value)}}
/> <br /> <br />
</div>
<Button variant='contained' onClick={handleRegister}>Register</Button>
</main>
</>
)
}
export default Register;

We’ve created a few text fields that will take the user’s inputs and save them using the useState hook that React provides. The handleRegister function will send the registration request to the Strapi backend and set the user’s authentication token in the browser’s local storage. Let’s see how this is implemented:

Press + to interact
import Head from 'next/head';
import React, {useState} from 'react'
import styles from '../../styles/Home.module.css'
import AuthHeader from '../../components/AuthHeader';
import { Button, CircularProgress, TextField } from '@mui/material';
import { useRouter } from 'next/router';
import { useAuthContext } from '../../context/AuthContext';
import { API } from '../../utils/constants';
import { setToken } from '../../utils/helpers';
const Register = () => {
const router = useRouter();
const { setUser } = useAuthContext();
const [isLoading, setIsLoading] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const handleRegister = async () => {
const values = {
username: username,
password: password,
email: email,
}
setIsLoading(true);
try {
const response = await fetch(`${API}/auth/local/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
const data = await response.json();
if (data?.error) {
throw data?.error;
} else {
// set the token
setToken(data.jwt);
// set the user
setUser(data.user);
router.push('/')
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<>
<Head>
<title>Register</title>
</Head>
<AuthHeader page='register' />
<main className={styles.main}>
<h3>Welcome back, please login to your account</h3>
<div className={styles.searchbarDiv}>
<TextField fullWidth
placeholder='Username'
onChange={(e)=> {setUsername(e.target.value)}}
/> <br /> <br />
<TextField fullWidth
placeholder='Email'
onChange={(e) => {setEmail(e.target.value)}}
/> <br /> <br />
<TextField fullWidth
placeholder='Password'
type='password'
onChange={(e) => {setPassword(e.target.value)}}
/> <br /> <br />
</div>
<Button disabled={isLoading} variant='contained' onClick={handleRegister}>Register</Button>
{
isLoading ? <CircularProgress /> : null
}
</main>
</>
)
}
export default Register;

Let’s discuss what the handleRegister function does for us. First, we create the values object, which contains the user’s details that we have to send to the Strapi API. We then send the POST request to the /auth/local/register endpoint that Strapi provides by default for our applications.

Note: We send the body of the POST request after using the JSON.stringify function. We have to do this because Strapi stores the keys and values in the from of a string.

After that, we check if the API call returns an error. If it doesn’t, we set the authToken by using the setToken function we created in our helpers.js file and set the user by using the setUser method of the authentication context. And that’s all there is for registering a new user.

Login

We have successfully added the functionality for registering users. Now, we need to ensure that the user can also log back into the application.

Let’s take a look at the markup of the login page:

Press + to interact
import Head from 'next/head';
import React from 'react'
import styles from '../../styles/Home.module.css'
import AuthHeader from '../../components/AuthHeader';
import { Button, TextField } from '@mui/material';
const Login = () => {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const loginHandler = async () => {
console.log("logging in user: " + email)
};
return (
<>
<Head>
<title>Login</title>
</Head>
<AuthHeader page='login' />
<main className={styles.main}>
<h3>Welcome back, please login to your account</h3>
<div className={styles.searchbarDiv}>
<TextField fullWidth
placeholder='Email'
onChange={(e) => setEmail(e.target.value)}
/> <br /> <br />
<TextField fullWidth
placeholder='password'
type='password'
onChange={(e) => setPassword(e.target.value)}
/> <br /> <br />
</div>
<Button variant='contained' onClick={loginHandler}>Login</Button>
</main>
</>
)
}
export default Login;

We create a couple of text fields to take the user’s email and password. Once this is done, we can add logic to the loginHandler function that allows us to log in the user. Let’s take a look at the implementation of the function:

Press + to interact
import Head from 'next/head';
import React from 'react'
import styles from '../../styles/Home.module.css'
import AuthHeader from '../../components/AuthHeader';
import { Button, TextField } from '@mui/material';
import { useRouter } from 'next/router';
import { useAuthContext } from '../../context/AuthContext';
import { API } from '../../utils/constants';
import { setToken } from '../../utils/helpers';
const Login = () => {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [isLoading, setIsLoading] = React.useState(false);
const router = useRouter();
const { setUser } = useAuthContext();
const loginHandler = async () => {
setIsLoading(true);
try {
const values = {
identifier: email,
password: password,
};
const response = await fetch(`${API}/auth/local`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
});
const data = await response.json();
if (data?.error) {
throw data?.error;
} else {
// set the token
setToken(data.jwt);
// set the user
setUser(data.user);
router.push('/')
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<>
<Head>
<title>Login</title>
</Head>
<AuthHeader page='login' />
<main className={styles.main}>
<h3>Welcome back, please login to your account</h3>
<div className={styles.searchbarDiv}>
<TextField fullWidth
placeholder='Email'
onChange={(e) => setEmail(e.target.value)}
/> <br /> <br />
<TextField fullWidth
placeholder='password'
type='password'
onChange={(e) => setPassword(e.target.value)}
/> <br /> <br />
</div>
<Button variant='contained' onClick={loginHandler}>Login</Button>
{
isLoading ? <CircularProgress /> : null
}
</main>
</>
)
}
export default Login;

The functionality of the loginHandler function is the same as the handleRegister function we created to register the users. The only difference is the API endpoint that it posts to. The endpoint for the loginHandler function is /auth/local.

With that, we have completed the authentication functionality. We can now take a look at the complete running application.

Putting it all together

Here is the complete application that we’ve built up until now.

You can launch the application by pressing the “Run” button in the code widget below. The Strapi backend will be started automatically. It’s accessible on the 3000 port of your Educative URL; to view the back-end admin panel, append :3000 to the end of the URL after opening it in a new tab.

To start the Next.js frontend, open a new terminal and use the command cd usercode/frontend && npm run build && npm start to run in a production environment, or cd usercode/frontend && npm run dev to open in the development environment.

Note: A Strapi administrator user has already been created for this course. The details of the user are as follows:

  • Email: jane.doe@email.com

  • Password: Password123

[build]
  command = "npm run build"
  publish = ".next"

[[plugins]]
  package = "@netlify/plugin-nextjs"

# Comment
The application with authentication