Docs /features/user-roles-frontend-example

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

  1. Always check permissions on both frontend and backend

    • Frontend checks improve UX
    • Backend checks ensure security
  2. Provide clear feedback

    • Show why users can't access features
    • Suggest how to gain access (if applicable)
  3. Hide rather than disable when appropriate

    • Don't show features users can never access
    • Disable features users might gain access to later
  4. Cache permission checks

    • User role rarely changes during a session
    • Use React hooks to avoid repeated checks
  5. Handle errors gracefully

    • Show user-friendly messages
    • Log permission errors for monitoring

Reading this with an agent? /docs/features/user-roles-frontend-example.md serves the raw markdown.

All docs