File: setup-rbac.md | Updated: 11/15/2025
Search...
+ K
Auto
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide
Documentation
Framework
React
Version
Latest
Search...
+ K
Menu
Getting Started
Installation Guides
Routing
Guides
API
Integrations
ESLint
Router Examples
Framework
React
Version
Latest
Menu
Getting Started
Installation Guides
Routing
Guides
API
Integrations
ESLint
Router Examples
On this page
Copy Markdown
This guide covers implementing role-based access control (RBAC) and permission-based routing in TanStack Router applications.
Extend your authentication context to include roles and permissions, create role-protected layout routes, and use beforeLoad to check user permissions before rendering routes.
Extend Authentication Context
-----------------------------
### 1. Add Roles to User Type
Update your authentication context to include roles:
tsx
// src/auth.tsx
import React, { createContext, useContext, useState } from 'react'
interface User {
id: string
username: string
email: string
roles: string[]
permissions: string[]
}
interface AuthState {
isAuthenticated: boolean
user: User | null
hasRole: (role: string) => boolean
hasAnyRole: (roles: string[]) => boolean
hasPermission: (permission: string) => boolean
hasAnyPermission: (permissions: string[]) => boolean
login: (username: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthState | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const hasRole = (role: string) => {
return user?.roles.includes(role) ?? false
}
const hasAnyRole = (roles: string[]) => {
return roles.some((role) => user?.roles.includes(role)) ?? false
}
const hasPermission = (permission: string) => {
return user?.permissions.includes(permission) ?? false
}
const hasAnyPermission = (permissions: string[]) => {
return (
permissions.some((permission) =>
user?.permissions.includes(permission),
) ?? false
)
}
const login = async (username: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (response.ok) {
const userData = await response.json()
setUser(userData)
setIsAuthenticated(true)
} else {
throw new Error('Authentication failed')
}
}
const logout = () => {
setUser(null)
setIsAuthenticated(false)
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
user,
hasRole,
hasAnyRole,
hasPermission,
hasAnyPermission,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
// src/auth.tsx
import React, { createContext, useContext, useState } from 'react'
interface User {
id: string
username: string
email: string
roles: string[]
permissions: string[]
}
interface AuthState {
isAuthenticated: boolean
user: User | null
hasRole: (role: string) => boolean
hasAnyRole: (roles: string[]) => boolean
hasPermission: (permission: string) => boolean
hasAnyPermission: (permissions: string[]) => boolean
login: (username: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthState | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const hasRole = (role: string) => {
return user?.roles.includes(role) ?? false
}
const hasAnyRole = (roles: string[]) => {
return roles.some((role) => user?.roles.includes(role)) ?? false
}
const hasPermission = (permission: string) => {
return user?.permissions.includes(permission) ?? false
}
const hasAnyPermission = (permissions: string[]) => {
return (
permissions.some((permission) =>
user?.permissions.includes(permission),
) ?? false
)
}
const login = async (username: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (response.ok) {
const userData = await response.json()
setUser(userData)
setIsAuthenticated(true)
} else {
throw new Error('Authentication failed')
}
}
const logout = () => {
setUser(null)
setIsAuthenticated(false)
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
user,
hasRole,
hasAnyRole,
hasPermission,
hasAnyPermission,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
### 2. Update Router Context Types
Update src/routes/__root.tsx:
tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface AuthState {
isAuthenticated: boolean
user: {
id: string
username: string
email: string
roles: string[]
permissions: string[]
} | null
hasRole: (role: string) => boolean
hasAnyRole: (roles: string[]) => boolean
hasPermission: (permission: string) => boolean
hasAnyPermission: (permissions: string[]) => boolean
login: (username: string, password: string) => Promise<void>
logout: () => void
}
interface MyRouterContext {
auth: AuthState
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<div>
<Outlet />
</div>
),
})
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface AuthState {
isAuthenticated: boolean
user: {
id: string
username: string
email: string
roles: string[]
permissions: string[]
} | null
hasRole: (role: string) => boolean
hasAnyRole: (roles: string[]) => boolean
hasPermission: (permission: string) => boolean
hasAnyPermission: (permissions: string[]) => boolean
login: (username: string, password: string) => Promise<void>
logout: () => void
}
interface MyRouterContext {
auth: AuthState
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<div>
<Outlet />
</div>
),
})
Create Role-Protected Routes
----------------------------
### 1. Admin-Only Routes
Create src/routes/_authenticated/_admin.tsx:
tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_admin')({
beforeLoad: ({ context, location }) => {
if (!context.auth.hasRole('admin')) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
},
})
}
},
component: AdminLayout,
})
function AdminLayout() {
return (
<div>
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<strong>Admin Area:</strong> You have administrative privileges.
</div>
<Outlet />
</div>
)
}
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_admin')({
beforeLoad: ({ context, location }) => {
if (!context.auth.hasRole('admin')) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
},
})
}
},
component: AdminLayout,
})
function AdminLayout() {
return (
<div>
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<strong>Admin Area:</strong> You have administrative privileges.
</div>
<Outlet />
</div>
)
}
Create src/routes/_authenticated/_moderator.tsx:
tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_moderator')({
beforeLoad: ({ context, location }) => {
const allowedRoles = ['admin', 'moderator']
if (!context.auth.hasAnyRole(allowedRoles)) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
reason: 'insufficient_role',
},
})
}
},
component: ModeratorLayout,
})
function ModeratorLayout() {
const { auth } = Route.useRouteContext()
return (
<div>
<div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded mb-4">
<strong>Moderator Area:</strong> Role: {auth.user?.roles.join(', ')}
</div>
<Outlet />
</div>
)
}
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_moderator')({
beforeLoad: ({ context, location }) => {
const allowedRoles = ['admin', 'moderator']
if (!context.auth.hasAnyRole(allowedRoles)) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
reason: 'insufficient_role',
},
})
}
},
component: ModeratorLayout,
})
function ModeratorLayout() {
const { auth } = Route.useRouteContext()
return (
<div>
<div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded mb-4">
<strong>Moderator Area:</strong> Role: {auth.user?.roles.join(', ')}
</div>
<Outlet />
</div>
)
}
### 3. Permission-Based Routes
Create src/routes/_authenticated/_users.tsx:
tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_users')({
beforeLoad: ({ context, location }) => {
const requiredPermissions = ['users:read', 'users:write']
if (!context.auth.hasAnyPermission(requiredPermissions)) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
reason: 'insufficient_permissions',
},
})
}
},
component: () => <Outlet />,
})
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_users')({
beforeLoad: ({ context, location }) => {
const requiredPermissions = ['users:read', 'users:write']
if (!context.auth.hasAnyPermission(requiredPermissions)) {
throw redirect({
to: '/unauthorized',
search: {
redirect: location.href,
reason: 'insufficient_permissions',
},
})
}
},
component: () => <Outlet />,
})
Create Specific Protected Pages
-------------------------------
### 1. Admin Dashboard
Create src/routes/_authenticated/_admin/dashboard.tsx:
tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_admin/dashboard')({
component: AdminDashboard,
})
function AdminDashboard() {
const { auth } = Route.useRouteContext()
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">Admin Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">User Management</h2>
<p className="text-gray-600">Manage all users in the system</p>
<button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
View Users
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">System Settings</h2>
<p className="text-gray-600">Configure system-wide settings</p>
<button className="mt-4 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
Open Settings
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">Reports</h2>
<p className="text-gray-600">View system reports and analytics</p>
<button className="mt-4 bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700">
View Reports
</button>
</div>
</div>
<div className="mt-8 bg-gray-100 p-4 rounded">
<h3 className="font-semibold">Your Info:</h3>
<p>Username: {auth.user?.username}</p>
<p>Roles: {auth.user?.roles.join(', ')}</p>
<p>Permissions: {auth.user?.permissions.join(', ')}</p>
</div>
</div>
)
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_admin/dashboard')({
component: AdminDashboard,
})
function AdminDashboard() {
const { auth } = Route.useRouteContext()
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">Admin Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">User Management</h2>
<p className="text-gray-600">Manage all users in the system</p>
<button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
View Users
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">System Settings</h2>
<p className="text-gray-600">Configure system-wide settings</p>
<button className="mt-4 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
Open Settings
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">Reports</h2>
<p className="text-gray-600">View system reports and analytics</p>
<button className="mt-4 bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700">
View Reports
</button>
</div>
</div>
<div className="mt-8 bg-gray-100 p-4 rounded">
<h3 className="font-semibold">Your Info:</h3>
<p>Username: {auth.user?.username}</p>
<p>Roles: {auth.user?.roles.join(', ')}</p>
<p>Permissions: {auth.user?.permissions.join(', ')}</p>
</div>
</div>
)
}
Create src/routes/_authenticated/_users/manage.tsx:
tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_users/manage')({
beforeLoad: ({ context }) => {
// Additional permission check at the page level
if (!context.auth.hasPermission('users:write')) {
throw new Error('You need write permissions to manage users')
}
},
component: UserManagement,
})
function UserManagement() {
const { auth } = Route.useRouteContext()
const canEdit = auth.hasPermission('users:write')
const canDelete = auth.hasPermission('users:delete')
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">User Management</h1>
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 whitespace-nowrap">John Doe</td>
<td className="px-6 py-4 whitespace-nowrap">john@example.com</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
User
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{canEdit && (
<button className="text-blue-600 hover:text-blue-900 mr-4">
Edit
</button>
)}
{canDelete && (
<button className="text-red-600 hover:text-red-900">
Delete
</button>
)}
</td>
</tr>
</tbody>
</table>
</div>
<div className="mt-6 p-4 bg-blue-50 rounded">
<h3 className="font-semibold text-blue-800">Your Permissions:</h3>
<ul className="text-blue-700 text-sm">
{auth.user?.permissions.map((permission) => (
<li key={permission}>✓ {permission}</li>
))}
</ul>
</div>
</div>
)
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/_users/manage')({
beforeLoad: ({ context }) => {
// Additional permission check at the page level
if (!context.auth.hasPermission('users:write')) {
throw new Error('You need write permissions to manage users')
}
},
component: UserManagement,
})
function UserManagement() {
const { auth } = Route.useRouteContext()
const canEdit = auth.hasPermission('users:write')
const canDelete = auth.hasPermission('users:delete')
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">User Management</h1>
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 whitespace-nowrap">John Doe</td>
<td className="px-6 py-4 whitespace-nowrap">john@example.com</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
User
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{canEdit && (
<button className="text-blue-600 hover:text-blue-900 mr-4">
Edit
</button>
)}
{canDelete && (
<button className="text-red-600 hover:text-red-900">
Delete
</button>
)}
</td>
</tr>
</tbody>
</table>
</div>
<div className="mt-6 p-4 bg-blue-50 rounded">
<h3 className="font-semibold text-blue-800">Your Permissions:</h3>
<ul className="text-blue-700 text-sm">
{auth.user?.permissions.map((permission) => (
<li key={permission}>✓ {permission}</li>
))}
</ul>
</div>
</div>
)
}
Create Unauthorized Page
------------------------
Create src/routes/unauthorized.tsx:
tsx
import { createFileRoute, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/unauthorized')({
validateSearch: (search) => ({
redirect: (search.redirect as string) || '/dashboard',
reason: (search.reason as string) || 'insufficient_permissions',
}),
component: UnauthorizedPage,
})
function UnauthorizedPage() {
const { redirect, reason } = Route.useSearch()
const { auth } = Route.useRouteContext()
const reasonMessages = {
insufficient_role: 'You do not have the required role to access this page.',
insufficient_permissions:
'You do not have the required permissions to access this page.',
default: 'You are not authorized to access this page.',
}
const message =
reasonMessages[reason as keyof typeof reasonMessages] ||
reasonMessages.default
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h1>
<p className="text-gray-600 mb-6">{message}</p>
<div className="mb-6 text-sm text-gray-500">
<p>
<strong>Your roles:</strong> {auth.user?.roles.join(', ') || 'None'}
</p>
<p>
<strong>Your permissions:</strong>{' '}
{auth.user?.permissions.join(', ') || 'None'}
</p>
</div>
<div className="space-y-3">
<Link
to="/dashboard"
className="block w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition-colors"
>
Go to Dashboard
</Link>
<Link
to={redirect}
className="block w-full bg-gray-200 text-gray-800 py-2 px-4 rounded hover:bg-gray-300 transition-colors"
>
Try Again
</Link>
</div>
</div>
</div>
)
}
import { createFileRoute, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/unauthorized')({
validateSearch: (search) => ({
redirect: (search.redirect as string) || '/dashboard',
reason: (search.reason as string) || 'insufficient_permissions',
}),
component: UnauthorizedPage,
})
function UnauthorizedPage() {
const { redirect, reason } = Route.useSearch()
const { auth } = Route.useRouteContext()
const reasonMessages = {
insufficient_role: 'You do not have the required role to access this page.',
insufficient_permissions:
'You do not have the required permissions to access this page.',
default: 'You are not authorized to access this page.',
}
const message =
reasonMessages[reason as keyof typeof reasonMessages] ||
reasonMessages.default
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h1>
<p className="text-gray-600 mb-6">{message}</p>
<div className="mb-6 text-sm text-gray-500">
<p>
<strong>Your roles:</strong> {auth.user?.roles.join(', ') || 'None'}
</p>
<p>
<strong>Your permissions:</strong>{' '}
{auth.user?.permissions.join(', ') || 'None'}
</p>
</div>
<div className="space-y-3">
<Link
to="/dashboard"
className="block w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition-colors"
>
Go to Dashboard
</Link>
<Link
to={redirect}
className="block w-full bg-gray-200 text-gray-800 py-2 px-4 rounded hover:bg-gray-300 transition-colors"
>
Try Again
</Link>
</div>
</div>
</div>
)
}
Component-Level Permission Checks
---------------------------------
### 1. Conditional Rendering Hook
Create src/hooks/usePermissions.ts:
tsx
import { useRouter } from '@tanstack/react-router'
export function usePermissions() {
const router = useRouter()
const auth = router.options.context.auth
return {
hasRole: auth.hasRole,
hasAnyRole: auth.hasAnyRole,
hasPermission: auth.hasPermission,
hasAnyPermission: auth.hasAnyPermission,
user: auth.user,
}
}
import { useRouter } from '@tanstack/react-router'
export function usePermissions() {
const router = useRouter()
const auth = router.options.context.auth
return {
hasRole: auth.hasRole,
hasAnyRole: auth.hasAnyRole,
hasPermission: auth.hasPermission,
hasAnyPermission: auth.hasAnyPermission,
user: auth.user,
}
}
### 2. Permission Guard Component
Create src/components/PermissionGuard.tsx:
tsx
interface PermissionGuardProps {
children: React.ReactNode
roles?: string[]
permissions?: string[]
requireAll?: boolean
fallback?: React.ReactNode
}
export function PermissionGuard({
children,
roles = [],
permissions = [],
requireAll = false,
fallback = null,
}: PermissionGuardProps) {
const { hasAnyRole, hasAnyPermission, hasRole, hasPermission } =
usePermissions()
const hasRequiredRoles =
roles.length === 0 ||
(requireAll ? roles.every((role) => hasRole(role)) : hasAnyRole(roles))
const hasRequiredPermissions =
permissions.length === 0 ||
(requireAll
? permissions.every((permission) => hasPermission(permission))
: hasAnyPermission(permissions))
if (hasRequiredRoles && hasRequiredPermissions) {
return <>{children}</>
}
return <>{fallback}</>
}
interface PermissionGuardProps {
children: React.ReactNode
roles?: string[]
permissions?: string[]
requireAll?: boolean
fallback?: React.ReactNode
}
export function PermissionGuard({
children,
roles = [],
permissions = [],
requireAll = false,
fallback = null,
}: PermissionGuardProps) {
const { hasAnyRole, hasAnyPermission, hasRole, hasPermission } =
usePermissions()
const hasRequiredRoles =
roles.length === 0 ||
(requireAll ? roles.every((role) => hasRole(role)) : hasAnyRole(roles))
const hasRequiredPermissions =
permissions.length === 0 ||
(requireAll
? permissions.every((permission) => hasPermission(permission))
: hasAnyPermission(permissions))
if (hasRequiredRoles && hasRequiredPermissions) {
return <>{children}</>
}
return <>{fallback}</>
}
### 3. Using Permission Guards
tsx
import { PermissionGuard } from '../components/PermissionGuard'
function SomeComponent() {
return (
<div>
<h1>Dashboard</h1>
<PermissionGuard roles={['admin']}>
<button className="bg-red-600 text-white px-4 py-2 rounded">
Admin Only Button
</button>
</PermissionGuard>
<PermissionGuard
permissions={['users:write']}
fallback={<p className="text-gray-500">You cannot edit users</p>}
>
<button className="bg-blue-600 text-white px-4 py-2 rounded">
Edit Users
</button>
</PermissionGuard>
<PermissionGuard
roles={['admin', 'moderator']}
permissions={['content:moderate']}
requireAll={true}
>
<button className="bg-yellow-600 text-white px-4 py-2 rounded">
Moderate Content (Admin/Mod + Permission)
</button>
</PermissionGuard>
</div>
)
}
import { PermissionGuard } from '../components/PermissionGuard'
function SomeComponent() {
return (
<div>
<h1>Dashboard</h1>
<PermissionGuard roles={['admin']}>
<button className="bg-red-600 text-white px-4 py-2 rounded">
Admin Only Button
</button>
</PermissionGuard>
<PermissionGuard
permissions={['users:write']}
fallback={<p className="text-gray-500">You cannot edit users</p>}
>
<button className="bg-blue-600 text-white px-4 py-2 rounded">
Edit Users
</button>
</PermissionGuard>
<PermissionGuard
roles={['admin', 'moderator']}
permissions={['content:moderate']}
requireAll={true}
>
<button className="bg-yellow-600 text-white px-4 py-2 rounded">
Moderate Content (Admin/Mod + Permission)
</button>
</PermissionGuard>
</div>
)
}
Advanced Permission Patterns
----------------------------
### 1. Resource-Based Permissions
tsx
// Check if user can edit a specific resource
function canEditResource(auth: AuthState, resourceId: string, ownerId: string) {
// Admin can edit anything
if (auth.hasRole('admin')) return true
// Owner can edit their own resources
if (auth.user?.id === ownerId && auth.hasPermission('resource:edit:own'))
return true
// Moderators can edit with permission
if (auth.hasRole('moderator') && auth.hasPermission('resource:edit:any'))
return true
return false
}
// Usage in component
function ResourceEditor({ resource }) {
const { auth } = Route.useRouteContext()
if (!canEditResource(auth, resource.id, resource.ownerId)) {
return <div>You cannot edit this resource</div>
}
return <EditForm resource={resource} />
}
// Check if user can edit a specific resource
function canEditResource(auth: AuthState, resourceId: string, ownerId: string) {
// Admin can edit anything
if (auth.hasRole('admin')) return true
// Owner can edit their own resources
if (auth.user?.id === ownerId && auth.hasPermission('resource:edit:own'))
return true
// Moderators can edit with permission
if (auth.hasRole('moderator') && auth.hasPermission('resource:edit:any'))
return true
return false
}
// Usage in component
function ResourceEditor({ resource }) {
const { auth } = Route.useRouteContext()
if (!canEditResource(auth, resource.id, resource.ownerId)) {
return <div>You cannot edit this resource</div>
}
return <EditForm resource={resource} />
}
tsx
function hasTimeBasedPermission(auth: AuthState, permission: string) {
const userPermissions = auth.user?.permissions || []
const hasPermission = userPermissions.includes(permission)
// Check if permission has time restrictions
const timeRestricted = userPermissions.find((p) =>
p.startsWith(`${permission}:time:`),
)
if (timeRestricted) {
const [, , startHour, endHour] = timeRestricted.split(':')
const currentHour = new Date().getHours()
return (
currentHour >= parseInt(startHour) && currentHour <= parseInt(endHour)
)
}
return hasPermission
}
function hasTimeBasedPermission(auth: AuthState, permission: string) {
const userPermissions = auth.user?.permissions || []
const hasPermission = userPermissions.includes(permission)
// Check if permission has time restrictions
const timeRestricted = userPermissions.find((p) =>
p.startsWith(`${permission}:time:`),
)
if (timeRestricted) {
const [, , startHour, endHour] = timeRestricted.split(':')
const currentHour = new Date().getHours()
return (
currentHour >= parseInt(startHour) && currentHour <= parseInt(endHour)
)
}
return hasPermission
}
Common Problems
---------------
### Role/Permission Data Not Loading
Problem: User roles/permissions are undefined in routes.
Solution: Ensure your authentication API returns complete user data:
tsx
const login = async (username: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (response.ok) {
const userData = await response.json()
// Ensure userData includes roles and permissions
console.log('User data:', userData) // Debug log
setUser(userData)
setIsAuthenticated(true)
}
}
const login = async (username: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (response.ok) {
const userData = await response.json()
// Ensure userData includes roles and permissions
console.log('User data:', userData) // Debug log
setUser(userData)
setIsAuthenticated(true)
}
}
### Permission Checks Too Restrictive
Problem: Users locked out of areas they should access.
Solution: Use hierarchical permissions and role inheritance:
tsx
const roleHierarchy = {
admin: ['admin', 'moderator', 'user'],
moderator: ['moderator', 'user'],
user: ['user'],
}
const hasRole = (requiredRole: string) => {
const userRoles = user?.roles || []
return userRoles.some((userRole) =>
roleHierarchy[userRole]?.includes(requiredRole),
)
}
const roleHierarchy = {
admin: ['admin', 'moderator', 'user'],
moderator: ['moderator', 'user'],
user: ['user'],
}
const hasRole = (requiredRole: string) => {
const userRoles = user?.roles || []
return userRoles.some((userRole) =>
roleHierarchy[userRole]?.includes(requiredRole),
)
}
### Performance Issues with Many Permission Checks
Problem: Too many permission checks slowing down renders.
Solution: Memoize permission computations:
tsx
import { useMemo } from 'react'
function usePermissions() {
const { auth } = Route.useRouteContext()
const permissions = useMemo(
() => ({
canEditUsers: auth.hasPermission('users:write'),
canDeleteUsers: auth.hasPermission('users:delete'),
isAdmin: auth.hasRole('admin'),
isModerator: auth.hasAnyRole(['admin', 'moderator']),
}),
[auth.user?.roles, auth.user?.permissions],
)
return permissions
}
import { useMemo } from 'react'
function usePermissions() {
const { auth } = Route.useRouteContext()
const permissions = useMemo(
() => ({
canEditUsers: auth.hasPermission('users:write'),
canDeleteUsers: auth.hasPermission('users:delete'),
isAdmin: auth.hasRole('admin'),
isModerator: auth.hasAnyRole(['admin', 'moderator']),
}),
[auth.user?.roles, auth.user?.permissions],
)
return permissions
}
Common Next Steps
-----------------
After setting up RBAC, you might want to:
Related Resources
-----------------
