React Library
npm install @mpofusindie/iam-reactThe library renders what the backend permits. It never decides access — it consumes the
resolved permission object from /me/permissions and keeps your UI honest. It is
router-agnostic: no react-router-dom peer dependency, so it works with react-router,
TanStack Router, or no router at all.
Wrap your app
Section titled “Wrap your app”IAMProvider takes a single config object. It fetches GET /me/permissions on mount, caches
the result in React state (TTL from the backend’s resolved object), and refreshes on schedule.
import { IAMProvider } from '@mpofusindie/iam-react';
<IAMProvider config={{ apiUrl: '/api/iam/v1', token: jwtToken, // the access token from login tenantId: 'optional-tenant-uuid', // header-based multi-tenancy refreshMode: 'body', // 'body' | 'cookie' | 'both' }} loadingFallback={<Spinner />}> <App /></IAMProvider>Log in (before the provider)
Section titled “Log in (before the provider)”Password login happens before any token exists, so it lives outside IAMProvider — no
hook can serve it. loginWithPassword is a stateless helper that owns the POST /auth/login
URL shape and the X-Tenant-ID rule:
import { loginWithPassword, LoginError } from '@mpofusindie/iam-react';
const tokens = await loginWithPassword( { apiUrl: '/api/iam/v1', tenantId }, // no token — login mints one { email, password },);if (tokens.requires2fa) { /* route to your 2FA challenge */ }// The app owns persistence; the helper does NOT store or refresh tokens.Failures throw a typed LoginError (kind: 'invalid-credentials' | 'http' | 'network'). There
is no useLogin() hook — pre-provider login has no context to read config from.
Guards
Section titled “Guards”import { FeatureGuard, ActionGuard, FieldGuard, ProtectedRoute } from '@mpofusindie/iam-react';
// whole module — renders children only if the resource is accessible<FeatureGuard resource="loans" fallback={<NoAccess />}> <LoansModule /></FeatureGuard>
// a button that only renders for users who can approve<ActionGuard resource="loans" action="approve"> <ApproveButton onClick={approve} /></ActionGuard>
// disable instead of hide<ActionGuard resource="loans" action="approve" mode="disable"> <ApproveButton onClick={approve} /></ActionGuard>
// a sensitive field — masked when not visible<FieldGuard resource="loans" field="ssn" maskChar="***-**-****"> {loan.ssn}</FieldGuard>Guard props take a resource (not feature), and FieldGuard a field — both must match the
backend resource/field codes exactly, which are the DTO’s serialized (camelCase) names.
Routing (router-agnostic)
Section titled “Routing (router-agnostic)”ProtectedRoute renders children when access is granted and a caller-supplied fallback
otherwise — it never imports a router. Wire your own redirect through fallback:
import { ProtectedRoute } from '@mpofusindie/iam-react';import { Navigate } from 'react-router-dom'; // YOUR router, not the library's
<Route path="/loans" element={ <ProtectedRoute resource="loans" fallback={<Navigate to="/no-access" replace />}> <LoansPage /> </ProtectedRoute>} />With TanStack Router (or any other) pass its redirect; with no router pass an inline element;
omit fallback to render nothing when denied. To avoid repeating the fallback, bind it once in a
thin wrapper (the react-loan-app example
does this with a RouteGuard).
import { usePermissions } from '@mpofusindie/iam-react';
const { canAccess, canDo, isFieldVisible, hasRole, loading } = usePermissions();
if (canDo('loans', 'export')) { /* show export menu item */ }const columns = base.filter(c => !c.field || isFieldVisible('loans', c.field));usePermissions() returns { permissions, loading, canAccess, canDo, isFieldVisible, hasRole, … }
— guards return null while loading is true. Other hooks mirror the backend surface:
useOAuth2, useTwoFactor, usePasswordReset, useSessions, useScopedPermissions,
useFieldVisibility, plus the explain API (useCheckAccess, useAccessSimulation) and the typed
admin client useIAMAdmin. See the package README
for the full reference.
The contract
Section titled “The contract”Everything the library renders derives from one backend response (GET /me/permissions) —
serialized camelCase, exactly as the guards read it:
{ "roles": ["LOAN_OFFICER"], "features": { "loans": { "access": true, "actions": { "crud": ["read"], "crossCutting": ["export"], "workflow": ["approve"] } } }, "fieldAccess": { "loans": { "ssn": { "visible": false, "editable": false, "isSensitive": true } } }, "ttl": 300}Remember the rule the whole system is built on: guards are UX, the backend is enforcement.
A hidden button is a courtesy; the 403 is the security.