Adding auth UI elements

Adding UI elements to handle user registration and login.

Project Goals

In this eighth stage of your MERN project, you will:

  • Add UI elements to handle user registration and login.

Adding the MUI icons

Before implementing the UI elements for the user authentication flow, install the following icon pack from the MUI library in your /frontend folder:

npm i @mui/icons-material @emotion/react @emotion/styled

Creating the AuthPage.jsx page

In your frontend/src/pages sub-folder, create an AuthPage.jsx page component that will display either the login form or registration form based on the user’s choice.


import { Grid } from "@mui/material";
import React, { useState } from "react";
import LoginForm from "../components/LoginForm.jsx";
import RegisterForm from "../components/RegisterForm.jsx";
import NavBar from "../components/NavBar.jsx";

const AuthPage = () => {
  const [loginShow, setLoginShow] = useState(true);

  const handleSignup = () => {
    setLoginShow(!loginShow);
  };

  return (
    <>
      <NavBar />

      <Grid
        container
        direction="column"
        alignContent="center"
        justifyContent="center"
        gap={5}
        style={{ paddingTop: "50px" }}
      >
        <Grid item>
          {loginShow ? (
            <LoginForm showSignup={handleSignup} />
          ) : (
            <RegisterForm showSignup={handleSignup} />
          )}
        </Grid>
      </Grid>
    </>
  );
};

export default AuthPage;

Creating the LoginForm.jsx component

Now, in your frontend/src/components/ sub-folder, create the LoginForm.jsx component as shown below.


import React, { useState } from "react";
import { Paper, TextField, Grid, Button, Box } from "@mui/material";
import { useNavigate, useLocation } from "react-router-dom";
import { AuthContext } from "../context/authContext.jsx";
import axios from "axios";

const baseURL = import.meta.env.VITE_BASE_URL;
axios.defaults.baseURL = baseURL;

const LoginForm = (props) => {
  const navigate = useNavigate();
  const authContext = React.useContext(AuthContext);
  const [loginData, setLoginData] = useState({
    email: "",
    password: "",
  });

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setLoginData({ ...loginData, [name]: value });
  };

  const handleSubmit = async () => {
    console.log(loginData);

    try {
      const response = await axios.post(`${baseURL}/users/login`, {
        data: loginData,
      });

      console.log(response.data);
      if (response) {
        if (Object.keys(props)[0] !== "closeForm") {
          console.log(response.data);
          authContext.login(
            response.data.token,
            response.data.userId,
            response.data.isAdmin
          );
          if (response.data.isAdmin === true) {
            navigate("/admin");
          } else {
            navigate("/");
          }
        } else {
          authContext.login(
            response.data.token,
            response.data.userId,
            response.data.isAdmin
          );
          props.closeForm();
        }
      }
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <>
      <Paper
        elevation={3}
        style={{
          width: 500,
        }}
      >
        <Grid
          container
          direction="column"
          alignContent="center"
          justifyContent="center"
          gap={5}
          style={{ paddingTop: "50px" }}
        >
          <Grid item>
            <TextField
              label="E-mail"
              variant="outlined"
              type="text"
              name="email"
              onChange={handleInputChange}
              InputLabelProps={{
                shrink: true,
              }}
            />
          </Grid>
          <Grid item>
            <TextField
              label="Password"
              variant="outlined"
              type="password"
              name="password"
              onChange={handleInputChange}
              InputLabelProps={{
                shrink: true,
              }}
            />
          </Grid>
          <Grid item>
            <Box
              textAlign="center"
              justifyContent="center"
              sx={{ display: "flex", flexDirection: "row", gap: "10px" }}
            >
              <Button variant="contained" onClick={handleSubmit}>
                Login
              </Button>
              {Object.keys(props)[0] !== "closeForm" && (
                <Button
                  variant="contained"
                  color="secondary"
                  onClick={props.showSignup}
                >
                  Sign up
                </Button>
              )}
            </Box>
          </Grid>
          <Grid item />
        </Grid>
      </Paper>
    </>
  );
};

export default LoginForm;

Creating the RegisterForm.jsx component

Next, in the same frontend/src/components/ sub-folder, create the RegisterForm.jsx component as shown below.


import React, { useState } from "react";
import { Paper, TextField, Grid, Button, Box } from "@mui/material";
import { AuthContext } from "../context/authContext.jsx";
import { useNavigate } from "react-router-dom";
import axios from "axios";

const baseURL = import.meta.env.VITE_BASE_URL;
axios.defaults.baseURL = baseURL;

const RegisterForm = (props) => {
  const authContext = React.useContext(AuthContext);
  const navigate = useNavigate();
  const [checked, setChecked] = useState(false);
  const [userData, setUserData] = useState({
    username: "",
    email: "",
    password: "",
  });

  const handleInputChange = (event) => {
    const { name, value } = event.target;

    setUserData({ ...userData, [name]: value });
  };

  const handleRegister = async () => {
    console.log(userData);

    try {
      const response = await axios.post(`${baseURL}/users/register`, {
        data: userData,
      });

      console.log(response);
      if (response) {
        if (Object.keys(props)[0] !== "closeForm") {
          authContext.login(
            response.data.token,
            response.data.id,
            response.data.isAdmin
          );
          if (response.data.isAdmin === true) {
            navigate("/admin");
          } else {
            navigate("/");
          }
        } else {
          authContext.login(
            response.data.token,
            response.data.id,
            response.data.isAdmin
          );
          props.closeForm();
        }
      }
    } catch (e) {
      console.log(e);
    }
  };

  const handleIsAdmin = (e) => {
    console.log(e.target.value);
    console.log(checked);
    setChecked(!checked);
  };

  return (
    <>
      <Paper
        elevation={3}
        style={{
          width: 500,
        }}
      >
        <Grid
          container
          direction="column"
          alignContent="center"
          justifyContent="center"
          gap={5}
          style={{ paddingTop: "50px" }}
        >
          <Grid item>
            <TextField
              label="Username"
              variant="outlined"
              type="text"
              name="username"
              onChange={handleInputChange}
              InputLabelProps={{
                shrink: true,
              }}
            />
          </Grid>
          <Grid item>
            <TextField
              label="E-mail"
              variant="outlined"
              type="text"
              name="email"
              onChange={handleInputChange}
              InputLabelProps={{
                shrink: true,
              }}
            />
          </Grid>
          <Grid item>
            <TextField
              label="Password"
              variant="outlined"
              type="password"
              name="password"
              onChange={handleInputChange}
              InputLabelProps={{
                shrink: true,
              }}
            />
          </Grid>
          {/* 
          <Grid item>
            <FormControlLabel
              control={<Checkbox onChange={handleIsAdmin} value={checked} />}
              label="Admin"
            />
          </Grid>
          */}
          <Grid item>
            <Box
              textAlign="center"
              justifyContent="center"
              sx={{ display: "flex", flexDirection: "row", gap: "10px" }}
            >
              <Button variant="contained" onClick={handleRegister}>
                Register
              </Button>
              {Object.keys(props)[0] !== "closeForm" && (
                <Button
                  variant="contained"
                  color="secondary"
                  onClick={props.showSignup}
                >
                  Login
                </Button>
              )}
            </Box>
          </Grid>
          <Grid item />
        </Grid>
      </Paper>
    </>
  );
};

export default RegisterForm;

In your frontend/src/components sub-folder, update the NavBar.jsx component to a add a new button for login/logout. Which button is displayed will depend on whether the authentication token is present in the local storage.

You will also verify the tokens using a useEffect hook.


import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";

const NavBar = () => {
  const navigate = useNavigate();
  const [token, setToken] = useState();
  const [isAdmin, setIsAdmin] = useState();

  useEffect(() => {
    setToken(localStorage.getItem("token"));
    setIsAdmin(localStorage.getItem("isAdmin"));
  }, [token]);

  const goToHome = () => {
    navigate("/");
  };

  const goToAddProduct = () => {
    navigate("/addProduct");
  };

  const goToLogin = () => {
    navigate("/login");
  };

  const logOut = () => {
    localStorage.clear();
    navigate("/");
  };

  return (
    <>
      <Box sx={{ flexGrow: 1 }}>
        <AppBar position="static">
          <Toolbar>
            <Typography
              variant="h6"
              component="div"
              sx={{
                flexGrow: 1,
              }}
            >
              E-COM
            </Typography>
            <Button color="inherit" onClick={goToHome}>
              Home
            </Button>
            {isAdmin && (
              <Button color="inherit" onClick={goToAddProduct}>
                Add product
              </Button>
            )}
            {!token ? (
              <Button color="inherit" onClick={goToLogin}>
                Login
              </Button>
            ) : (
              <Button color="inherit" onClick={logOut}>
                LogOut
              </Button>
            )}
          </Toolbar>
        </AppBar>
      </Box>
    </>
  );
};

export default NavBar;

Updating your ProductCard.jsx component

In your frontend/src/components sub-folder, modify your ProductCard.jsx component by adding a new section called “Card Actions” where users can add or remove a product from the shopping cart:


import React, { useState, useEffect, useRef } from "react";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import Card from "@mui/material/Card";
import CardHeader from "@mui/material/CardHeader";
import CardMedia from "@mui/material/CardMedia";
import CardContent from "@mui/material/CardContent";
import CardActions from "@mui/material/CardActions";
import Rating from "@mui/material/Rating";
import Stack from "@mui/material/Stack";
import { Button } from "@mui/material";
import { useNavigate } from "react-router";
import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";

import axios from "axios";

const ProductCard = (props) => {
  const navigate = useNavigate();
  const [product, setProduct] = useState(props.product);
  const [token, setToken] = useState();
  const [isAdmin, setIsAdmin] = useState();
  const amountInputRef = useRef();

  useEffect(() => {
    setToken(localStorage.getItem("token"));
    setIsAdmin(localStorage.getItem("isAdmin"));
  }, [token]);

  const handleUpdate = (id) => {
    navigate("/update/" + id);
  };

  const handleDelete = async (id) => {
    try {
      const response = await axios.delete(`${baseURL}/delete/${id}`);
      console.log(response.data);
      if (response.data === "Product deleted!") {
        props.getProduct();
      }
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <>
      <Card
        sx={{
          width: 345,
          height: 550,
          display: "flex",
          justifyContent: "space-between",
          flexDirection: "column",
        }}
      >
        <CardHeader title={product.title} />
        <CardMedia
          component="img"
          height="194"
          image={product.images}
          alt="Product image"
        />
        <CardContent>
          <Stack direction="column" spacing={1}>
            <Typography variant="body2" color="text.secondary">
              {product.description}
            </Typography>
            <Stack direction="row" spacing={1}>
              <Rating
                name="half-rating-read"
                defaultValue={product.rating}
                precision={0.5}
                readOnly
              />
              <Typography variant="body1" color="text.primary">
                {product.rating}
              </Typography>
            </Stack>
            <Stack direction="column">
              <Typography variant="body1" color="text.primary">
                {product.price} $
              </Typography>
              <Typography variant="body1" color="text.primary">
                Price discount: {product.discountPercentage}%
              </Typography>
            </Stack>
          </Stack>
        </CardContent>

        <CardActions>
          {token && isAdmin ? (
            <Stack direction="row" gap={2}>
              <Button
                color="primary"
                variant="contained"
                onClick={() => handleUpdate(product._id)}
              >
                Update
              </Button>
              <Button
                color="error"
                variant="contained"
                onClick={() => handleDelete(product._id)}
              >
                Delete
              </Button>
            </Stack>
          ) : (
            <>
              <Stack direction="row" spacing={2}>
                <Button
                  variant="contained"
                  color="primary"
                  endIcon={<AddShoppingCartIcon />}
                  // onClick={addToCartHandler}
                >
                  + Add
                </Button>
                <TextField
                  inputRef={amountInputRef}
                  sx={{ width: 70 }}
                  label="Amount"
                  id={"amount_" + props.id}
                  type="number"
                  min={1}
                  max={5}
                  step={1}
                  defaultValue={1}
                />
              </Stack>
            </>
          )}
        </CardActions>
      </Card>
    </>
  );
};

export default ProductCard;

Preventing CORS issues

To prevent possible CORS issues, update the middleware section of your backend/index.js file as shown below:


// Middleware
const corsOptions = {
  origin: "http://localhost:5173",
  methods: ["GET", "POST", "PUT", "DELETE"],
  "content-type": "application/json;charset=UTF-8",
  "Access-Control-Allow-Origin": "*",
  allowedHeaders: ["Authorization", "Content-Type", "Accept"],
  exposedHeaders: ["*"],
  credentials: true,
};

app.use(cors(corsOptions));

Verifying your user register/login features

Your home page should now look like that shown below.

screenshot

Verify your user routes as follows:

  1. On the home page screen, click the LOGIN button at the top-right.
  2. On the next screen, click SIGN UP.
  3. Enter a Username (at least eight characters), an Email, a Password, and then click SIGN UP.   Do not forget this Password. It will not be stored in plain-text format in MongoDB.
  4. You are returned to the home page. The LOGIN button at the top-right is now replaced by the LOGOUT button.
  5. Click the LOGOUT button. This will log you out of the app.   However, you will need to reload your web browser for the LOGIN button to reppear.
  6. If your MongoDB Compass app is open, restart it. You should now see the new user you have created in the users collection. screenshot
  7. Click LOGIN again, and verify you can log in to your app with the new user's Email and Password.

Project checklist and next step

Before continuing:

icon

CHECK that your user register/login features are working correctly.