Creating the Authentication Frontend
Learn how to fetch user details and send requests to the Strapi backend.
We'll cover the following
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.
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, thelocalStorage
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.
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:
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.Providervalue={{ 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:
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:
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 fullWidthplaceholder='Username'onChange={(e)=> {setUsername(e.target.value)}}/> <br /> <br /><TextField fullWidthplaceholder='Email'onChange={(e) => {setEmail(e.target.value)}}/> <br /> <br /><TextField fullWidthplaceholder='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:
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 tokensetToken(data.jwt);// set the usersetUser(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 fullWidthplaceholder='Username'onChange={(e)=> {setUsername(e.target.value)}}/> <br /> <br /><TextField fullWidthplaceholder='Email'onChange={(e) => {setEmail(e.target.value)}}/> <br /> <br /><TextField fullWidthplaceholder='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 theJSON.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:
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 fullWidthplaceholder='Email'onChange={(e) => setEmail(e.target.value)}/> <br /> <br /><TextField fullWidthplaceholder='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:
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 tokensetToken(data.jwt);// set the usersetUser(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 fullWidthplaceholder='Email'onChange={(e) => setEmail(e.target.value)}/> <br /> <br /><TextField fullWidthplaceholder='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