Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions RestroHub-FrontEnd/src/components/admin/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 },
],
},
{
Expand All @@ -75,30 +79,49 @@ 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 },
],
},
{
type: 'expandable',
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 },
],
},
],
},
{
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
// ============================================
Expand Down Expand Up @@ -369,7 +392,7 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
{/* NAVIGATION */}
{/* ================================= */}
<div className="flex-1 overflow-y-auto overflow-x-hidden px-3 py-4">
{navSections.map((section, sectionIndex) => (
{visibleNavSections.map((section, sectionIndex) => (
<div key={section.label} className={sectionIndex > 0 ? 'mt-4' : ''}>
{/* Section Label */}
{!collapsed ? (
Expand Down Expand Up @@ -458,4 +481,4 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => {
);
};

export default Sidebar;
export default Sidebar;
7 changes: 4 additions & 3 deletions RestroHub-FrontEnd/src/pages/public/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -180,7 +181,7 @@ const Login = () => {

toast.success("Login successful!");

navigate("/admin/dashboard");
navigate(getDefaultAdminPath(roles));
} else {
toast.error(result.message || "Login failed");
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -440,4 +441,4 @@ const handleGoogleLogin = async (credentialResponse) => {
);
};

export default Login;
export default Login;
23 changes: 23 additions & 0 deletions RestroHub-FrontEnd/src/pages/public/Unauthorized.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Link } from 'react-router-dom';

const Unauthorized = () => {
return (
<div className="flex min-h-[60vh] items-center justify-center px-4">
<div className="w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm">
<p className="text-sm font-semibold uppercase tracking-wide text-red-600">Access denied</p>
<h1 className="mt-2 text-2xl font-bold text-gray-900">This page is restricted</h1>
<p className="mt-3 text-sm text-gray-600">
Your account does not have permission to view this admin area.
</p>
<Link
to="/"
className="mt-6 inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700"
>
Go to home
</Link>
</div>
</div>
);
};

export default Unauthorized;
47 changes: 22 additions & 25 deletions RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
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");

if (!accessToken) {
return <Navigate to="/login" replace />;
}

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 <Navigate to="/unauthorized" replace state={{ from: location }} />;
}

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 <Navigate to="/admin/kds" replace />;
return <Navigate to={getDefaultAdminPath(roles)} replace />;
}
}

return children;
};

export default ProtectedRoute;
export default ProtectedRoute;
4 changes: 3 additions & 1 deletion RestroHub-FrontEnd/src/routes/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// Customer Pages
import RestaurantMenu from '../pages/customer/RestaurantMenu';
Expand All @@ -39,6 +40,7 @@ const AppRoutes = () => {
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
<Route path="/unauthorized" element={<Unauthorized />} />
</Route>

{/* ========== CUSTOMER ROUTES ========== */}
Expand Down Expand Up @@ -74,4 +76,4 @@ const AppRoutes = () => {
);
};

export default AppRoutes;
export default AppRoutes;
1 change: 1 addition & 0 deletions RestroHub-FrontEnd/src/services/public/ApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ try {
console.error("Failed to parse response:", err);
throw new Error("Invalid server response");
}
};

const ApiService = {
// ============================================
Expand Down
31 changes: 31 additions & 0 deletions RestroHub-FrontEnd/src/utils/auth.js
Original file line number Diff line number Diff line change
@@ -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';
};