MERN Stack App pt-8

MERN APP pt 8 (Part 2 on User Authentication)

December 16th 2022

In the last part of the series we started working on user authentication . We will try to wrap everything up in this post so we can finally have a rounded app.

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 fe5da722e12d4e87f5805de2b0e76974ce22d013
    

You also need to set up your mongoDB cloud instance. You can do that by following This article

Last time we left our application in such a state so that if the user just submits anything in the login form (even without inputting data!), they get a fake token set in the client side and can view the app contents. That's bad as we need to make sure that the user actually inputs some credentials and that the credentials are valid.

In the front end of our app, in the LoginForm component, in the logIn() method, right before logging the user credentials add the below:

      if (!email || !password) {
        console.error("no credentials were input! X__X");
      }
    

You will notice now that this check is triggered if we submit the form without inputing anything or while inputing just a space. We will need now to implement some client side alert function that will prompt the user to put the credentials in the form.

First, create an Alert.js component in the 'components' directory and paste the below in it:

      import Typography from "@mui/material/Typography";

export const Alert = ({ msg }) => {
  if (msg) {
    return (
      <Typography
        variant="h2"
        component="div"
        color="white"
        backgroundColor="orange"
        width="20rem"
        margin="2rem auto"
        padding="1rem"
        fontSize="1.5rem"
      >
        {msg}
      </Typography>
    );
  }
};

    

Next, define the below piece of state in LoginForm.js:

      const [alert, setAlert] = useState(null);
    

Furthermore, import the Alert component in LoginForm and add it as per below inside the first Box element:

      <Alert msg={alert} />
    

Next, modify the logIn() method as per below:

      const logIn = () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password) {
          console.error("no credentials were input! X__X");
          setAlert("Bad email or weak password (1 lowercase char, 1 uppercase, 1 special symbol, 1 digit, minim 5 chars at least)");
        } else {
          setToken("someToken");
        }
      };
    

They can no longer input nothing and log in. Now we need to actually validate the email and the password. Add the below methods in the LoginForm.js file:

/*
 * validation (email and password)
 */
const validateEmail = (email) => {
  const re = /\S+@\S+\.\S+/;
  return re.test(email);
};

//uppercase, lowercase, 1 digit, one special symbol, more than 4 chars length
const validatePassword = (pw) => {
  return (
    /[A-Z]/.test(pw) &&
    /[a-z]/.test(pw) &&
    /[0-9]/.test(pw) &&
    /[^A-Za-z0-9]/.test(pw) &&
    pw.length > 4
  );
};
    

Finally, update the logIn() method as per below:

      const logIn = () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password) {
          console.error("no credentials were input! X__X");
          setAlert(
            "Bad email or weak password (1 lowercase char, 1 uppercase, 1 special symbol, 1 digit, minim 5 chars at least)"
          );
        } else {
          if (validateEmail(email) && validatePassword(password)) {
            setToken("someToken");
          }
        }
      };
    

Next, let's hit the back-end with some payload. In the LoginForm add the below import:

      import { postData } from "../services/http";
    

Next, add the below method above logIn:

      //signIn method
      const signIn = async (credentials) => {
        const url = "http://localhost:4000/signin";
        const resData = await postData(url, credentials);
        return resData;
      };
    

Change the logIn() method as per below:

      const logIn = () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password) {
          console.error("no credentials were input! X__X");
          setAlert(
            "Bad email or weak password (1 lowercase char, 1 uppercase, 1 special symbol, 1 digit, minim 5 chars at least)"
          );
        } else {
          if (validateEmail(email) && validatePassword(password)) {
            setAlert(null);
            signIn(userCredentials);
            setToken("someToken");
          }
        }
      };
    

Now, we could test our code, but we'd get a 405 error as we are obviously not handling the /signin endpoint on our back-end. Let's start working on that. In the 'backend' add an auth.js file in the /routes directory. Inside of it paste the below:

import express from "express";

import { logIn } from "../db/db.js";

import { User } from "../db/models/User.js";

export const userRoutes = express();

/*
 * POST /signin (logs user in)
 *
 * @payload:
 * {
 *  email: string,
 *  password: string,
 * }
 *
 * @returns success reply
 */
userRoutes.post("/signin", async (req, res) => {
  await logIn(req.body, User);
  res.send(JSON.stringify({ msg: "user created successfully" }));
});

    

Let's add a User.js model in the 'models' directory:

import mongoose from "mongoose";

/*
 * User (object schema for the User element)
 */
const UserSchema = {
  userId: String,
  email: String,
  password: String,
};

//export the model and define table for mongoDB Atlas
export const User = mongoose.model("Users", UserSchema);
    

Next we need a slight alteration in the MealItem model to keep track of whose user the meal items actually are:

import mongoose from "mongoose";

/*
 * MealItemSchema (object schema for the MealItem element)
 */
const MealItemSchema = {
  userId: String,
  mealItem: String,
  mealQty: Number,
  mealCals: Number,
  mealProtein: Number,
};

//export the model and define table for mongoDB Atlas
export const MealItem = mongoose.model("Meals", MealItemSchema);

    

Finally, in the db.js let's add the logIn() method:

/*
 * logs user in
 *
 * @params:
 * {
 *  payload (contains user credentials)
 * }
 */
export const logIn = (credentials) => {
  console.log(`logging user ${JSON.stringify(credentials)} in...`);
};

    

The db.js file looks like some mix of a user and a meals item controller. We will split and refactor it later. For now, we can use this setup.

Finally, in the server.jslet's add our new auth routes (don't forget to import them):

//mount routes
app.use("/", mealRoutes);
app.use("/", userRoutes)
    

With all these changes, we can now test our signIn method and you'll see the credentials logged.

Next, let's work on the signup functionality. Let's start by moving the validateEmail() and validatePassword() methods in a utils.js file and import them back in the LoginForm component. I will create a utils directory alongside the 'components' one and inside of it create utils.js:

/*
 * validation (email and password)
 */
export const validateEmail = (email) => {
  const re = /\S+@\S+\.\S+/;
  return re.test(email);
};

//uppercase, lowercase, 1 digit, one special symbol, more than 4 chars length
export const validatePassword = (pw) => {
  return (
    /[A-Z]/.test(pw) &&
    /[a-z]/.test(pw) &&
    /[0-9]/.test(pw) &&
    /[^A-Za-z0-9]/.test(pw) &&
    pw.length > 4
  );
};

    

Next, import the 2 methods in the SignUp.page.js file. Also define one more piece of state:

      const [repeatPassword, setRepeatPassword] = useState("");
    

Next, adjust the input for the confirm password:

    <CustomTextField
      placeholder="confirm password"
      type="password"
      changeHandler={(e) => setRepeatPassword(e.target.value)}
      value={repeatPassword}
    />
    

*don't forget to change the 'changeHandler' as per above in the other 2 inputs too (for email and password) so replace changeHandler={someSetterFunction} with changeHandler={(e) => someSetterFunction(e.target.value)}

This will be our first version of the signUp function:

      const signUp = () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password || !repeatPassword || password !== repeatPassword) {
          console.error("no credentials or bad credentials were input! X__X");
        } else {
          if (validateEmail(email) && validatePassword(password)) {
            console.log(userCredentials);
          }
        }
      };
    

Add the handler on the 'Sign up' button too:

      onClick={signUp}
    

If you test your code now, you should see that it will work only when u input a valid email, a strong enough password and repeat the same exact password (the error is logged for now).

Let's display the error in a similar way to how we did it for the LoginForm:

import React, { useState } from "react";

import { Box } from "@mui/material";
import Typography from "@mui/material/Typography";
import FormGroup from "@mui/material/FormGroup";
import Button from "@mui/material/Button";

import { Navigation } from "../components/Navigation/Navigation";
import { CustomTextField } from "../components/CustomTextField/CustomTextField";
import { Item } from "../components/MainCard/Item";
import { validateEmail, validatePassword } from "../utils/utils";
import { Alert } from "../components/Alert";

/*
 * @SignUp Page (contents of the page)
 *
 */

export const SignUpPage = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [repeatPassword, setRepeatPassword] = useState("");
  const [alert, setAlert] = useState(null);

  const signUp = () => {
    const userCredentials = {
      email,
      password,
    };
    if (!email || !password || !repeatPassword || password !== repeatPassword) {
      console.error("no credentials or bad credentials were input! X__X");
      setAlert(
        "no credentials or bad credentials were input! X__X"
      );
    } else {
      if (validateEmail(email) && validatePassword(password)) {
        console.log(userCredentials);
      }
    }
  };

  return (
    <Box
      sx={{
        bgcolor: "background.defaultColor",
        p: 0,
        m: -1,
        height: "100vh",
      }}
    >
  
      <div className="App">
        <Navigation />
        <Typography variant="h3" gutterBottom align="center" color="white">
          Sign Up Page
        </Typography>
        <Alert msg={alert} />
        <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}
            />
            <Typography
              variant="h5"
              component="div"
              color="#fff"
              sx={{ margin: "0.5rem" }}
            >
              Confirm Password
            </Typography>
            <CustomTextField
              placeholder="confirm password"
              type="password"
              changeHandler={(e) => setRepeatPassword(e.target.value)}
              value={repeatPassword}
            />
            <div>
              <Button
                sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }}
                variant="contained"
                onClick={signUp}
              >
                Sign Up
              </Button>
            </div>
          </FormGroup>
        </Item>
      </div>
    </Box>
  );
};
    

Next let's add the import { postData } from "../services/http"; import in the SignUp page and send some data to the backend and slightly adjust the signUp method:

      const signUp = async () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password || !repeatPassword || password !== repeatPassword) {
          console.error("no credentials or bad credentials were input! X__X");
          setAlert(
            "no credentials or bad credentials were input! X__X"
          );
        } else {
          if (validateEmail(email) && validatePassword(password)) {
            const url = "http://localhost:4000/signup";
            await postData(url, userCredentials);
          }
        }
      };
    

Let's move 'back' to the back-end (I know I'm hilarious 😂 ) and paste the route in the auth.js routes file:

/*
 * POST /signup (signs user up)
 *
 * @payload:
 * {
 *  email: string,
 *  password: string,
 * }
 *
 * @returns success reply
 */
userRoutes.post("/signup", async (req, res) => {
  await signUp(req.body, User);
  res.send(JSON.stringify({ msg: "user signed up successfully" }));
});
    

Don't forget to add the signUp method in the import up top alongside logIn and paste it in the db.js:

/*
 * signs user up
 *
 * @params:
 * {
 *  payload (contains user credentials)
 * }
 */
export const signUp = (credentials) => {
  console.log(`signing user ${JSON.stringify(credentials)} up...`);
};
    

Test your code again at this point, to make sure it still works and you should see our pretty log above printed.

Now, what do we want to do with this data? There's 2 things: first we want to create a user in our Users table. Secondly, we want to redirect the user to the homepage on the front so they can sign up with their newly created account. Let's create the user first. Edit the signUp method as per below:

/*
 * signs user up
 *
 * @params:
 * {
 *  payload (contains user credentials)
 *  model: mongoose model (User)
 * }
 */
 export const signUp = async (credentials, model) => {
  const { email, password } = credentials;
  const userId =
    Math.random().toString(12).substring(2, 17) +
    Math.random().toString(12).substring(2, 17);

  const newUser = new model({
    userId,
    email,
    password,
  });

  try {
    await newUser.save();
    console.log(`signing user ${JSON.stringify(credentials)} up...`);
    return { msg: "user signed up successfully" };
  } catch (err) {
    console.log(err.code, typeof err.code);
    if (err.code === 11000) {
      return {
        msg: "duplicate entry a user with this email address already exists",
      };
    } else {
      console.log(err);
      return;
    }
  }
};
    

Next, modify the route a bit too in auth.js

      userRoutes.post("/signup", (req, res) => {
        signUp(req.body, User)
          .then((msg) => {
            return res.send(msg);
          })
          .catch((err) => {
            console.log(err);
            return res.send({ msg: "some err occurred" });
          });
      });
    

Now, everything works, we can only sign up with one email account once, but we have made 1 SUPER BAD mistake. We stored the password as plain text. NEVER do this! We did this in this post just for demonstration purposes, but it is a super bad idea. We have finally implemented user signup but we need a bit more security.

Let's add another dependency in the backend:

      yarn add bcrypt
    

Import it inside db.js and let's finish our implementation of the signUp() method for now:

      export const signUp = async (credentials, model) => {
        const { email, password } = credentials;
        const saltRounds = 10;
      
        const encrptedPass = await bcrypt.hash(password, saltRounds);
      
        const userId =
          Math.random().toString(12).substring(2, 17) +
          Math.random().toString(12).substring(2, 17);
        const newUser = new model({
          userId,
          email,
          password: encrptedPass,
        });
      
        try {
          await newUser.save();
          return { msg: "user signed up successfully" };
        } catch (err) {
          console.log(err);
          return { msg: "some error occurred at signUp" };
        }
      };
    

Finally, we want some handling on the front end too (if the signup was successful or not). Let's bring the Alert component in the SignUp page too:

import React, { useState } from "react";

import { Box } from "@mui/material";
import Typography from "@mui/material/Typography";
import FormGroup from "@mui/material/FormGroup";
import Button from "@mui/material/Button";
import { useNavigate } from "react-router-dom";
import { Navigation } from "../components/Navigation/Navigation";
import { CustomTextField } from "../components/CustomTextField/CustomTextField";
import { Item } from "../components/MainCard/Item";
import { validateEmail, validatePassword } from "../utils/utils";
import { postData } from "../services/http";
import { Alert } from "../components/Alert";

/*
 * @SignUp Page (contents of the page)
 *
 */

export const SignUpPage = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [repeatPassword, setRepeatPassword] = useState("");
  const [alert, setAlert] = useState(null);
  const [color, setColor] = useState(null);

  let navigate = useNavigate();
  const routeChange = () => {
    let path = "/";
    navigate(path);
  };

  const signUp = async () => {
    const userCredentials = {
      email,
      password,
    };
    if (!email || !password || !repeatPassword || password !== repeatPassword) {
      console.error("no credentials or bad credentials were input! X__X");
    } else {
      if (validateEmail(email) && validatePassword(password)) {
        const url = "http://localhost:4000/signup";
        const resData = await postData(url, userCredentials);
        if (resData.msg === "user signed up successfully") {
          setColor("green");
          setTimeout(() => {
            routeChange();
          }, 3000);
        } else {
          setColor("orange");
        }
        setAlert(resData.msg);
      }
    }
  };
  return (
    <Box
      sx={{
        bgcolor: "background.defaultColor",
        p: 0,
        m: -1,
        height: "100vh",
      }}
    >
      <div className="App">
        <Navigation />
        <Typography variant="h3" gutterBottom align="center" color="white">
          Sign Up Page
        </Typography>
        <Alert msg={alert} color={color} />
        <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}
            />
            <Typography
              variant="h5"
              component="div"
              color="#fff"
              sx={{ margin: "0.5rem" }}
            >
              Confirm Password
            </Typography>
            <CustomTextField
              placeholder="confirm password"
              type="password"
              changeHandler={(e) => setRepeatPassword(e.target.value)}
              value={repeatPassword}
            />
            <div>
              <Button
                sx={{ bgcolor: "submitBtnColor.main", width: "12.5rem" }}
                variant="contained"
                onClick={signUp}
              >
                Sign Up
              </Button>
            </div>
          </FormGroup>
        </Item>
      </div>
    </Box>
  );
};

    

Change the Alert.js too:

import Typography from "@mui/material/Typography";

export const Alert = ({ msg, color = "orange" }) => {
  if (msg) {
    return (
      <Typography
        variant="h2"
        component="div"
        color="white"
        backgroundColor={color}
        width="20rem"
        margin="2rem auto"
        padding="1rem"
        fontSize="1.5rem"
      >
        {msg}
      </Typography>
    );
  }
};
    

Don't forget to import bcrypt at the top of the file:

      import bcrypt from "bcrypt";
    

Now the signup is complete, and we even redirect to homepage upon successfully sign up. We currently handle only the cases of duplicate email addresses or bad credentials. I think this is good enough for demo purposes in our app. Next, let's finish up the login functionality and adjust the meals model (so that users can only see their own meals).

Let's finish with the login setup by moving back to the logIn() method in db.js:

Chande the logIn() method in db.js:

/*
 * logs user in
 *
 * @params:
 * {
 *  payload (contains user credentials)
 *  model: mongoose model (User)
 * }
 */
export const logIn = async (credentials, model) => {
  const { email, password } = credentials;
  const dbUser = await model.find({ email });
  if (dbUser && dbUser.length > 0) {
    const correctPass = await bcrypt.compare(password, dbUser[0].password);
    if (correctPass === true) {
      console.log(`logging user ${JSON.stringify(credentials)} in...`);
      return { token: "someTokenHere" };
    } else {
      return { msg: "bad credentials" };
    }
  } else {
    return { msg: "bad credentials" };
  }
};
    

Change the /signin post route in auth.js:

/*
 * POST /signin (logs user in)
 *
 * @payload:
 * {
 *  email: string,
 *  password: string,
 * }
 *
 * @returns success reply
 */
userRoutes.post("/signin", async (req, res) => {
  const resData = await logIn(req.body, User);
  return res.send(resData);
});
    

Finally, in the front, in LoginForm modify the logIn() ,method as per below:

      const logIn = async () => {
        const userCredentials = {
          email,
          password,
        };
        if (!email || !password) {
          console.error("no credentials were input! X__X");
          setAlert(
            "Bad email or weak password (1 lowercase char, 1 uppercase, 1 special symbol, 1 digit, minim 5 chars at least)"
          );
        } else {
          if (validateEmail(email) && validatePassword(password)) {
            setAlert(null);
            const resData = await signIn(userCredentials);
            if (resData.token) {
              setToken(resData.token);
            } else {
              setAlert("Bad credentials X___X!");
              setTimeout(() => {
                setAlert(null);
              }, 3000);
            }
          }
        }
      };
    

Now the sign up logic is ready, but we need to return some token (similar to an actual JWT in which non-sensitive user data is encoded usually. ). Change the login method in the db.js:

        /*
        * logs user in
        *
        * @params:
        * {
        *  payload (contains user credentials)
        *  model: mongoose model (User)
        * }
        */
        export const logIn = async (credentials, model) => {
          try { 
            const { email, password } = credentials;
            const usr = await model.find({email}).exec();
            const correctPass = await bcrypt.compare(password, usr[0].password);
            if(correctPass){
              return { token: "someTokenHere", userId: usr[0]._id };
            } else {
              return { msg: "bad credentials" };
            }
          
          } catch(err){
            console.log(err);
            return 'someErr';
          }
        };
    

For now, we are setting a proper token and handling bad credentials use case. Let's focus now on the model a bit more, as we want to display only the user's meals. Currently meals have no user id but we are returning now one from the back-end. Let's set the user id in state for now. Add the below line in the User.module.js in the front right under the token line.

      const [userId, setUserId] = useState();
    

Next, add it in the return statement so we can use it throughout the front app:

      return (
        <UserContext.Provider
          value={{
            token,
            setToken,
            userId,
            setUserId
          }}
        >
          {children}
        </UserContext.Provider>
      );
    

Next, let's save the userId in the app state too upon login. In the LoginForm.js add the below line under the one where you set the token:

      setUserId(resData.userId);
    

Next, let's retrieve the userId when creating a new meal in the MealItems.module.js. Import the userContext and add the below line before the above the state pieces:

      const { userId } = useContext(UserContext);
    

Next, in the saveMeal() method before the if() check, add the below line:

        newMealItem.userId = userId;
     

Now, we just need to save the userId when saving the meal (remember we did have the userId key in the user model). Just destructure the userId and save it in the addItem() in the db.js file:

/*
 * adds item to the Meals table
 *
 * @params:
 * {
 *  payload (contains data used to create new DB meal item)
 *  model: mongoose model (MealItem)
 * }
 */
export const addItem = async (payload, model) => {
  connectDB();
  const { mealItem, mealQty, mealCals, mealProtein, userId } = payload;
  const newMealItem = new model({
    mealItem,
    mealQty,
    mealCals,
    mealProtein,
    userId,
  });
  try {
    await newMealItem.save();
  } catch (err) {
    console.log(err);
  }
};
    

Now we need to send the userId as a query string parameter in the http.js service. Unfortunately we cannot use the UserContext there so we have to rely on our ClientStorage.js. We will change our implementation a bit. First, let's store the userId with the ClientStorage in the LoginForm.js. Add the below import in the LoginForm:

      import { storeItem } from "../services/ClientStorage";
    

Next, store the userId and token with the ClientStorage when you set it in state:

      storeItem('userId', resData.userId);
      storeItem('token', resData.token);
    

Let's also retrieve this token (together with the userId) in the User.module.js:

      const storedToken = getItem('token');
      const storedUserId = getItem('userId');
      const [token, setToken] = useState(storedToken);
      const [userId, setUserId] = useState(storedUserId);
    

Next, let's set the default userId in the User.module.js:

      useEffect(() => {
        const isUserId = getItem('userId');
        if(isUserId) {
          setUserId(isUserId);
        } 
      }, []);
    

*don't forget to import the getItem and useEffect hook.

Now, we can move back to http.js. Add these lines to the top of the file:

      import { getItem } from './ClientStorage';

      const userId = getItem('userId');
    

Next, change the getData() method:

      export const getData = async (url) => {
        const rawData = await fetch('http://localhost:4000/meals', {
          method: "GET",
          headers: {
            Accept: "application/json, text/plain, */*",
            "Content-Type": "application/json",
            userId,
          },
        });
        const serialisedData = await rawData.json();
        return serialisedData;
      };
    

Next, in the meals.js routes file, change the get() method:

/*
 * GET /meals
 *
 * @returns array of meal items
 * @gets userId as queryString parameter
 */
mealRoutes.get("/meals", async (req, res) => {
  const { userid } = req.headers;
  const meals = await getItems(MealItem, userid);
  return res.send(meals);
});
    

Finally, in the db.js change the getItems() method:

      /*
      * gets meal items from DB
      *
      * @params:
      * {
      *    model: mongoose model (MealItem)
      *    userId: string (user id)
      * }
      */
     export const getItems = async (model, userId) => {
       connectDB();
       try {
         const meals = await model.find({ userId });
         return meals;
       } catch (err) {
         console.log(err);
       }
     };
    

Now, if you go back to the calories page, you will see only the logged in user's meal items.

If we reached this point, I will also change the AppContainer a bit:

      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/> 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>
  );
};

    

I noticed we have a little bug when signing in. We need to refresh the page in order for the meal items to be displayed. That's bad user experience. Let's fix that.

First of all, I just realized we made a mistake with the getData() method in the http service, please replace it as per below:

      export const getData = async (url) => {
        const rawData = await fetch(url, {
          method: "GET",
          headers: {
            Accept: "application/json, text/plain, */*",
            "Content-Type": "application/json",
            userId,
          },
        });
        const serialisedData = await rawData.json();
        return serialisedData;
      };
    

Now, in order to fix this bug for now, just add the below line in the LoginForm.js right below the storeItem() lines:

      window.location.reload(false);
    

This is very ugly and we should never do this. We have got to this bug in the first place because some of the state handling that we do is bad, or a combination of that and some slow back-end response. In the next article we will finalize this project with a more in-depth refactor and we will properly address this issue.

For now, let's also implement a quick logout function. Add the below route in the App.js

      <Route path="/logout" element={<LogOut />} />
    

Next, create the page inside the 'pages' directory:

import { useEffect } from 'react';
import { useNavigate } from "react-router-dom";
import { clearItems } from "../services/ClientStorage";

export const LogOut = () => {
    const navigate = useNavigate();
    useEffect(() => {
        clearItems();
        navigate("/");
        window.location.reload(false);
    }, []);
    return'';
}
    

Add the clearItems() method in the ClientStorage.js file:

      export const clearItems = () => {
        localStorage.clear();
      };
    

And you are done. We have implemented full login functionality and user authentication. This is a very basic authentication (we just check for the email and password and we don't even care) if for instance the email exists. We could also integrate our app with 3rd party authentication plugins from gmail, github, etc.. to let users sign in with their accounts.

As you may have noticed, we definitely need to improve our front-end data flow a bit and state handling (so that we get rid of the forced page reloads). There is also some cleanup and refactoring I'd like to do in the back-end but these will have to be done in the next and final part of this series.

Thanks for sticking with me this far! If you want to compare your code changes against mine, here's a Link just for that.