Skip to content

React Library

Terminal window
npm install @mpofusindie/iam-react

The 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.

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>

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.

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.

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.

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.