React Hooks

@devcraft-ts/abac-admin-react

Headless React hooks for building custom ABAC admin interfaces. Built on @devcraft-ts/abac-admin-core which leverages abac-engine for policy evaluation.

Built on abac-engine

These hooks provide a React-friendly interface to manage ABAC policies powered by abac-engine. While the hooks handle UI state and data fetching, abac-engine provides the underlying policy evaluation logic. Learn more →

Headless

Bring your own UI - complete design freedom

Context-Based

Centralized state management with React Context

Type-Safe

Full TypeScript support with abac-engine types

Lightweight

~30KB minified - minimal overhead

Optimistic Updates

Built-in optimistic UI updates for smooth UX

Installation

npm install @devcraft-ts/abac-admin-react

# Peer dependencies
npm install react react-dom

Quick Start

1. Wrap Your App with ABACProvider

import { ABACProvider } from '@devcraft-ts/abac-admin-react';

function App() {
  return (
    <ABACProvider
      config={{
        baseURL: '/api/abac',
        headers: {
          'Authorization': `Bearer ${token}`
        }
      }}
    >
      <YourApp />
    </ABACProvider>
  );
}

2. Use Hooks in Your Components

import { usePolicies } from '@devcraft-ts/abac-admin-react';

function PolicyList() {
  const { policies, isLoading, error, refetch } = usePolicies();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {policies.map(policy => (
        <div key={policy.id}>
          <h3>{policy.description}</h3>
          <span>{policy.effect}</span>
        </div>
      ))}
    </div>
  );
}

Policy Hooks

usePolicies

Hook for managing multiple policies with filtering and CRUD operations.

const {
  policies,        // Policy[]
  isLoading,       // boolean
  error,           // Error | null
  refetch,         // () => Promise<void>
  createPolicy,    // (policy: PolicyInput) => Promise<Policy>
  updatePolicy,    // (id: string, update: PolicyUpdate) => Promise<Policy>
  deletePolicy,    // (id: string) => Promise<void>
  activatePolicy,  // (id: string) => Promise<Policy>
  deactivatePolicy // (id: string) => Promise<Policy>
} = usePolicies({
  filters?: {
    isActive?: boolean;
    category?: string;
    tags?: string[];
  }
});

// Example usage
function PolicyManager() {
  const {
    policies,
    isLoading,
    error,
    createPolicy,
    deletePolicy
  } = usePolicies({ filters: { isActive: true } });

  const handleCreate = async () => {
    await createPolicy({
      policyId: 'new-policy',
      version: '1.0.0',
      effect: 'PERMIT',
      description: 'New policy',
      isActive: true
    });
  };

  const handleDelete = async (id: string) => {
    await deletePolicy(id);
  };

  return (
    <div>
      <button onClick={handleCreate}>Create Policy</button>
      {policies.map(policy => (
        <div key={policy.id}>
          <span>{policy.description}</span>
          <button onClick={() => handleDelete(policy.id)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

usePolicy

Hook for managing a single policy.

const {
  policy,       // Policy | null
  isLoading,    // boolean
  error,        // Error | null
  refetch,      // () => Promise<void>
  update,       // (update: PolicyUpdate) => Promise<Policy>
  delete: del   // () => Promise<void>
} = usePolicy(policyId);

function PolicyDetails({ policyId }: { policyId: string }) {
  const { policy, isLoading, update } = usePolicy(policyId);

  if (isLoading) return <div>Loading...</div>;
  if (!policy) return <div>Not found</div>;

  const handleUpdate = async () => {
    await update({
      description: 'Updated description'
    });
  };

  return (
    <div>
      <h2>{policy.description}</h2>
      <button onClick={handleUpdate}>Update</button>
    </div>
  );
}

usePolicyTest

Hook for testing policies with sample data.

const {
  testPolicy,  // (request: PolicyTestRequest) => Promise<PolicyTestResult>
  result,      // PolicyTestResult | null
  isLoading,   // boolean
  error        // Error | null
} = usePolicyTest();

function PolicyTester({ policyId }: { policyId: string }) {
  const { testPolicy, result, isLoading } = usePolicyTest();

  const handleTest = async () => {
    await testPolicy({
      policyId,
      subject: { id: 'user-123', role: 'admin' },
      resource: { id: 'doc-456' },
      action: { id: 'read' },
      environment: {}
    });
  };

  return (
    <div>
      <button onClick={handleTest}>Test Policy</button>
      {result && (
        <div>
          <p>Decision: {result.decision}</p>
          <p>Matched: {result.matched ? 'Yes' : 'No'}</p>
        </div>
      )}
    </div>
  );
}

Attribute Hooks

useAttributes

const {
  attributes,     // Record<string, any>
  isLoading,      // boolean
  error,          // Error | null
  refetch,        // () => Promise<void>
  setAttribute,   // (key: string, value: any) => Promise<void>
  bulkSet,        // (attrs: Record<string, any>) => Promise<void>
  deleteAttribute // (key: string) => Promise<void>
} = useAttributes(resourceType, resourceId);

function UserAttributeManager({ userId }: { userId: string }) {
  const {
    attributes,
    setAttribute,
    deleteAttribute
  } = useAttributes('user', userId);

  const handleSetDepartment = async () => {
    await setAttribute('department', 'engineering');
  };

  return (
    <div>
      {Object.entries(attributes).map(([key, value]) => (
        <div key={key}>
          <span>{key}: {value}</span>
          <button onClick={() => deleteAttribute(key)}>
            Delete
          </button>
        </div>
      ))}
      <button onClick={handleSetDepartment}>
        Set Department
      </button>
    </div>
  );
}

useAttributeHistory

const {
  history,    // AttributeValue[]
  isLoading,  // boolean
  error       // Error | null
} = useAttributeHistory(resourceType, resourceId, attributeKey?);

function AttributeHistory({ userId }: { userId: string }) {
  const { history, isLoading } = useAttributeHistory('user', userId, 'role');

  return (
    <div>
      <h3>Role History</h3>
      {history.map((entry, i) => (
        <div key={i}>
          <span>{entry.value}</span>
          <span>{new Date(entry.timestamp).toLocaleString()}</span>
        </div>
      ))}
    </div>
  );
}

Audit Hooks

useAuditLog

const {
  entries,    // AuditLogEntry[]
  total,      // number
  hasMore,    // boolean
  isLoading,  // boolean
  error,      // Error | null
  refetch     // () => Promise<void>
} = useAuditLog({
  entityType?: 'policy' | 'attribute';
  entityId?: string;
  action?: AuditAction;
  startDate?: string;
  endDate?: string;
  limit?: number;
  offset?: number;
});

function AuditLog() {
  const { entries, isLoading } = useAuditLog({
    entityType: 'policy',
    limit: 50
  });

  return (
    <div>
      {entries.map(entry => (
        <div key={entry.id}>
          <span>{entry.action}</span>
          <span>{entry.entityId}</span>
          <span>{new Date(entry.timestamp).toLocaleString()}</span>
        </div>
      ))}
    </div>
  );
}

Best Practices

Use Context Provider at App Root

Place the ABACProvider at the root of your app to ensure all components can access the hooks.

Handle Loading and Error States

Always handle loading and error states in your UI for better user experience.

Use Filters to Reduce Data

Apply filters to hooks to fetch only the data you need, improving performance.

Leverage Refetch for Updates

Use the refetch function to manually refresh data when needed.

Next Steps