npm package: @ldauth/react
Description: React permission checking library for the auth system
Author: Bart Riepe
Latest version: 0.10.1

# @ldauth/react

A React library for fine-grained permission checking in applications using our authentication system. This library provides components, hooks, and utilities to conditionally render UI based on user permissions.

## Installation

```bash
pnpm add @ldauth/react react-oidc-context
```

## Quick Start

### 1. Wrap your app with PermissionProvider

```tsx
import { AuthProvider } from 'react-oidc-context';
import { PermissionProvider } from '@ldauth/react';

const oidcConfig = {
  authority: 'http://localhost:7844',
  client_id: 'admin-panel',
  redirect_uri: 'http://localhost:7802/callback',
  // ... other OIDC config
};

function App() {
  return (
    <AuthProvider {...oidcConfig}>
      <PermissionProvider
        apiUrl="http://localhost:7801"
        clientId="admin-panel"
        autoFetch={true}
        cacheDuration={5 * 60 * 1000} // 5 minutes
      >
        <YourApp />
      </PermissionProvider>
    </AuthProvider>
  );
}
```

### 2. Use permission components

```tsx
import { CanAccess, RequireRole } from '@ldauth/react';

function MyComponent() {
  return (
    <div>
      {/* Show delete button only if user has permission */}
      <CanAccess resource="users" action="delete">
        <button>Delete User</button>
      </CanAccess>

      {/* Show admin panel only for admin role */}
      <RequireRole role="admin">
        <AdminPanel />
      </RequireRole>

      {/* With fallback */}
      <CanAccess 
        resource="settings" 
        action="update"
        fallback={<p>Read-only access</p>}
      >
        <SettingsForm />
      </CanAccess>
    </div>
  );
}
```

### 3. Use hooks for permissions and user information

```tsx
import {
  usePermission,
  useRole,
  useUser,
  useUserPicture,
  useUserGroups
} from '@ldauth/react';

function ItemList() {
  // Get user information from ID token (zero backend calls)
  const user = useUser();
  const avatarUrl = useUserPicture();
  const groups = useUserGroups();

  // Check permissions
  const { allowed: canDeleteItems, isLoading: deleteLoading } = usePermission('items', 'delete');
  const { allowed: canCreateItems } = usePermission('items', 'create');
  const isManager = useRole('manager');

  const handleDelete = (itemId) => {
    if (!canDeleteItems) {
      alert('You do not have permission to delete items');
      return;
    }
    // ... delete logic
  };

  return (
    <div>
      {/* User profile section */}
      <div className="user-profile">
        {avatarUrl && <img src={avatarUrl} alt="Avatar" />}
        <span>{user?.name || user?.email}</span>
        <span>Groups: {groups.join(', ')}</span>
      </div>

      {/* Actions based on permissions */}
      {canCreateItems && (
        <button>Create Item</button>
      )}

      {items.map(item => (
        <div key={item.id}>
          {item.name}
          {canDeleteItems && (
            <button onClick={() => handleDelete(item.id)}>Delete</button>
          )}
        </div>
      ))}
    </div>
  );
}
```

## Components

### `<CanAccess>`
Conditionally renders children based on a permission check.

```tsx
<CanAccess 
  resource="users" 
  action="delete"
  profile="__default" // optional
  fallback={<span>No permission</span>} // optional
  loadingFallback={<span>Checking permissions...</span>} // optional
>
  <DeleteButton />
</CanAccess>
```

### `<CanAccessMultiple>`
Check multiple permissions with AND/OR logic.

```tsx
<CanAccessMultiple
  permissions={[
    { resource: 'users', action: 'read' },
    { resource: 'groups', action: 'read' }
  ]}
  requireAll={true} // true = AND, false = OR
>
  <UserGroupManager />
</CanAccessMultiple>
```

### `<RequireRole>`
Conditionally renders based on role membership.

```tsx
<RequireRole role="admin" fallback={<Unauthorized />}>
  <AdminDashboard />
</RequireRole>
```

### `<PermissionGate>`
Custom permission logic.

```tsx
<PermissionGate 
  check={perms => perms.hasRole('admin') || perms.hasPermission('users', 'manage')}
>
  <AdvancedFeature />
</PermissionGate>
```

## Hooks

### Core Hooks

#### `useAuth()`
Access the underlying `react-oidc-context` authentication state without importing another package. This is a direct re-export of `useAuth` from `react-oidc-context`.

```tsx
import { useAuth } from '@ldauth/react';

function IdentityBadge() {
  const auth = useAuth();

  if (!auth.isAuthenticated) {
    return <button onClick={() => auth.signinRedirect()}>Sign in</button>;
  }

  return (
    <div>
      <span>{auth.user?.profile?.email}</span>
      <button onClick={() => auth.signoutRedirect()}>Logout</button>
    </div>
  );
}
```

#### `usePermissions()`
Access the permission context directly.

```tsx
const { 
  permissions,      // Current permission evaluation
  loading,         // Loading state
  error,           // Error state
  hasPermission,   // Check specific permission
  hasRole,         // Check role membership
  refresh          // Refresh permissions
} = usePermissions();
```

#### `usePermission(resource, action, profile?)`
Check a single permission. Returns an object with permission status and loading state.

```tsx
const { allowed, isLoading } = usePermission('users', 'delete');
// Or destructure with custom names:
const { allowed: canDelete, isLoading: deleteLoading } = usePermission('users', 'delete');
```

#### `useRole(role)`
Check role membership.

```tsx
const isAdmin = useRole('admin');
```

### User Information Hooks

#### `useUser()`
Access all authenticated user information from the ID token.

```tsx
const user = useUser();

if (user) {
  console.log(user.email);    // user@example.com
  console.log(user.name);     // John Doe
  console.log(user.picture);  // https://api.example.com/avatars/123.jpg
  console.log(user.groups);   // ["admin", "users"]
  console.log(user.sub);      // User ID
}
```

#### `useUserPicture()`
Get the current user's profile picture URL. Returns `null` if not authenticated or no picture available.

```tsx
const avatarUrl = useUserPicture();

return (
  <div>
    {avatarUrl ? (
      <img src={avatarUrl} alt="User avatar" className="avatar" />
    ) : (
      <DefaultAvatarIcon />
    )}
  </div>
);
```

#### `useUserGroups()`
Get the current user's group memberships. Returns an empty array if not authenticated.

```tsx
const groups = useUserGroups();
const isAdmin = groups.includes('admin');
const isModerator = groups.includes('moderator');

return (
  <div>
    <p>Your groups: {groups.join(', ')}</p>
    {isAdmin && <AdminBadge />}
  </div>
);
```

**Note**: All user information hooks read from the cached ID token (zero backend calls). The data is automatically refreshed when the user re-authenticates.

### Utility Hooks

#### `useFeatureFlags()`
Create feature flags based on permissions.

```tsx
const features = useFeatureFlags({
  canEditUsers: { resource: 'users', action: 'update' },
  canDeleteUsers: { resource: 'users', action: 'delete' },
  canViewAudit: { resource: 'audit', action: 'read' }
});

if (features.canEditUsers) {
  // Show edit button
}
```

#### `useFilterByPermission()`
Filter items based on permissions.

```tsx
const menuItems = [
  { label: 'Users', resource: 'users', action: 'read', path: '/users' },
  { label: 'Groups', resource: 'groups', action: 'read', path: '/groups' },
  { label: 'Audit', resource: 'audit', action: 'read', path: '/audit' }
];

const visibleMenuItems = useFilterByPermission(menuItems);
// Returns only items the user has permission to access
```

#### `useRequirePermission()`
Throw an error if permission check fails (useful for route protection).

```tsx
function ProtectedPage() {
  useRequirePermission('admin', 'access');
  // Will throw if user doesn't have permission
  return <AdminContent />;
}
```

## HOC (Higher Order Component)

### `withPermission()`
Wrap a component with permission check.

```tsx
const ProtectedUserList = withPermission(UserList, {
  resource: 'users',
  action: 'read',
  fallback: <div>Access denied</div>
});
```

## Integration Example

### Define Your Application's Permissions

Each application defines its own permission structure based on its needs:

```tsx
// myAppPermissions.ts
const MY_APP_FEATURES = {
  // Define feature flags based on YOUR application's resources
  canViewDashboard: { resource: 'dashboard', action: 'view' },
  canEditSettings: { resource: 'settings', action: 'edit' },
  canManageContent: { resource: 'content', action: 'manage' },
  // ... whatever permissions make sense for your app
};

function useMyAppFeatures() {
  return useFeatureFlags(MY_APP_FEATURES);
}
```

### Use in Navigation

```tsx
function AppNavigation() {
  // Define navigation based on YOUR app's permissions
  const { allowed: canViewDashboard } = usePermission('dashboard', 'view');
  const { allowed: canViewReports } = usePermission('reports', 'view');
  const { allowed: canManageSettings } = usePermission('settings', 'manage');
  
  return (
    <nav>
      {canViewDashboard && (
        <Link to="/">Dashboard</Link>
      )}
      {canViewReports && (
        <Link to="/reports">Reports</Link>
      )}
      {canManageSettings && (
        <Link to="/settings">Settings</Link>
      )}
    </nav>
  );
}
```

### Protect Routes

```tsx
import { Routes, Route } from 'react-router-dom';

function AdminRoutes() {
  return (
    <Routes>
      <Route path="/" element={
        <CanAccess resource="dashboard" action="read" fallback={<Unauthorized />}>
          <Dashboard />
        </CanAccess>
      } />
      
      <Route path="/users/*" element={
        <CanAccess resource="users" action="read" fallback={<Unauthorized />}>
          <UserRoutes />
        </CanAccess>
      } />
      
      <Route path="/settings" element={
        <RequireRole role="admin" fallback={<Unauthorized />}>
          <Settings />
        </RequireRole>
      } />
    </Routes>
  );
}
```

## Performance Considerations

1. **Caching**: Permissions are cached by default for 5 minutes. Adjust `cacheDuration` in PermissionProvider.

2. **Batch Checks**: Use `CanAccessMultiple` or `useAllPermissions` for checking multiple permissions at once.

3. **Memoization**: Permission check functions are memoized to prevent unnecessary re-renders.

4. **Lazy Loading**: Set `autoFetch={false}` and manually call `refresh()` when needed.

## Error Handling

```tsx
<PermissionProvider
  apiUrl="..."
  clientId="..."
  onError={(error) => {
    console.error('Permission check failed:', error);
    // Show notification to user
  }}
>
  {children}
</PermissionProvider>
```

## TypeScript Support

The library is fully typed and provides type-safe component factories for schema-based type safety.

### Type-Safe Components with Schema

Create typed components that provide full autocomplete for your permission schema:

```tsx
import {
  createUsePermission,
  createCanAccess,
  createCanAccessMultiple,
  createPermissionGate
} from '@ldauth/react';

// Define your permission schema
interface MyAppSchema {
  resources: {
    user: { actions: ['create', 'read', 'update', 'delete'] };
    post: { actions: ['create', 'read', 'publish', 'archive'] };
    admin: { actions: ['access', 'manage'] };
  };
}

// Create typed versions
const useTypedPermission = createUsePermission<MyAppSchema>();
const TypedCanAccess = createCanAccess<MyAppSchema>();
const TypedCanAccessMultiple = createCanAccessMultiple<MyAppSchema>();
const TypedPermissionGate = createPermissionGate<MyAppSchema>();

// Usage with full autocomplete support
function MyComponent() {
  // Typed hook with autocomplete
  const { allowed: canCreateUser, isLoading } = useTypedPermission('user', 'create');

  return (
    <div>
      {/* Single permission with autocomplete */}
      <TypedCanAccess
        resource="user"
        action="delete"
        fallback={<div>Cannot delete users</div>}
        loadingFallback={<div>Checking permissions...</div>}
      >
        <DeleteUserButton />
      </TypedCanAccess>

      {/* Multiple permissions with type safety */}
      <TypedCanAccessMultiple
        permissions={[
          { resource: "user", action: "read" },
          { resource: "post", action: "read" }
        ]}
        requireAll={true}
        fallback={<div>Need both user and post read access</div>}
      >
        <AdminDashboard />
      </TypedCanAccessMultiple>

      {/* Custom logic with type-safe context */}
      <TypedPermissionGate
        check={(client, loading) => {
          if (loading) return false;
          return client?.hasRole("admin") || client?.hasPermission("admin", "access");
        }}
        fallback={<div>Admin access required</div>}
      >
        <AdminPanel />
      </TypedPermissionGate>
    </div>
  );
}
```

### Basic Types

Import types as needed:

```tsx
import type {
  Permission,
  PermissionCheck,
  PermissionEvaluation,
  PermissionProviderConfig,
  PermissionSchemaBase
} from '@ldauth/react';
```

## Best Practices

1. **Use specific checks**: Prefer `usePermission('users', 'delete')` over `useRole('admin')`

2. **Provide fallbacks**: Always provide fallback UI for users without permissions

3. **Handle loading states**: Use `loadingFallback` to show different content while checking permissions

4. **Group related checks**: Use `useFeatureFlags()` to group related permission checks

5. **Cache appropriately**: Balance between fresh data and performance

6. **Test with different roles**: Always test your UI with different permission levels

## License

MIT