# @shield-acl/react

<div align="center">
  <img src="https://raw.githubusercontent.com/andersondrosa/andersondrosa/refs/heads/main/images/shield-acl.png" alt="Shield ACL React" width="400" />
</div>

<div align="center">
  <p><strong>Hooks e componentes declarativos para o Shield ACL — multi-app,
  assíncrono e reativo em tempo real.</strong></p>
</div>

Poucos primitivos, muita composição. Cada hook mapeia 1:1 num método do
[`@shield-acl/core`](../core), com **override de scope**, **assíncrono** e
**reatividade** quando roles ou grants mudam.

> Guia detalhado dos hooks: [`docs/REACT-HOOKS.md`](./docs/REACT-HOOKS.md).
> Design da v3: [`../../docs/REACT-HOOKS-DESIGN-v3.md`](../../docs/REACT-HOOKS-DESIGN-v3.md).
> Porquês das decisões: [`DECISIONS.md`](./docs/DECISIONS.md).

## Instalação

```bash
pnpm add @shield-acl/react @shield-acl/core react
```

- React **18+** (usa `useSyncExternalStore`).

## Setup

```tsx
import { ACL } from "@shield-acl/core";
import { ACLProvider } from "@shield-acl/react";

const acl = new ACL();
acl.defineRole({
  name: "editor",
  permissions: [{ action: "read", resource: "posts" }],
});

const user = { id: 1, grants: [{ scope: "app:crm", roles: ["editor"] }] };

function App() {
  return (
    <ACLProvider engine={acl} user={user} scope="app:crm" environment={{ mfa }}>
      <Dashboard />
    </ACLProvider>
  );
}
```

Props do Provider:

```typescript
interface ACLProviderProps {
  engine: ACL;
  user?: User | null; // inicial (não-controlado) OU atualize a prop (controlado)
  scope?: Scope; // default "*" — o app desta subárvore
  environment?: Environment; // reativo (MFA/hora/IP)
}
```

## Componentes declarativos

```tsx
import { Can, Cannot } from "@shield-acl/react"

<Can action="update" resource="posts" record={post} fallback={<Locked />}>
  <EditButton />
</Can>

<Can action="delete" resource="posts" scope="app:outro">…</Can> {/* outro app */}
<Cannot action="publish" resource="posts">Sem permissão para publicar</Cannot>

<Can.Any checks={[["create", "posts"], ["update", "posts"]]}>…</Can.Any>
<Can.All checks={[["read", "reports"], ["export", "reports"]]}>…</Can.All>

<Can.Async action="edit" resource="docs" record={doc}
  pending={<Spinner />} fallback={<Denied />}>
  <Editor />
</Can.Async>
```

- `resource` = tipo (string, matching). `record` = instância (conditions ABAC).
- `scope` = override do scope do Provider.

## Hooks

### `useCan` / `useCannot`

```tsx
const canEdit = useCan("update", "posts", { record: post });
const canInB = useCan("read", "posts", { scope: "app:B" }); // outro app
const cannotDelete = useCannot("delete", "posts");
```

`CheckOptions`:

```typescript
interface CheckOptions<TRecord = unknown> {
  scope?: Scope; // override do scope
  record?: TRecord; // instância do recurso (conditions)
  environment?: Environment; // merge com o do Provider
}
```

### `useEvaluate`

```tsx
const { allowed, reason, matchedRule, scope } = useEvaluate("delete", "posts");
```

### `useCanAsync` — conditions assíncronas / ReBAC / policySource

```tsx
const { allowed, loading, error, refetch } = useCanAsync("edit", "docs", {
  record: doc,
});
if (loading) return <Spinner />;
return allowed ? <Editor /> : <Denied />;
```

### `useChecks` + `anyOf` / `allOf` — batch tipado

```tsx
const c = useChecks({
  edit: ["update", "posts", { record: post }],
  del: ["delete", "posts", { record: post }],
});
// c: { edit: boolean; del: boolean }
if (anyOf(c)) {
  /* ... */
}
if (allOf(c)) {
  /* ... */
}
```

### `useResource` — vincula tipo + instância

```tsx
const acl = useResource("posts", post)
acl.canRead()   acl.canUpdate()   acl.canDelete()
acl.can("publish")
acl.can("read", { scope: "app:B" }) // override
```

### Introspecção

```tsx
const roles = useGrantedRoles(); // ["editor"] no scope
const { allRoles, hasRole, hasAnyRole } = useRoleHierarchy();
const { all, direct, byRole, actions } = usePermissions(); // admin/debug UIs
```

### `useAcl` — o primitivo

```tsx
const { user, setUser, scope, can, cannot, evaluate, canAsync, engine } =
  useAcl();
```

## Reatividade em tempo real

Quando um admin muda permissões, a UI precisa atualizar **ao vivo**. Há dois
tipos de mudança:

### A) Grants do usuário atual mudaram — `useUserSync`

```tsx
// PUSH: a notificação traz o novo usuário
useUserSync((apply) => {
  return socket.on("acl:user-changed", (msg) => apply(msg.user));
});

// PULL: a notificação é só um sinal → refetch → aplica
useUserSync((apply) => {
  return socket.on("acl:invalidate", async () => apply(await api.getMe()));
});
```

### B) A definição de uma role mudou (afeta todos que a têm) — `useRolesSync`

```tsx
useRolesSync((reload) => {
  return socket.on("acl:roles-changed", async () =>
    reload(await api.getRoles()),
  );
});
```

Isso funciona porque o Provider **assina o engine** (`useSyncExternalStore`):
qualquer `defineRole/setRoles/touch` reavalia toda a árvore.

### Reagir a ganho/perda de permissão

```tsx
usePermissionEffect("admin.access", undefined, {
  onGain: () => toast.success("Você agora é admin"),
  onLose: () => router.push("/"), // tira da tela proibida na hora
});
```

> **⚠️ Segurança:** o update no front é **UX, não fronteira**. A verdade é o
> backend (que rechecha cada request). Quando a permissão **cai**, prefira
> fail-safe: esconder/redirecionar no `onLose`.

## Migração da 2.x

A 2.x (API simples, sem scope) continua publicada. Mapa de-para completo em
[`docs/REACT-HOOKS.md`](./docs/REACT-HOOKS.md). Resumo:

| 2.x                                       | 3.x                                                      |
| ----------------------------------------- | -------------------------------------------------------- |
| `useCan(a, r, ctx)`                       | `useCan(a, r, { record, scope })`                        |
| `useCanAny/All/Multiple/Map/Array`        | `useChecks` + `anyOf`/`allOf`                            |
| `usePermissionHelpers` / `useResourceACL` | `useResource`                                            |
| `usePermissionChange*`                    | `useUserSync` / `usePermissionEffect`                    |
| —                                         | `useCanAsync`, override de `scope`, `environment` tipado |

## Compatibilidade

- React **18.x / 19.x** · TypeScript 5+.

## Testes

```bash
pnpm test
pnpm test:coverage
```

## Licença

MIT © Anderson D. Rosa
