Routing & Route Guards
BonardaHR uses React Router 7 (createBrowserRouter) with layout wrappers and declarative guard components.
Route Tree Hierarchy​
[ createBrowserRouter ]
│
├── /login ──► LoginPage (Public)
├── /auth/callback ──► AuthCallbackPage (Public)
├── * ──► NotFoundPage (404 Fallback)
│
└── ProtectedRoute ──► Checks isAuthenticated
│
└── MainLayout ──► Sidebar + Header + ImpersonationBanner + Outlet
│
├── / ──► DashboardPage
├── /employees ──► EmployeePage
├── /employees/new ──► PermissionGuard(EMPLOYEE_CREATE) → CreateEmployeeWizard
├── /employees/import ──► PermissionGuard([EMPLOYEE_CREATE, EMPLOYEE_READ_ALL]) → BulkImportPage
├── /hibob/sync ──► RoleGuard([ADMIN, HR_MANAGER]) → HibobSyncPage
├── /employees/:id ──► EmployeeProfilePage
├── /time-off ──► TimeOffPage
├── /attendance ──► AttendancePage
├── /attendance/:id ──► TimesheetDetail
├── /documents ──► DocumentsPage
├── /documents/:id ──► DocumentDetail
├── /tasks ──► TasksPage
├── /notifications ──► PermissionGuard(NOTIFICATION_READ_OWN) → NotificationsPage
├── /impersonation-audit ──► PermissionGuard(IMPERSONATION_AUDIT_READ) → ImpersonationAuditPage
├── /workflows ──► PermissionGuard(WORKFLOW_TEMPLATE_READ) → WorkflowsPage
├── /workflows/templates/new ──► PermissionGuard(WORKFLOW_TEMPLATE_READ) → CreateTemplatePage
├── /feedback/:id ──► FeedbackFormPage
├── /reports ──► PermissionGuard(REPORT_READ) → ReportsPage
├── /admin ──► PermissionGuard(ADMIN_ACCESS) → AdminPage
├── /admin/attendance-policies/new ──► PermissionGuard(ATTENDANCE_POLICY_CREATE) → AttendancePolicyWizardPage
├── /admin/attendance-policies/:id/edit ─► PermissionGuard(ATTENDANCE_POLICY_UPDATE) → AttendancePolicyWizardPage
├── /admin/attendance-policies/:id/summary ► PermissionGuard(ATTENDANCE_POLICY_READ) → AttendancePolicySummaryPage
├── /admin/onboarding-templates/new ──► PermissionGuard(ONBOARDING_TEMPLATE_CREATE) → CreateOnboardingTemplatePage
├── /admin/calendars/:id ──► PermissionGuard(EVENT_READ) → CalendarDetailPage
├── /access-control ──► PermissionGuard(ROLE_READ) → AccessControlPage
└── /legacy-admin ──► PermissionGuard(ROLE_READ) → LegacyAdminPage
Guard Components​
1. ProtectedRoute​
Protects all internal app routes. If unauthenticated, it redirects to /login while preserving the intended target URL in navigation state:
// features/auth/components/ProtectedRoute.tsx
export default function ProtectedRoute() {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
2. PermissionGuard​
Checks if the authenticated user possesses the required permission(s). Supports a single permission or an array of permissions:
// features/auth/components/PermissionGuard.tsx
interface PermissionGuardProps {
permission: Permission | Permission[];
children: ReactNode;
fallback?: ReactNode;
}
export default function PermissionGuard({
permission,
children,
fallback = <Navigate to="/" replace />,
}: PermissionGuardProps) {
const { hasPermission } = useAuth();
return hasPermission(permission) ? <>{children}</> : <>{fallback}</>;
}
3. RoleGuard​
Restricts access based on high-level user roles (e.g. ADMIN, HR_MANAGER):
// features/auth/components/RoleGuard.tsx
interface RoleGuardProps {
roles: string[];
children: ReactNode;
fallback?: ReactNode;
}
export default function RoleGuard({ roles, children, fallback = <Navigate to="/" replace /> }: RoleGuardProps) {
const { hasRole } = useAuth();
return hasRole(roles) ? <>{children}</> : <>{fallback}</>;
}
Error Boundary in Layout​
The MainLayout wraps its <Outlet /> inside an ErrorBoundary. If an individual page or widget encounters a runtime render crash, the error is isolated to the page content area while keeping the navigation sidebar and header fully functional:
// shared/components/layout/MainLayout.tsx
<div className="flex-1 overflow-y-auto">
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</div>