Usage Guide

A comprehensive guide to using @iamjs/react for role-based access control in React applications.

Introduction

@iamjs/react is a powerful library for implementing role-based access control (RBAC) in React applications. It provides a set of tools and components to easily manage and check permissions based on user roles, allowing for fine-grained control over what users can see and do in your application.

Basic Concepts

  • Role: Defines a set of permissions for various resources and actions.
  • Schema: A collection of roles used for authorization.
  • Resources: The objects or entities that are being protected (e.g., 'posts', 'users').
  • Actions: The operations that can be performed on resources (e.g., 'read', 'write', 'delete').

Setting Up Roles and Schema

Let's create a more comprehensive example with multiple roles for a blog application:

import { Role } from '@iamjs/core';
import { createSchema } from '@iamjs/react';
 
const readerRole = new Role({
  name: 'reader',
  config: {
    posts: {
      base: 'r--l',
      custom: {
        like: true,
        comment: true
      }
    },
    profile: {
      base: 'r---',
      custom: {
        viewPublicInfo: true
      }
    }
  }
});
 
const authorRole = new Role({
  name: 'author',
  config: {
    posts: {
      base: 'crudl',
      custom: {
        publish: true,
        unpublish: true
      }
    },
    profile: {
      base: 'rud-',
      custom: {
        changePassword: true,
        updateBio: true
      }
    },
    comments: {
      base: 'crudl',
      custom: {
        moderate: true
      }
    }
  }
});
 
const adminRole = new Role({
  name: 'admin',
  config: {
    posts: {
      base: 'crudl',
      custom: {
        publish: true,
        unpublish: true,
        feature: true
      }
    },
    users: {
      base: 'crudl',
      custom: {
        ban: true,
        unban: true,
        changeRole: true
      }
    },
    comments: {
      base: 'crudl',
      custom: {
        moderate: true,
        delete: true
      }
    },
    system: {
      base: 'crudl',
      custom: {
        configureSite: true,
        viewAnalytics: true
      }
    }
  }
});
 
const schema = createSchema({ reader: readerRole, author: authorRole, admin: adminRole });

Using the useAuthorization Hook

Here's a more detailed example of using the useAuthorization hook in a component:

import React from 'react';
import { useAuthorization } from '@iamjs/react';
 
function BlogPost({ post, currentUser }) {
  const auth = useAuthorization(schema);
 
  const canEdit = auth.can(currentUser.role, 'posts', 'update');
  const canDelete = auth.can(currentUser.role, 'posts', 'delete');
  const canModerateComments = auth.can(currentUser.role, 'comments', 'moderate');
 
  return (
    <div className="blog-post">
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {canEdit && <button onClick={() => editPost(post.id)}>Edit Post</button>}
      {canDelete && <button onClick={() => deletePost(post.id)}>Delete Post</button>}
      
      <CommentsSection postId={post.id} />
      
      {canModerateComments && <ModerateCommentsPanel postId={post.id} />}
    </div>
  );
}

Authorization Methods

can

Here's a more complex example using can with multiple actions:

function UserProfileActions({ userId, currentUser }) {
  const auth = useAuthorization(schema);
 
  const canManageUser = auth.can(
    currentUser.role,
    'users',
    ['update', 'delete', 'ban'],
    true // strict mode
  );
 
  return (
    <div className="user-actions">
      {canManageUser ? (
        <>
          <button onClick={() => updateUser(userId)}>Update User</button>
          <button onClick={() => deleteUser(userId)}>Delete User</button>
          <button onClick={() => banUser(userId)}>Ban User</button>
        </>
      ) : (
        <p>You don't have permission to manage this user.</p>
      )}
    </div>
  );
}

authorize

Using authorize for more granular control:

function AdminDashboard({ currentUser }) {
  const auth = useAuthorization(schema);
 
  const canAccessAnalytics = auth.authorize({
    role: currentUser.role,
    resources: 'system',
    actions: 'viewAnalytics'
  });
 
  const canManageSite = auth.authorize({
    role: currentUser.role,
    resources: 'system',
    actions: ['configureSite', 'viewAnalytics'],
    strict: true
  });
 
  return (
    <div className="admin-dashboard">
      <h1>Admin Dashboard</h1>
      {canAccessAnalytics && <AnalyticsPanel />}
      {canManageSite && <SiteConfigurationPanel />}
    </div>
  );
}

use

Utilizing the use method for role-specific logic:

function UserRoleInfo({ userId }) {
  const auth = useAuthorization(schema);
  const [userRole, setUserRole] = React.useState(null);
 
  React.useEffect(() => {
    // Simulating an API call to get user's role
    fetchUserRole(userId).then(role => setUserRole(role));
  }, [userId]);
 
  if (!userRole) return <p>Loading...</p>;
 
  const roleInstance = auth.use(userRole);
 
  return (
    <div className="user-role-info">
      <h2>User Permissions</h2>
      <ul>
        <li>Can create posts: {roleInstance.can('posts', 'create') ? 'Yes' : 'No'}</li>
        <li>Can moderate comments: {roleInstance.can('comments', 'moderate') ? 'Yes' : 'No'}</li>
        <li>Can access analytics: {roleInstance.can('system', 'viewAnalytics') ? 'Yes' : 'No'}</li>
      </ul>
    </div>
  );
}

Conditional Rendering with Show Component

A more complex example using nested Show components:

function BlogPostManager({ post, currentUser }) {
  const auth = useAuthorization(schema);
 
  return (
    <div className="blog-post-manager">
      <h1>{post.title}</h1>
      
      <auth.Show role={currentUser.role} resources="posts" actions="read">
        <p>{post.content}</p>
        
        <auth.Show role={currentUser.role} resources="posts" actions="update">
          <button onClick={() => editPost(post.id)}>Edit Post</button>
        </auth.Show>
        
        <auth.Show role={currentUser.role} resources="posts" actions="delete">
          <button onClick={() => deletePost(post.id)}>Delete Post</button>
        </auth.Show>
        
        <auth.Show role={currentUser.role} resources="comments" actions={['read', 'create']}>
          <CommentsSection postId={post.id} />
          
          <auth.Show role={currentUser.role} resources="comments" actions="moderate">
            <ModerateCommentsPanel postId={post.id} />
          </auth.Show>
        </auth.Show>
      </auth.Show>
    </div>
  );
}

Dynamic Role Building

An example of using dynamic role building in a user management component:

function UserPermissionManager({ userId }) {
  const auth = useAuthorization(schema);
  const [userPermissions, setUserPermissions] = React.useState(null);
 
  React.useEffect(() => {
    // Simulating an API call to get user's custom permissions
    fetchUserPermissions(userId).then(permissions => setUserPermissions(permissions));
  }, [userId]);
 
  if (!userPermissions) return <p>Loading...</p>;
 
  const dynamicRole = auth.build(userPermissions);
 
  return (
    <div className="user-permission-manager">
      <h2>User Custom Permissions</h2>
      
      <dynamicRole.Show resources="posts" actions="create">
        <p>User can create posts</p>
      </dynamicRole.Show>
      
      <dynamicRole.Show resources="comments" actions="moderate">
        <p>User can moderate comments</p>
      </dynamicRole.Show>
      
      <h3>Detailed Permissions:</h3>
      <ul>
        <li>Edit any post: {dynamicRole.can('posts', 'update') ? 'Yes' : 'No'}</li>
        <li>Delete comments: {dynamicRole.can('comments', 'delete') ? 'Yes' : 'No'}</li>
        <li>Ban users: {dynamicRole.can('users', 'ban') ? 'Yes' : 'No'}</li>
      </ul>
      
      {dynamicRole.can('system', 'configureSite') && (
        <button onClick={() => openSiteConfiguration()}>Configure Site</button>
      )}
    </div>
  );
}

TypeScript Support

Enhanced TypeScript example with custom types:

import { Role, Roles, TRoleOptions } from '@iamjs/core';
import { useAuthorization, TShowProps, TBuildShowProps } from '@iamjs/react';
 
interface BlogResources {
  posts: string;
  comments: string;
  users: string;
  system: string;
}
 
interface BlogActions {
  posts: 'create' | 'read' | 'update' | 'delete' | 'publish' | 'unpublish';
  comments: 'create' | 'read' | 'update' | 'delete' | 'moderate';
  users: 'create' | 'read' | 'update' | 'delete' | 'ban' | 'unban';
  system: 'configureSite' | 'viewAnalytics';
}
 
type BlogRoles = {
  reader: Role<TRoleOptions<BlogResources, BlogActions>>;
  author: Role<TRoleOptions<BlogResources, BlogActions>>;
  admin: Role<TRoleOptions<BlogResources, BlogActions>>;
};
 
const schema = createSchema<BlogRoles>({ /* ... role definitions ... */ });
 
function BlogComponent() {
  const auth = useAuthorization<BlogRoles>(schema);
 
  // TypeScript will provide autocomplete and type checking
  const canCreatePost: boolean = auth.can('author', 'posts', 'create');
  const canModerateComments: boolean = auth.can('admin', 'comments', 'moderate');
 
  const ShowAdminControls: React.FC<TShowProps<BlogRoles>> = ({ children }) => (
    <auth.Show
      role="admin"
      resources={['posts', 'comments']}
      actions={['delete', 'moderate']}
      strict={true}
    >
      {children}
    </auth.Show>
  );
 
  return (
    <div>
      {canCreatePost && <CreatePostButton />}
      {canModerateComments && <ModerateCommentsPanel />}
      <ShowAdminControls>
        <AdminControlPanel />
      </ShowAdminControls>
    </div>
  );
}

Advanced Usage Patterns

Composing Multiple Permissions

function ComplexPermissionCheck({ user, post }) {
  const auth = useAuthorization(schema);
 
  const canEditPost = auth.can(user.role, 'posts', 'update');
  const isPostAuthor = user.id === post.authorId;
  const canModerateComments = auth.can(user.role, 'comments', 'moderate');
 
  const canPerformAction = canEditPost && (isPostAuthor || canModerateComments);
 
  return (
    <div>
      {canPerformAction ? (
        <EditPostForm post={post} />
      ) : (
        <p>You don't have permission to edit this post.</p>
      )}
    </div>
  );
}

Using with React Context

import React from 'react';
import { useAuthorization } from '@iamjs/react';
 
const AuthContext = React.createContext(null);
 
export function AuthProvider({ children, schema }) {
  const auth = useAuthorization(schema);
  return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
 
export function useAuth() {
  const context = React.useContext(AuthContext);
  if (context === null) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}
 
// Usage
function App() {
  return (
    <AuthProvider schema={schema}>
      <YourComponents />
    </AuthProvider>
  );
}
 
function ProtectedComponent() {
  const auth = useAuth();
  // Use auth methods here
}

Performance Optimized Usage Examples

Optimized useAuthorization Hook Usage

Use useMemo to memoize the result of useAuthorization to prevent unnecessary re-renders:

import React, { useMemo } from 'react';
import { useAuthorization } from '@iamjs/react';
 
function BlogPost({ post, currentUser }) {
  const auth = useMemo(() => useAuthorization(schema), []);
 
  const permissions = useMemo(() => ({
    canEdit: auth.can(currentUser.role, 'posts', 'update'),
    canDelete: auth.can(currentUser.role, 'posts', 'delete'),
    canModerateComments: auth.can(currentUser.role, 'comments', 'moderate')
  }), [currentUser.role, auth]);
 
  return (
    <div className="blog-post">
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {permissions.canEdit && <EditButton postId={post.id} />}
      {permissions.canDelete && <DeleteButton postId={post.id} />}
      
      <CommentsSection postId={post.id} />
      
      {permissions.canModerateComments && <ModerateCommentsPanel postId={post.id} />}
    </div>
  );
}
 
const EditButton = React.memo(({ postId }) => (
  <button onClick={() => editPost(postId)}>Edit Post</button>
));
 
const DeleteButton = React.memo(({ postId }) => (
  <button onClick={() => deletePost(postId)}>Delete Post</button>
));

Optimized Dynamic Role Building

Use useMemo and useCallback for optimized dynamic role building:

function UserPermissionManager({ userId }) {
  const auth = useMemo(() => useAuthorization(schema), []);
  const [userPermissions, setUserPermissions] = useState(null);
 
  useEffect(() => {
    fetchUserPermissions(userId).then(setUserPermissions);
  }, [userId]);
 
  const dynamicRole = useMemo(() => {
    if (!userPermissions) return null;
    return auth.build(userPermissions);
  }, [auth, userPermissions]);
 
  const openSiteConfiguration = useCallback(() => {
    // Implementation
  }, []);
 
  if (!dynamicRole) return <p>Loading...</p>;
 
  return (
    <div className="user-permission-manager">
      <h2>User Custom Permissions</h2>
      
      <dynamicRole.Show resources="posts" actions="create">
        <p>User can create posts</p>
      </dynamicRole.Show>
      
      <dynamicRole.Show resources="comments" actions="moderate">
        <p>User can moderate comments</p>
      </dynamicRole.Show>
      
      <PermissionList dynamicRole={dynamicRole} />
      
      {dynamicRole.can('system', 'configureSite') && (
        <button onClick={openSiteConfiguration}>Configure Site</button>
      )}
    </div>
  );
}
 
const PermissionList = React.memo(({ dynamicRole }) => (
  <>
    <h3>Detailed Permissions:</h3>
    <ul>
      <li>Edit any post: {dynamicRole.can('posts', 'update') ? 'Yes' : 'No'}</li>
      <li>Delete comments: {dynamicRole.can('comments', 'delete') ? 'Yes' : 'No'}</li>
      <li>Ban users: {dynamicRole.can('users', 'ban') ? 'Yes' : 'No'}</li>
    </ul>
  </>
));

Advanced Usage Patterns with Performance Optimization

Custom Hook for Permissions

Create a custom hook to encapsulate permission checks and memoize results:

function usePermissions(role) {
  const auth = useMemo(() => useAuthorization(schema), []);
 
  return useMemo(() => ({
    canCreatePost: auth.can(role, 'posts', 'create'),
    canEditPost: auth.can(role, 'posts', 'update'),
    canDeletePost: auth.can(role, 'posts', 'delete'),
    canModerateComments: auth.can(role, 'comments', 'moderate'),
  }), [auth, role]);
}
 
function BlogComponent({ currentUser }) {
  const permissions = usePermissions(currentUser.role);
 
  return (
    <div>
      {permissions.canCreatePost && <CreatePostButton />}
      {permissions.canModerateComments && <ModerateCommentsPanel />}
      <PostList permissions={permissions} />
    </div>
  );
}
 
const PostList = React.memo(({ permissions }) => {
  // Render posts with appropriate action buttons based on permissions
});

Optimized Context Usage

Optimize the AuthContext usage with useMemo and useCallback:

import React, { useMemo, useCallback } from 'react';
import { useAuthorization } from '@iamjs/react';
 
const AuthContext = React.createContext(null);
 
export function AuthProvider({ children, schema }) {
  const auth = useMemo(() => useAuthorization(schema), [schema]);
 
  const contextValue = useMemo(() => ({
    can: useCallback((...args) => auth.can(...args), [auth]),
    Show: auth.Show,
    build: useCallback((data) => auth.build(data), [auth]),
  }), [auth]);
 
  return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
}
 
export function useAuth() {
  const context = React.useContext(AuthContext);
  if (context === null) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Best Practices and Tips

  1. Centralize Role Definitions: Keep your role definitions in a separate file for easy management.

  2. Use Granular Permissions: Define specific permissions for different actions on resources.

  3. Leverage TypeScript: Use TypeScript to catch permission-related errors early.

  4. Combine with Authentication: Integrate @iamjs/react with your authentication system.

  5. Memoize Permission Checks: For performance in large applications, consider memoizing permission check results.

  6. Testing: Write unit tests for your permission logic.

  7. Dynamic Permissions: Use the build method for runtime permission changes.

  8. Consistent Usage: Stick to either can method or Show component throughout your app.

  9. Error Boundaries: Implement error boundaries to gracefully handle unauthorized access attempts.

  10. Documentation: Keep your roles and permissions well-documented for team reference.