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;
Updating your NavBar.jsx
component
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.
Verify your user routes as follows:
- On the home page screen, click the LOGIN button at the top-right.
- On the next screen, click SIGN UP.
- 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.
- You are returned to the home page. The LOGIN button at the top-right is now replaced by the LOGOUT button.
- 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.
- If your MongoDB Compass app is open, restart it. You should now see the new user you have created in the users collection.
- 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:
CHECK that your user register/login features are working correctly.