Introduction
Building an admin panel means deciding who can see or do what. A simple static approach — roles and permissions defined in code, not fetched from an API — is often enough for small/medium projects. Here’s a clean pattern to guard both routes and UI controls, with room to grow later.
What we’ll cover
- Define roles, pages, and permissions in one place.
- Protect routes so only allowed roles can visit them.
- Hide or show UI actions based on permissions.
1) Define roles and permissions
Keep everything in a single roles.ts to avoid scattered logic.
// roles.ts
export const ROLES = {
ADMIN: "ADMIN",
EDITOR: "EDITOR",
VIEWER: "VIEWER",
} as const;
export const PERMISSIONS = {
USER_CREATE: "USER_CREATE",
USER_DELETE: "USER_DELETE",
USER_VIEW: "USER_VIEW",
} as const;
export const roleAccess = {
[ROLES.ADMIN]: ["dashboard", "users", "settings"],
[ROLES.EDITOR]: ["dashboard", "users"],
[ROLES.VIEWER]: ["dashboard"],
};
export const rolePermissions = {
[ROLES.ADMIN]: [
PERMISSIONS.USER_CREATE,
PERMISSIONS.USER_DELETE,
PERMISSIONS.USER_VIEW,
],
[ROLES.EDITOR]: [PERMISSIONS.USER_VIEW],
[ROLES.VIEWER]: [PERMISSIONS.USER_VIEW],
};
You now have one source of truth: which role can visit which pages, and which actions each role can perform.
2) Route-level access with ProtectedRoute
Block navigation if the user’s role isn’t allowed.
// ProtectedRoute.tsx
import { Navigate } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
import { roleAccess, ROLES } from "./roles";
export function ProtectedRoute({
children,
allowed,
}: {
children: JSX.Element;
allowed: string[];
}) {
const { role } = useAuth();
if (!role || !allowed.includes(role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
Use it in your routes:
// routes.tsx
import { ProtectedRoute } from "./ProtectedRoute";
import { roleAccess, ROLES } from "./roles";
import DashboardPage from "@/features/dashboard/pages/DashboardPage";
import UsersPage from "@/features/users/pages/UsersPage";
export const routes = [
{
path: "/dashboard",
element: (
<ProtectedRoute allowed={roleAccess[ROLES.VIEWER]}>
<DashboardPage />
</ProtectedRoute>
),
},
{
path: "/users",
element: (
<ProtectedRoute allowed={roleAccess[ROLES.EDITOR]}>
<UsersPage />
</ProtectedRoute>
),
},
];
Only roles declared in roleAccess can reach each page.
3) Component-level access with AccessControl
Hide or show specific UI actions based on permissions.
// AccessControl.tsx
import { ReactNode } from "react";
import { useAuth } from "@/hooks/useAuth";
import { rolePermissions } from "./roles";
type Props = {
permission: string;
children: ReactNode;
};
export function AccessControl({ permission, children }: Props) {
const { role } = useAuth();
if (!role) return null;
const permissions = rolePermissions[role] || [];
return permissions.includes(permission) ? <>{children}</> : null;
}
Example usage inside a page:
import { AccessControl } from "@/components/AccessControl";
import { PERMISSIONS } from "@/routes/roles";
function UsersPage() {
return (
<div>
<h1>User List</h1>
<AccessControl permission={PERMISSIONS.USER_CREATE}>
<button>Add User</button>
</AccessControl>
<AccessControl permission={PERMISSIONS.USER_DELETE}>
<button>Delete User</button>
</AccessControl>
</div>
);
}
Summary
- Define roles, routes, and permissions in one module (
roles.ts). - Use
ProtectedRouteto guard navigation. - Use
AccessControlto gate UI actions. - Start static; later you can swap
roleAccessandrolePermissionswith API-driven data.
Conclusion
With a few lightweight helpers, you get predictable, centralized access control that keeps your admin UI clean today and flexible enough to go dynamic tomorrow. Happy coding! 🚀