wacrm/src/components/auth/require-role.tsx
Arnas Donauskas 7593253c85
feat(auth): extend useAuth with account context + RequireRole + useCan
PR 5 of the multi-user accounts series. Surfaces the account
+ role state from the profile row through useAuth so every UI
gate downstream can read it without its own fetch, and lands
two reusable patterns the upcoming Members tab / Join page /
sidebar gating PRs will all consume.

What's new in useAuth
- Profile SELECT now includes `account_id`, `account_role`, and
  the joined `accounts(id, name)` via the existing `account_id`
  FK. `!inner` mirrors the server-side `getCurrentAccount`
  helper from PR #169.
- Context value adds `accountId`, `accountRole`, `account`
  (`{id, name}` summary), and six derived booleans: `isOwner`,
  `isAdmin`, `isAgent`, `isViewer`, plus the capability-level
  `canManageMembers`, `canEditSettings`, `canSendMessages`.
- All derived booleans memoise off `profile.account_role`, so
  consumers can use them as React dep-array entries without
  re-renders cascading on unrelated profile fields.
- The no-provider fallback returns every new field at its
  least-privileged value (null / false) — UI gates fail closed
  if a component is somehow rendered outside the AuthProvider.

`<RequireRole min="admin" fallback={…}>{children}</RequireRole>`
- Renders `children` iff `accountRole >= min`, else `fallback`.
- Treats `profileLoading` as "below threshold" so under-
  privileged users never see a flash of gated content during
  the initial profile fetch.

`useCan(action)` hook
- Typed action union: `manage-members` | `edit-settings` |
  `send-messages` | `view-only` | `delete-account` |
  `transfer-ownership`. Closed set lets the compiler catch
  typos at every call site.
- Returns `false` while loading. The boolean alternative to
  `<RequireRole>` — useful for `disabled={!useCan('edit-settings')}`
  on buttons / inputs / tooltip copy without restructuring JSX.

Why no React-renderer tests
- The wacrm test convention is pure unit tests (`vitest` without
  any React testing library). The role predicates that back
  every new piece here are already covered by the test suite
  added in PR #169 — the new code is thin glue, and adding a
  React testing harness for it would be a much larger change
  than this PR warrants.

Verification
- `npm run lint`      → 0 errors. Warnings drop from 20 → 19
                        because the rewritten useEffect now
                        lists `fetchProfile` in its deps.
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:58:53 +03:00

48 lines
1.5 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { useAuth } from "@/hooks/use-auth";
import { hasMinRole, type AccountRole } from "@/lib/auth/roles";
interface RequireRoleProps {
/** Minimum role to render `children`. Uses the standard hierarchy
* owner > admin > agent > viewer. */
min: AccountRole;
/** What to render while the role is below `min` OR while we don't
* yet know the role (`profileLoading` is true). Defaults to
* `null` — most call sites just want the gated element to be
* absent until we're sure. Pass a placeholder if a layout slot
* would collapse and re-flow when the role resolves. */
fallback?: ReactNode;
children: ReactNode;
}
/**
* `<RequireRole min="admin">…</RequireRole>` — conditional render
* helper for UI gated by account role.
*
* Three states:
* 1. profileLoading → render `fallback` (we don't know the role
* yet; fail closed so we never flash the gated content to an
* under-privileged user).
* 2. role ≥ min → render `children`.
* 3. role < min → render `fallback`.
*
* Mirrors the server-side `requireRole(min)` from `@/lib/auth/account`
* so client and server gates stay aligned by construction.
*/
export function RequireRole({
min,
fallback = null,
children,
}: RequireRoleProps) {
const { profileLoading, accountRole } = useAuth();
if (profileLoading) return <>{fallback}</>;
if (!accountRole) return <>{fallback}</>;
if (!hasMinRole(accountRole, min)) return <>{fallback}</>;
return <>{children}</>;
}