zynactradocs

React

AccessProvider, useCan and Can show or hide controls from the server's decisions, with batched record checks.

@zynactra/access-react talks to the access API your service mounts (default /access). It never decides on its own: the server re-checks every request, so UI checks only decide what to show.

Leave requests

WhoDaysStatusAction
Amara Okafor2pending
You1pending

Client and provider

import { AccessProvider, createAccessClient } from "@zynactra/access-react"

const client = createAccessClient({
  baseUrl: "/access",
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }), // or cookies
})

export function Root() {
  return (
    <AccessProvider client={client}>
      <App />
    </AccessProvider>
  )
}

The provider loads GET /access/me, the caller's grants. Pass initialSummary when the server already has them (server rendering), and call useAccess().refresh() after the user's own access changes; it also forgets cached record decisions.

useCan and Can

const create = useCan(P.LEAVE_REQUEST_CREATE)                // holds it anywhere? no request
const approve = useCan(P.LEAVE_REQUEST_APPROVE, request.id)  // this record? batched

<Can permission={P.LEAVE_REQUEST_APPROVE} id={request.id} fallback={null} loading={<Skeleton className="h-8 w-20" />}>
  <Button onClick={approveIt}>Approve</Button>
</Can>
  • Without id, the answer comes from /me: does the user hold the permission at some scope? Use it for "New" buttons, navigation and whole screens.
  • With id, every useCan and <Can> in one render goes out as a single POST /access/check. The server loads each record with its registered loader and decides; the UI never sends attributes.
  • allowed is undefined while loading; <Can loading> renders something meanwhile.
  • Errors count as not allowed.
  • The decision can change between render and click: still handle 403 from the real request.

Typed calls

The client covers the whole API: client.me(), client.check(...), client.explain(...), client.catalog(), client.listRoles(), client.createAssignment({...}), client.listChanges({ kind, actor, target, severity, limit, before }) and the rest. Errors are AccessApiError with status, code, reason, permission, field (the request field a validation problem is about) and the full problem.

Tests and demos

@zynactra/access-react/testing exports fakeAccessApi, an in-memory API that answers like the real one, plus adminSummary and globalGrant():

import { fakeAccessApi, globalGrant } from "@zynactra/access-react/testing"

const api = fakeAccessApi({
  summary: { userId: "u_mgr", orgId: "o", permissions: { "leave.request:approve": globalGrant() } },
  decisions: { "leave.request:approve:lr_1": true },
})
render(<AccessProvider client={createAccessClient({ fetch: api.fetch })}><Page /></AccessProvider>)
expect(api.calls.at(-1)?.path).toBe("/access/check")

It applies the rules the admin screens depend on, but it is not the rule engine: never use it outside tests, demos and docs.

Edit on GitHub

On this page