19th October 2022
Now it's high time we implemented user authentication because we do not want just anyone to access our application. Only our registered users should be able to do so.
If you've reached to this article, I suppose you did read the previous ones. The application repo is here . If you just want to follow along, you can clone the repo above and then run:
git reset --hard 86227dfae77b56ab8b13cd1aa0edbba88a406168
You also need to set up your mongoDB cloud instance. You can do that by following This article
If you start your app, you will see that on the home route, you even have a sign in form created. The first step is to make sure that a user cannot access the /calories route if they are not logged in.
We will build this incrementally. First create a LoginForm.js component and paste the below inside:
import React, { useState } from "react"; import { Item } from "./MainCard/Item"; import FormGroup from "@mui/material/FormGroup"; import { CustomTextField } from "./CustomTextField/CustomTextField"; import { Typography } from "@mui/material"; import Button from "@mui/material/Button"; import { Box } from "@mui/material"; export const LoginForm = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( <Box component="form" sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, "& .MuiTextField-root": { m: 1, width: "25ch" }, height: "100vh", textAlign: "center", }} noValidate autoComplete="off" > <Item sx={{ maxWidth: "25rem", margin: "auto", padding: "3rem 1rem", }} > <FormGroup sx={{ textAlign: "center" }}> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Email </Typography> <CustomTextField placeholder="email" type="email" changeHandler={(e) => setEmail(e.target.value)} value={email} /> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Password </Typography> <CustomTextField placeholder="password" type="password" changeHandler={(e) => setPassword(e.target.value)} value={password} /> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > Sign In </Button> </div> </FormGroup> </Item> </Box> ); };
Next, replace the AppContainer.js contents:
import React, { useToken } from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import Button from "@mui/material/Button"; import { Link } from "react-router-dom"; import { LoginForm } from "./LoginForm"; import { Navigation } from "./Navigation/Navigation"; /* * @AppContainer (functional component) * */ export const AppContainer = () => { const [token, setToken] = useState() return ( <Box component="form" sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, "& .MuiTextField-root": { m: 1, width: "25ch" }, height: "100vh", textAlign: "center", }} noValidate autoComplete="off" > <Navigation /> <Typography variant="h3" component="div" color="#fff" sx={{ margin: "1rem" }} > Welcome to <br></br> <FastfoodIcon sx={{ marginLeft: "2rem" }} /> Meals! </Typography> {!token ? <LoginForm /> : ""} <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > No account yet? </Typography> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > <Link style={{ textDecoration: "none", color: "#fff" }} to="/signup"> Sign Up </Link> </Button> </div> </Box> ); };
Now, if you go to http://localhost:3000, you will see the login form (unless you set a default value in the token piece of state).
Let's set up a context provider for the user context (so we can check who the user is, if they are logged in, etc...). Create inside the 'state' directory a User.module.js file and paste the below:
import React, { createContext, useState } from "react"; export const UserContext = createContext(); export const UserContextProvider = ({ children }) => { const [token, setToken] = useState("someToken"); return ( <UserContext.Provider value={{ token, setToken, }} > {children} </UserContext.Provider> ); };
Next, let's implement the new UserContext in the index.js file:
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { UIContextProvider } from "./state/UI.module"; import { UserContextProvider } from "./state/User.module"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <UIContextProvider> <UserContextProvider> <App /> </UserContextProvider> </UIContextProvider> </React.StrictMode> );
Now, let's use the token value from the User module inside the Calories page:
import React, { useContext } from "react"; import { Box } from "@mui/material"; import { Navigation } from "../components/Navigation/Navigation"; import { MainCard } from "../components/MainCard/MainCard"; import { LoginForm } from "../components/LoginForm"; import { UserContext } from "../state/User.module"; import { MealItemsProvider } from "../state/MealItems.module"; /* * @Calories Page (contents of the page) * */ export const CaloriesPage = () => { const { token } = useContext(UserContext); return ( <Box sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, height: "auto", }} > <div className="App"> <Navigation /> {token ? ( <MealItemsProvider> <MainCard /> </MealItemsProvider> ) : ( <> <br /> <LoginForm /> </> )} </div> </Box> ); };
Now, we have the userToken available in our app globally. Note how we are reading the token from the User.module.js. Let's replace the default value for the token in the User module with nothing. Furthermore, in the LoginForm component let's implement a login method and set the token back once the user logs in:
import React, { useState, useContext } from "react"; import { Item } from "./MainCard/Item"; import FormGroup from "@mui/material/FormGroup"; import { CustomTextField } from "./CustomTextField/CustomTextField"; import { Typography } from "@mui/material"; import Button from "@mui/material/Button"; import { Box } from "@mui/material"; import { UserContext } from "../state/User.module"; /* * @LoginForm (functional component) */ export const LoginForm = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const { setToken } = useContext(UserContext); const logIn = () => { const userCredentials = { email, password, }; console.log(userCredentials); setToken("someToken"); }; return ( <Box component="form" sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, "& .MuiTextField-root": { m: 1, width: "25ch" }, height: "100vh", textAlign: "center", }} noValidate autoComplete="off" > <Item sx={{ maxWidth: "25rem", margin: "auto", padding: "3rem 1rem", }} > <FormGroup sx={{ textAlign: "center" }}> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Email </Typography> <CustomTextField placeholder="email" type="email" changeHandler={(e) => setEmail(e.target.value)} value={email} /> <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > Password </Typography> <CustomTextField placeholder="password" type="password" changeHandler={(e) => setPassword(e.target.value)} value={password} /> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" onClick={logIn} > Sign In </Button> </div> </FormGroup> </Item> </Box> ); };
If you log in with whatever credentials, now the token will be set and you can see the calories page. If you try to navigate back to the homepage, you might notice this error:
In order to get rid of this error, just go to the AppContainer.js and for the first Box component replace component="form" with component="div".
Next, let's bring our token implementation from the UserContext in the AppContainer.js:
import React, { useContext } from "react"; import { Box } from "@mui/material"; import Typography from "@mui/material/Typography"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import Button from "@mui/material/Button"; import { Link } from "react-router-dom"; import { LoginForm } from "./LoginForm"; import { Navigation } from "./Navigation/Navigation"; import { UserContext } from "../state/User.module"; /* * @AppContainer (functional component) * */ export const AppContainer = () => { const { token } = useContext(UserContext); return ( <Box component="div" sx={{ bgcolor: "background.defaultColor", p: 0, m: -1, "& .MuiTextField-root": { m: 1, width: "25ch" }, height: "auto", minHeight: "100vh", textAlign: "center", }} noValidate autoComplete="off" > <Navigation /> <Typography variant="h3" component="div" color="#fff" sx={{ margin: "1rem" }} > Welcome to <br></br> <FastfoodIcon sx={{ marginLeft: "2rem" }} /> Meals! </Typography> {token ? ( <Typography color="#fff" component="h3"> Welcome User! </Typography> ) : ( <> <br /> <LoginForm /> </> )} <Typography variant="h5" component="div" color="#fff" sx={{ margin: "0.5rem" }} > No account yet? </Typography> <div> <Button sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }} variant="contained" > <Link style={{ textDecoration: "none", color: "#fff" }} to="/signup"> Sign Up </Link> </Button> </div> </Box> ); };
We have almost implemented a 'hard-coded' token in all our screens (if you try to log in on the calories screen and then go to the homepage from the menu item (not from the logo button!)), you still stay logged in. However, that does not work if you go to the homepage by clicking on the hamburger button from the Navigation.js. The reason for that is the usage of a href attribute in there. Replace the first occurrence of the Typography component in the navigation with the below:
<Typography variant="h6" noWrap sx={{ mr: 2, display: { xs: "none", md: "flex" }, fontFamily: "monospace", fontWeight: 700, letterSpacing: ".3rem", color: "inherit", textDecoration: "none", }} > <Link to="/" style={{ ...linkStyles, color: mode === "dark" ? "#fff" : "#000", }} > {appTitle} </Link> </Typography>
There you go! Now if we 'sign in' on any of the pages, we will stay 'signed in' unless we refresh the page (which obviously re-initializes the state and loses our dummy token). Thank you very much for sticking with me until now. If you want to check your code against mine, here is a Commit Link with my exact changes.
Next part we will start working on the sign up/sign in parts in the backend, so that we can actually create legitimate user accounts and use them.