@rajeshkola/react-rbac
v0.2.0
Published
Framework-agnostic RBAC core with React bindings (provider, hook, <Can>, route guard).
Maintainers
Readme
@rajeshkola/react-rbac
Framework-agnostic RBAC engine with React bindings. One can(role, path, action)
primitive powers component gating, route guards, and (if you reuse rbac-core
server-side) API-level enforcement — so the client and server never disagree
about what's allowed.
Written in TypeScript — ships .ts/.tsx source directly (no build step),
so consumers get full type inference with no separate @types package.
Run npm run typecheck to type-check the library and examples.
Install
npm install @rajeshkola/react-rbac
# optional, only needed for <ProtectedRoute>
npm install react-router-domData model
A permission tree is { role: { path: { action: boolean } } }.
- role — any string key:
"admin","viewer","supervisor", etc. - path — a dot-delimited resource path of any depth:
"zone","zone.hardware","zone.hardware.rebootBtn". Depth is a config choice, not a plugin limitation. - action — any string:
"visible","disable","readonly", or your own custom actions.
{
"admin": {
"zone.hardware.rebootBtn": { "visible": true, "disable": false }
},
"viewer": {
"zone.hardware.rebootBtn": { "visible": false }
}
}Quickstart
import { ACLProvider, Can, useACL } from "@rajeshkola/react-rbac/react";
function App({ user, permissions }) {
return (
<ACLProvider permissions={permissions} currentRole={user.role}>
<Toolbar />
</ACLProvider>
);
}
function Toolbar() {
const { can } = useACL();
return (
<>
<Can path="zone.hardware.rebootBtn" action="visible">
<button disabled={can("zone.hardware.rebootBtn", "disable")}>
Reboot
</button>
</Can>
</>
);
}Route guards
import { ProtectedRoute } from "@rajeshkola/react-rbac/router";
<Routes>
<Route
path="/hardware"
element={<ProtectedRoute path="zone.hardware" element={<HardwarePage />} />}
/>
</Routes>Route access and button visibility both resolve through the same can()
call — no separate "page access" system to keep in sync.
Class components
import { withACL } from "@rajeshkola/react-rbac/react";
class Toolbar extends React.Component {
render() {
const { acl } = this.props;
return <button disabled={acl.can("zone.hardware.rebootBtn", "disable")} />;
}
}
export default withACL(Toolbar);Adapting an existing backend shape
If your API already returns nested objects instead of flat paths (e.g.
{ role: { nav: { component: { action: bool } } } }), don't reshape your
backend — pass an adapter:
import { flattenNestedToPathTree } from "@rajeshkola/react-rbac/adapters";
<ACLProvider
permissions={rawApiResponse}
adapter={flattenNestedToPathTree}
currentRole={user.role}
/>flattenNestedToPathTree handles any nesting depth automatically — a node
is treated as a leaf once every value under it is a boolean.
Cascading permissions (optional)
By default (fallback: "strict"), a rule only applies to its exact path.
Set fallback: "cascade" to let a rule on a parent path apply to all
children that don't define their own override:
<ACLProvider permissions={tree} currentRole={role} fallback="cascade" />{ "admin": { "zone": { "visible": false } } }With cascade on, can("admin", "zone.hardware.rebootBtn", "visible") is
false even though nothing was defined for that exact path — it inherits
from "zone".
Server-side enforcement (Node/Express)
Never trust the client-side <Can> check as security — reuse the same
engine on the server:
import { createACL } from "@rajeshkola/react-rbac";
const acl = createACL(permissionTree, { defaultAction: false });
app.post("/api/hardware/reboot", (req, res, next) => {
if (!acl.can(req.user.role, "zone.hardware.rebootBtn", "disable" ? false : true)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
});@rajeshkola/react-rbac (root export) has zero React or DOM dependency, so
this works identically in a backend service.
API reference
Core (@rajeshkola/react-rbac)
createACL(tree, { defaultAction, fallback })→{ can(role, path, action), hasRole(role), getRolePermissions(role), setTree(tree) }- Permission-editing primitives (pure, framework-free — power
usePermissionEditor):getActions,levelForActions,setActions,setLevel,setColumns,setModuleForRole,moduleHasRoleAccess,diffTree
React (@rajeshkola/react-rbac/react)
<ACLProvider permissions currentRole adapter? defaultAction? fallback?>useACL()→{ can(path, action), role }<Can path action invert? fallback?>withACL(Component)→ injectsaclprop<PermissionManager roles levels modules tree onSave? title? subtitle? defaultOpenModules?>— drop-in "manage roles & permissions" admin screen: module cards, per-role level dropdowns (or a column picker for table-type components), and a dirty-tracked Save Changes button. Seeexamples/permission-manager.jsxfor a full usage sample.usePermissionEditor({ tree, levels })→ the headless state<PermissionManager>is built on, in case you want to build your own UI:{ tree, isDirty, dirtyChanges, getLevel(role, path), setLevel(role, path, levelKey), setColumns(role, path, columns), toggleModuleForRole(module, role, enabled), hasModuleAccess(module, role), save(onSave), reset(tree) }
Router (@rajeshkola/react-rbac/router)
<ProtectedRoute path action? redirectTo? element? children?>
Adapters (@rajeshkola/react-rbac/adapters)
flattenNestedToPathTree(raw)— converts arbitrary-depth nested payloads into the flat path-tree shape
What this deliberately doesn't do
- No row-level / "own resource" conditions (that's ABAC territory — see CASL if you need it).
- No relationship graphs (that's Zanzibar/OpenFGA territory).
- No policy versioning or audit log — bring your own, or hook into
setTree()/your save handler to log diffs before committing a change.
This is a mid-size-app RBAC library, not a distributed policy engine — by design.
