diff --git a/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx b/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx
index 1953e8f8..d22d7515 100644
--- a/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx
+++ b/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx
@@ -17,11 +17,15 @@ import {
ChefHat,
} from 'lucide-react';
import { useAdminTheme } from '@context/AdminThemeContext';
+import { FULL_ADMIN_ROLES, hasAnyRole, readStoredRoles } from '../../utils/auth';
const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
const location = useLocation();
const sidebarRef = useRef(null);
const { isDark } = useAdminTheme();
+ const roles = readStoredRoles();
+ const limitedAdminRoles = ['MANAGER', 'STAFF'];
+ const allAdminRoles = [...FULL_ADMIN_ROLES, ...limitedAdminRoles];
const [expandedMenus, setExpandedMenus] = useState({
store: false,
@@ -61,10 +65,10 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
{
label: 'Menu',
items: [
- { type: 'link', name: 'Dashboard', path: '/admin/dashboard', icon: LayoutDashboard },
- { type: 'link', name: 'Kitchen Display', path: '/admin/kds', icon: ChefHat },
- { type: 'link', name: 'Menus', path: '/admin/menus', icon: UtensilsCrossed },
- { type: 'link', name: 'Orders', path: '/admin/orders', icon: ShoppingCart },
+ { type: 'link', name: 'Dashboard', path: '/admin/dashboard', icon: LayoutDashboard, allowedRoles: FULL_ADMIN_ROLES },
+ { type: 'link', name: 'Kitchen Display', path: '/admin/kds', icon: ChefHat, allowedRoles: allAdminRoles },
+ { type: 'link', name: 'Menus', path: '/admin/menus', icon: UtensilsCrossed, allowedRoles: FULL_ADMIN_ROLES },
+ { type: 'link', name: 'Orders', path: '/admin/orders', icon: ShoppingCart, allowedRoles: allAdminRoles },
],
},
{
@@ -75,8 +79,9 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
name: 'Store',
icon: Store,
menuKey: 'store',
+ allowedRoles: FULL_ADMIN_ROLES,
children: [
- { name: 'Branches', path: '/admin/store/branches', icon: Building2 },
+ { name: 'Branches', path: '/admin/store/branches', icon: Building2, allowedRoles: FULL_ADMIN_ROLES },
],
},
{
@@ -84,9 +89,10 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
name: 'Marketing',
icon: Megaphone,
menuKey: 'marketing',
+ allowedRoles: FULL_ADMIN_ROLES,
children: [
- { name: 'Website', path: '/admin/marketing/website', icon: Globe },
- { name: 'QR Display', path: '/admin/marketing/qr-display', icon: QrCode },
+ { name: 'Website', path: '/admin/marketing/website', icon: Globe, allowedRoles: FULL_ADMIN_ROLES },
+ { name: 'QR Display', path: '/admin/marketing/qr-display', icon: QrCode, allowedRoles: FULL_ADMIN_ROLES },
],
},
],
@@ -94,11 +100,28 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
{
label: 'Payments',
items: [
- { type: 'link', name: 'UPI Links', path: '/admin/upi-links', icon: CreditCard },
+ { type: 'link', name: 'UPI Links', path: '/admin/upi-links', icon: CreditCard, allowedRoles: FULL_ADMIN_ROLES },
],
},
];
+ const canViewItem = (item) => !item.allowedRoles || hasAnyRole(roles, item.allowedRoles);
+
+ const visibleNavSections = navSections
+ .map((section) => ({
+ ...section,
+ items: section.items
+ .map((item) => {
+ if (item.type !== 'expandable') return item;
+ return {
+ ...item,
+ children: item.children.filter(canViewItem),
+ };
+ })
+ .filter((item) => canViewItem(item) && (item.type !== 'expandable' || item.children.length > 0)),
+ }))
+ .filter((section) => section.items.length > 0);
+
// ============================================
// SINGLE NAV LINK
// ============================================
@@ -369,7 +392,7 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
{/* NAVIGATION */}
{/* ================================= */}
- {navSections.map((section, sectionIndex) => (
+ {visibleNavSections.map((section, sectionIndex) => (
0 ? 'mt-4' : ''}>
{/* Section Label */}
{!collapsed ? (
@@ -458,4 +481,4 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
);
};
-export default Sidebar;
\ No newline at end of file
+export default Sidebar;
diff --git a/RestroHub-FrontEnd/src/pages/public/Login.jsx b/RestroHub-FrontEnd/src/pages/public/Login.jsx
index edf9e2ec..97190616 100644
--- a/RestroHub-FrontEnd/src/pages/public/Login.jsx
+++ b/RestroHub-FrontEnd/src/pages/public/Login.jsx
@@ -10,6 +10,7 @@ import { GoogleLogin } from "@react-oauth/google";
import { ArrowLeft } from "lucide-react";
import api from "@services/common/api";
import { useTheme } from "@context/ThemeContext";
+import { getDefaultAdminPath } from "../../utils/auth";
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL || "http://localhost:8181/restroly";
@@ -180,7 +181,7 @@ const Login = () => {
toast.success("Login successful!");
- navigate("/admin/dashboard");
+ navigate(getDefaultAdminPath(roles));
} else {
toast.error(result.message || "Login failed");
}
@@ -215,7 +216,7 @@ const handleGoogleLogin = async (credentialResponse) => {
toast.success("Google login successful!");
- navigate("/admin/dashboard");
+ navigate(getDefaultAdminPath(roles));
} else {
toast.error(result.message || "Google login failed");
}
@@ -440,4 +441,4 @@ const handleGoogleLogin = async (credentialResponse) => {
);
};
-export default Login;
\ No newline at end of file
+export default Login;
diff --git a/RestroHub-FrontEnd/src/pages/public/Unauthorized.jsx b/RestroHub-FrontEnd/src/pages/public/Unauthorized.jsx
new file mode 100644
index 00000000..06a2968f
--- /dev/null
+++ b/RestroHub-FrontEnd/src/pages/public/Unauthorized.jsx
@@ -0,0 +1,23 @@
+import { Link } from 'react-router-dom';
+
+const Unauthorized = () => {
+ return (
+
+
+
Access denied
+
This page is restricted
+
+ Your account does not have permission to view this admin area.
+
+
+ Go to home
+
+
+
+ );
+};
+
+export default Unauthorized;
diff --git a/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx b/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx
index 574fbe86..d222f2b4 100644
--- a/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx
+++ b/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx
@@ -1,6 +1,16 @@
import { Navigate, useLocation } from "react-router-dom";
-
-const ProtectedRoute = ({ children }) => {
+import {
+ ADMIN_ACCESS_ROLES,
+ FULL_ADMIN_ROLES,
+ LIMITED_ADMIN_ROLES,
+ getDefaultAdminPath,
+ hasAnyRole,
+ readStoredRoles,
+} from "../utils/auth";
+
+const LIMITED_ADMIN_PATHS = ["/admin/kds", "/admin/orders", "/admin/profile"];
+
+const ProtectedRoute = ({ children, allowedRoles = ADMIN_ACCESS_ROLES }) => {
const location = useLocation();
const accessToken = localStorage.getItem("accessToken");
@@ -8,36 +18,23 @@ const ProtectedRoute = ({ children }) => {
return
;
}
- let roles = [];
- try {
- const rolesStr = localStorage.getItem("roles");
- if (rolesStr) roles = JSON.parse(rolesStr);
- } catch (e) {
- console.error("Failed to parse roles");
- }
-
- const hasRole = (roleToCheck) => {
- if (!Array.isArray(roles)) return false;
- return roles.some(r => {
- const roleName = typeof r === 'string' ? r : r.authority || r.name;
- return roleName === roleToCheck || roleName === `ROLE_${roleToCheck}`;
- });
- };
+ const roles = readStoredRoles();
- const isAdmin = hasRole("ADMIN");
- const isManager = hasRole("MANAGER");
- const isStaff = hasRole("STAFF");
+ if (!hasAnyRole(roles, allowedRoles)) {
+ return
;
+ }
- if (!isAdmin && (isManager || isStaff)) {
- const allowedPaths = ["/admin/kds", "/admin/orders", "/admin/profile"];
- const isAllowed = allowedPaths.some(p => location.pathname.startsWith(p));
+ const hasFullAdminAccess = hasAnyRole(roles, FULL_ADMIN_ROLES);
+ const hasLimitedAdminAccess = hasAnyRole(roles, LIMITED_ADMIN_ROLES);
+ if (!hasFullAdminAccess && hasLimitedAdminAccess) {
+ const isAllowed = LIMITED_ADMIN_PATHS.some((path) => location.pathname.startsWith(path));
if (!isAllowed || location.pathname === "/admin" || location.pathname === "/admin/dashboard") {
- return
;
+ return
;
}
}
return children;
};
-export default ProtectedRoute;
\ No newline at end of file
+export default ProtectedRoute;
diff --git a/RestroHub-FrontEnd/src/routes/index.jsx b/RestroHub-FrontEnd/src/routes/index.jsx
index ab1299a4..6e74ed75 100644
--- a/RestroHub-FrontEnd/src/routes/index.jsx
+++ b/RestroHub-FrontEnd/src/routes/index.jsx
@@ -13,6 +13,7 @@ import Login from '../pages/public/Login';
import Register from '../pages/public/Register';
import ForgotPassword from '../pages/public/ForgotPassword';
import PrivacyPolicy from '../pages/public/PrivacyPolicy';
+import Unauthorized from '../pages/public/Unauthorized';
import NotFound from '../pages/public/NotFound';
// Customer Pages
@@ -40,6 +41,7 @@ const AppRoutes = () => {
} />
} />
} />
+
} />
{/* ========== CUSTOMER ROUTES ========== */}
@@ -75,4 +77,4 @@ const AppRoutes = () => {
);
};
-export default AppRoutes;
\ No newline at end of file
+export default AppRoutes;
diff --git a/RestroHub-FrontEnd/src/services/public/ApiService.js b/RestroHub-FrontEnd/src/services/public/ApiService.js
index 9d3c4623..2ec5bd96 100644
--- a/RestroHub-FrontEnd/src/services/public/ApiService.js
+++ b/RestroHub-FrontEnd/src/services/public/ApiService.js
@@ -60,6 +60,7 @@ try {
console.error("Failed to parse response:", err);
throw new Error("Invalid server response");
}
+};
const ApiService = {
// ============================================
diff --git a/RestroHub-FrontEnd/src/utils/auth.js b/RestroHub-FrontEnd/src/utils/auth.js
new file mode 100644
index 00000000..aeb31fe4
--- /dev/null
+++ b/RestroHub-FrontEnd/src/utils/auth.js
@@ -0,0 +1,31 @@
+export const ADMIN_ACCESS_ROLES = ['SUPER_ADMIN', 'ADMIN', 'MANAGER', 'STAFF'];
+export const FULL_ADMIN_ROLES = ['SUPER_ADMIN', 'ADMIN'];
+export const LIMITED_ADMIN_ROLES = ['MANAGER', 'STAFF'];
+
+export const normalizeRole = (role) => {
+ const roleName = typeof role === 'string' ? role : role?.authority || role?.name || '';
+ return roleName.replace(/^ROLE_/, '').toUpperCase();
+};
+
+export const readStoredRoles = () => {
+ try {
+ const rolesStr = localStorage.getItem('roles');
+ const roles = rolesStr ? JSON.parse(rolesStr) : [];
+ return Array.isArray(roles) ? roles.map(normalizeRole).filter(Boolean) : [];
+ } catch {
+ console.error('Failed to parse roles');
+ return [];
+ }
+};
+
+export const hasAnyRole = (roles, allowedRoles = []) => {
+ const normalizedRoles = roles.map(normalizeRole);
+ const allowed = allowedRoles.map(normalizeRole);
+ return normalizedRoles.some((role) => allowed.includes(role));
+};
+
+export const getDefaultAdminPath = (roles) => {
+ if (hasAnyRole(roles, FULL_ADMIN_ROLES)) return '/admin/dashboard';
+ if (hasAnyRole(roles, LIMITED_ADMIN_ROLES)) return '/admin/kds';
+ return '/unauthorized';
+};