UI Integration
How the Access Control Admin UI integrates with the capabilities API to gate routes and actions.
Capabilities Hook
The admin app uses a shared hook to check capabilities:
File: apps/access-control/access-control-admin/src/app/hooks/useCapabilities.ts
import { useCapability } from '@digiwedge/access-control-pages';
export function useCapabilities() {
const canTenantRead = useCapability({ feature: 'TENANT_MGMT', action: 'read' });
const canUserRead = useCapability({ feature: 'USER_MGMT', action: 'read' });
return { canTenantRead, canUserRead };
}
Page → Feature/Action Mapping
| Page/Route | Feature | Actions |
|---|---|---|
/dashboard | – | – |
/tenants | TENANT_MGMT | read |
/tenants/create | TENANT_MGMT | create |
/tenants/:tenantId/edit | TENANT_MGMT | update |
/user-profiles | USER_MGMT | read |
| Invitations | INVITATION_MGMT | create/read/update |
| Assignments | PERMISSION_ASSIGNMENT | ASSIGN/UNASSIGN/UPDATE_ASSIGNMENT |
| Sessions | SESSION_MGMT | REVOKE |
Layout Gating
AppLayout hides navigation items based on capabilities:
// apps/access-control/access-control-admin/src/app/layout/AppLayout.tsx
import { useCapabilities } from '../hooks/useCapabilities';
export function AppLayout({ children }) {
const { canTenantRead, canUserRead } = useCapabilities();
return (
<Layout>
<Sidebar>
{canTenantRead.allowed && <NavItem to="/tenants">Tenants</NavItem>}
{canUserRead.allowed && <NavItem to="/user-profiles">Users</NavItem>}
</Sidebar>
{children}
</Layout>
);
}
Tests: AppLayout.capabilities.spec.tsx verifies visibility toggles.
Route Guard
Protect routes with the RequireCapability wrapper:
import { RequireCapability } from './RequireCapability';
<Route
path="/user-profiles"
element={
<RequireCapability feature="USER_MGMT" action="read">
<UserProfilesPage />
</RequireCapability>
}
/>
Action Buttons
Use useCapability to conditionally enable buttons:
import { useCapability } from '@digiwedge/access-control-pages';
export function EditButton({ userId }) {
const canUpdate = useCapability({ feature: 'USER_MGMT', action: 'update' });
return (
<button
disabled={!canUpdate.allowed}
onClick={() => openEditModal(userId)}
>
Edit user
</button>
);
}
Destructive Actions
Disable destructive actions when capability is missing:
const canDelete = useCapability({ feature: 'TENANT_MGMT', action: 'delete' });
<Button
danger
disabled={!canDelete.allowed}
onClick={archiveTenant}
>
Archive
</Button>
Capability Provider
The CapabilityProvider caches capability checks to minimize API requests:
// App root
import { CapabilityProvider } from '@digiwedge/access-control-pages';
export function App() {
return (
<CapabilityProvider>
<AppLayout>
<Routes />
</AppLayout>
</CapabilityProvider>
);
}
Capabilities API
The provider calls POST /capabilities/can under the hood:
Request:
{
"checks": [
{ "feature": "TENANT_MGMT", "action": "read" },
{ "feature": "USER_MGMT", "action": "read" }
]
}
Response:
{
"results": [
{ "feature": "TENANT_MGMT", "action": "read", "allowed": true },
{ "feature": "USER_MGMT", "action": "read", "allowed": false }
]
}
Guard & Tenant Scope
- Guard:
FeaturePermissionGuardmaps HTTP method/path to action (GET→read, POST→create, etc.) - Tenant resolution:
x-tenant-idheader or first tenant claim - Admin bypass: Only
access-control.adminbypasses capability checks
Testing Capabilities
Test capability-based visibility:
// AppLayout.capabilities.spec.tsx
import { render } from '@testing-library/react';
import { CapabilityProvider } from '@digiwedge/access-control-pages';
describe('AppLayout capabilities', () => {
it('hides Tenants when canTenantRead is false', () => {
// Mock capability provider to return false
const { queryByText } = render(
<CapabilityProvider value={{ canTenantRead: { allowed: false } }}>
<AppLayout />
</CapabilityProvider>
);
expect(queryByText('Tenants')).toBeNull();
});
});