Frontend Integration Examples for User Roles
Overview
This document provides practical examples of how to integrate the user role system into your frontend application.
Basic Permission Checking
Check if User Can Create Organization
import { hasPermission, type UserRole } from '@izri/shared'
interface User {
id: string
email: string
name: string
role: string
}
function OrganizationCreateButton({ user }: { user: User }) {
const canCreate = hasPermission(user.role as UserRole, 'canCreateOrganization')
if (!canCreate) {
return null // Don't show button if user can't create
}
return (
<button onClick={handleCreateOrganization}>
Create Organization
</button>
)
}
Show Role Badge
import { USER_ROLES, type UserRole } from '@izri/shared'
function UserRoleBadge({ role }: { role: string }) {
const userRole = role as UserRole
const badgeColors = {
[USER_ROLES.USER]: 'bg-blue-100 text-blue-800',
[USER_ROLES.ADMIN]: 'bg-purple-100 text-purple-800',
[USER_ROLES.SUSPENDED]: 'bg-red-100 text-red-800',
}
return (
<span className={`px-2 py-1 rounded text-xs ${badgeColors[userRole]}`}>
{role}
</span>
)
}
Conditional Navigation Menu
import { hasPermission, type UserRole } from '@izri/shared'
function NavigationMenu({ user }: { user: User }) {
const canCreateOrg = hasPermission(user.role as UserRole, 'canCreateOrganization')
const canCreateProject = hasPermission(user.role as UserRole, 'canCreateProject')
const isAdmin = user.role === 'ADMIN'
return (
<nav>
<a href="/dashboard">Dashboard</a>
{canCreateOrg && (
<a href="/organizations/new">New Organization</a>
)}
{canCreateProject && (
<a href="/projects/new">New Project</a>
)}
{isAdmin && (
<a href="/admin">Admin Panel</a>
)}
</nav>
)
}
React Hook for Permissions
Create a reusable hook:
// hooks/usePermission.ts
import { hasPermission, type UserRole, type RolePermissions } from '@izri/shared'
import { useAuth } from './useAuth' // Your auth hook
export function usePermission(permission: keyof RolePermissions): boolean {
const { user } = useAuth()
if (!user) return false
return hasPermission(user.role as UserRole, permission)
}
export function useIsAdmin(): boolean {
const { user } = useAuth()
return user?.role === 'ADMIN'
}
Usage:
function CreateOrgButton() {
const canCreate = usePermission('canCreateOrganization')
return canCreate ? (
<button>Create Organization</button>
) : null
}
Handling Permission Errors
import { trpc } from '@/utils/trpc'
import { toast } from 'sonner'
function CreateOrganizationForm() {
const createOrg = trpc.organizations.create.useMutation({
onError: (error) => {
if (error.data?.code === 'FORBIDDEN') {
toast.error('Permission Denied', {
description: error.message,
})
} else {
toast.error('Failed to create organization')
}
},
onSuccess: () => {
toast.success('Organization created successfully!')
},
})
// ... rest of component
}
Disabled State for Insufficient Permissions
function CreateOrgButton({ user }: { user: User }) {
const canCreate = hasPermission(user.role as UserRole, 'canCreateOrganization')
return (
<button
disabled={!canCreate}
className={!canCreate ? 'opacity-50 cursor-not-allowed' : ''}
title={!canCreate ? 'You do not have permission to create organizations' : ''}
>
Create Organization
</button>
)
}
Protected Routes
// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom'
import { hasPermission, type UserRole, type RolePermissions } from '@izri/shared'
import { useAuth } from '@/hooks/useAuth'
interface ProtectedRouteProps {
children: React.ReactNode
requiredPermission?: keyof RolePermissions
adminOnly?: boolean
}
export function ProtectedRoute({
children,
requiredPermission,
adminOnly = false
}: ProtectedRouteProps) {
const { user, isLoading } = useAuth()
if (isLoading) {
return <div>Loading...</div>
}
if (!user) {
return <Navigate to="/login" />
}
if (adminOnly && user.role !== 'ADMIN') {
return <Navigate to="/unauthorized" />
}
if (requiredPermission && !hasPermission(user.role as UserRole, requiredPermission)) {
return <Navigate to="/unauthorized" />
}
return <>{children}</>
}
Usage in router:
import { ProtectedRoute } from '@/components/ProtectedRoute'
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/organizations/new"
element={
<ProtectedRoute requiredPermission="canCreateOrganization">
<CreateOrganization />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute adminOnly>
<AdminPanel />
</ProtectedRoute>
}
/>
</Routes>
)
}
Suspended User Notice
function SuspendedNotice({ user }: { user: User }) {
if (user.role !== 'SUSPENDED') return null
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<h3 className="text-red-800 font-semibold">Account Suspended</h3>
<p className="text-red-700 text-sm mt-1">
Your account has been suspended. You have read-only access.
Please contact support for more information.
</p>
</div>
)
}
Feature Flags Component
import { hasPermission, type UserRole, type RolePermissions } from '@izri/shared'
import { useAuth } from '@/hooks/useAuth'
interface FeatureFlagProps {
permission: keyof RolePermissions
children: React.ReactNode
fallback?: React.ReactNode
}
export function FeatureFlag({ permission, children, fallback = null }: FeatureFlagProps) {
const { user } = useAuth()
if (!user) return <>{fallback}</>
const hasAccess = hasPermission(user.role as UserRole, permission)
return <>{hasAccess ? children : fallback}</>
}
Usage:
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<FeatureFlag
permission="canCreateOrganization"
fallback={<p className="text-gray-500">Upgrade to create organizations</p>}
>
<button>Create Organization</button>
</FeatureFlag>
</div>
)
}
Admin Panel User Management Example
import { trpc } from '@/utils/trpc'
import { USER_ROLES } from '@izri/shared'
function UserManagementTable() {
const { data: users } = trpc.users.list.useQuery() // Hypothetical endpoint
const updateRole = trpc.users.updateRole.useMutation() // Hypothetical endpoint
const handleRoleChange = (userId: string, newRole: string) => {
updateRole.mutate({ userId, role: newRole })
}
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users?.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<select
value={user.role}
onChange={(e) => handleRoleChange(user.id, e.target.value)}
>
<option value={USER_ROLES.USER}>User</option>
<option value={USER_ROLES.ADMIN}>Admin</option>
<option value={USER_ROLES.SUSPENDED}>Suspended</option>
</select>
</td>
<td>
{/* Actions */}
</td>
</tr>
))}
</tbody>
</table>
)
}
Complete Example: Organization Create Page
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { trpc } from '@/utils/trpc'
import { hasPermission, type UserRole } from '@izri/shared'
import { useAuth } from '@/hooks/useAuth'
import { toast } from 'sonner'
export function CreateOrganizationPage() {
const navigate = useNavigate()
const { user } = useAuth()
const [name, setName] = useState('')
const [slug, setSlug] = useState('')
const [description, setDescription] = useState('')
const createOrg = trpc.organizations.create.useMutation({
onSuccess: (data) => {
toast.success('Organization created!')
navigate(`/orgs/${data.organization.slug}`)
},
onError: (error) => {
if (error.data?.code === 'FORBIDDEN') {
toast.error('Permission denied', {
description: error.message,
})
} else {
toast.error('Failed to create organization')
}
},
})
// Check permission
const canCreate = user && hasPermission(user.role as UserRole, 'canCreateOrganization')
if (!canCreate) {
return (
<div className="p-8">
<h1 className="text-2xl font-bold text-red-600">Permission Denied</h1>
<p className="mt-2 text-gray-700">
You do not have permission to create organizations.
</p>
</div>
)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
createOrg.mutate({
name,
slug,
description,
userEmail: user.email,
})
}
return (
<div className="p-8 max-w-2xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Create Organization</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">
Organization Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full border rounded px-3 py-2"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Slug
</label>
<input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value)}
className="w-full border rounded px-3 py-2"
pattern="[a-z0-9-]+"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Description (optional)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full border rounded px-3 py-2"
rows={3}
/>
</div>
<button
type="submit"
disabled={createOrg.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{createOrg.isPending ? 'Creating...' : 'Create Organization'}
</button>
</form>
</div>
)
}
Best Practices
Always check permissions on both frontend and backend
- Frontend checks improve UX
- Backend checks ensure security
Provide clear feedback
- Show why users can't access features
- Suggest how to gain access (if applicable)
Hide rather than disable when appropriate
- Don't show features users can never access
- Disable features users might gain access to later
Cache permission checks
- User role rarely changes during a session
- Use React hooks to avoid repeated checks
Handle errors gracefully
- Show user-friendly messages
- Log permission errors for monitoring