Skip to main content

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/RouteFeatureActions
/dashboard
/tenantsTENANT_MGMTread
/tenants/createTENANT_MGMTcreate
/tenants/:tenantId/editTENANT_MGMTupdate
/user-profilesUSER_MGMTread
InvitationsINVITATION_MGMTcreate/read/update
AssignmentsPERMISSION_ASSIGNMENTASSIGN/UNASSIGN/UPDATE_ASSIGNMENT
SessionsSESSION_MGMTREVOKE

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: FeaturePermissionGuard maps HTTP method/path to action (GET→read, POST→create, etc.)
  • Tenant resolution: x-tenant-id header or first tenant claim
  • Admin bypass: Only access-control.admin bypasses 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();
});
});