From b629c350c30a486d72269d8d5fa7b4ca3f80e96d Mon Sep 17 00:00:00 2001
From: "hanan o." <77591083+heosman@users.noreply.github.com>
Date: Sun, 21 Jul 2024 20:53:23 -0700
Subject: [PATCH]
belindas-closet-nextjs_10_504_implement-role-based-authorization (#505)
* Restricted Add Product page to admins & creators
* Restricted Archived Products page to admins & creators
* Restricted Creator page to creators
* Restricted Admin page to admins
* Restricted Edit User Role page to admins
* Updated tests to fix errors and improve coverage
* Restricted Dashboard page to admins & creators
I also updated the dashboard page tests
---
app/add-product-page/page.tsx | 370 +++++++++++-----------
app/admin-page/page.tsx | 91 +++---
app/archived-products-page/page.tsx | 80 +++--
app/creator-page/page.tsx | 85 +++--
app/dashboard/page.tsx | 52 +--
app/edit-user-role-page/page.tsx | 31 +-
components/UnauthorizedPageMessage.tsx | 16 +
tests/unit/add-product-page/page.test.tsx | 67 ++--
tests/unit/admin-page/admin.test.tsx | 43 ++-
tests/unit/creator-page/page.test.tsx | 80 +++--
tests/unit/dashboard-page/page.test.tsx | 36 ++-
11 files changed, 578 insertions(+), 373 deletions(-)
create mode 100644 components/UnauthorizedPageMessage.tsx
diff --git a/app/add-product-page/page.tsx b/app/add-product-page/page.tsx
index 2000a894..dec3fcfb 100644
--- a/app/add-product-page/page.tsx
+++ b/app/add-product-page/page.tsx
@@ -21,6 +21,7 @@ import {
ProductSizePantsWaistList,
ProductSizePantsInseamList,
} from "./product-prop-list";
+import UnauthorizedPageMessage from "@/components/UnauthorizedPageMessage";
// WARNING: You won't be able to connect to local backend unless you remove the env variable below.
const URL =
process.env.BELINDAS_CLOSET_PUBLIC_API_URL || "http://localhost:3000/api";
@@ -148,202 +149,215 @@ const AddProduct = () => {
const notSizeApplicable = ["", "Pants", "Shoes"];
- return (
-
- );
+ {/* Product Upload Image Field */}
+ {productType == "" ? null : (
+
+ {previewUrl && (
+
+ )}
+
+
+ )}
+
+ {/* Submit Button */}
+ {productType == "" ? null : (
+
+ )}
+
+
+ );
+ } else {
+ return ;
+ }
};
export default AddProduct;
diff --git a/app/admin-page/page.tsx b/app/admin-page/page.tsx
index e0bc6777..e052933a 100644
--- a/app/admin-page/page.tsx
+++ b/app/admin-page/page.tsx
@@ -4,49 +4,64 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import ButtonGroup from '@mui/material/ButtonGroup'
import { useMediaQuery, useTheme } from '@mui/material';
+import UnauthorizedPageMessage from '@/components/UnauthorizedPageMessage';
+import { useState, useEffect } from 'react';
const Admin = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
- return (
-
-
- My Account
-
-
-
-
-
-
-
-
- );
+ const [userRole, setUserRole] = useState("");
+ useEffect(() => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ const userRole = JSON.parse(atob(token.split(".")[1])).role;
+ setUserRole(userRole);
+ }
+ }, []);
+
+ if ((userRole === "admin")) {
+ return (
+
+
+ My Account
+
+
+
+
+
+
+
+
+ );
+ } else {
+ return
+ }
};
export default Admin;
diff --git a/app/archived-products-page/page.tsx b/app/archived-products-page/page.tsx
index 388ac278..7dec1d2e 100644
--- a/app/archived-products-page/page.tsx
+++ b/app/archived-products-page/page.tsx
@@ -4,6 +4,7 @@ import React, { useState, useEffect, Dispatch, SetStateAction } from "react";
import ProductCard from "@/components/ProductCard";
import logo from "@/public/belinda-images/logo.png";
import { Container, Grid, Typography } from "@mui/material";
+import UnauthorizedPageMessage from "@/components/UnauthorizedPageMessage";
// WARNING: You won't be able to connect to local backend unless you remove the env variable below.
const URL = process.env.BELINDAS_CLOSET_PUBLIC_API_URL || "http://localhost:3000/api";
const placeholderImg = logo;
@@ -62,39 +63,52 @@ const ViewProduct = ({ categoryId }: { categoryId: string }) => {
);
}, [products]);
- return (
-
-
- Found {filteredProducts.length} products in Archived Products
-
-
- {filteredProducts.map((product, index) => (
-
-
-
- ))}
-
-
- );
+ const [userRole, setUserRole] = useState("");
+ useEffect(() => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ const userRole = JSON.parse(atob(token.split(".")[1])).role;
+ setUserRole(userRole);
+ }
+ }, []);
+
+ if ((userRole === "admin" || userRole === "creator")) {
+ return (
+
+
+ Found {filteredProducts.length} products in Archived Products
+
+
+ {filteredProducts.map((product, index) => (
+
+
+
+ ))}
+
+
+ );
+ } else {
+ return ;
+ }
};
export default function ProductList({
params,
diff --git a/app/creator-page/page.tsx b/app/creator-page/page.tsx
index 78866233..1955e37e 100644
--- a/app/creator-page/page.tsx
+++ b/app/creator-page/page.tsx
@@ -4,46 +4,61 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import ButtonGroup from '@mui/material/ButtonGroup';
import { useMediaQuery, useTheme } from '@mui/material';
+import UnauthorizedPageMessage from '@/components/UnauthorizedPageMessage';
+import { useState, useEffect } from 'react';
const Creator = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
- return (
-
-
- My Account
-
-
-
-
-
-
-
- );
+ const [userRole, setUserRole] = useState("");
+ useEffect(() => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ const userRole = JSON.parse(atob(token.split(".")[1])).role;
+ setUserRole(userRole);
+ }
+ }, []);
+
+ if ((userRole === "creator")) {
+ return (
+
+
+ My Account
+
+
+
+
+
+
+
+ );
+ } else {
+ return ;
+ }
};
export default Creator;
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 1704d9e2..6e24fe33 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,7 +1,8 @@
"use client";
import { Typography, Drawer, List, ListItem, ListItemText, IconButton } from "@mui/material";
-import { SetStateAction, useState } from "react";
+import { SetStateAction, useEffect, useState } from "react";
import MenuIcon from '@mui/icons-material/Menu';
+import UnauthorizedPageMessage from "@/components/UnauthorizedPageMessage";
const Dashboard = () => {
const [drawerOpen, setDrawerOpen] = useState(false);
@@ -53,24 +54,37 @@ const Dashboard = () => {
);
- return (
-
-
- {drawerContent}
-
-
- {drawerOpen ? : }
-
-
- Dashboard
-
- {/* Add your dashboard content here */}
-
- );
+ const [userRole, setUserRole] = useState("");
+ useEffect(() => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ const userRole = JSON.parse(atob(token.split(".")[1])).role;
+ setUserRole(userRole);
+ }
+ }, []);
+
+ if ((userRole === "admin" || userRole === "creator")) {
+ return (
+
+
+ {drawerContent}
+
+
+ {drawerOpen ? : }
+
+
+ Dashboard
+
+ {/* Add your dashboard content here */}
+
+ );
+ } else {
+ return ;
+ };
};
export default Dashboard;
\ No newline at end of file
diff --git a/app/edit-user-role-page/page.tsx b/app/edit-user-role-page/page.tsx
index d51852de..43292a9c 100644
--- a/app/edit-user-role-page/page.tsx
+++ b/app/edit-user-role-page/page.tsx
@@ -2,6 +2,7 @@
import React, { useState, useEffect } from "react";
import UserCard from "../../components/UserCard";
import { Stack, Typography } from "@mui/material";
+import UnauthorizedPageMessage from "@/components/UnauthorizedPageMessage";
// WARNING: You won't be able to connect to local backend unless you remove the env variable below.
const URL =
process.env.BELINDAS_CLOSET_PUBLIC_API_URL || "http://localhost:3000/api";
@@ -53,22 +54,32 @@ async function fetchUser(setUserInfo: (userInfo: User[]) => void, userToken: JWT
*/
const EditUserRolePage = () => {
const [userInfo, setUserInfo] = useState([]);
+ const [userRole, setUserRole] = useState("");
useEffect(() => {
const userToken:JWToken = localStorage.getItem("token")
+ const token = localStorage.getItem("token");
+ if (token) {
+ const userRole = JSON.parse(atob(token.split(".")[1])).role;
+ setUserRole(userRole);
+ }
fetchUser(setUserInfo,userToken);
}, []);
- return (
-
-
- User Management
-
- {userInfo.map((user, index) => (
-
- ))}
-
- );
+ if ((userRole === "admin")) {
+ return (
+
+
+ User Management
+
+ {userInfo.map((user, index) => (
+
+ ))}
+
+ );
+ } else {
+ return
+ }
};
export default EditUserRolePage;
diff --git a/components/UnauthorizedPageMessage.tsx b/components/UnauthorizedPageMessage.tsx
new file mode 100644
index 00000000..11c731a8
--- /dev/null
+++ b/components/UnauthorizedPageMessage.tsx
@@ -0,0 +1,16 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+
+const UnauthorizedPageMessage: React.FC = () => {
+ return (
+
+
+ 401 Unauthorized
+
+
+ You are not authorized to access this page
+
+
+ );
+}
+export default UnauthorizedPageMessage;
diff --git a/tests/unit/add-product-page/page.test.tsx b/tests/unit/add-product-page/page.test.tsx
index 60ebbb0f..4a8705f7 100644
--- a/tests/unit/add-product-page/page.test.tsx
+++ b/tests/unit/add-product-page/page.test.tsx
@@ -1,32 +1,59 @@
import AddProduct from '@/app/add-product-page/page'
import { ProductTypeList } from '@/app/add-product-page/product-prop-list'
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
+const roles = ['admin', 'creator', 'user'];
-describe('add-product-page tests', () => {
- // Arrange
- beforeEach(() => {
+describe.each(roles)('add-product-page tests for role: %s', (role) => {
+ beforeEach(() => {
render()
})
- it('contains title', () => {
- // Act
- const title = screen.getByText(/Add a Product/i )
- // Assert
- expect(title).toBeInTheDocument()
-
- })
+ beforeAll(() => {
+ const mockToken = btoa(JSON.stringify({ role }));
+ localStorage.setItem('token', `fakeHeader.${mockToken}.fakeSignature`);
+ });
+
+ afterAll(() => {
+ localStorage.removeItem('token');
+ });
- it('checks if each product type option are rendered after clicking', () => {
- // Act
- const selectType = screen.getByLabelText(/Product Type/i)
- fireEvent.mouseDown(selectType)
+ if (role === 'user') {
+ it('displays UnauthorizedPageMessage for user role', async () => {
+ await waitFor(() => {
+ expect(screen.getByText(/401 Unauthorized/i)).toBeInTheDocument();
+ });
- // Assert
- Object.values(ProductTypeList).forEach((type) => {
- const option = screen.getByRole('option', { name: new RegExp(type, 'i') })
- expect(option).toBeInTheDocument()
+ const unauthorizedMessage = screen.getByText(/You are not authorized to access this page/i);
+ expect(unauthorizedMessage).toBeInTheDocument();
+ });
+ } else {
+ it('contains title', async () => {
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+ });
+ // Act
+ const title = screen.getByText(/Add a Product/i )
+
+ // Assert
+ expect(title).toBeInTheDocument()
+
})
- })
+
+ it('checks if each product type option are rendered after clicking', async () => {
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+ });
+ // Act
+ const selectType = screen.getByLabelText(/Product Type/i)
+ fireEvent.mouseDown(selectType)
+
+ // Assert
+ Object.values(ProductTypeList).forEach((type) => {
+ const option = screen.getByRole('option', { name: new RegExp(type, 'i') })
+ expect(option).toBeInTheDocument()
+ })
+ })
+ }
})
\ No newline at end of file
diff --git a/tests/unit/admin-page/admin.test.tsx b/tests/unit/admin-page/admin.test.tsx
index bec2e4db..75691f1a 100644
--- a/tests/unit/admin-page/admin.test.tsx
+++ b/tests/unit/admin-page/admin.test.tsx
@@ -1,12 +1,41 @@
import '@testing-library/jest-dom'
-import { render, screen } from '@testing-library/react'
+import { render, screen, waitFor } from '@testing-library/react'
import Admin from '../../../app/admin-page/page';
-describe('Admin', () => {
- it('renders the buttons', () => {
- render();
- const links = screen.getAllByRole('link'); // Update to search for links
- const buttonText = screen.getByText('Edit User Roles');
- expect(links && buttonText).toBeInTheDocument();
+const roles = ['admin', 'creator', 'user'];
+
+describe.each(roles)('admin-page tests for role: %s', (role) => {
+ beforeEach(() => {
+ render()
+ })
+
+ beforeAll(() => {
+ const mockToken = btoa(JSON.stringify({ role }));
+ localStorage.setItem('token', `fakeHeader.${mockToken}.fakeSignature`);
+ });
+
+ afterAll(() => {
+ localStorage.removeItem('token');
});
+
+ if (role === 'creator' || role === 'user') {
+ it('displays UnauthorizedPageMessage for unauthorized role', async () => {
+ await waitFor(() => {
+ expect(screen.getByText(/401 Unauthorized/i)).toBeInTheDocument();
+ });
+
+ const unauthorizedMessage = screen.getByText(/You are not authorized to access this page/i);
+ expect(unauthorizedMessage).toBeInTheDocument();
+ });
+ } else {
+ it('renders the buttons', async () => {
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+ });
+
+ const links = screen.getAllByRole('link'); // Update to search for links
+ const buttonText = screen.getByText('Edit User Roles');
+ expect(links && buttonText).toBeInTheDocument();
+ });
+ };
});
\ No newline at end of file
diff --git a/tests/unit/creator-page/page.test.tsx b/tests/unit/creator-page/page.test.tsx
index f9df60f7..3c4aacfa 100644
--- a/tests/unit/creator-page/page.test.tsx
+++ b/tests/unit/creator-page/page.test.tsx
@@ -1,34 +1,62 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Creator from '@/app/creator-page/page';
-describe('Creator component tests', () => {
- it('displays the placeholder text and interacts with buttons', async () => {
- render();
-
- // Check for the heading text
- const heading = screen.getByRole('heading', { level: 1 });
- expect(heading).toBeInTheDocument();
- expect(heading.textContent).toBe('My Account');
-
- // Check for the links and their href attributes
- const addButton = screen.getByRole('link', { name: /Add Product/i });
- const allProductsButton = screen.getByRole('link', { name: /All Products/i });
- const archivedProductsButton = screen.getByRole('link', { name: /Archived Products/i })
-
- expect(addButton).toBeInTheDocument();
- expect(addButton).toHaveAttribute('href', '/add-product-page');
-
- expect(allProductsButton).toBeInTheDocument();
- expect(allProductsButton).toHaveAttribute('href', '/category-page/all-products');
+const roles = ['admin', 'creator', 'user'];
- expect(archivedProductsButton).toBeInTheDocument();
- expect(archivedProductsButton).toHaveAttribute('href', '/archived-products-page');
+describe.each(roles)('creator-page tests for role: %s', (role) => {
+ beforeEach(() => {
+ render()
+ })
+
+ beforeAll(() => {
+ const mockToken = btoa(JSON.stringify({ role }));
+ localStorage.setItem('token', `fakeHeader.${mockToken}.fakeSignature`);
+ });
- // Simulate link clicks
- await userEvent.click(addButton);
- await userEvent.click(allProductsButton);
- await userEvent.click(archivedProductsButton);
+ afterAll(() => {
+ localStorage.removeItem('token');
});
+
+ if (role === 'admin' || role === 'user') {
+ it('displays UnauthorizedPageMessage for unauthorized role', async () => {
+ await waitFor(() => {
+ expect(screen.getByText(/401 Unauthorized/i)).toBeInTheDocument();
+ });
+
+ const unauthorizedMessage = screen.getByText(/You are not authorized to access this page/i);
+ expect(unauthorizedMessage).toBeInTheDocument();
+ });
+ } else {
+ it('displays the placeholder text and interacts with buttons', async () => {
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+ });
+
+ // Check for the heading text
+ const heading = screen.getByRole('heading', { level: 1 });
+ expect(heading).toBeInTheDocument();
+ expect(heading.textContent).toBe('My Account');
+
+ // Check for the links and their href attributes
+ const addButton = screen.getByRole('link', { name: /Add Product/i });
+ const allProductsButton = screen.getByRole('link', { name: /All Products/i });
+ const archivedProductsButton = screen.getByRole('link', { name: /Archived Products/i })
+
+ expect(addButton).toBeInTheDocument();
+ expect(addButton).toHaveAttribute('href', '/add-product-page');
+
+ expect(allProductsButton).toBeInTheDocument();
+ expect(allProductsButton).toHaveAttribute('href', '/category-page/all-products');
+
+ expect(archivedProductsButton).toBeInTheDocument();
+ expect(archivedProductsButton).toHaveAttribute('href', '/archived-products-page');
+
+ // Simulate link clicks
+ await userEvent.click(addButton);
+ await userEvent.click(allProductsButton);
+ await userEvent.click(archivedProductsButton);
+ });
+ };
});
\ No newline at end of file
diff --git a/tests/unit/dashboard-page/page.test.tsx b/tests/unit/dashboard-page/page.test.tsx
index 82d66729..f6b53dc3 100644
--- a/tests/unit/dashboard-page/page.test.tsx
+++ b/tests/unit/dashboard-page/page.test.tsx
@@ -1,16 +1,38 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import Dashboard from '@/app/dashboard/page';
-describe('Dashboard component tests', () => {
- it('renders heading with correct text and style', () => {
- render();
+const roles = ['admin', 'creator', 'user'];
- const heading = screen.getByRole('heading', { level: 1 });
+describe.each(roles)('dashboard-page tests for role: %s', (role) => {
+ beforeEach(() => {
+ render()
+ })
- expect(heading).toBeInTheDocument();
- expect(heading).toHaveTextContent('Dashboard');
+ beforeAll(() => {
+ const mockToken = btoa(JSON.stringify({ role }));
+ localStorage.setItem('token', `fakeHeader.${mockToken}.fakeSignature`);
});
+ afterAll(() => {
+ localStorage.removeItem('token');
+ });
+
+ if (role === 'user') {
+ it('displays UnauthorizedPageMessage for user role', async () => {
+ await waitFor(() => {
+ expect(screen.getByText(/401 Unauthorized/i)).toBeInTheDocument();
+ });
+
+ const unauthorizedMessage = screen.getByText(/You are not authorized to access this page/i);
+ expect(unauthorizedMessage).toBeInTheDocument();
+ });
+ } else {
+ it('renders heading with correct text and style', () => {
+ const heading = screen.getByRole('heading', { level: 1 });
+ expect(heading).toBeInTheDocument();
+ expect(heading).toHaveTextContent('Dashboard');
+ });
+ };
});
\ No newline at end of file