Skip to content

astakhovaskold/react-protected

Repository files navigation

React Protected

Access decisions for React applications.

RBAC, ABAC, authenticated and unauthenticated route checks without baking app-specific redirect policy into the library.

Docs (EN)Core APIReact Router APIContributingMIT License

Features

  • Framework-agnostic core for pure access-control decisions
  • React package with AccessProvider, useHasAccess, and HasAccess
  • React Router helpers for middleware, loaders, and actions
  • Explicit denied handling via onDenied
  • No built-in loginPath, forbiddenPath, defaultPath, or callback URL policy

Packages

Package Description
@react-protected/core Pure access-control logic
@react-protected/react React context, hooks, and HasAccess
@react-protected/react-router React Router helpers and AccessRoute fallback

Quick Start

Data router

import { createBrowserRouter, redirect } from 'react-router-dom'
import {
  createAccessLoader,
  createAccessMiddleware,
} from '@react-protected/react-router'

const accessOptions = {
  getUser: () => authStore.getState().user,
  hasRole: (user, roles) => roles.some((role) => user.roles.includes(role)),
  hasPermission: (user, permissions) =>
    permissions.every((permission) => user.permissions.includes(permission)),
  onDenied: ({ result, request }) => {
    const url = new URL(request.url)

    switch (result.reason) {
      case 'unauthenticated':
        return redirect(`/login?next=${encodeURIComponent(url.pathname + url.search)}`)
      case 'authenticated':
        return redirect('/dashboard')
      case 'forbidden':
        return redirect('/403')
    }
  },
}

const accessMiddleware = createAccessMiddleware(accessOptions)
const accessLoader = createAccessLoader(accessOptions)

export const router = createBrowserRouter(
  [
    {
      path: '/login',
      middleware: [accessMiddleware({ access: 'unauthenticated' })],
      element: <LoginPage />,
    },
    {
      path: '/dashboard',
      middleware: [accessMiddleware({ access: 'authenticated' })],
      element: <DashboardPage />,
    },
    {
      path: '/reports',
      loader: accessLoader(
        { access: 'authenticated', permissions: ['reports:read'] },
        async () => fetch('/api/reports').then((response) => response.json())
      ),
      element: <ReportsPage />,
    },
    { path: '/403', element: <ForbiddenPage /> },
  ],
  { future: { v8_middleware: true } }
)

Component-level access

import { AccessProvider, HasAccess } from '@react-protected/react-router'

function App() {
  return (
    <AccessProvider
      getUser={() => authStore.getState().user}
      hasRole={(user, roles) => roles.some((role) => user.roles.includes(role))}
    >
      <HasAccess roles={['admin']}>
        <button>Delete tenant</button>
      </HasAccess>
    </AccessProvider>
  )
}

AccessRoute fallback

<AccessRoute
  access="authenticated"
  renderDenied={({ reason }) => {
    if (reason === 'unauthenticated') return <Navigate to="/login" replace />
    if (reason === 'authenticated') return <Navigate to="/dashboard" replace />
    return <Navigate to="/403" replace />
  }}
>
  <DashboardPage />
</AccessRoute>

About

RBAC and ABAC without re-implementing guards in every project

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors