Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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.
For authentication calls, we will make a POST request to the respective register/login endpoints to register new users and log in 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 out username in the header bar.
The Strapi documentation on authentication can be found here: https://strapi.io/documentation/3.0.0-beta.x/guides/auth-request.html#authenticated-request
Authentication with Next requires some additional consideration outside of a normal client-side authentication system because we have to be mindful of whether the code is being rendered on the client or the server. Because of the different constructs between a server and client, client-only code should be prevented from running on the server.
One thing to keep in mind is that cookies are sent to the server in the request headers, so using something like next-cookies to universally retrieve the cookie value would work well. I'm not taking this approach in the tutorial, I will use the componentDidMount lifecycle inside the _app.js file to grab my cookie. componentDidMount only fires client-side ensuring that we will have access to the cookie.
A simple check for the window object will prevent client-only code from being executed on the server.
This code would only run on the client:
1
2
3
if (typeof window === "undefined") {
return;
}
Let's install the following packages:
axios 0.19.2 js-cookie v2.2.1 isomorphic-fetch v2.2.1
1
$ yarn add axios@0.19.2 js-cookie@2.2.1 isomorphic-fetch@2.2.1
For authentication, we are going to use Strapi's built-in JWT authentication. This will allow us to easily register, manage, log in, 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.
User Management in Strapi:
The premise of the JWT system is, we will make a POST request to http://localhost:1337/auth/local/register
with our username, email, and password to register a new user. This will return a user object and a JWT token we will store in a cookie on user's browser.
The same thing for logging in a user, a POST to http://localhost:1337/auth/local
with an email/password will return the same user object and JWT token if successful.
Strapi will also expose a http://localhost:1337/users/me
route that we will make a GET request to, passing our token as an authorization header. This will return just the user object for user verification. We will place this user object in a global context to share throughout the application.
Here is a sample response for the user object that we will retrieve the username/userid from for our orders:
As you can see Strapi will tell us certain user properties such as if the user is confirmed, how they signed up (local/other login service like Discord etc.), their email, username, and id among others.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"id":1,
"username":"ryan",
"email":"ryan@gmail.com",
"provider":"local",
"confirmed":true,
"blocked":null,
"role":
{
"id":1,
"name":"Authenticated",
"description":"Default role given to authenticated
user.",
"type":"authenticated"
},
"created_at":"2020-04-17T01:23:49.285Z",
"updated_at":"2020-04-17T01:23:49.292Z"
}
Let's create a file for our common authentication methods /lib/auth.js
1
2
$ cd lib
$ touch auth.js
Inside our auth.js
file we will create helper functions to log in, register and log out our users. An example of a Higher Order Component called withAuthSync is provided as well. This is used to sync logouts across multiple logged in tabs. However it's not used in the tutorial, only provided for example if need. A HOC is a component that returns a component, this allows us to increase the reusability of our components across our application, a common code pattern in React.
Read more here about HOCs
/lib/auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/* /lib/auth.js */
import { useEffect } from "react";
import Router from "next/router";
import Cookie from "js-cookie";
import axios from "axios";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337";
//register a new user
export const registerUser = (username, email, password) => {
//prevent function from being ran on the server
if (typeof window === "undefined") {
return;
}
return new Promise((resolve, reject) => {
axios
.post(`${API_URL}/auth/local/register`, { username, email, password })
.then((res) => {
//set token response from Strapi for server validation
Cookie.set("token", res.data.jwt);
//resolve the promise to set loading to false in SignUp form
resolve(res);
//redirect back to home page for restaurance selection
Router.push("/");
})
.catch((error) => {
//reject the promise and pass the error object back to the form
reject(error);
});
});
};
export const login = (identifier, password) => {
//prevent function from being ran on the server
if (typeof window === "undefined") {
return;
}
return new Promise((resolve, reject) => {
axios
.post(`${API_URL}/auth/local/`, { identifier, password })
.then((res) => {
//set token response from Strapi for server validation
Cookie.set("token", res.data.jwt);
//resolve the promise to set loading to false in SignUp form
resolve(res);
//redirect back to home page for restaurance selection
Router.push("/");
})
.catch((error) => {
//reject the promise and pass the error object back to the form
reject(error);
});
});
};
export const logout = () => {
//remove token and user cookie
Cookie.remove("token");
delete window.__user;
// sync logout between multiple windows
window.localStorage.setItem("logout", Date.now());
//redirect to the home page
Router.push("/");
};
//Higher Order Component to wrap our pages and logout simultaneously logged in tabs
// THIS IS NOT USED in the tutorial, only provided if you wanted to implement
export const withAuthSync = (Component) => {
const Wrapper = (props) => {
const syncLogout = (event) => {
if (event.key === "logout") {
Router.push("/login");
}
};
useEffect(() => {
window.addEventListener("storage", syncLogout);
return () => {
window.removeEventListener("storage", syncLogout);
window.localStorage.removeItem("logout");
};
}, []);
return <Component {...props} />;
};
if (Component.getInitialProps) {
Wrapper.getInitialProps = Component.getInitialProps;
}
return Wrapper;
};
For authentication we are going to be utilizing a JWT token returned from Strapi that is stored as a cookie on the client's browser.
Nothing related to this food tutorial...
Most of the time, progressive web apps store a JSON Web Token (JWT) in the local storage. That works pretty well if you don't have server-side rendering (tokens are also stored as a cookie).
Since Next.js renders code on the server, we will need to store our token returned from Strapi as a cookie in the browser, since localStorage is not accessible on the server. This allows the client to set the cookie with a package like js-cookie using Cookie.set(cookie).
Our token management will happen client-side only, however, your application could be developed differently in the real world.
In order to store our user object, we will need to create a global context state inside of our application. The context in React allows us to prevent prop-drilling multiple levels down and lets us grab and use the context state locally from a component.
This is a powerful construct in React, and definitely worth reading more on here: https://reactjs.org/docs/context.html
Let's start by creating a folder where we will store our default context state.
1
2
3
4
$ cd ..
$ mkdir context
$ cd context
$ touch AppContext.js
path: /context/AppContext.js
1
2
3
4
5
6
7
8
/* /context/AppContext.js */
import React from "react";
// create auth context with default value
// set backup default for isAuthenticated if none is provided in Provider
const AppContext = React.createContext({ isAuthenticated: false });
export default AppContext;
Now, let's add state to our _app.js file to share across the application.
You could also locate this in any container component in your application.
path: /pages/_app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* _app.js */
import React from "react";
import App from "next/app";
import Head from "next/head";
import Cookie from "js-cookie";
import fetch from "isomorphic-fetch";
import Layout from "../components/Layout";
import AppContext from "../context/AppContext";
import withData from "../lib/apollo";
class MyApp extends App {
state = {
user: null,
};
componentDidMount() {
// grab token value from cookie
const token = Cookie.get("token");
if (token) {
// authenticate the token on the server and place set user object
fetch(`${process.env.NEXT_PUBLIC_API_URL}users/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then(async (res) => {
// if res comes back not valid, token is not valid
// delete the token and log the user out on client
if (!res.ok) {
Cookie.remove("token");
this.setState({ user: null });
return null;
}
const user = await res.json();
this.setUser(user);
});
}
}
setUser = (user) => {
this.setState({ user });
};
render() {
const { Component, pageProps } = this.props;
return (
<AppContext.Provider
value={{
user: this.state.user,
isAuthenticated: !!this.state.user,
setUser: this.setUser,
}}
>
<Head>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossOrigin="anonymous"
/>
</Head>
<Layout>
<Component {...pageProps} />
</Layout>
</AppContext.Provider>
);
}
}
export default withData(MyApp);
Let's update our header bar as well to display our username and a logout button if a user is signed in:
/path/components/Layout.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/* /components/Layout.js */
import React, { useContext } from "react";
import Head from "next/head";
import Link from "next/link";
import { Container, Nav, NavItem } from "reactstrap";
import { logout } from "../lib/auth";
import AppContext from "../context/AppContext";
const Layout = (props) => {
const title = "Welcome to Nextjs";
const { user, setUser } = useContext(AppContext);
return (
<div>
<Head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossOrigin="anonymous"
/>
<script src="https://js.stripe.com/v3" />
</Head>
<header>
<style jsx>
{`
a {
color: white;
}
h5 {
color: white;
padding-top: 11px;
}
`}
</style>
<Nav className="navbar navbar-dark bg-dark">
<NavItem>
<Link href="/">
<a className="navbar-brand">Home</a>
</Link>
</NavItem>
<NavItem className="ml-auto">
{user ? (
<h5>{user.username}</h5>
) : (
<Link href="/register">
<a className="nav-link"> Sign up</a>
</Link>
)}
</NavItem>
<NavItem>
{user ? (
<Link href="/">
<a
className="nav-link"
onClick={() => {
logout();
setUser(null);
}}
>
Logout
</a>
</Link>
) : (
<Link href="/login">
<a className="nav-link">Sign in</a>
</Link>
)}
</NavItem>
</Nav>
</header>
<Container>{props.children}</Container>
</div>
);
};
export default Layout;
To register a user we will pass a username, email, and password through a POST request to the route /auth/local/register
. This will register a user in Strapi and log the user in. Inside of our signup page, we will call the registerUser function we created inside of our auth.js file to register the user, then set the JWT cookie inside the browser.
Request body options for auth:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NewUsers-PermissionsUser{
username* string
minLength: 3
email* string
minLength: 6
provider string
password string
minLength: 6
resetPasswordToken string
confirmed boolean
default: false
blocked boolean
default: false
role string
}
Swagger docs for the register endpoint:
Path: /frontend/pages/register.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/* /pages/register.js */
import React, { useState, useContext } from "react";
import {
Container,
Row,
Col,
Button,
Form,
FormGroup,
Label,
Input,
} from "reactstrap";
import { registerUser } from "../lib/auth";
import AppContext from "../context/AppContext";
const Register = () => {
const [data, setData] = useState({ email: "", username: "", password: "" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState({});
const appContext = useContext(AppContext);
return (
<Container>
<Row>
<Col sm="12" md={{ size: 5, offset: 3 }}>
<div className="paper">
<div className="header">
<img src="https://strapi.io/assets/images/logo.png" />
</div>
<section className="wrapper">
{Object.entries(error).length !== 0 &&
error.constructor === Object &&
error.message.map((error) => {
return (
<div
key={error.messages[0].id}
style={{ marginBottom: 10 }}
>
<small style={{ color: "red" }}>
{error.messages[0].message}
</small>
</div>
);
})}
<Form>
<fieldset disabled={loading}>
<FormGroup>
<Label>Username:</Label>
<Input
disabled={loading}
onChange={(e) =>
setData({ ...data, username: e.target.value })
}
value={data.username}
type="text"
name="username"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup>
<Label>Email:</Label>
<Input
onChange={(e) =>
setData({ ...data, email: e.target.value })
}
value={data.email}
type="email"
name="email"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup style={{ marginBottom: 30 }}>
<Label>Password:</Label>
<Input
onChange={(e) =>
setData({ ...data, password: e.target.value })
}
value={data.password}
type="password"
name="password"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup>
<span>
<a href="">
<small>Forgot Password?</small>
</a>
</span>
<Button
style={{ float: "right", width: 120 }}
color="primary"
disabled={loading}
onClick={() => {
setLoading(true);
registerUser(data.username, data.email, data.password)
.then((res) => {
// set authed user in global context object
appContext.setUser(res.data.user);
setLoading(false);
})
.catch((error) => {
setError(error.response.data);
setLoading(false);
});
}}
>
{loading ? "Loading.." : "Submit"}
</Button>
</FormGroup>
</fieldset>
</Form>
</section>
</div>
</Col>
</Row>
<style jsx>
{`
.paper {
border: 1px solid lightgray;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 2px 1px -1px rgba(0, 0, 0, 0.12);
border-radius: 6px;
margin-top: 90px;
}
.notification {
color: #ab003c;
}
.header {
width: 100%;
height: 120px;
background-color: #2196f3;
margin-bottom: 30px;
border-radius-top: 6px;
}
.wrapper {
padding: 10px 30px 20px 30px !important;
}
a {
color: blue !important;
}
img {
margin: 15px 30px 10px 50px;
}
`}
</style>
</Container>
);
};
export default Register;
Similar to our login page, the sign-in page will use a token to log the user in and set the cookie.
Path: /frontend/pages/login.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/* /pages/login.js */
import React, { useState, useEffect, useContext } from "react";
import { useRouter } from "next/router";
import {
Container,
Row,
Col,
Button,
Form,
FormGroup,
Label,
Input,
} from "reactstrap";
import { login } from "../lib/auth";
import AppContext from "../context/AppContext";
function Login(props) {
const [data, updateData] = useState({ identifier: "", password: "" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const router = useRouter();
const appContext = useContext(AppContext);
useEffect(() => {
if (appContext.isAuthenticated) {
router.push("/"); // redirect if you're already logged in
}
}, []);
function onChange(event) {
updateData({ ...data, [event.target.name]: event.target.value });
}
return (
<Container>
<Row>
<Col sm="12" md={{ size: 5, offset: 3 }}>
<div className="paper">
<div className="header">
<img src="https://strapi.io/assets/images/logo.png" />
</div>
<section className="wrapper">
{Object.entries(error).length !== 0 &&
error.constructor === Object &&
error.message.map((error) => {
return (
<div
key={error.messages[0].id}
style={{ marginBottom: 10 }}
>
<small style={{ color: "red" }}>
{error.messages[0].message}
</small>
</div>
);
})}
<Form>
<fieldset disabled={loading}>
<FormGroup>
<Label>Email:</Label>
<Input
onChange={(event) => onChange(event)}
name="identifier"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup style={{ marginBottom: 30 }}>
<Label>Password:</Label>
<Input
onChange={(event) => onChange(event)}
type="password"
name="password"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup>
<span>
<a href="">
<small>Forgot Password?</small>
</a>
</span>
<Button
style={{ float: "right", width: 120 }}
color="primary"
onClick={() => {
setLoading(true);
login(data.identifier, data.password)
.then((res) => {
setLoading(false);
// set authed User in global context to update header/app state
appContext.setUser(res.data.user);
})
.catch((error) => {
setError(error.response.data);
setLoading(false);
});
}}
>
{loading ? "Loading... " : "Submit"}
</Button>
</FormGroup>
</fieldset>
</Form>
</section>
</div>
</Col>
</Row>
<style jsx>
{`
.paper {
border: 1px solid lightgray;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 2px 1px -1px rgba(0, 0, 0, 0.12);
border-radius: 6px;
margin-top: 90px;
}
.notification {
color: #ab003c;
}
.header {
width: 100%;
height: 120px;
background-color: #2196f3;
margin-bottom: 30px;
border-radius-top: 6px;
}
.wrapper {
padding: 10px 30px 20px 30px !important;
}
a {
color: blue !important;
}
img {
margin: 15px 30px 10px 50px;
}
`}
</style>
</Container>
);
}
export default Login;
Your user registration, login, and logout should be set correctly!
Next, we will create the cart functionality.
🛒 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.