Access Manager

Next

A comprehensive access based control system with session validation

Docs
rigiduicomponent

Preview

Loading preview…
r/new-york/access-manager/access-manager.tsx
"use client"

import React, { createContext, useContext, useMemo, ReactNode } from 'react'
import { cn } from '@/lib/utils'

export interface UserSession {
  id: string
  email?: string
  username?: string
  name?: string
  avatar?: string
  roles: string[]
  permissions: string[]
  metadata?: Record<string, unknown>
  isActive?: boolean
  expiresAt?: Date | string
  lastActivity?: Date | string
  sessionId?: string
}

export interface RolePermissionMap {
  [role: string]: string[]
}

export interface AccessManagerContextType {
  user: UserSession | null
  rolePermissionMap: RolePermissionMap
  hasRole: (role: string | string[]) => boolean
  hasPermission: (permission: string | string[]) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasAllRoles: (roles: string[]) => boolean
  hasAnyPermission: (permissions: string[]) => boolean
  hasAllPermissions: (permissions: string[]) => boolean
  isSessionValid: () => boolean
  getSessionTimeRemaining: () => number | null
  isSessionExpiring: (thresholdMinutes?: number) => boolean
}

const AccessManagerContext = createContext<AccessManagerContextType | null>(null)

export const useAccessManager = () => {
  const context = useContext(AccessManagerContext)
  if (!context) {
    throw new Error('useAccessManager must be used within an AccessManagerProvider')
  }
  return context
}

export const useRoleBasedAccess = useAccessManager

export interface AccessManagerProviderProps {
  children: ReactNode
  user: UserSession | null
  rolePermissionMap?: RolePermissionMap
  onUnauthorized?: () => void
}

export function AccessManagerProvider({
  children,
  user,
  rolePermissionMap = {}
}: AccessManagerProviderProps) {
  const contextValue = useMemo(() => {
    const hasRole = (role: string | string[]): boolean => {
      if (!user || !user.roles) return false
      if (Array.isArray(role)) {
        return role.some(r => user.roles.includes(r))
      }
      return user.roles.includes(role)
    }

    const hasPermission = (permission: string | string[]): boolean => {
      if (!user) return false

      const userPermissions = new Set([
        ...(user.permissions || []),
        ...user.roles.flatMap(role => rolePermissionMap[role] || [])
      ])

      if (Array.isArray(permission)) {
        return permission.some(p => userPermissions.has(p))
      }
      return userPermissions.has(permission)
    }

    const hasAnyRole = (roles: string[]): boolean => {
      if (!user || !user.roles) return false
      return roles.some(role => user.roles.includes(role))
    }

    const hasAllRoles = (roles: string[]): boolean => {
      if (!user || !user.roles) return false
      return roles.every(role => user.roles.includes(role))
    }

    const hasAnyPermission = (permissions: string[]): boolean => {
      if (!user) return false

      const userPermissions = new Set([
        ...(user.permissions || []),
        ...user.roles.flatMap(role => rolePermissionMap[role] || [])
      ])

      return permissions.some(permission => userPermissions.has(permission))
    }

    const hasAllPermissions = (permissions: string[]): boolean => {
      if (!user) return false

      const userPermissions = new Set([
        ...(user.permissions || []),
        ...user.roles.flatMap(role => rolePermissionMap[role] || [])
      ])

      return permissions.every(permission => userPermissions.has(permission))
    }

    const isSessionValid = (): boolean => {
      if (!user) return false
      if (user.isActive === false) return false

      if (user.expiresAt) {
        const expirationDate = typeof user.expiresAt === 'string'
          ? new Date(user.expiresAt)
          : user.expiresAt
        return expirationDate > new Date()
      }

      return true
    }

    const getSessionTimeRemaining = (): number | null => {
      if (!user || !user.expiresAt) return null

      const expirationDate = typeof user.expiresAt === 'string'
        ? new Date(user.expiresAt)
        : user.expiresAt

      const now = new Date()
      const timeRemaining = expirationDate.getTime() - now.getTime()

      return timeRemaining > 0 ? timeRemaining : 0
    }

    const isSessionExpiring = (thresholdMinutes: number = 5): boolean => {
      if (!user || !user.expiresAt) return false

      const expirationDate = typeof user.expiresAt === 'string'
        ? new Date(user.expiresAt)
        : user.expiresAt

      const now = new Date()
      const timeUntilExpiry = expirationDate.getTime() - now.getTime()
      const thresholdMs = thresholdMinutes * 60 * 1000

      return timeUntilExpiry > 0 && timeUntilExpiry <= thresholdMs
    }

    return {
      user,
      rolePermissionMap,
      hasRole,
      hasPermission,
      hasAnyRole,
      hasAllRoles,
      hasAnyPermission,
      hasAllPermissions,
      isSessionValid,
      getSessionTimeRemaining,
      isSessionExpiring
    }
  }, [user, rolePermissionMap])

  return (
    <AccessManagerContext.Provider value={contextValue}>
      {children}
    </AccessManagerContext.Provider>
  )
}

export interface AccessGateProps {
  children: ReactNode
  roles?: string | string[]
  permissions?: string | string[]
  requireAllRoles?: boolean
  requireAllPermissions?: boolean
  requireValidSession?: boolean
  fallback?: ReactNode
  onUnauthorized?: () => void
  mode?: 'hide' | 'disable' | 'show-fallback'
  className?: string
  disabledClassName?: string
}

export function AccessGate({
  children,
  roles,
  permissions,
  requireAllRoles = false,
  requireAllPermissions = false,
  requireValidSession = true,
  fallback = null,
  onUnauthorized,
  mode = 'hide',
  className,
  disabledClassName
}: AccessGateProps) {
  const {
    hasRole,
    hasPermission,
    hasAllRoles,
    hasAllPermissions,
    isSessionValid
  } = useAccessManager()

  const checkAccess = (): boolean => {
    if (requireValidSession && !isSessionValid()) {
      return false
    }

    let hasRequiredRoles = true
    let hasRequiredPermissions = true

    if (roles) {
      const roleArray = Array.isArray(roles) ? roles : [roles]
      hasRequiredRoles = requireAllRoles
        ? hasAllRoles(roleArray)
        : hasRole(roles)
    }

    if (permissions) {
      const permissionArray = Array.isArray(permissions) ? permissions : [permissions]
      hasRequiredPermissions = requireAllPermissions
        ? hasAllPermissions(permissionArray)
        : hasPermission(permissions)
    }

    return hasRequiredRoles && hasRequiredPermissions
  }

  const hasAccess = checkAccess()

  if (!hasAccess && onUnauthorized) {
    onUnauthorized()
  }

  if (!hasAccess) {
    switch (mode) {
      case 'hide':
        return fallback ? <>{fallback}</> : null
      case 'show-fallback':
        return fallback ? <>{fallback}</> : null
      case 'disable':
        return (
          <div className={cn(className, disabledClassName, "opacity-50 pointer-events-none cursor-not-allowed")}>
            {children}
          </div>
        )
      default:
        return null
    }
  }

  return <div className={className}>{children}</div>
}

export interface PermissionCheckProps {
  roles?: string | string[]
  permissions?: string | string[]
  requireAllRoles?: boolean
  requireAllPermissions?: boolean
  requireValidSession?: boolean
}

export function usePermissionCheck({
  roles,
  permissions,
  requireAllRoles = false,
  requireAllPermissions = false,
  requireValidSession = true
}: PermissionCheckProps = {}) {
  const {
    hasRole,
    hasPermission,
    hasAllRoles,
    hasAllPermissions,
    isSessionValid
  } = useAccessManager()

  return useMemo(() => {
    if (requireValidSession && !isSessionValid()) {
      return false
    }

    let hasRequiredRoles = true
    let hasRequiredPermissions = true

    if (roles) {
      const roleArray = Array.isArray(roles) ? roles : [roles]
      hasRequiredRoles = requireAllRoles
        ? hasAllRoles(roleArray)
        : hasRole(roles)
    }

    if (permissions) {
      const permissionArray = Array.isArray(permissions) ? permissions : [permissions]
      hasRequiredPermissions = requireAllPermissions
        ? hasAllPermissions(permissionArray)
        : hasPermission(permissions)
    }

    return hasRequiredRoles && hasRequiredPermissions
  }, [
    roles,
    permissions,
    requireAllRoles,
    requireAllPermissions,
    requireValidSession,
    hasRole,
    hasPermission,
    hasAllRoles,
    hasAllPermissions,
    isSessionValid
  ])
}

export interface ConditionalWrapperProps {
  children: ReactNode
  roles?: string | string[]
  permissions?: string | string[]
  requireAllRoles?: boolean
  requireAllPermissions?: boolean
  requireValidSession?: boolean
  wrapper: (children: ReactNode) => ReactNode
  fallbackWrapper?: (children: ReactNode) => ReactNode
}

export function ConditionalWrapper({
  children,
  roles,
  permissions,
  requireAllRoles = false,
  requireAllPermissions = false,
  requireValidSession = true,
  wrapper,
  fallbackWrapper
}: ConditionalWrapperProps) {
  const hasAccess = usePermissionCheck({
    roles,
    permissions,
    requireAllRoles,
    requireAllPermissions,
    requireValidSession
  })

  if (hasAccess) {
    return <>{wrapper(children)}</>
  }

  if (fallbackWrapper) {
    return <>{fallbackWrapper(children)}</>
  }

  return <>{children}</>
}

export interface SessionGuardProps {
  children: ReactNode
  fallback?: ReactNode
  redirectTo?: string
  onSessionExpired?: () => void
}

export function SessionGuard({
  children,
  fallback,
  redirectTo,
  onSessionExpired
}: SessionGuardProps) {
  const { user, isSessionValid } = useAccessManager()

  const sessionValid = isSessionValid()

  if (!user || !sessionValid) {
    if (onSessionExpired) {
      onSessionExpired()
    }

    if (redirectTo && typeof window !== 'undefined') {
      window.location.href = redirectTo
      return null
    }

    if (fallback) {
      return <>{fallback}</>
    }

    return null
  }

  return <>{children}</>
}

export const RoleBasedAccessProvider = AccessManagerProvider
export const RoleGate = AccessGate

export const RoleBasedAccess = {
  Provider: AccessManagerProvider,
  Gate: AccessGate,
  SessionGuard,
  ConditionalWrapper,
  useRoleBasedAccess: useAccessManager,
  usePermissionCheck
}

Installation

npx shadcn@latest add @rigidui/access-manager

Usage

import { AccessManager } from "@/components/access-manager"
<AccessManager />