@alphinex/permissions
v1.1.0
Published
Ability/permission checking: <Can>, usePermission, policy resolution.
Readme
@alphinex/permissions
Ability/permission checking for the UI layer: a PermissionsProvider/usePermission context,
a <Can> component for declarative conditional rendering, and a pluggable adapter model for
turning whatever shape your backend returns (roles, ability rules, a flat string list, ...)
into a uniform PermissionResolver. This package never decides how permissions are computed
— that's the app's PermissionsAdapter, per documentation/ARCHITECTURE.md §3.3.
PermissionsProvider + usePermission
PermissionsProvider takes a resolver: PermissionResolver — a
(action: string, subject: string, field?: string) => boolean function — and makes it
available to the tree below. usePermission(action, subject, field?) reads it:
import {
PermissionsProvider,
usePermission,
createListPermissionsAdapter,
} from "@alphinex/permissions";
const resolver = createListPermissionsAdapter(["view:Invoice", "edit:Invoice"]);
function App() {
return (
<PermissionsProvider resolver={resolver}>
<InvoiceToolbar />
</PermissionsProvider>
);
}
function InvoiceToolbar() {
const canEdit = usePermission("edit", "Invoice");
return <button disabled={!canEdit}>Edit invoice</button>;
}usePermission() throws if called outside a <PermissionsProvider>.
Can
A CASL-flavored declarative gate for JSX, built on usePermission() under the hood. I is the
action, a is the subject, field is an optional field-level check, fallback renders when
the check fails (defaults to nothing), and children may also be a render-prop function that
receives the boolean directly:
import { Can } from "@alphinex/permissions";
function DeleteInvoiceButton() {
return (
<Can I="delete" a="Invoice" fallback={<p className="text-fg-muted text-xs">Admins only</p>}>
<button className="text-danger">Delete invoice</button>
</Can>
);
}
function AmountField() {
return (
<Can I="edit" a="Invoice" field="amount">
{(allowed) => <input readOnly={!allowed} />}
</Can>
);
}createListPermissionsAdapter
A ready-made PermissionsAdapter<string[]> for the common case of a flat "action:subject"
rule list — the shape most simple Laravel policy-to-JSON serializers produce (e.g.
["view:Invoice", "edit:Invoice", "*:Report"]). "*" acts as a wildcard for either the action
or the subject (or both). Field-level checks always pass, since this source shape carries no
field granularity — write a custom PermissionsAdapter if you need that:
import { createListPermissionsAdapter } from "@alphinex/permissions";
const resolver = createListPermissionsAdapter(["*:Invoice", "view:Report"]);
resolver("edit", "Invoice"); // true — matches the "*:Invoice" wildcard rule
resolver("edit", "Report"); // falsePermissionResolver / PermissionsAdapter<TSource>
The two types that make this package pluggable. A PermissionsAdapter<TSource> is
(source: TSource) => PermissionResolver — write your own to map any backend shape (a roles
array, CASL rules, a permissions object keyed by subject, ...) into a PermissionResolver, and
hand the result to PermissionsProvider.
Composing with @alphinex/auth
The common pattern is to derive the resolver from the currently authenticated user, so
permissions update automatically on login/logout/role change. Compute the rule list from
useAuth()'s user, memoize it, and feed it through an adapter:
import { useAuth } from "@alphinex/auth";
import { PermissionsProvider, createListPermissionsAdapter } from "@alphinex/permissions";
import { useMemo, type ReactNode } from "react";
const ROLE_RULES: Record<string, string[]> = {
admin: ["*:Invoice"],
member: ["view:Invoice", "edit:Invoice"],
};
function PermissionsFromAuth({ children }: { children: ReactNode }) {
const { user } = useAuth();
const resolver = useMemo(
() => createListPermissionsAdapter(user ? (ROLE_RULES[user.role] ?? []) : []),
[user]
);
return <PermissionsProvider resolver={resolver}>{children}</PermissionsProvider>;
}Mount PermissionsFromAuth inside <AuthProvider> (from @alphinex/auth) so useAuth()
resolves, and outside anything that calls usePermission()/<Can>.
See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
