Tutorial updated by Fredrick Emmanuel and Paul Bratslavsky
This tutorial is part of the « Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe » tutorial series. Table of contents
Note: The source code is available on GitHub here
For authentication calls, we will use our GraphQL endpoint to register new users and login to existing users.
Strapi will return a JWT token and a user object, the former can be used to verify transactions on the server while the user object will display the username in the header bar.
The Strapi documentation on authentication can be found here: Strapi Auth
Since we are going to be using GraphQL this is the documentation we will use.
Authentication with Next requires some additional consideration outside of a normal client-side authentication system because you have to be mindful of whether the code is being rendered on the client or the server.
We are going to keep in simple and handle it with cookie-js
npm library that will store our user data.
Than we can use the jwt token that we store to revalidate out login user.
Strapi's built-in JWT authentication will be used in this tutorial. This will allow you to easily register, manage, login, and check users' status in our application.
Your backend admin panel will provide a GUI for user management out of the box to view, edit, activate, and delete users if needed.
The premise of the JWT system is, a request will be sent to our GraphQL endpoint with a username, email, and password to register a new user.
1 mutation {
2 register(input: { username: "username", email: "email", password: "password" }) {
3 jwt
4 user {
5 username
6 email
7 }
8 }
9 }
This will return a user object and a JWT token in the user object will be stored in a cookie on the user's browser.
The same thing for logging in as a user with an email/password will return the same user object and JWT token if successful.
1 mutation {
2 login(input: { identifier: "email", password: "password" }) {
3 jwt
4 }
5 }
We can use the me
query with a JWT as an authorization header to revalidate the user if logged in. We can store the returned user data in a global context to share throughout the application.
Before creating the login and register forms, let's set up our global context and install js-cookies
package.
To store our token we will use a package named js-cookie using the code, Cookie.set(cookie).
Our token management will happen client-side only, however, your application could be developed differently in the real world.
Open the frontend directory in your terminal and install the following package:
npm install js-cookie
To store the user object, you will need to create a global context state inside of the application.
The context in React allows you to prevent prop-drilling multiple levels down and lets you grab and use the context state locally from a component.
This is a powerful construct in React, and definitely worth reading more info here: React Context.
Now, create a file named AppContext.js in the context folder. This file will store our user context.
1import { useState, createContext, useContext, useEffect } from "react";
2import Cookie from "js-cookie";
3import { gql } from "@apollo/client";
4import { client } from "@/pages/_app.js";
5
6const AppContext = createContext();
7
8export const AppProvider = ({ children }) => {
9 const [user, setUser] = useState(null);
10
11 useEffect(() => {
12 const fetchData = async () => {
13 const userData = await getUser();
14 setUser(userData);
15 };
16 fetchData();
17 }, []);
18
19 return (
20 <AppContext.Provider
21 value={{
22 user,
23 setUser,
24 }}
25 >
26 {children}
27 </AppContext.Provider>
28 );
29};
30
31const getUser = async () => {
32 const token = Cookie.get("token");
33 if (!token) return null;
34 const { data } = await client.query({
35 query: gql`
36 query {
37 me {
38 id
39 email
40 username
41 }
42 }
43 `,
44 context: {
45 headers: {
46 Authorization: `Bearer ${token}`,
47 },
48 },
49 });
50 return data.me;
51};
52
53export const useAppContext = () => {
54 const context = useContext(AppContext);
55 if (context === undefined)
56 throw new Error("useAppContext must be used within an AppProvider");
57 return context;
58};
Now let's replace our code found the _app.js file include our AppContext
provider.
1import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
2import { AppProvider } from "@/context/AppContext";
3import "@/styles/globals.css";
4import Layout from "@/components/Layout";
5
6const API_URL = process.env.STRAPI_URL || "http://localhost:1337";
7
8export const client = new ApolloClient({
9 uri: `${API_URL}/graphql`,
10 cache: new InMemoryCache(),
11 defaultOptions: {
12 mutate: {
13 errorPolicy: "all",
14 },
15 query: {
16 errorPolicy: "all",
17 },
18 },
19});
20
21export default function App({ Component, pageProps }) {
22 return (
23 <ApolloProvider client={client}>
24 <AppProvider>
25 <Layout>
26 <Component {...pageProps} />
27 </Layout>
28 </AppProvider>
29 </ApolloProvider>
30 );
31}
Then update your header navbar as well in the Layout.jsx to display our username and a logout button if a user is signed in:
1import { useRouter } from "next/router";
2import { useAppContext } from "@/context/AppContext";
3import Cookie from "js-cookie";
4
5import Head from "next/head";
6import Link from "next/link";
7
8function Navigation() {
9 const { user, setUser } = useAppContext();
10 const router = useRouter();
11
12 function handleLogout() {
13 setUser(null);
14 Cookie.remove("token");
15 router.push("/");
16 }
17
18 return (
19 <header className="bg-green-800">
20 <nav className="flex justify-between p-6 px-4">
21 <div className="flex justify-between items-center w-full mx-16">
22 <div className="xl:w-1/3">
23 <Link
24 className="block text-2xl max-w-max text-white font-medium"
25 href="/"
26 >
27 Food Order App
28 </Link>
29 </div>
30
31 <div className="xl:block xl:w-1/3">
32 <div className="flex items-center justify-end">
33 <Link
34 className="text-gray-50 hover:text-yellow-200 font-bold"
35 href="/"
36 >
37 Home
38 </Link>
39
40 <div className="hxl:block">
41 {user ? (
42 <div className="flex items-center justify-end">
43 <span className="inline-block py-2 px-4 mr-2 leading-5 text-gray-50 hover:text-gray-100 bg-transparent font-medium rounded-md">
44 {user.username}
45 </span>
46 <button
47 className="inline-block py-2 px-4 text-sm leading-5 text-green-50 bg-green-500 hover:bg-green-600 font-medium focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 rounded-md"
48 onClick={handleLogout}
49 >
50 Log Out
51 </button>
52 </div>
53 ) : (
54 <div className="flex items-center justify-end">
55 <Link
56 className="inline-block py-2 px-4 mr-2 leading-5 text-gray-50 hover:text-yellow-200 font-bold bg-transparent rounded-md"
57 href="/login"
58 >
59 Log In
60 </Link>
61 <Link
62 className="inline-block py-2 px-4 text-sm leading-5 text-green-50 bg-green-600 hover:bg-green-700 font-medium focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 rounded-md"
63 href="/register"
64 >
65 Sign Up
66 </Link>
67 </div>
68 )}
69 </div>
70 </div>
71 </div>
72 </div>
73 </nav>
74 </header>
75 );
76}
77
78export default function Layout(props) {
79 const title = "Welcome to Next JS";
80
81 return (
82 <div>
83 <Head>
84 <title>{title}</title>
85 <meta charSet="utf-8" />
86 <meta name="viewport" content="initial-scale=1.0, width=device-width" />
87 </Head>
88 <Navigation />
89 <div className="container mx-auto px-4">{props.children}</div>
90 </div>
91 );
92}
Before we can test everything we need to add our signin and register logic. So first inside the components folder let's create a file called Form.jsx and add the following code.
1import React from "react";
2
3export default function Form({
4 title,
5 buttonText,
6 formData,
7 setFormData,
8 callback,
9 error,
10}) {
11 return (
12 <section className="py-24 md:py-32 bg-white">
13 <div className="container px-4 mx-auto">
14 <div className="max-w-sm mx-auto">
15 <div className="mb-6 text-center">
16 <h3 className="mb-4 text-2xl md:text-3xl font-bold">{title}</h3>
17 </div>
18 <form onSubmit={callback}>
19 <div className="mb-6">
20 <label
21 className="block mb-2 text-coolGray-800 font-medium"
22 htmlFor="email"
23 >
24 Email
25 </label>
26 <input
27 id="email"
28 className="appearance-none block w-full p-3 leading-5 text-gray-900 border border-gray-200 rounded-lg shadow-md placeholder-text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
29 type="email"
30 name="email"
31 placeholder="Enter your email"
32 value={formData.email}
33 onChange={(e) =>
34 setFormData({ ...formData, email: e.target.value })
35 }
36 />
37 </div>
38 <div className="mb-4">
39 <label
40 className="block mb-2 text-coolGray-800 font-medium"
41 htmlFor="password"
42 >
43 Password
44 </label>
45 <input
46 id="password"
47 className="appearance-none block w-full p-3 leading-5 text-gray-900 border border-gray-200 rounded-lg shadow-md placeholder-text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
48 type="password"
49 name="password"
50 placeholder="************"
51 value={formData.password}
52 onChange={(e) =>
53 setFormData({ ...formData, password: e.target.value })
54 }
55 />
56 </div>
57 {error && (
58 <div className="text-center my-4 text-red-600">
59 Error: {error.message}
60 </div>
61 )}
62 <button
63 className="inline-block py-3 px-7 mb-6 w-full text-base text-green-50 font-medium text-center leading-6 bg-green-500 hover:bg-green-600 focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 rounded-md shadow-sm"
64 type="submit"
65 >
66 {buttonText}
67 </button>
68 </form>
69 </div>
70 </div>
71 </section>
72 );
73}
Now lets add the logic to handle our registration.
Add the following code inside the /frontend/pages/register.js
file.
1import { useState } from "react";
2import { useRouter } from "next/router";
3import { useAppContext } from "@/context/AppContext";
4import { gql, useMutation } from "@apollo/client";
5import Cookie from "js-cookie";
6
7import Form from "@/components/Form";
8import Loader from "@/components/Loader";
9
10const REGISTER_MUTATION = gql`
11 mutation Register($username: String!, $email: String!, $password: String!) {
12 register(
13 input: { username: $username, email: $email, password: $password }
14 ) {
15 jwt
16 user {
17 username
18 email
19 }
20 }
21 }
22`;
23
24export default function RegisterRoute() {
25 const { setUser } = useAppContext();
26 const router = useRouter();
27
28 const [formData, setFormData] = useState({ email: "", password: "" });
29 const [registerMutation, { loading, error }] = useMutation(REGISTER_MUTATION);
30
31 const handleRegister = async () => {
32 const { email, password } = formData;
33 const { data } = await registerMutation({
34 variables: { username: email, email: email, password },
35 });
36 if (data?.register.user) {
37 setUser(data.register.user);
38 router.push("/");
39 Cookie.set("token", data.register.jwt);
40 }
41 };
42
43 if (loading) return <Loader />;
44
45 return (
46 <Form
47 title="Sign Up"
48 buttonText="Sign Up"
49 formData={formData}
50 setFormData={setFormData}
51 callback={handleRegister}
52 error={error}
53 />
54 );
55}
You should get an output similar to the one shown below;
Similar to the sign-up page, the sign-in page will use a token to log the user in and set the cookie.
Path: frontend/pages/login.js
1import { useState } from "react";
2import { useRouter } from "next/router";
3import { useAppContext } from "@/context/AppContext";
4import { gql, useMutation } from "@apollo/client";
5import Cookie from "js-cookie";
6
7import Form from "@/components/Form";
8import Loader from "@/components/Loader";
9
10const LOGIN_MUTATION = gql`
11 mutation Login($identifier: String!, $password: String!) {
12 login(input: { identifier: $identifier, password: $password }) {
13 jwt
14 user {
15 username
16 email
17 }
18 }
19 }
20`;
21
22export default function LoginRoute() {
23 const { setUser } = useAppContext();
24 const router = useRouter();
25
26 const [formData, setFormData] = useState({ email: "", password: "" });
27 const [loginMutation, { loading, error }] = useMutation(LOGIN_MUTATION);
28
29 const handleLogin = async () => {
30 const { email, password } = formData;
31 const { data } = await loginMutation({
32 variables: { identifier: email, password },
33 });
34 if (data?.login.user) {
35 setUser(data.login.user);
36 Cookie.set("token", data.login.jwt);
37 router.push("/");
38 }
39 };
40
41 if (loading) return <Loader />;
42
43 return (
44 <Form
45 title="Login"
46 buttonText="Login"
47 formData={formData}
48 setFormData={setFormData}
49 callback={handleLogin}
50 error={error}
51 />
52 );
53}
You should get an output similar to the one shown below;
Your user registration, login, and logout should be set correctly!
🛒 In the next section, you will learn how to create a full-featured shopping cart: https://strapi.io/blog/nextjs-react-hooks-strapi-shopping-cart-5
Ryan is an active member of the Strapi community and he's been contributing at a very early stage by writing awesome tutorial series to help fellow Strapier grow and learn.