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 denieddisable
<TextField
name="status"
label="Status"
access={{
action: 'edit',
resource: 'booking.status',
onUnauthorized: 'disable',
}}
/>
// Component renders disabled when permission deniedreadonly
<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.