MERN Stack App pt-7

MERN APP pt 7 (Intro to User Authentication)

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.