From f921de656dc111291662f613a8e61e9d7b31e391 Mon Sep 17 00:00:00 2001 From: Eknoor Singh Date: Wed, 12 Feb 2025 18:29:09 +0530 Subject: [PATCH] Profile API and User specific window --- package.json | 4 +- src/components/MenuContent/index.tsx | 153 ++++++++++++---------- src/components/OptionsMenu/index.tsx | 12 +- src/components/SideMenu/index.tsx | 188 +++++++++++++++------------ src/lib/https.ts | 25 ++-- src/pages/ProfilePage/index.tsx | 102 +++++++++++++++ src/redux/slices/authSlice.ts | 114 ++++++++++++---- src/router.tsx | 146 ++++++++++++--------- src/superAdminRouter.tsx | 25 ++++ tsconfig.node.json | 43 +++--- 10 files changed, 537 insertions(+), 275 deletions(-) create mode 100644 src/pages/ProfilePage/index.tsx create mode 100644 src/superAdminRouter.tsx diff --git a/package.json b/package.json index fa93fc5..3416a16 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@mui/x-charts": "^7.23.2", "@mui/x-data-grid": "^7.23.5", "@mui/x-date-pickers": "^7.23.3", - "@mui/x-tree-view": "^7.23.2", + "@mui/x-tree-view": "^7.23.2", "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.5.0", "AdapterDayjs": "link:@mui/x-date-pickers/AdapterDayjs", @@ -73,4 +73,4 @@ "@types/react-dom": "^19.0.2", "typescript": "^5.7.3" } -} +} \ No newline at end of file diff --git a/src/components/MenuContent/index.tsx b/src/components/MenuContent/index.tsx index c38a4e3..d1b724c 100644 --- a/src/components/MenuContent/index.tsx +++ b/src/components/MenuContent/index.tsx @@ -1,76 +1,95 @@ -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import Stack from '@mui/material/Stack'; -import HomeRoundedIcon from '@mui/icons-material/HomeRounded'; -import AnalyticsRoundedIcon from '@mui/icons-material/AnalyticsRounded'; -import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; -import { Link, useLocation } from 'react-router-dom'; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Stack from "@mui/material/Stack"; +import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; +import AnalyticsRoundedIcon from "@mui/icons-material/AnalyticsRounded"; +import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; +import { Link, useLocation } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { RootState } from "../../redux/store/store"; -const mainListItems = [ - { - text: 'Home', - icon: , - url: '/panel/dashboard', - }, - { - text: 'Vehicles', - icon: , - url: '/panel/vehicles', - }, - //created by Eknnor and Jaanvi - { - text: 'Admin List', - icon: , - url: '/panel/adminlist', - }, +const baseMenuItems = [ + { + text: "Home", + icon: , + url: "/panel/dashboard", + }, + { + text: "Vehicles", + icon: , + url: "/panel/vehicles", + }, + //created by Eknnor and Jaanvi +]; + +//Eknoor singh and Jaanvi +//date:- 12-Feb-2025 +//Made a different variable for super admin to access all the details. + +const superAdminOnlyItems = [ + { + text: "Admin List", + icon: , + url: "/panel/adminlist", + }, ]; type PropType = { - hidden: boolean; + hidden: boolean; }; export default function MenuContent({ hidden }: PropType) { - const location = useLocation(); + const location = useLocation(); + const userRole = useSelector((state: RootState) => state.auth.user?.role); - return ( - - - {mainListItems.map((item, index) => ( - - {/* Wrap ListItemButton with Link to enable routing */} - - - {item.icon} - - - - - ))} - - - ); + const mainListItems = [ + ...baseMenuItems, + ...(userRole === "superadmin" ? superAdminOnlyItems : []), + ]; + + return ( + + + {mainListItems.map((item, index) => ( + + {/* Wrap ListItemButton with Link to enable routing */} + + + {item.icon} + + + + + ))} + + + ); } diff --git a/src/components/OptionsMenu/index.tsx b/src/components/OptionsMenu/index.tsx index 43a33fe..0fa5cf3 100644 --- a/src/components/OptionsMenu/index.tsx +++ b/src/components/OptionsMenu/index.tsx @@ -11,6 +11,7 @@ import LogoutRoundedIcon from "@mui/icons-material/LogoutRounded"; import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded"; import MenuButton from "../MenuButton"; import { Avatar } from "@mui/material"; +import { useNavigate } from "react-router-dom"; const MenuItem = styled(MuiMenuItem)({ margin: "2px 0", @@ -20,11 +21,18 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); + setAnchorEl(event?.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; + //Eknoor singh and jaanvi + //date:- 12-Feb-2025 + //Made a navigation page for the profile page + const navigate = useNavigate(); + const handleProfile = () => { + navigate("/auth/profile"); + }; return ( - Profile + Profile My account Add another account diff --git a/src/components/SideMenu/index.tsx b/src/components/SideMenu/index.tsx index 1ede6a6..6510422 100644 --- a/src/components/SideMenu/index.tsx +++ b/src/components/SideMenu/index.tsx @@ -1,93 +1,111 @@ -import { styled } from '@mui/material/styles'; -import Avatar from '@mui/material/Avatar'; -import MuiDrawer, { drawerClasses } from '@mui/material/Drawer'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import MenuContent from '../MenuContent'; -import OptionsMenu from '../OptionsMenu'; -import React from 'react'; -import { ArrowLeftIcon, ArrowRightIcon } from '@mui/x-date-pickers'; -import { Button } from '@mui/material'; +import { styled } from "@mui/material/styles"; +import Avatar from "@mui/material/Avatar"; +import MuiDrawer, { drawerClasses } from "@mui/material/Drawer"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import MenuContent from "../MenuContent"; +import OptionsMenu from "../OptionsMenu"; +import { useDispatch, useSelector } from "react-redux"; +import React, { useEffect } from "react"; +import { ArrowLeftIcon, ArrowRightIcon } from "@mui/x-date-pickers"; +import { AppDispatch, RootState } from "../../redux/store/store"; +import { Button } from "@mui/material"; +import { fetchAdminProfile } from "../../redux/slices/authSlice"; const drawerWidth = 240; const Drawer = styled(MuiDrawer)({ - width: drawerWidth, - flexShrink: 0, - boxSizing: 'border-box', - mt: 10, - [`& .${drawerClasses.paper}`]: { - width: drawerWidth, - boxSizing: 'border-box', - }, + width: drawerWidth, + flexShrink: 0, + boxSizing: "border-box", + mt: 10, + [`& .${drawerClasses.paper}`]: { + width: drawerWidth, + boxSizing: "border-box", + }, }); export default function SideMenu() { - const [open, setOpen] = React.useState(true); - return ( - - - - - - ); + const [open, setOpen] = React.useState(true); + + //Eknoor singh + //date:- 12-Feb-2025 + //Dispatch is called with user from Authstate Interface + + const dispatch = useDispatch(); + const { user } = useSelector((state: RootState) => state?.auth); + + useEffect(() => { + dispatch(fetchAdminProfile()); + }, [dispatch]); + + return ( + + + + + + ); } diff --git a/src/lib/https.ts b/src/lib/https.ts index b7443bd..cd3d31b 100644 --- a/src/lib/https.ts +++ b/src/lib/https.ts @@ -15,27 +15,30 @@ // export default http; -import axios, { AxiosInstance } from 'axios'; +//Eknoor singh +//date:- 10-Feb-2025 +//Made different functions for calling different backends and sent them in a function for clarity + +import axios, { AxiosInstance } from "axios"; const backendHttp = axios.create({ - baseURL: process.env.REACT_APP_BACKEND_URL, + baseURL: process.env.REACT_APP_BACKEND_URL, }); // Axios instance for the local API const apiHttp = axios.create({ - baseURL: "http://localhost:5000/api", + baseURL: "http://localhost:5000/api", }); - // Add interceptors to both instances const addAuthInterceptor = (instance: AxiosInstance) => { - instance.interceptors.request.use((config) => { - const authToken = localStorage.getItem('authToken'); - if (authToken) { - config.headers.Authorization = authToken; - } - return config; - }); + instance.interceptors.request.use((config) => { + const authToken = localStorage.getItem("authToken"); + if (authToken) { + config.headers.Authorization = `Bearer ${authToken}`; // <-- Add "Bearer " + } + return config; + }); }; addAuthInterceptor(backendHttp); diff --git a/src/pages/ProfilePage/index.tsx b/src/pages/ProfilePage/index.tsx new file mode 100644 index 0000000..bc0e7bb --- /dev/null +++ b/src/pages/ProfilePage/index.tsx @@ -0,0 +1,102 @@ +//Eknoor singh +//date:- 12-Feb-2025 +//Made a special page for showing the profile details + +import { useEffect } from "react"; +import { + Container, + Typography, + CircularProgress, + Card, + CardContent, + Grid, + Avatar, + Box, +} from "@mui/material"; + +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, RootState } from "../../redux/store/store"; +import { fetchAdminProfile } from "../../redux/slices/authSlice"; + +const ProfilePage = () => { + //Eknoor singh + //date:- 12-Feb-2025 + //Dispatch is called and user, isLoading, and error from Authstate Interface + const dispatch = useDispatch(); + const { user, isLoading, error } = useSelector( + (state: RootState) => state?.auth + ); + + useEffect(() => { + dispatch(fetchAdminProfile()); + }, [dispatch]); + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + +

An error occurred while loading profile

+
+
+ ); + } + + console.log(user?.name); + console.log(user?.email); + console.log(user?.role); + + return ( + + + Profile + + + + + + + + + + {user?.name || "N/A"} + + + Email: {user?.email || "N/A"} + + + Phone: {user?.phone || "N/A"} + + + Role: {user?.role || "N/A"} + + + + + + + ); +}; + +export default ProfilePage; diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index 3189bb8..2835d05 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -1,35 +1,41 @@ -import { - createSlice, - createAsyncThunk, - PayloadAction, -} from "@reduxjs/toolkit"; +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import axios from "axios"; import { backendHttp, apiHttp } from "../../lib/https"; import { toast } from "react-toastify"; // Define types for state +//Eknoor singh +//date:- 12-Feb-2025 +//Token for the user has been declared interface User { + token: string | null; map( arg0: ( - admin: { name: any; role: any }, + admin: { name: any; role: any; email: any; phone: any }, index: number - ) => { srno: number; name: any; role: any } + ) => { srno: number; name: any; role: any; email: any; phone: any } ): unknown; id: string; + name: string; email: string; + role: string; + phone: string; } interface Admin { id: string; name: string; role: string; + email: string; } + interface AuthState { user: User | null; admins: Admin[]; isAuthenticated: boolean; isLoading: boolean; error: object | string | null; + token: string | null; } // Async thunk for login @@ -39,7 +45,7 @@ export const loginUser = createAsyncThunk< { rejectValue: string } >("auth/login", async ({ email, password }, { rejectWithValue }) => { try { - const response = await backendHttp.post("admin/login", { + const response = await apiHttp.post("auth/login", { email, password, }); @@ -85,10 +91,16 @@ export const adminList = createAsyncThunk< const response = await apiHttp.get("/auth"); console.log(response?.data?.data); return response?.data?.data?.map( - (admin: { id: string; name: string; role: string }) => ({ - id: admin.id, + (admin: { + id: string; + name: string; + role: string; + email: string; + }) => ({ + id: admin?.id, name: admin?.name, role: admin?.role || "N/A", + email: admin?.email, }) ); } catch (error: any) { @@ -127,8 +139,8 @@ export const updateAdmin = createAsyncThunk( try { const response = await apiHttp.put(`/auth/${id}`, { name, role }); toast.success("Admin updated successfully"); - console.log(response.data); - return response.data; + console.log(response?.data); + return response?.data; } catch (error: any) { return rejectWithValue( error.response?.data?.message || "An error occurred" @@ -137,12 +149,50 @@ export const updateAdmin = createAsyncThunk( } ); +//Eknoor singh +//date:- 12-Feb-2025 +//Function for fetching profile of a particular user has been implemented with Redux. +export const fetchAdminProfile = createAsyncThunk< + User, + void, + { rejectValue: string } +>("auth/fetchAdminProfile", async (_, { rejectWithValue }) => { + try { + const token = localStorage?.getItem("authToken"); + if (!token) throw new Error("No token found"); + + const response = await apiHttp?.get("/auth/profile", { + headers: { Authorization: `Bearer ${token}` }, // Ensure 'Bearer' prefix + }); + + console.log("API Response:", response?.data); // Debugging + + if (!response.data?.data) { + throw new Error("Invalid API response"); + } + + return response?.data?.data; // Fix: Return only `data`, assuming it contains user info. + } catch (error: any) { + console.error( + "Profile Fetch Error:", + error?.response?.data || error?.message + ); + return rejectWithValue( + error?.response?.data?.message || "An error occurred" + ); + } +}); + const initialState: AuthState = { user: null, admins: [], isAuthenticated: false, isLoading: false, error: null, + //Eknoor singh + //date:- 12-Feb-2025 + //initial state of token set to null + token: null, }; const authSlice = createSlice({ @@ -152,6 +202,11 @@ const authSlice = createSlice({ logout: (state) => { state.user = null; state.isAuthenticated = false; + //Eknoor singh + //date:- 12-Feb-2025 + //Token is removed from local storage and set to null + state.token = null; + localStorage.removeItem("authToken"); }, }, extraReducers: (builder) => { @@ -161,14 +216,12 @@ const authSlice = createSlice({ state.isLoading = true; state.error = null; }) - .addCase( - loginUser.fulfilled, - (state, action: PayloadAction) => { - state.isLoading = false; - state.isAuthenticated = true; - state.user = action.payload; - } - ) + .addCase(loginUser.fulfilled, (state, action) => { + state.isLoading = false; + state.isAuthenticated = true; + state.user = action.payload; // Fix: Extract correct payload + state.token = action.payload.token; // Store token in Redux + }) .addCase( loginUser.rejected, (state, action: PayloadAction) => { @@ -241,8 +294,8 @@ const authSlice = createSlice({ }) .addCase(updateAdmin.fulfilled, (state, action) => { const updatedAdmin = action.payload; - state.admins = state.admins.map((admin) => - admin.id === updatedAdmin.id ? updatedAdmin : admin + state.admins = state?.admins?.map((admin) => + admin?.id === updatedAdmin?.id ? updatedAdmin : admin ); state.isLoading = false; @@ -252,6 +305,23 @@ const authSlice = createSlice({ state.error = action.payload || "Something went wrong while updating Admin!!"; + }) + + //Eknoor singh + //date:- 12-Feb-2025 + //Reducers for fetching profiles has been implemented + .addCase(fetchAdminProfile.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchAdminProfile.fulfilled, (state, action) => { + state.isLoading = false; + state.user = action.payload; + state.isAuthenticated = true; + }) + .addCase(fetchAdminProfile.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload || "Failed to fetch admin profile"; }); }, }); diff --git a/src/router.tsx b/src/router.tsx index 27a2df2..d848c13 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,74 +1,92 @@ -import { Routes as BaseRoutes, Navigate, Route } from "react-router-dom" -// import useAuth from "./hooks/useAuth"; -import React, { Suspense } from "react" -import LoadingComponent from "./components/Loading" -import DashboardLayout from "./layouts/DashboardLayout" -import Login from "./pages/Auth/Login" -import SignUp from "./pages/Auth/SignUp" -import Dashboard from "./pages/Dashboard" -import Vehicles from "./pages/Vehicles" -import AdminList from "./pages/AdminList" +import { Routes as BaseRoutes, Navigate, Route } from "react-router-dom"; +import React, { Suspense } from "react"; +import LoadingComponent from "./components/Loading"; +import DashboardLayout from "./layouts/DashboardLayout"; +import Login from "./pages/Auth/Login"; +import SignUp from "./pages/Auth/SignUp"; +import Dashboard from "./pages/Dashboard"; +import Vehicles from "./pages/Vehicles"; +import AdminList from "./pages/AdminList"; +import ProfilePage from "./pages/ProfilePage"; + +import SuperAdminRouter from "./superAdminRouter"; function ProtectedRoute({ - caps, - component, + caps, + component, }: { - caps: string[] - component: React.ReactNode + caps: string[]; + component: React.ReactNode; }) { - if (!localStorage.getItem("authToken")) - return + if (!localStorage.getItem("authToken")) + return ; - return component + return component; } export default function AppRouter() { - return ( - }> - - } index /> + return ( + }> + + } index /> - - } - index - /> - } /> - } /> - - }> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - 404} /> - - 404} /> - - - ) + + } + index + /> + } /> + } /> + + + }> + } + /> + } + /> + } + /> + } + /> + + + + } + /> + } + /> + + 404} /> + + } /> + } + /> + + 404} /> + + + ); } diff --git a/src/superAdminRouter.tsx b/src/superAdminRouter.tsx new file mode 100644 index 0000000..812d459 --- /dev/null +++ b/src/superAdminRouter.tsx @@ -0,0 +1,25 @@ +//Eknoor singh and jaanvi +//date:- 12-Feb-2025 +//seperate route for super admin implemented + +import React from "react"; +import { useSelector } from "react-redux"; +import { Navigate } from "react-router-dom"; +import { RootState } from "./redux/store/store"; + +interface SuperAdminRouteProps { + children: React.ReactNode; +} + +const SuperAdminRouter: React.FC = ({ children }) => { + const userRole = useSelector((state: RootState) => state.auth.user?.role); + + if (userRole !== "superadmin") { + // Redirect to dashboard if user is not a superadmin + return ; + } + + return <>{children}; +}; + +export default SuperAdminRouter; diff --git a/tsconfig.node.json b/tsconfig.node.json index 57c317e..61fcf6e 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,23 +1,22 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": [ - "ES2023" - ], - "module": "ESNext", - "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - } -} \ No newline at end of file + "compilerOptions": { + "jsx": "react", + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + } +}