From c3ed50dd5e2c6fe1fd30c2b2bba4289508dd9668 Mon Sep 17 00:00:00 2001 From: Bertin M Date: Tue, 23 Jul 2024 02:07:07 +0200 Subject: [PATCH] User: this pr enables user to logout from one's session --- docker-compose.yml | 67 +++++--- src/__test__/LogoutContext.test.tsx | 126 +++++++++++++++ src/__test__/addProducts.test.tsx | 109 +++++++++---- src/__test__/login.test.tsx | 153 +++++++++++++++--- src/components/common/ProfileDropdown.tsx | 86 +++++----- src/components/dashboard/Header.tsx | 58 ++++--- src/components/dashboard/HomeButton.tsx | 23 +++ src/components/dashboard/SideBar.tsx | 16 +- .../dashboard/admin/AdminSideBar.tsx | 17 +- .../dashboard/admin/LogoutContext.tsx | 57 +++++++ .../dashboard/admin/LogoutModal.tsx | 34 ++++ src/dashboard/admin/Products.tsx | 4 +- src/pages/Login.tsx | 25 ++- src/redux/ProtectAdminDashboard.tsx | 45 +++--- src/redux/ProtectDashboard.tsx | 41 +++-- src/redux/api/api.ts | 6 + src/routes/AppRoutes.tsx | 113 ++++++------- src/utils/logoutUtils.ts | 24 +++ 18 files changed, 757 insertions(+), 247 deletions(-) create mode 100644 src/__test__/LogoutContext.test.tsx create mode 100644 src/components/dashboard/HomeButton.tsx create mode 100644 src/components/dashboard/admin/LogoutContext.tsx create mode 100644 src/components/dashboard/admin/LogoutModal.tsx create mode 100644 src/utils/logoutUtils.ts diff --git a/docker-compose.yml b/docker-compose.yml index 3c813eb..0bb4fd4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,21 @@ version: '3.1' services: - web: - build: - context: . - dockerfile: Dockerfile - container_name: eagle-ec-fe-container - image: mugemanebertin/eagle-ec-fe - ports: - - "5173:5173" - env_file: - - .env - + # Backend Service backend: image: mugemanebertin/eagle_ec_be:latest - container_name: eagle-ec-be-container + container_name: express-server-container ports: - - "499:499" - command: sh -c "npm run migrate && (npm run seed || true) && npm run dev" + - "${PORT}:${PORT}" + volumes: + - ./backend:/usr/src/app + - /usr/src/app/node_modules + command: sh -c "npm run migrate && npm run seed || true && npm run dev" depends_on: - - db + - postgres_db - redis + env_file: + - ./.env environment: - DB_CONNECTION=${DOCKER_DB_CONNECTION} - JWT_SECRET=${JWT_SECRET} @@ -35,12 +30,14 @@ services: - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL} - - REDIS_HOST=redis - - REDIS_PORT=6379 + - FE_URL=${FE_URL} + networks: + - eagle-ec - db: + # PostgreSQL Database Service + postgres_db: image: postgres:latest - container_name: eagle-ec-db-container + container_name: postgres-db-container ports: - "5433:5432" environment: @@ -49,11 +46,41 @@ services: - POSTGRES_DB=${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data + networks: + - eagle-ec + # Redis Service redis: image: redis:latest + container_name: redis-container ports: - "6379:6379" + networks: + - eagle-ec + + # Web Frontend Service + frontend: + build: + context: . + dockerfile: Dockerfile + container_name: eagle-ec-fe-container + image: mugemanebertin/eagle-ec-fe + ports: + - "5173:5173" + env_file: + - ./.env + volumes: + - ./frontend:/app + - /app/node_modules + networks: + - eagle-ec + command: npm run dev -- --host + depends_on: + - backend volumes: - postgres_data: \ No newline at end of file + postgres_data: + +networks: + eagle-ec: + driver: bridge diff --git a/src/__test__/LogoutContext.test.tsx b/src/__test__/LogoutContext.test.tsx new file mode 100644 index 0000000..0f143a3 --- /dev/null +++ b/src/__test__/LogoutContext.test.tsx @@ -0,0 +1,126 @@ +import "@testing-library/jest-dom"; +import React from "react"; +import { + render, screen, fireEvent, waitFor, +} from "@testing-library/react"; + +import { + LogoutProvider, + useLogout, +} from "../components/dashboard/admin/LogoutContext"; +import api from "../redux/api/api"; + +// Mock the API and localStorage +jest.mock("../redux/api/api"); +const mockPost = api.post as jest.MockedFunction; + +// Mock LogoutModal component +jest.mock("../components/dashboard/admin/LogoutModal", () => ({ + __esModule: true, + default: ({ isOpen, onClose, onConfirm }: any) => + (isOpen ? ( +
+
+

Confirm Logout

+

Are you sure you want to logout?

+
+ + +
+
+
+ ) : null), +})); + +const TestComponent: React.FC = () => { + const { openLogoutModal } = useLogout(); + return ; +}; + +describe("LogoutProvider Component", () => { + beforeAll(() => { + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterAll(() => { + (console.error as jest.Mock).mockRestore(); + }); + + it("should render LogoutProvider and trigger logout modal", () => { + render( + + + , + ); + + const openModalButton = screen.getByText("Open Logout Modal"); + fireEvent.click(openModalButton); + + // Verify that the modal is rendered + expect(screen.getByText("Confirm Logout")).toBeInTheDocument(); + expect( + screen.getByText("Are you sure you want to logout?"), + ).toBeInTheDocument(); + expect(screen.getByText("Cancel")).toBeInTheDocument(); + expect(screen.getByText("Logout")).toBeInTheDocument(); + }); + + it("should call confirmLogout and perform logout", async () => { + localStorage.setItem("accessToken", "mockToken"); + mockPost.mockResolvedValue({}); + + render( + + + , + ); + + fireEvent.click(screen.getByText("Open Logout Modal")); + fireEvent.click(screen.getByText("Logout")); + + await waitFor(() => { + expect(mockPost).toHaveBeenCalledWith( + "/users/logout", + {}, + { + headers: { + Authorization: `Bearer mockToken`, + Accept: "*/*", + }, + }, + ); + expect(localStorage.getItem("accessToken")).toBeNull(); + }); + }); + + it("should handle logout failure", async () => { + localStorage.setItem("accessToken", "mockToken"); + mockPost.mockRejectedValue(new Error("Network error")); + + render( + + + , + ); + + fireEvent.click(screen.getByText("Open Logout Modal")); + fireEvent.click(screen.getByText("Logout")); + + await waitFor(() => { + expect(console.error).toHaveBeenCalledWith( + "Failed to logout:", + expect.any(Error), + ); + }); + }); +}); diff --git a/src/__test__/addProducts.test.tsx b/src/__test__/addProducts.test.tsx index ba3524a..6624942 100644 --- a/src/__test__/addProducts.test.tsx +++ b/src/__test__/addProducts.test.tsx @@ -16,6 +16,7 @@ import TextInput from "../components/common/TextInput"; import AddProduct from "../dashboard/sellers/AddProduct"; import FileUpload from "../components/dashboard/FileUpload"; import { fetchCategories } from "../redux/reducers/categoriesSlice"; +import { LogoutProvider } from "../components/dashboard/admin/LogoutContext"; beforeAll(() => { const mockPayload = { @@ -64,7 +65,13 @@ describe("FileUpload component", () => { }); it("should render the component with no files", () => { - render(); + render( + + {" "} + {/* Wrap with LogoutProvider */} + + , + ); expect(screen.getByText("Browse Images...")).toBeInTheDocument(); expect(screen.getByText(/Browse Images.../i)).toBeInTheDocument(); @@ -73,14 +80,22 @@ describe("FileUpload component", () => { it("should render the component with files", () => { render( - , + + {" "} + {/* Wrap with LogoutProvider */} + + , ); expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument(); }); it("should call remove when the remove button is clicked", () => { render( - , + + {" "} + {/* Wrap with LogoutProvider */} + + , ); const removeButton = screen.getByRole("button", { name: /remove/i }); @@ -95,9 +110,13 @@ describe("test seller dashboard components", () => { render( - -

Seller's dashboard

-
+ + {" "} + {/* Wrap with LogoutProvider */} + +

Seller's dashboard

+
+
, ); @@ -110,7 +129,11 @@ describe("test seller dashboard components", () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); @@ -142,11 +165,15 @@ describe("test seller dashboard components", () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); @@ -169,12 +196,16 @@ describe("test seller dashboard components", () => { const Component = () => { const { register } = useForm(); return ( - + + {" "} + {/* Wrap with LogoutProvider */} + + ); }; @@ -198,13 +229,17 @@ describe("test seller dashboard components", () => { const Component = () => { const { register } = useForm(); return ( - + + {" "} + {/* Wrap with LogoutProvider */} + + ); }; @@ -225,7 +260,11 @@ describe("test seller dashboard components", () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); @@ -261,7 +300,11 @@ describe("test seller dashboard components", () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); @@ -278,7 +321,11 @@ it("should display error messages for invalid inputs", async () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); @@ -294,7 +341,11 @@ it("should open and close AddCategory modal", () => { render( - + + {" "} + {/* Wrap with LogoutProvider */} + + , ); diff --git a/src/__test__/login.test.tsx b/src/__test__/login.test.tsx index 304065c..470224c 100644 --- a/src/__test__/login.test.tsx +++ b/src/__test__/login.test.tsx @@ -11,6 +11,7 @@ import Login from "../pages/Login"; import store from "../redux/store"; import { login } from "../redux/api/loginApiSlice"; +// Mock Adapter for API requests const mock = new MockAdapter(api); const mockData = { @@ -18,36 +19,40 @@ const mockData = { token: "mockAccessToken", }; +// Setup and teardown for network requests const mockNetworkRequests = () => { - mock.onPost("/").reply(200, mockData); + mock.onPost("/login").reply(200, mockData); }; + const unMockNetworkRequests = () => { mock.resetHistory(); }; -beforeEach(() => { - mockNetworkRequests(); -}); - -afterEach(() => { - unMockNetworkRequests(); -}); - +// Mock localStorage const localStorageMock = (() => { - let lstore: Record = {}; + let store: Record = {}; return { - getItem: (key: string) => lstore[key] || null, + getItem: (key: string) => store[key] || null, setItem: (key: string, value: string) => { - lstore[key] = value?.toString(); + store[key] = value?.toString(); }, clear: () => { - lstore = {}; + store = {}; }, }; })(); + Object.defineProperty(window, "localStorage", { value: localStorageMock }); -describe("test user login", () => { +beforeEach(() => { + mockNetworkRequests(); +}); + +afterEach(() => { + unMockNetworkRequests(); +}); + +describe("Login Component", () => { test("should render login page", () => { render( @@ -57,20 +62,126 @@ describe("test user login", () => { , ); - const logo = screen.getByText("eagles"); + + const logo = screen.getByText(/login to/i); // Adjust if needed const emailInput = screen.getByPlaceholderText("Email"); const passwordInput = screen.getByPlaceholderText("Password"); const loginButton = screen.getByRole("button", { name: /login/i }); expect(logo).toBeInTheDocument(); - userEvent.type(emailInput, "jaboinnovates@gmail.com"); - userEvent.type(passwordInput, "Test@123"); + expect(emailInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(loginButton).toBeInTheDocument(); + }); + + test("should handle form validation", async () => { + render( + + + + + + , + ); + + const emailInput = screen.getByPlaceholderText("Email"); + const passwordInput = screen.getByPlaceholderText("Password"); + const loginButton = screen.getByRole("button", { name: /login/i }); + + userEvent.type(emailInput, "invalid-email"); + userEvent.type(passwordInput, "short"); + userEvent.click(loginButton); + }); + + test("should handle successful login", async () => { + render( + + + + + + , + ); + + const emailInput = screen.getByPlaceholderText("Email"); + const passwordInput = screen.getByPlaceholderText("Password"); + const loginButton = screen.getByRole("button", { name: /login/i }); + userEvent.type(emailInput, "testuser@example.com"); + userEvent.type(passwordInput, "password123"); userEvent.click(loginButton); - expect(emailInput).toBeDefined(); - expect(passwordInput).toBeDefined(); - expect(localStorage.getItem("accessToken")).toBeDefined(); + expect(localStorage.getItem("accessToken")).toBe(null); + }); + + test("should handle failed login", async () => { + mock.onPost("/login").reply(401, { message: "Invalid credentials" }); + + render( + + + + + + , + ); + + const emailInput = screen.getByPlaceholderText("Email"); + const passwordInput = screen.getByPlaceholderText("Password"); + const loginButton = screen.getByRole("button", { name: /login/i }); + + userEvent.type(emailInput, "wronguser@example.com"); + userEvent.type(passwordInput, "wrongpassword"); + userEvent.click(loginButton); + expect(localStorage.getItem("accessToken")).toBe(null); + }); + + test("should navigate based on user role after login", async () => { + const mockAdminToken = "mockAdminToken"; + const mockSellerToken = "mockSellerToken"; + const mockCustomerToken = "mockCustomerToken"; + const mockAdminData = { message: "success", token: mockAdminToken }; + const mockSellerData = { message: "success", token: mockSellerToken }; + const mockCustomerData = { message: "success", token: mockCustomerToken }; + + mock.onPost("/login").reply((config) => { + if (config.data.includes("admin@example.com")) return [200, mockAdminData]; + if (config.data.includes("seller@example.com")) return [200, mockSellerData]; + return [200, mockCustomerData]; + }); + + render( + + + + + + , + ); + + // Test for admin role + userEvent.type(screen.getByPlaceholderText("Email"), "admin@example.com"); + userEvent.type(screen.getByPlaceholderText("Password"), "password123"); + userEvent.click(screen.getByRole("button", { name: /login/i })); + + expect(localStorage.getItem("accessToken")).toBe(null); + + // Test for seller role + userEvent.type(screen.getByPlaceholderText("Email"), "seller@example.com"); + userEvent.type(screen.getByPlaceholderText("Password"), "password123"); + userEvent.click(screen.getByRole("button", { name: /login/i })); + + expect(localStorage.getItem("accessToken")).toBe(null); + + // Test for customer role + userEvent.type( + screen.getByPlaceholderText("Email"), + "customer@example.com", + ); + userEvent.type(screen.getByPlaceholderText("Password"), "password123"); + userEvent.click(screen.getByRole("button", { name: /login/i })); + + expect(localStorage.getItem("accessToken")).toBe(null); }); it("should handle initial state", () => { @@ -90,7 +201,7 @@ describe("test user login", () => { }); it("should handle login fulfilled", () => { - const mockLoginData = { message: "Youre're logged in!" }; + const mockLoginData: any = { message: "You're logged in!" }; // @ts-ignore store.dispatch(login.fulfilled(mockLoginData, "", {})); expect(store.getState().login).toEqual({ diff --git a/src/components/common/ProfileDropdown.tsx b/src/components/common/ProfileDropdown.tsx index bc4322f..1f38906 100644 --- a/src/components/common/ProfileDropdown.tsx +++ b/src/components/common/ProfileDropdown.tsx @@ -1,64 +1,70 @@ import React from "react"; import { Link } from "react-router-dom"; +import { useLogout } from "../dashboard/admin/LogoutContext"; + interface ProfileDropdownProps { userInfo: any; } -const ProfileDropdown: React.FC = ({ userInfo }) => ( -
-
    - {userInfo ? ( - <> -
  • - - Profile - -
  • - {userInfo.roleId === 1 && ( +const ProfileDropdown: React.FC = ({ userInfo }) => { + const { openLogoutModal } = useLogout(); + + return ( +
    +
      + {userInfo ? ( + <>
    • - My Orders + Profile
    • - )} - {(userInfo.roleId === 2 || userInfo.roleId === 3) && ( + {userInfo.roleId === 1 && ( +
    • + + My Orders + +
    • + )} + {(userInfo.roleId === 2 || userInfo.roleId === 3) && ( +
    • + + My Dashboard + +
    • + )}
    • - - My Dashboard - + Logout +
    • - )} + + ) : (
    • - Logout + Login
    • - - ) : ( -
    • - - Login - -
    • - )} -
    -
    -); + )} +
+
+ ); +}; export default ProfileDropdown; diff --git a/src/components/dashboard/Header.tsx b/src/components/dashboard/Header.tsx index 48c8aef..1ee19b6 100644 --- a/src/components/dashboard/Header.tsx +++ b/src/components/dashboard/Header.tsx @@ -1,9 +1,10 @@ import { FiSearch, FiMenu } from "react-icons/fi"; -import { FaRegBell, FaCircle } from "react-icons/fa"; +import { FaRegBell, FaCircle, FaUserAlt } from "react-icons/fa"; import { FaAngleDown } from "react-icons/fa6"; import { BiSolidMessageDetail } from "react-icons/bi"; import React, { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; + import { getProfile } from "../../redux/reducers/profileSlice"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { RootState } from "../../redux/store"; @@ -25,6 +26,7 @@ const Header: React.FC = ({ toggleSidebar }) => { useEffect(() => { dispatch(getProfile()); }, [dispatch]); + const profileImage = profile?.profileImage; const userInfo = localStorage.getItem("accessToken") ? JSON.parse(atob(localStorage.getItem("accessToken")!.split(".")[1])) @@ -54,14 +56,12 @@ const Header: React.FC = ({ toggleSidebar }) => { <>
- -
- -
+ +
-
-
- +
+
+ = ({ toggleSidebar }) => {
setTarget(e.currentTarget)} > - -

+ +

{unreadCount}

- User Avatar setShowDropdown(!showDropdown)} - /> -

{profile?.fullName}

- setShowDropdown(!showDropdown)} /> - {showDropdown && } -
+ className="relative flex items-center h-full space-x-1 cursor-pointer" + ref={profileDropdownRef} + > + {profileImage ? ( + User Avatar setShowDropdown(!showDropdown)} + /> + ) : ( + setShowDropdown(!showDropdown)} + /> + )} +

{profile?.fullName}

+ setShowDropdown(!showDropdown)} + /> + {showDropdown && } +
diff --git a/src/components/dashboard/HomeButton.tsx b/src/components/dashboard/HomeButton.tsx new file mode 100644 index 0000000..ccc2b31 --- /dev/null +++ b/src/components/dashboard/HomeButton.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { FiLogOut } from "react-icons/fi"; +import { useNavigate } from "react-router-dom"; + +const HomeButton: React.FC = () => { + const navigate = useNavigate(); + + const handleRedirect = () => { + navigate("/"); + }; + + return ( + + ); +}; + +export default HomeButton; diff --git a/src/components/dashboard/SideBar.tsx b/src/components/dashboard/SideBar.tsx index e9e7bca..7110c52 100644 --- a/src/components/dashboard/SideBar.tsx +++ b/src/components/dashboard/SideBar.tsx @@ -3,10 +3,11 @@ import { RiHome3Line, RiAddBoxLine } from "react-icons/ri"; import { IoBriefcaseOutline, IoSettingsOutline } from "react-icons/io5"; import { AiFillProduct } from "react-icons/ai"; import { MdInsertChartOutlined } from "react-icons/md"; -import { FiLogOut } from "react-icons/fi"; import { Link, NavLink, useLocation } from "react-router-dom"; import { FaCircle } from "react-icons/fa"; +import HomeButton from "./HomeButton"; + interface SidebarProps { isOpen: boolean; } @@ -25,15 +26,15 @@ const SideBar: React.FC = ({ isOpen }) => { isOpen ? "translate-x-0" : "-translate-x-full" } lg:translate-x-0`} > -
-
+
+
-
+
eagles
-
-
- - Log Out -
+
); diff --git a/src/components/dashboard/admin/AdminSideBar.tsx b/src/components/dashboard/admin/AdminSideBar.tsx index 1a17353..e2b995f 100644 --- a/src/components/dashboard/admin/AdminSideBar.tsx +++ b/src/components/dashboard/admin/AdminSideBar.tsx @@ -1,14 +1,14 @@ -// AdminSideBar.tsx import React from "react"; import { RiHome3Line } from "react-icons/ri"; import { TbUsers } from "react-icons/tb"; import { AiFillProduct } from "react-icons/ai"; import { SiSimpleanalytics } from "react-icons/si"; import { IoSettingsOutline } from "react-icons/io5"; -import { FiLogOut } from "react-icons/fi"; import { FaCircle } from "react-icons/fa"; import { useLocation } from "react-router-dom"; +import HomeButton from "../HomeButton"; + import NavItem from "./NavItem"; interface SidebarProps { @@ -48,11 +48,9 @@ const AdminSideBar: React.FC = ({ isOpen }) => { return (
@@ -73,10 +71,7 @@ const AdminSideBar: React.FC = ({ isOpen }) => { ))}
-
- - Log Out -
+
); diff --git a/src/components/dashboard/admin/LogoutContext.tsx b/src/components/dashboard/admin/LogoutContext.tsx new file mode 100644 index 0000000..70f589d --- /dev/null +++ b/src/components/dashboard/admin/LogoutContext.tsx @@ -0,0 +1,57 @@ +import React, { + createContext, + useContext, + useState, + ReactNode, + useMemo, +} from "react"; + +import { performLogout } from "../../../utils/logoutUtils"; + +import LogoutModal from "./LogoutModal"; + +interface LogoutContextType { + openLogoutModal: () => void; +} + +const LogoutContext = createContext(undefined); + +export const useLogout = () => { + const context = useContext(LogoutContext); + if (!context) { + throw new Error("useLogout must be used within a LogoutProvider"); + } + return context; +}; + +export const LogoutProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const openLogoutModal = () => { + setIsModalOpen(true); + }; + + const closeLogoutModal = () => { + setIsModalOpen(false); + }; + + const handleLogout = async () => { + await performLogout(); + closeLogoutModal(); + }; + + const value = useMemo(() => ({ openLogoutModal }), []); + + return ( + + {children} + + + ); +}; diff --git a/src/components/dashboard/admin/LogoutModal.tsx b/src/components/dashboard/admin/LogoutModal.tsx new file mode 100644 index 0000000..301184e --- /dev/null +++ b/src/components/dashboard/admin/LogoutModal.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +const LogoutModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; +}> = ({ isOpen, onClose, onConfirm }) => { + if (!isOpen) return null; + + return ( +
+
+

Confirm Logout

+

Are you sure you want to logout?

+
+ + +
+
+
+ ); +}; + +export default LogoutModal; diff --git a/src/dashboard/admin/Products.tsx b/src/dashboard/admin/Products.tsx index 70315f4..3a6bc08 100644 --- a/src/dashboard/admin/Products.tsx +++ b/src/dashboard/admin/Products.tsx @@ -2,9 +2,9 @@ import React from "react"; import AdminLayout from "../../components/layouts/AdminLayout"; -const Products = (props) => ( +const ProductsForAdmin = (props) => (
Products page
); -export default Products; +export default ProductsForAdmin; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index dc68a61..36565e5 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -74,9 +74,30 @@ const Login = () => { }; const token = getTokenFromUrl(); + if (token) { localStorage.setItem("accessToken", token); - navigate("/"); + + try { + const decodedToken = decodeToken(token); + // @ts-ignore + const roleId = decodedToken?.roleId; + + if (roleId === 3) { + navigate("/admin/dashboard"); + } else if (roleId === 2) { + navigate("/dashboard"); + } else if (roleId === 1) { + navigate("/"); + } else { + navigate("/login"); + } + } catch (error) { + console.error("Error decoding token:", error); + navigate("/login"); + } + } else { + navigate("/login"); } }, [navigate]); @@ -88,7 +109,7 @@ const Login = () => {
-

+

Login to diff --git a/src/redux/ProtectAdminDashboard.tsx b/src/redux/ProtectAdminDashboard.tsx index baca0ed..5323833 100644 --- a/src/redux/ProtectAdminDashboard.tsx +++ b/src/redux/ProtectAdminDashboard.tsx @@ -2,36 +2,45 @@ import React, { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { decodeToken, isExpired } from "react-jwt"; -interface ProtectDashboardProps { +import { performLogout } from "../utils/logoutUtils"; + +interface ProtectAdminDashboardProps { children: JSX.Element; } -const ProtectAdminDashboard: React.FC = ({ +const ProtectAdminDashboard: React.FC = ({ children, }) => { const [isAuthorized, setIsAuthorized] = useState(true); useEffect(() => { - const accessToken = localStorage.getItem("accessToken"); - - if (!accessToken) { - setIsAuthorized(false); - return; - } + const checkAuthorization = async () => { + const accessToken = localStorage.getItem("accessToken"); - try { - const decodedToken = decodeToken(accessToken); - const isTokenExpired = isExpired(accessToken); + if (!accessToken) { + setIsAuthorized(false); + return; + } - // @ts-ignore - if (!decodedToken || isTokenExpired || decodedToken.roleId !== 3) { + try { + const decodedToken = decodeToken(accessToken); + const isTokenExpired = isExpired(accessToken); + // @ts-ignore + if (!decodedToken || isTokenExpired || decodedToken.roleId !== 3) { + if (isTokenExpired) { + await performLogout(); + } + setIsAuthorized(false); + } else { + setIsAuthorized(true); + } + } catch (error) { + await performLogout(); setIsAuthorized(false); - } else { - setIsAuthorized(true); } - } catch (error) { - setIsAuthorized(false); - } + }; + + checkAuthorization(); }, []); if (!isAuthorized) { diff --git a/src/redux/ProtectDashboard.tsx b/src/redux/ProtectDashboard.tsx index 3160fd8..5fd9f0a 100644 --- a/src/redux/ProtectDashboard.tsx +++ b/src/redux/ProtectDashboard.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { decodeToken, isExpired } from "react-jwt"; +import { performLogout } from "../utils/logoutUtils"; + interface ProtectDashboardProps { children: JSX.Element; } @@ -10,26 +12,33 @@ const ProtectDashboard: React.FC = ({ children }) => { const [isAuthorized, setIsAuthorized] = useState(true); useEffect(() => { - const accessToken = localStorage.getItem("accessToken"); - - if (!accessToken) { - setIsAuthorized(false); - return; - } + const checkAuthorization = async () => { + const accessToken = localStorage.getItem("accessToken"); - try { - const decodedToken = decodeToken(accessToken); - const isTokenExpired = isExpired(accessToken); + if (!accessToken) { + setIsAuthorized(false); + return; + } - // @ts-ignore - if (!decodedToken || isTokenExpired || decodedToken.roleId !== 2) { + try { + const decodedToken = decodeToken(accessToken); + const isTokenExpired = isExpired(accessToken); + // @ts-ignore + if (!decodedToken || isTokenExpired || decodedToken.roleId !== 2) { + if (isTokenExpired) { + await performLogout(); // Call the logout function + } + setIsAuthorized(false); + } else { + setIsAuthorized(true); + } + } catch (error) { + await performLogout(); // Handle any errors by logging out setIsAuthorized(false); - } else { - setIsAuthorized(true); } - } catch (error) { - setIsAuthorized(false); - } + }; + + checkAuthorization(); }, []); if (!isAuthorized) { diff --git a/src/redux/api/api.ts b/src/redux/api/api.ts index 0da8ef6..5bdf85c 100644 --- a/src/redux/api/api.ts +++ b/src/redux/api/api.ts @@ -14,11 +14,17 @@ api.interceptors.response.use( (response) => response, (error) => { const excludeRoute = "/"; + const currentPath = window.location.pathname; + const pathParts = currentPath.split("/"); + const isSingleProductView = pathParts.length === 3 + && pathParts[1] === "products" + && !Number.isNaN(Number(pathParts[2])); if ( !isNotificationSent && error.response && error.response.status === 401 && window.location.pathname !== excludeRoute + && !isSingleProductView ) { isNotificationSent = true; if (navigateFunction) { diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 7d68147..3b64d9f 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -23,29 +23,30 @@ import Dashboard from "../dashboard/admin/Dashboard"; import CartManagement from "../pages/CartManagement"; import SellerNotifications from "../dashboard/sellers/SellerNotifications"; import NotificationDetail from "../dashboard/sellers/NotificationDetail"; -import { setNavigate } from "../redux/api/api"; import ChatPage from "../pages/ChatPage"; import BuyerWishesList from "../pages/Wishes"; import Wishes from "../dashboard/sellers/wishesList"; -// import { setNavigateFunction } from "../redux/api/api"; import SellerOrder from "../components/dashboard/orders/SellerOrder"; import BuyerOrders from "../pages/BuyerOrders"; import SignupVerification from "../pages/SignupVerification"; import SmoothScroll from "../utils/SmoothScroll"; import NotFound from "../pages/NotFound"; import Payment, { SuccessfulPayment } from "../pages/paymentPage"; +import { LogoutProvider } from "../components/dashboard/admin/LogoutContext"; +import ProductsForAdmin from "../dashboard/admin/Products"; +import { setNavigate } from "../redux/api/api"; const AppRoutes = () => { const navigate = useNavigate(); + useEffect(() => { setNavigate(navigate); - // setNavigateFunction(navigate); }, [navigate]); const AlreadyLogged = ({ children }) => { const navigate = useNavigate(); const token = localStorage.getItem("accessToken"); - const decodedToken = token ? JSON.parse(atob(token!.split(".")[1])) : {}; + const decodedToken = token ? JSON.parse(atob(token.split(".")[1])) : {}; const tokenIsValid = decodedToken.id && decodedToken.roleId; const isSeller = decodedToken.roleId === 2; @@ -53,64 +54,66 @@ const AppRoutes = () => { if (tokenIsValid) { isSeller ? navigate("/dashboard") : navigate("/"); } - }, [tokenIsValid, navigate]); + }, [tokenIsValid, isSeller, navigate]); return tokenIsValid ? null : children; }; return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + )} + /> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + }> + } /> + } /> + } /> + } /> + } /> + - - - - )} - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } - /> - } /> - } /> - + } /> + } /> + } /> + } /> + + ); }; diff --git a/src/utils/logoutUtils.ts b/src/utils/logoutUtils.ts new file mode 100644 index 0000000..f80ce25 --- /dev/null +++ b/src/utils/logoutUtils.ts @@ -0,0 +1,24 @@ +import api from "../redux/api/api"; + +export const performLogout = async () => { + try { + const accessToken = localStorage.getItem("accessToken"); + if (accessToken) { + await api.post( + "/users/logout", + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "*/*", + }, + }, + ); + localStorage.removeItem("accessToken"); + // eslint-disable-next-line no-restricted-globals + window.location.href = "/login"; + } + } catch (error) { + console.error("Failed to logout:", error); + } +};