Dashforge RBAC Integration | Dashforge-UI
DocsStarter Kits
v0.1.0-alpha

Dashforge Integration

How RBAC reduces real work inside Dashforge applications through declarative component APIs and built-in helpers.

Why Dashforge Integration

The difference between scattered manual checks and declarative access control

Without Dashforge integration, you'd need to manually wrap every component with RBAC checks. Dashforge components accept an access prop that handles permissions declaratively.

Manual Approach (Without Dashforge)

function BookingForm() {
  const { can } = useRbac();
  const canEditSalary = can({ action: 'edit', resource: 'booking.salary' });
  const canEditStatus = can({ action: 'edit', resource: 'booking.status' });

  return (
    <form>
      {canEditSalary ? (
        <TextField name="salary" label="Salary" />
      ) : null}
      
      <TextField 
        name="status" 
        label="Status"
        disabled={!canEditStatus}
      />
    </form>
  );
}

Dashforge Approach (Declarative)

function BookingForm() {
  return (
    <DashForm>
      <TextField 
        name="salary" 
        label="Salary"
        access={{
          action: 'edit',
          resource: 'booking.salary',
          onUnauthorized: 'hide',
        }}
      />
      
      <TextField 
        name="status" 
        label="Status"
        access={{
          action: 'edit',
          resource: 'booking.status',
          onUnauthorized: 'disable',
        }}
      />
    </DashForm>
  );
}

Key benefit: Permission logic lives with the component it protects. No scattered useCan() calls. No manual conditional rendering.


Component-level Access

How Dashforge components consume RBAC directly

Dashforge form components support an access prop that accepts an AccessRequirement. The component handles permission evaluation internally.

TextField with access prop

<TextField
  name="customerEmail"
  label="Customer Email"
  access={{
    action: 'read',
    resource: 'customer.email',
    onUnauthorized: 'hide',
  }}
/>

Three unauthorized behaviors

hide

<TextField
  name="salary"
  label="Salary"
  access={{
    action: 'read',
    resource: 'booking.salary',
    onUnauthorized: 'hide',
  }}
/>
// Component renders null when permission denied

disable

<TextField
  name="status"
  label="Status"
  access={{
    action: 'edit',
    resource: 'booking.status',
    onUnauthorized: 'disable',
  }}
/>
// Component renders disabled when permission denied

readonly

<TextField
  name="createdAt"
  label="Created At"
  access={{
    action: 'edit',
    resource: 'booking.createdAt',
    onUnauthorized: 'readonly',
  }}
/>
// Component renders readonly when permission denied

ℹ️

Current support: The access prop is implemented on form field components (TextField, Select, Autocomplete, etc.). Other components use useCan() directly.


UI Actions

Controlling buttons and actions with RBAC

For actions and buttons, use useCan() directly to control visibility or disabled state.

Conditional action rendering

function BookingActions({ bookingId }: { bookingId: string }) {
  const { can } = useRbac();
  
  const canDelete = can({ action: 'delete', resource: 'booking' });
  const canEdit = can({ action: 'edit', resource: 'booking' });

  return (
    <Stack direction="row" spacing={2}>
      {canEdit && (
        <Button onClick={() => editBooking(bookingId)}>
          Edit Booking
        </Button>
      )}
      
      {canDelete && (
        <Button 
          color="error"
          onClick={() => deleteBooking(bookingId)}
        >
          Delete Booking
        </Button>
      )}
    </Stack>
  );
}

Disabled state control

function CustomerActions({ customerId }: { customerId: string }) {
  const { can } = useRbac();
  
  const canArchive = can({ action: 'archive', resource: 'customer' });

  return (
    <Button 
      disabled={!canArchive}
      onClick={() => archiveCustomer(customerId)}
    >
      Archive Customer
    </Button>
  );
}

Resource-specific permissions

function InvoiceActions({ invoice }: { invoice: Invoice }) {
  const { can } = useRbac();
  
  const canApprove = can({
    action: 'approve',
    resource: 'invoice',
    resourceData: { ownerId: invoice.ownerId },
  });

  return (
    <Button 
      disabled={!canApprove}
      onClick={() => approveInvoice(invoice.id)}
    >
      Approve Invoice
    </Button>
  );
}

ℹ️

Current implementation: Button and other action components do not yet have a built-in access prop. Use useCan() for now.


Forms + Visibility

How visibleWhen and access work together

Dashforge fields support both visibleWhen (UI/form logic) and access (permissions). Both must be satisfied for a field to be visible.

Combining visibleWhen and access

<TextField
  name="otherReason"
  label="Other Reason"
  visibleWhen={(engine) => 
    engine.getNode('category')?.value === 'other'
  }
  access={{
    action: 'edit',
    resource: 'booking.otherReason',
    onUnauthorized: 'hide',
  }}
/>

// Field is visible ONLY when:
// 1. category === 'other' (UI logic)
// 2. User has 'edit booking.otherReason' permission (RBAC)

Important distinction

visibleWhen

UI business logic (form state, field dependencies, conditional sections)

access

Permissions (who can see/edit this field based on their role)

⚠️

Do not use visibleWhen for permissions. Use access. Do not use access for UI logic. Use visibleWhen. They serve different purposes.


Filtering

Using filterNavigationItems and filterActions

Dashforge provides utilities for filtering navigation items and actions based on RBAC permissions.

filterNavigationItems

import { filterNavigationItems } from '@dashforge/rbac';

function AppNav() {
  const { can } = useRbac();

  const allItems = [
    {
      id: 'bookings',
      label: 'Bookings',
      path: '/bookings',
      access: { action: 'read', resource: 'booking' },
    },
    {
      id: 'customers',
      label: 'Customers',
      path: '/customers',
      access: { action: 'read', resource: 'customer' },
    },
    {
      id: 'admin',
      label: 'Admin',
      path: '/admin',
      access: { action: 'manage', resource: 'settings' },
    },
  ];

  const visibleItems = filterNavigationItems(allItems, can);

  return <LeftNav items={visibleItems} />;
}

filterActions

import { filterActions } from '@dashforge/rbac';

function BookingToolbar() {
  const { can } = useRbac();

  const allActions = [
    {
      id: 'create',
      label: 'Create Booking',
      onClick: handleCreate,
      access: { action: 'create', resource: 'booking' },
    },
    {
      id: 'export',
      label: 'Export',
      onClick: handleExport,
      access: { action: 'export', resource: 'booking' },
    },
    {
      id: 'archive',
      label: 'Archive All',
      onClick: handleArchive,
      access: { action: 'archive', resource: 'booking' },
    },
  ];

  const visibleActions = filterActions(allActions, can);

  return <ActionMenu actions={visibleActions} />;
}

ℹ️

V1 limitation: filterActions only filters by visibility. Disable and readonly states are not propagated. For those, use resolveAccessState() directly.


Putting It Together

A realistic admin workflow combining forms, actions, and routing

Here's a complete example showing how RBAC integrates across components, actions, and navigation in a booking management workflow.

import { DashForm, TextField, Select } from '@dashforge/ui';
import { useRbac, filterNavigationItems } from '@dashforge/rbac';

function BookingManagementPage() {
  const { can } = useRbac();

  // Actions
  const canDeleteBooking = can({ action: 'delete', resource: 'booking' });
  const canExport = can({ action: 'export', resource: 'booking' });

  return (
    <Stack spacing={4}>
      {/* Form with access-controlled fields */}
      <DashForm>
        <TextField
          name="customerName"
          label="Customer Name"
          access={{
            action: 'read',
            resource: 'booking.customerName',
            onUnauthorized: 'hide',
          }}
        />

        <TextField
          name="totalAmount"
          label="Total Amount"
          access={{
            action: 'edit',
            resource: 'booking.totalAmount',
            onUnauthorized: 'readonly',
          }}
        />

        <Select
          name="status"
          label="Status"
          options={['pending', 'confirmed', 'completed']}
          access={{
            action: 'edit',
            resource: 'booking.status',
            onUnauthorized: 'disable',
          }}
        />
      </DashForm>

      {/* Actions controlled by RBAC */}
      <Stack direction="row" spacing={2}>
        {canDeleteBooking && (
          <Button color="error" onClick={handleDelete}>
            Delete Booking
          </Button>
        )}

        {canExport && (
          <Button onClick={handleExport}>
            Export Data
          </Button>
        )}
      </Stack>
    </Stack>
  );
}

// Navigation filtered by permissions
function AppLayout() {
  const { can } = useRbac();

  const allNavItems = [
    {
      id: 'bookings',
      label: 'Bookings',
      path: '/bookings',
      access: { action: 'read', resource: 'booking' },
    },
    {
      id: 'reports',
      label: 'Reports',
      path: '/reports',
      access: { action: 'read', resource: 'reports' },
    },
    {
      id: 'admin',
      label: 'Admin',
      path: '/admin',
      access: { action: 'manage', resource: 'settings' },
    },
  ];

  const visibleNavItems = filterNavigationItems(allNavItems, can);

  return <LeftNav items={visibleNavItems} />;
}

Result: Admins see everything. Editors see most fields but some are readonly. Viewers see limited fields and no delete actions. All controlled declaratively.


Best Practices

Recommendations for using RBAC effectively in Dashforge

Follow these guidelines to keep your RBAC implementation clean, maintainable, and secure.

Use RBAC for permissions, not UI logic

Don't use access to show/hide fields based on form state. Use visibleWhen for that. RBAC is for "who", not "when".

Keep policies centralized

Define your RbacPolicy in one place (e.g., config/rbac-policy.ts). Don't scatter role definitions across components.

Prefer component access prop when available

Use the access prop on form components instead of manual useCan() checks. It's more declarative and less code.

Avoid duplicating raw can() checks everywhere

If you find yourself writing the same can({ action, resource }) in multiple places, consider using filterActions or filterNavigationItems instead.

Keep resource naming consistent

Use a consistent naming convention for resources (e.g., booking.salary, customer.email). Establish a resource naming guide for your team.

⚠️

Remember: Client-side RBAC is for UX only. Always enforce permissions on the server. Never trust the client.

On This Page