DEV Community

Cover image for ๐Ÿ Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe - ๐Ÿ” Authentication (part 4/7)
Ryan
Ryan

Posted on โ€ข Edited on

4

๐Ÿ Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe - ๐Ÿ” Authentication (part 4/7)

Strapi Next.js tutorial

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: https://github.com/strapi/strapi-examples/tree/master/nextjs-react-strapi-deliveroo-clone-tutorial*.

๐Ÿ” Authentication

For authentication, we can use the Strapi SDK to register and login our users. Strapi will return a JWT token that can be used to verify transactions on the server (although we will not setup server validation in this tutorial you should in a real world application).

The strapi documentation on users can be found here: https://strapi.io/documentation/1.x.x/users.html.

Authentication

For authentication we are going to use 2 higher order components defaultPage.js and securePage.js to wrap our pages and pass an isAuthenticated prop down to the necesseray components.

Make a new directory in the root of the project:

mkdir hocs
cd hocs
touch defaultPage.js
touch securePage.js
Enter fullscreen mode Exit fullscreen mode

Path: /frontend/components/hocs/defaultPage.js

/* hocs/defaultPage.js */
import React from "react";
import Router from "next/router";
import { getUserFromServerCookie, getUserFromLocalCookie } from "../lib/auth";
export default Page =>
class DefaultPage extends React.Component {
static async getInitialProps({ req }) {
const loggedUser = process.browser
? getUserFromLocalCookie()
: getUserFromServerCookie(req);
const pageProps = Page.getInitialProps && Page.getInitialProps(req);
console.log("is authenticated");
console.log(loggedUser);
let path = req ? req.pathname : "";
path = "";
return {
...pageProps,
loggedUser,
currentUrl: path,
isAuthenticated: !!loggedUser
};
}
logout = eve => {
if (eve.key === "logout") {
Router.push(`/?logout=${eve.newValue}`);
}
};
componentDidMount() {
window.addEventListener("storage", this.logout, false);
}
componentWillUnmount() {
window.removeEventListener("storage", this.logout, false);
}
render() {
return <Page {...this.props} />;
}
};
view raw defaultPage.js hosted with โค by GitHub

Path: /frontend/components/hocs/securePage.js

/* components/hocs/securePage.js */
import React from "react";
import PropTypes from "prop-types";
import defaultPage from "./defaultPage";
const securePageHoc = Page =>
class SecurePage extends React.Component {
static propTypes = {
isAuthenticated: PropTypes.bool.isRequired
};
static getInitialProps(ctx) {
return Page.getInitialProps && Page.getInitialProps(ctx);
}
render() {
const { isAuthenticated } = this.props;
return isAuthenticated ? <Page {...this.props} /> : "Not Authorized";
}
};
export default Page => defaultPage(securePageHoc(Page));
view raw securePage.js hosted with โค by GitHub

To setup our authentication functions we will create a new file under the /lib folder called auth.js that will allow us to control and change authentication functionality in one place.

As you will, three new dependencies are imported in the upcoming files, so you need to install them:

cd ..
yarn add jwt-decode js-cookie strapi-sdk-javascript 

cd lib
touch auth.js


Enter fullscreen mode Exit fullscreen mode

Path: /frontend/lib/auth.js

/* /lib/auth.js */
import jwtDecode from "jwt-decode";
import Cookies from "js-cookie";
import Strapi from "strapi-sdk-javascript/build/main";
import Router from "next/router";
const apiUrl = process.env.API_URL || "http://localhost:1337";
const strapi = new Strapi(apiUrl);
export const strapiRegister = (username, email, password) => {
if (!process.browser) {
return undefined;
}
strapi.register(username, email, password).then(res => {
setToken(res);
});
return Promise.resolve();
};
//use strapi to get a JWT and token object, save
//to approriate cookei for future requests
export const strapiLogin = (email, password) => {
if (!process.browser) {
return;
}
// Get a token
strapi.login(email, password).then(res => {
setToken(res);
});
return Promise.resolve();
};
export const setToken = token => {
if (!process.browser) {
return;
}
Cookies.set("username", token.user.username);
Cookies.set("jwt", token.jwt);
if (Cookies.get("username")) {
Router.push("/");
}
};
export const unsetToken = () => {
if (!process.browser) {
return;
}
Cookies.remove("jwt");
Cookies.remove("username");
Cookies.remove("cart");
// to support logging out from all windows
window.localStorage.setItem("logout", Date.now());
Router.push("/");
};
export const getUserFromServerCookie = req => {
if (!req.headers.cookie || "") {
return undefined;
}
let username = req.headers.cookie
.split(";")
.find(user => user.trim().startsWith("username="));
if (username) {
username = username.split("=")[1];
}
const jwtCookie = req.headers.cookie
.split(";")
.find(c => c.trim().startsWith("jwt="));
if (!jwtCookie) {
return undefined;
}
const jwt = jwtCookie.split("=")[1];
return jwtDecode(jwt), username;
};
export const getUserFromLocalCookie = () => {
return Cookies.get("username");
};
//these will be used if you expand to a provider such as Auth0
const getQueryParams = () => {
const params = {};
window.location.href.replace(
/([^(?|#)=&]+)(=([^&]*))?/g,
($0, $1, $2, $3) => {
params[$1] = $3;
}
);
return params;
};
export const extractInfoFromHash = () => {
if (!process.browser) {
return undefined;
}
const { id_token, state } = getQueryParams();
return { token: id_token, secret: state };
};
view raw auth.js hosted with โค by GitHub

Why cookies? ๐Ÿช

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, and this is what the Strapi JavaScript SDK does by default (it also stores it as a cookie).

The fact is that we would like to display the username in the header (coming later in this tutorial). So we need to store it somewhere.

We could have store it in the local storage, but since Nuxt supports server-side rendering, which has not access to the local storage, we need to store it in the cookies.

Register

To register a user we will pass a username, email and password with the Strapi SDK. This will register a user in Strapi and log the user in. Inside of our signup page we will call the strapiRegister function inside of our auth.js file to register the user then set the appropriate JWT and username cookies inside the browser.

Path: /frontend/pages/signup.js

/* /pages/signup.js */
import React from "react";
import { strapiRegister } from "../lib/auth";
import Router from "next/router";
import {
Container,
Row,
Col,
Button,
Form,
FormGroup,
Label,
Input,
FormText
} from "reactstrap";
class SignUp extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {
email: "",
username: "",
password: ""
},
loading: false,
error: ""
};
}
onChange(propertyName, event) {
const { data } = this.state;
data[propertyName] = event.target.value;
this.setState({ data });
}
onSubmit() {
const {
data: { email, username, password }
} = this.state;
this.setState({ loading: true });
strapiRegister(username, email, password)
.then(() => this.setState({ loading: false }))
.catch(error => this.setState({ error: error }));
}
render() {
const { error } = this.state;
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">
<div className="notification">{error}</div>
<Form>
<FormGroup>
<Label>Username:</Label>
<Input
onChange={this.onChange.bind(this, "username")}
type="text"
name="username"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup>
<Label>Email:</Label>
<Input
onChange={this.onChange.bind(this, "email")}
type="email"
name="email"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup style={{ marginBottom: 30 }}>
<Label>Password:</Label>
<Input
onChange={this.onChange.bind(this, "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"
onClick={this.onSubmit.bind(this)}
>
Submit
</Button>
</FormGroup>
</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);
height: 540px;
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 SignUp;
view raw signup.js hosted with โค by GitHub

auth

Logout

Inside of our Layout.js component we check for an authenticated user using the isAuthenticated prop, and if a user is detected we display the username and a logout button.

The logout button will call the unsetToken function to delete the cookies and re-route to the home page.

Path: /frontend/components/Layout.js

/* /components/Layout.js */
import React from "react";
import Head from "next/head";
import Link from "next/link";
import { unsetToken } from "../lib/auth";
import { Container, Nav, NavItem } from "reactstrap";
import defaultPage from "../hocs/defaultPage";
import Cookie from "js-cookie";
class Layout extends React.Component {
constructor(props) {
super(props);
}
static async getInitialProps({ req }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps, isAuthenticated };
}
render() {
const { isAuthenticated, children } = this.props;
const title = "Welcome to Nextjs";
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>
<Nav className="navbar navbar-dark bg-dark">
<NavItem>
<Link href="/">
<a className="navbar-brand">Home</a>
</Link>
</NavItem>
{isAuthenticated ? (
<>
<NavItem className="ml-auto">
<span style={{ color: "white", marginRight: 30 }}>
{this.props.loggedUser}
</span>
</NavItem>
<NavItem>
<Link href="/">
<a className="logout" onClick={unsetToken}>
Logout
</a>
</Link>
</NavItem>
</>
) : (
<>
<NavItem className="ml-auto">
<Link href="/signin">
<a className="nav-link">Sign In</a>
</Link>
</NavItem>
<NavItem>
<Link href="/signup">
<a className="nav-link"> Sign Up</a>
</Link>
</NavItem>
</>
)}
</Nav>
</header>
<Container>{children}</Container>
{/* <footer className="footer">
{"Strapi footer"}
<style jsx>
{`
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
line-height: 60px;
background-color: #f5f5f5;
}
a:hover {
cursor: pointer;
color: yellow;
}
`}
</style>
</footer> */}
</div>
);
}
}
export default defaultPage(Layout);
view raw Layout.js hosted with โค by GitHub

Login

Similar to our login page, the sign-in page will use the Strapi SDK to login in the user and set the appropriate username and JWT cookies for later use.

Path: /frontend/pages/signin.js

/* /pages/signin.js */
import React from "react";
import defaultPage from "../hocs/defaultPage";
import { strapiLogin } from "../lib/auth";
import Router from "next/router";
import {
Container,
Row,
Col,
Button,
Form,
FormGroup,
Label,
Input,
FormText
} from "reactstrap";
import Cookies from "js-cookie";
class SignIn extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {
email: "",
password: ""
},
loading: false,
error: ""
};
}
componentDidMount() {
if (this.props.isAuthenticated) {
Router.push("/"); // redirect if you're already logged in
}
}
onChange(propertyName, event) {
const { data } = this.state;
data[propertyName] = event.target.value;
this.setState({ data });
}
onSubmit() {
const {
data: { email, username, password }
} = this.state;
const { context } = this.props;
this.setState({ loading: true });
strapiLogin(email, password).then(() => console.log(Cookies.get("user")));
}
render() {
const { error } = this.state;
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">
<div className="notification">{error}</div>
<Form>
<FormGroup>
<Label>Email:</Label>
<Input
onChange={this.onChange.bind(this, "email")}
type="email"
name="email"
style={{ height: 50, fontSize: "1.2em" }}
/>
</FormGroup>
<FormGroup style={{ marginBottom: 30 }}>
<Label>Password:</Label>
<Input
onChange={this.onChange.bind(this, "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"
onClick={this.onSubmit.bind(this)}
>
Submit
</Button>
</FormGroup>
</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);
height: 440px;
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 SignIn;
view raw signin.js hosted with โค by GitHub

signin

Now update your index.js page to use the newly created defaultPage HOC.

Path: /frontend/pages/index.js

/* /pages/index.js */
import RestaurantList from "../components/RestaurantList";
import React from "react";
import defaultPage from "../hocs/defaultPage";
import {
Alert,
Button,
Col,
Input,
InputGroup,
InputGroupAddon,
Row
} from "reactstrap";
class Index extends React.Component {
constructor(props) {
super(props);
//query state will be passed to RestaurantList for the filter query
this.state = {
query: ""
};
}
onChange(e) {
//set the state = to the input typed in the search Input Component
//this.state.query gets passed into RestaurantList to filter the results
this.setState({ query: e.target.value.toLowerCase() });
}
render() {
return (
<div className="container-fluid">
<Row>
<Col>
<div className="search">
<InputGroup>
<InputGroupAddon addonType="append"> Search </InputGroupAddon>
<Input onChange={this.onChange.bind(this)} />
</InputGroup>
</div>
<RestaurantList search={this.state.query} />
</Col>
</Row>
<style jsx>
{`
.search {
margin: 20px;
width: 500px;
}
`}
</style>
</div>
);
}
}
export default defaultPage(Index);
view raw index.js hosted with โค by GitHub

Next we wil setup React Context for our shopping cart, and allow our Layout header bar to recognize a user is logged in and display the username

๐Ÿ›’ In the next section, you will learn how to create a full featured shopping cart: https://dev.to/ryanrez/-cooking-a-deliveroo-clone-with-nextjs-react-graphql-strapi-and-stripe----shopping-cart-part-57-2h1e

Top comments (0)

Sentry image

See why 4M developers consider Sentry, โ€œnot bad.โ€

Fixing code doesnโ€™t have to be the worst part of your day. Learn how Sentry can help.

Learn more

๐Ÿ‘‹ Kindness is contagious

Please leave a โค๏ธ or a friendly comment on this post if you found it helpful!

Okay