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__/HomeButton.test.tsx b/src/__test__/HomeButton.test.tsx
new file mode 100644
index 0000000..7baa2f8
--- /dev/null
+++ b/src/__test__/HomeButton.test.tsx
@@ -0,0 +1,20 @@
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter, Routes, Route } from "react-router-dom";
+
+import HomeButton from "../components/dashboard/HomeButton";
+
+describe("HomeButton", () => {
+ it("should render the Home button", () => {
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ const homeButton = screen.getByText("Home");
+ expect(homeButton).toBeInTheDocument();
+ });
+});
diff --git a/src/__test__/LogoutContext.test.tsx b/src/__test__/LogoutContext.test.tsx
new file mode 100644
index 0000000..9acefd9
--- /dev/null
+++ b/src/__test__/LogoutContext.test.tsx
@@ -0,0 +1,62 @@
+import "@testing-library/jest-dom";
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+
+import {
+ LogoutProvider,
+ useLogout,
+} from "../components/dashboard/admin/LogoutContext";
+
+// 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", () => {
+ 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();
+ });
+});
diff --git a/src/__test__/ProfileDropdown.test.tsx b/src/__test__/ProfileDropdown.test.tsx
new file mode 100644
index 0000000..c5459a1
--- /dev/null
+++ b/src/__test__/ProfileDropdown.test.tsx
@@ -0,0 +1,97 @@
+import "@testing-library/jest-dom";
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { BrowserRouter as Router } from "react-router-dom";
+
+import { useLogout } from "../components/dashboard/admin/LogoutContext"; // Ensure the correct path
+import ProfileDropdown from "../components/common/ProfileDropdown";
+
+// Mock the useLogout hook
+jest.mock("../components/dashboard/admin/LogoutContext", () => ({
+ useLogout: jest.fn(),
+}));
+
+describe("ProfileDropdown", () => {
+ const openLogoutModalMock = jest.fn();
+
+ beforeEach(() => {
+ (useLogout as jest.Mock).mockReturnValue({
+ openLogoutModal: openLogoutModalMock,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should render profile and orders links for a user with roleId 1", () => {
+ const userInfo = { roleId: 1 };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("Profile")).toBeInTheDocument();
+ expect(screen.getByText("My Orders")).toBeInTheDocument();
+ expect(screen.getByText("Logout")).toBeInTheDocument();
+ });
+
+ it("should render the correct dashboard link for roleId 2", () => {
+ const userInfo = { roleId: 2 };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("My Dashboard")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: /My Dashboard/i })).toHaveAttribute(
+ "href",
+ "/dashboard",
+ );
+ });
+
+ it("should render the correct dashboard link for roleId 3", () => {
+ const userInfo = { roleId: 3 };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("My Dashboard")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: /My Dashboard/i })).toHaveAttribute(
+ "href",
+ "/admin/dashboard",
+ );
+ });
+
+ it("should call openLogoutModal when logout button is clicked", () => {
+ const userInfo = { roleId: 1 };
+
+ render(
+
+
+ ,
+ );
+
+ const logoutButton = screen.getByText("Logout");
+ fireEvent.click(logoutButton);
+
+ expect(openLogoutModalMock).toHaveBeenCalled();
+ });
+
+ it("should render login link if userInfo is not provided", () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("Login")).toBeInTheDocument();
+ });
+});
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/__test__/logout.test.tsx b/src/__test__/logout.test.tsx
new file mode 100644
index 0000000..23b3353
--- /dev/null
+++ b/src/__test__/logout.test.tsx
@@ -0,0 +1,83 @@
+import "@testing-library/jest-dom";
+import {
+ render, screen, fireEvent, waitFor,
+} from "@testing-library/react";
+import { isExpired } from "react-jwt";
+
+import {
+ useLogout,
+ LogoutProvider,
+} from "../components/dashboard/admin/LogoutContext";
+import { performLogout } from "../utils/logoutUtils";
+
+// Mocking performLogout and isExpired functions
+jest.mock("../utils/logoutUtils", () => ({
+ performLogout: jest.fn(),
+}));
+jest.mock("react-jwt", () => ({
+ isExpired: jest.fn(),
+}));
+
+const MockComponent = () => {
+ const { openLogoutModal } = useLogout();
+ return ;
+};
+
+describe("LogoutContext", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should render children", () => {
+ render(
+
+ Child Component
+ ,
+ );
+ expect(screen.getByText("Child Component")).toBeInTheDocument();
+ });
+
+ it("should open and close the logout modal", () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByText("Open Logout Modal"));
+ expect(screen.getByText("Confirm Logout")).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText("Cancel"));
+ expect(screen.queryByText("Confirm Logout")).not.toBeInTheDocument();
+ });
+
+ it("should handle logout", async () => {
+ (performLogout as jest.Mock).mockResolvedValueOnce(undefined);
+
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByText("Open Logout Modal"));
+ expect(screen.getByText("Confirm Logout")).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText("Logout"));
+ await waitFor(() => expect(performLogout).toHaveBeenCalled());
+ expect(screen.queryByText("Confirm Logout")).not.toBeInTheDocument();
+ });
+
+ it("should not redirect if token is not expired", async () => {
+ (isExpired as jest.Mock).mockReturnValue(false);
+
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => expect(isExpired).toHaveBeenCalled());
+ expect(window.location.href).not.toBe("/");
+ });
+});
diff --git a/src/__test__/performLogout.test.tsx b/src/__test__/performLogout.test.tsx
new file mode 100644
index 0000000..0df5bd7
--- /dev/null
+++ b/src/__test__/performLogout.test.tsx
@@ -0,0 +1,73 @@
+import "@testing-library/jest-dom";
+import { performLogout } from "../utils/logoutUtils";
+import api from "../redux/api/api";
+
+// Mock the API and localStorage
+jest.mock("../redux/api/api");
+const mockPost = api.post as jest.MockedFunction;
+
+describe("performLogout", () => {
+ let consoleErrorMock: jest.SpyInstance;
+ beforeAll(() => {
+ consoleErrorMock = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+ });
+
+ afterAll(() => {
+ consoleErrorMock.mockRestore();
+ });
+
+ beforeEach(() => {
+ localStorage.clear();
+ mockPost.mockReset();
+ consoleErrorMock.mockClear();
+ });
+
+ it("should perform logout successfully", async () => {
+ localStorage.setItem("accessToken", "mockToken");
+ mockPost.mockResolvedValue({});
+
+ await performLogout();
+
+ expect(mockPost).toHaveBeenCalledWith(
+ "/users/logout",
+ {},
+ {
+ headers: {
+ Authorization: `Bearer mockToken`,
+ Accept: "*/*",
+ },
+ },
+ );
+ expect(localStorage.getItem("accessToken")).toBeNull();
+ expect(window.location.href.startsWith("http")).toBe(true);
+ });
+
+ it("should handle logout failure", async () => {
+ localStorage.setItem("accessToken", "mockToken");
+ const error = new Error("Network error");
+ mockPost.mockRejectedValue(error);
+
+ await performLogout();
+
+ expect(mockPost).toHaveBeenCalledWith(
+ "/users/logout",
+ {},
+ {
+ headers: {
+ Authorization: `Bearer mockToken`,
+ Accept: "*/*",
+ },
+ },
+ );
+ expect(console.error).toHaveBeenCalledWith("Failed to logout:", error);
+ });
+
+ it("should not call API if accessToken is missing", async () => {
+ await performLogout();
+
+ expect(mockPost).not.toHaveBeenCalled();
+ expect(console.error).not.toHaveBeenCalled();
+ });
+});
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}
-
setShowDropdown(!showDropdown)}
- />
-
{profile?.fullName}
-
setShowDropdown(!showDropdown)} />
- {showDropdown && }
-
+ className="relative flex items-center h-full space-x-1 cursor-pointer"
+ ref={profileDropdownRef}
+ >
+ {profileImage ? (
+
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..8521e34 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 {
@@ -29,11 +29,6 @@ const AdminSideBar: React.FC
= ({ isOpen }) => {
icon: ,
label: "Users",
},
- {
- to: "/admin/products",
- icon: ,
- label: "Products",
- },
{
to: "/admin/analytics",
icon: ,
@@ -48,11 +43,9 @@ const AdminSideBar: React.FC = ({ isOpen }) => {
return (
@@ -73,10 +66,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..967c759
--- /dev/null
+++ b/src/components/dashboard/admin/LogoutContext.tsx
@@ -0,0 +1,78 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ ReactNode,
+ useMemo,
+ useEffect,
+} from "react";
+import { isExpired } from "react-jwt";
+
+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 }), []);
+
+ useEffect(() => {
+ const checkTokenExpiration = async () => {
+ const accessToken: any = localStorage.getItem("accessToken");
+
+ console.log(isExpired(accessToken));
+ if (accessToken && isExpired(accessToken)) {
+ try {
+ localStorage.removeItem("accessToken");
+ // eslint-disable-next-line no-restricted-globals
+ window.location.href = "/";
+ } catch (error) {
+ console.error("Failed to logout:", error);
+ }
+ }
+ };
+
+ checkTokenExpiration();
+ }, []);
+
+ return (
+
+ {children}
+
+
+ );
+};
diff --git a/src/components/dashboard/admin/LogoutModal.tsx b/src/components/dashboard/admin/LogoutModal.tsx
new file mode 100644
index 0000000..1032d02
--- /dev/null
+++ b/src/components/dashboard/admin/LogoutModal.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from "react";
+
+const LogoutModal: React.FC<{
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+}> = ({ isOpen, onClose, onConfirm }) => {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleConfirm = async () => {
+ setIsLoading(true);
+ try {
+ await onConfirm();
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
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) => (
);
-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..2d57eb0 100644
--- a/src/redux/ProtectAdminDashboard.tsx
+++ b/src/redux/ProtectAdminDashboard.tsx
@@ -2,36 +2,46 @@ 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);
+ console.log(isExpired);
+ // @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..6829c49 100644
--- a/src/routes/AppRoutes.tsx
+++ b/src/routes/AppRoutes.tsx
@@ -34,6 +34,7 @@ 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";
const AppRoutes = () => {
const navigate = useNavigate();
@@ -60,57 +61,59 @@ const AppRoutes = () => {
return (
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
- } />
- } />
- } />
- } />
- } />
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+ } />
+ } />
+ } />
-
-
-
- )}
- />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- }
- />
- } />
- } />
-
+
+
+
+ )}
+ />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ }
+ />
+ } />
+ } />
+
+
);
};
diff --git a/src/utils/logoutUtils.ts b/src/utils/logoutUtils.ts
new file mode 100644
index 0000000..25cec12
--- /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 = "/";
+ }
+ } catch (error) {
+ console.error("Failed to logout:", error);
+ }
+};