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'; +};