@jmruthers/pace-core 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -58
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +96 -0
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +441 -0
- package/src/rbac/hooks/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +359 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +503 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -58
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/appIdResolver.ts","../src/providers/EventProvider.tsx","../src/providers/index.ts"],"sourcesContent":["/**\n * App ID Resolution Utility\n * @package @jmruthers/pace-core\n * @module Utils/AppIdResolver\n * @since 1.0.0\n * \n * This module provides utilities to resolve app names to app IDs for database operations.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { Database } from '../types/database';\n\n/**\n * Resolves an app name to its corresponding app ID\n * \n * @param supabase - Supabase client instance\n * @param appName - The app name to resolve\n * @returns Promise resolving to the app ID or null if not found\n */\nexport async function getAppId(\n supabase: SupabaseClient<Database>,\n appName: string\n): Promise<string | null> {\n try {\n const { data, error } = await supabase\n .from('rbac_apps')\n .select('id')\n .ilike('name', appName)\n .eq('is_active', true)\n .single();\n\n if (error) {\n console.error('Failed to resolve app ID for app name:', appName, error);\n return null;\n }\n\n return (data as { id: string } | null)?.id || null;\n } catch (error) {\n console.error('Error resolving app ID for app name:', appName, error);\n return null;\n }\n}\n\n/**\n * Resolves multiple app names to their corresponding app IDs\n * \n * @param supabase - Supabase client instance\n * @param appNames - Array of app names to resolve\n * @returns Promise resolving to a map of app names to app IDs\n */\nexport async function getAppIds(\n supabase: SupabaseClient<Database>,\n appNames: string[]\n): Promise<Record<string, string | null>> {\n try {\n // For case-insensitive matching with multiple values, we need to use OR conditions\n // since PostgreSQL doesn't support case-insensitive IN with ILIKE\n const orConditions = appNames.map(name => `name.ilike.${name}`).join(',');\n \n const { data, error } = await supabase\n .from('rbac_apps')\n .select('id, name')\n .or(orConditions)\n .eq('is_active', true);\n\n if (error) {\n console.error('Failed to resolve app IDs for app names:', appNames, error);\n return {};\n }\n\n const result: Record<string, string | null> = {};\n \n // Initialize all app names with null\n appNames.forEach(name => {\n result[name] = null;\n });\n\n // Set resolved app IDs - match case-insensitively\n (data as { id: string; name: string }[] | null)?.forEach(app => {\n // Find the original app name that matches (case-insensitive)\n const originalName = appNames.find(name => \n name.toLowerCase() === app.name.toLowerCase()\n );\n if (originalName) {\n result[originalName] = app.id;\n }\n });\n\n return result;\n } catch (error) {\n console.error('Error resolving app IDs for app names:', appNames, error);\n return {};\n }\n}\n\n/**\n * Cached app ID resolver with TTL\n */\nclass CachedAppIdResolver {\n private cache = new Map<string, { id: string | null; expires: number }>();\n private ttl = 5 * 60 * 1000; // 5 minutes\n\n async getAppId(\n supabase: SupabaseClient<Database>,\n appName: string\n ): Promise<string | null> {\n const now = Date.now();\n const cached = this.cache.get(appName);\n\n if (cached && cached.expires > now) {\n return cached.id;\n }\n\n const id = await getAppId(supabase, appName);\n this.cache.set(appName, { id, expires: now + this.ttl });\n\n return id;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n\n clearCacheForApp(appName: string): void {\n this.cache.delete(appName);\n }\n}\n\n// Export singleton instance\nexport const cachedAppIdResolver = new CachedAppIdResolver();\n","import React, {\n createContext,\n useContext,\n useState,\n useEffect,\n useLayoutEffect,\n useCallback,\n useRef,\n} from 'react';\nimport { useUnifiedAuth } from './UnifiedAuthProvider';\nimport { useOrganisations } from './OrganisationProvider';\nimport { setOrganisationContext } from '../utils/organisationContext';\nimport { DebugLogger } from '../utils/debugLogger';\nimport { cachedAppIdResolver } from '../utils/appIdResolver';\n\nimport { Event } from '../types/unified';\n\nexport interface EventContextType {\n events: Event[];\n selectedEvent: Event | null;\n isLoading: boolean;\n error: Error | null;\n setSelectedEvent: (event: Event | null) => void;\n refreshEvents: () => Promise<void>;\n}\n\nconst EventContext = createContext<EventContextType | undefined>(undefined);\n\nexport const useEvents = () => {\n const context = useContext(EventContext);\n if (context === undefined) {\n throw new Error('useEvents must be used within an EventProvider');\n }\n return context;\n};\n\nexport interface EventProviderProps {\n children: React.ReactNode;\n}\n\n// Helper function to get the next event by date\nfunction getNextEventByDate(events: Event[]): Event | null {\n if (!events || events.length === 0) {\n return null;\n }\n\n const now = new Date();\n const futureEvents = events.filter(event => {\n if (!event.event_date) return false;\n const eventDate = new Date(event.event_date);\n return eventDate >= now;\n });\n\n if (futureEvents.length === 0) {\n return null;\n }\n\n // Sort by date (ascending) to get the next event\n const sortedFutureEvents = futureEvents.sort((a, b) => {\n const dateA = new Date(a.event_date!);\n const dateB = new Date(b.event_date!);\n return dateA.getTime() - dateB.getTime();\n });\n\n return sortedFutureEvents[0];\n}\n\nexport function EventProvider({ children }: EventProviderProps) {\n const [events, setEvents] = useState<Event[]>([]);\n const [selectedEvent, setSelectedEventState] = useState<Event | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const { user, session, supabase, appName, setSelectedEventId } = useUnifiedAuth();\n \n // Refs to prevent infinite loops\n const isInitializedRef = useRef(false);\n const isFetchingRef = useRef(false);\n const hasAutoSelectedRef = useRef(false);\n const userClearedEventRef = useRef(false);\n const setSelectedEventIdRef = useRef(setSelectedEventId);\n \n // Try to get organisation context, but don't fail if not available\n let selectedOrganisation = null;\n let ensureOrganisationContext = null;\n \n try {\n const orgContext = useOrganisations();\n selectedOrganisation = orgContext.selectedOrganisation;\n ensureOrganisationContext = orgContext.ensureOrganisationContext;\n } catch (error) {\n console.warn('[EventProvider] Organisation context not available:', error);\n }\n\n // Load persisted event selection with tab-scoped storage\n const loadPersistedEvent = useCallback(async (events: Event[]) => {\n try {\n // Try sessionStorage first (tab-specific)\n let persistedEventId = sessionStorage.getItem('pace-core-selected-event');\n \n // Fallback to localStorage if no sessionStorage value (for new tabs)\n if (!persistedEventId) {\n persistedEventId = localStorage.getItem('pace-core-selected-event');\n // If we found a value in localStorage, also store it in sessionStorage for this tab\n if (persistedEventId) {\n sessionStorage.setItem('pace-core-selected-event', persistedEventId);\n }\n }\n \n if (persistedEventId && events.length > 0) {\n const persistedEvent = events.find(event => event.event_id === persistedEventId);\n if (persistedEvent) {\n DebugLogger.log('EventProvider', 'Restoring persisted event:', persistedEvent.event_name);\n setSelectedEventState(persistedEvent);\n setSelectedEventId(persistedEventId);\n return true;\n }\n }\n } catch (error) {\n console.warn('[EventProvider] Failed to load persisted event:', error);\n }\n return false;\n }, [setSelectedEventId]);\n\n // Persist event selection with tab-scoped storage\n const persistEventSelection = useCallback((eventId: string) => {\n try {\n // Store in sessionStorage for tab-specific persistence\n sessionStorage.setItem('pace-core-selected-event', eventId);\n // Also store in localStorage as fallback for new tabs\n localStorage.setItem('pace-core-selected-event', eventId);\n } catch (error) {\n console.warn('[EventProvider] Failed to persist event selection:', error);\n }\n }, []);\n\n // Auto-select next event\n const autoSelectNextEvent = useCallback((events: Event[]) => {\n const nextEvent = getNextEventByDate(events);\n if (nextEvent) {\n DebugLogger.log('EventProvider', 'Auto-selecting next event:', nextEvent.event_name);\n setSelectedEventState(nextEvent);\n setSelectedEventId(nextEvent.event_id);\n persistEventSelection(nextEvent.event_id);\n }\n }, [setSelectedEventId, persistEventSelection]);\n\n // Main fetch function\n const fetchEvents = useCallback(async () => {\n if (!user || !session || !supabase || !appName || !selectedOrganisation) {\n DebugLogger.log('EventProvider', 'Missing required dependencies, skipping fetch');\n setIsLoading(false);\n return;\n }\n\n // Prevent multiple simultaneous fetches\n if (isFetchingRef.current) {\n DebugLogger.log('EventProvider', 'Already fetching events, skipping');\n return;\n }\n\n DebugLogger.log('EventProvider', 'User and organisation found, fetching events for:', {\n userId: user.id,\n appName: appName,\n organisationId: selectedOrganisation.id,\n organisationName: selectedOrganisation.display_name\n });\n\n isFetchingRef.current = true;\n let isMounted = true;\n\n try {\n // Ensure organisation context is set\n if (ensureOrganisationContext) {\n await ensureOrganisationContext();\n await setOrganisationContext(supabase, selectedOrganisation.id);\n }\n\n // Resolve app name to app ID\n const appId = await cachedAppIdResolver.getAppId(supabase, appName);\n \n if (!appId) {\n throw new Error(`App ID not found for app name: ${appName}`);\n }\n\n // Call the RPC function with app_id\n DebugLogger.log('EventProvider', 'Calling get_pace_user_events RPC with:', {\n user_uuid: user.id,\n app_id: appId,\n p_organisation_id: selectedOrganisation.id\n });\n\n const response = await supabase.rpc('get_pace_user_events', {\n user_uuid: user.id,\n app_id: appId,\n p_organisation_id: selectedOrganisation.id,\n });\n \n const { data, error: rpcError } = response || {};\n\n DebugLogger.log('EventProvider', 'RPC response:', {\n data,\n error: rpcError,\n dataLength: data?.length || 0,\n organisationId: selectedOrganisation.id\n });\n\n if (rpcError) {\n throw new Error(rpcError.message || 'Failed to fetch events');\n }\n\n if (isMounted) {\n const eventsData = data || [];\n setEvents(eventsData);\n setError(null);\n\n // Note: Event colors are now managed by useEventTheme hook based on route and selected event\n // This prevents unwanted color cycling on non-event pages\n\n // Reset auto-selection ref for new events\n hasAutoSelectedRef.current = false;\n\n // Try to restore persisted event first\n await loadPersistedEvent(eventsData);\n }\n } catch (err) {\n console.error('[EventProvider] Error fetching events:', err);\n const _error = err instanceof Error ? err : new Error('Unknown error occurred');\n \n if (isMounted) {\n setError(_error);\n setEvents([]);\n }\n } finally {\n if (isMounted) {\n setIsLoading(false);\n }\n isFetchingRef.current = false;\n }\n\n return () => {\n isMounted = false;\n };\n }, [user, session, supabase, appName, selectedOrganisation, ensureOrganisationContext, loadPersistedEvent, autoSelectNextEvent]);\n\n // Initialize events only once\n useEffect(() => {\n if (!isInitializedRef.current) {\n isInitializedRef.current = true;\n fetchEvents();\n }\n }, [fetchEvents]);\n\n // Update ref when setSelectedEventId changes\n useEffect(() => {\n setSelectedEventIdRef.current = setSelectedEventId;\n }, [setSelectedEventId]);\n\n // Auto-select next event when events are loaded and no event is selected\n useLayoutEffect(() => {\n if (events.length > 0 && !selectedEvent && !hasAutoSelectedRef.current && !userClearedEventRef.current) {\n const nextEvent = getNextEventByDate(events);\n if (nextEvent) {\n DebugLogger.log('EventProvider', 'Auto-selecting next event:', nextEvent.event_name);\n hasAutoSelectedRef.current = true;\n setSelectedEventState(nextEvent);\n setSelectedEventIdRef.current(nextEvent.event_id);\n persistEventSelection(nextEvent.event_id);\n }\n }\n }, [events, selectedEvent]);\n\n const setSelectedEvent = useCallback((event: Event | null) => {\n if (event) {\n // SECURITY: Validate event belongs to current organisation\n try {\n if (selectedOrganisation && event.organisation_id !== selectedOrganisation.id) {\n console.error('[EventProvider] Event organisation_id does not match selected organisation');\n return;\n }\n } catch (error) {\n // Silently handle validation errors\n }\n\n setSelectedEventState(event);\n setSelectedEventId(event.event_id);\n persistEventSelection(event.event_id);\n // Reset the user cleared flag when selecting an event\n userClearedEventRef.current = false;\n } else {\n setSelectedEventState(null);\n setSelectedEventId(null);\n // Clear both sessionStorage and localStorage\n sessionStorage.removeItem('pace-core-selected-event');\n localStorage.removeItem('pace-core-selected-event');\n // Reset the auto-selection flag when clearing the event\n hasAutoSelectedRef.current = false;\n // Mark that user explicitly cleared the event to prevent auto-selection\n userClearedEventRef.current = true;\n }\n }, [selectedOrganisation, setSelectedEventId, persistEventSelection]);\n\n const refreshEvents = useCallback(async () => {\n isInitializedRef.current = false;\n isFetchingRef.current = false;\n // Reset the user cleared flag when refreshing events\n userClearedEventRef.current = false;\n await fetchEvents();\n }, [fetchEvents]);\n\n const contextValue: EventContextType = {\n events,\n selectedEvent,\n isLoading,\n error,\n setSelectedEvent,\n refreshEvents,\n };\n\n return (\n <EventContext.Provider value={contextValue}>\n {children}\n </EventContext.Provider>\n );\n}","/**\n * @file Providers Export\n * @package @jmruthers/pace-core\n * @module Providers\n * @since 0.1.0\n */\n\nexport * from './EventProvider';\nexport * from './OrganisationProvider';\nexport * from './UnifiedAuthProvider';\n\n// Individual providers for advanced usage\nexport * from './AuthProvider';\nexport * from './RBACProvider';\nexport * from './InactivityProvider';\n"],"mappings":"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","names":["error","events"]}
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Page Permission Guard Component
|
|
3
|
-
* @package @jmruthers/pace-core
|
|
4
|
-
* @module Components/RBAC
|
|
5
|
-
* @since 0.3.0
|
|
6
|
-
*
|
|
7
|
-
* A unified component for page-level permission checks that provides:
|
|
8
|
-
* - Single source of truth for page permissions
|
|
9
|
-
* - Intelligent caching and performance optimization
|
|
10
|
-
* - Consistent permission checking across all pages
|
|
11
|
-
* - Type-safe permission management
|
|
12
|
-
* - Centralized logging and debugging
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```tsx
|
|
16
|
-
* import { PagePermissionGuard } from '@jmruthers/pace-core';
|
|
17
|
-
*
|
|
18
|
-
* function MealsPage() {
|
|
19
|
-
* return (
|
|
20
|
-
* <PagePermissionGuard pageId="meals">
|
|
21
|
-
* {(permissions) => (
|
|
22
|
-
* <div>
|
|
23
|
-
* {permissions.canRead && <MealsList />}
|
|
24
|
-
* {permissions.canCreate && <AddMealButton />}
|
|
25
|
-
* {permissions.canUpdate && <MealEditButtons />}
|
|
26
|
-
* {permissions.canDelete && <MealDeleteButtons />}
|
|
27
|
-
* </div>
|
|
28
|
-
* )}
|
|
29
|
-
* </PagePermissionGuard>
|
|
30
|
-
* );
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*
|
|
34
|
-
* @accessibility
|
|
35
|
-
* - Supports screen reader friendly conditional content
|
|
36
|
-
* - Maintains focus management in conditional renders
|
|
37
|
-
* - Provides accessible fallback content
|
|
38
|
-
*
|
|
39
|
-
* @security
|
|
40
|
-
* - Centralized permission validation
|
|
41
|
-
* - Database-backed permission checks
|
|
42
|
-
* - Organisation context enforcement
|
|
43
|
-
*
|
|
44
|
-
* @performance
|
|
45
|
-
* - Intelligent caching with TTL
|
|
46
|
-
* - Batch permission loading
|
|
47
|
-
* - Minimal re-renders
|
|
48
|
-
* - Memory-efficient storage
|
|
49
|
-
*
|
|
50
|
-
* @dependencies
|
|
51
|
-
* - React 18+ - Components and hooks
|
|
52
|
-
* - usePermissionCache hook - Permission caching
|
|
53
|
-
* - RBAC types - Type definitions
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
57
|
-
import { usePermissionCache } from '../../hooks/usePermissionCache';
|
|
58
|
-
import type { Operation } from '../../rbac/types';
|
|
59
|
-
|
|
60
|
-
// Page permissions interface
|
|
61
|
-
interface PagePermissions {
|
|
62
|
-
canRead: boolean;
|
|
63
|
-
canCreate: boolean;
|
|
64
|
-
canUpdate: boolean;
|
|
65
|
-
canDelete: boolean;
|
|
66
|
-
isLoading: boolean;
|
|
67
|
-
error: Error | null;
|
|
68
|
-
debugInfo?: {
|
|
69
|
-
cacheHits: number;
|
|
70
|
-
cacheMisses: number;
|
|
71
|
-
averageResponseTime: number;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Component props
|
|
76
|
-
interface PagePermissionGuardProps {
|
|
77
|
-
pageId: string;
|
|
78
|
-
children: (permissions: PagePermissions) => React.ReactNode;
|
|
79
|
-
fallback?: React.ReactNode;
|
|
80
|
-
enableDebug?: boolean;
|
|
81
|
-
cacheTTL?: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Standard page IDs for consistency
|
|
85
|
-
export const PAGE_IDS = {
|
|
86
|
-
// Core pages
|
|
87
|
-
DASHBOARD: 'dashboard',
|
|
88
|
-
ANALYTICS: 'analytics',
|
|
89
|
-
SETTINGS: 'settings',
|
|
90
|
-
|
|
91
|
-
// User management
|
|
92
|
-
USER_MANAGEMENT: 'user-management',
|
|
93
|
-
USER_ROLES: 'user-roles',
|
|
94
|
-
USER_PROFILES: 'user-profiles',
|
|
95
|
-
|
|
96
|
-
// Event management
|
|
97
|
-
EVENT_MANAGEMENT: 'event-management',
|
|
98
|
-
EVENT_SETTINGS: 'event-settings',
|
|
99
|
-
EVENT_PLANNING: 'event-planning',
|
|
100
|
-
|
|
101
|
-
// Content management
|
|
102
|
-
MEALS: 'meals',
|
|
103
|
-
DISHES: 'dishes',
|
|
104
|
-
MENUS: 'menus',
|
|
105
|
-
INGREDIENTS: 'ingredients',
|
|
106
|
-
|
|
107
|
-
// Administrative
|
|
108
|
-
ORGANISATION_SETTINGS: 'organisation-settings',
|
|
109
|
-
SYSTEM_ADMIN: 'system-admin',
|
|
110
|
-
AUDIT_LOGS: 'audit-logs',
|
|
111
|
-
|
|
112
|
-
// Custom pages (apps can extend)
|
|
113
|
-
CUSTOM: 'custom'
|
|
114
|
-
} as const;
|
|
115
|
-
|
|
116
|
-
export type PageId = typeof PAGE_IDS[keyof typeof PAGE_IDS] | string;
|
|
117
|
-
|
|
118
|
-
export function PagePermissionGuard({
|
|
119
|
-
pageId,
|
|
120
|
-
children,
|
|
121
|
-
fallback = null,
|
|
122
|
-
enableDebug = false,
|
|
123
|
-
cacheTTL
|
|
124
|
-
}: PagePermissionGuardProps) {
|
|
125
|
-
const {
|
|
126
|
-
checkMultiplePermissions,
|
|
127
|
-
getDebugInfo
|
|
128
|
-
} = usePermissionCache({
|
|
129
|
-
enableLogging: enableDebug,
|
|
130
|
-
enableAuditTrail: true
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// State
|
|
134
|
-
const [permissions, setPermissions] = useState<PagePermissions>({
|
|
135
|
-
canRead: false,
|
|
136
|
-
canCreate: false,
|
|
137
|
-
canUpdate: false,
|
|
138
|
-
canDelete: false,
|
|
139
|
-
isLoading: true,
|
|
140
|
-
error: null
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Load permissions for the page
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
let isMounted = true;
|
|
146
|
-
|
|
147
|
-
const loadPermissions = async () => {
|
|
148
|
-
try {
|
|
149
|
-
if (!isMounted) return;
|
|
150
|
-
|
|
151
|
-
setPermissions(prev => ({ ...prev, isLoading: true, error: null }));
|
|
152
|
-
|
|
153
|
-
const permissionChecks: Array<[Operation, string]> = [
|
|
154
|
-
['read', pageId],
|
|
155
|
-
['create', pageId],
|
|
156
|
-
['update', pageId],
|
|
157
|
-
['delete', pageId]
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
const results = await checkMultiplePermissions(permissionChecks, cacheTTL);
|
|
161
|
-
|
|
162
|
-
if (!isMounted) return;
|
|
163
|
-
|
|
164
|
-
const newPermissions: PagePermissions = {
|
|
165
|
-
canRead: results.find(r => r.operation === 'read')?.hasPermission || false,
|
|
166
|
-
canCreate: results.find(r => r.operation === 'create')?.hasPermission || false,
|
|
167
|
-
canUpdate: results.find(r => r.operation === 'update')?.hasPermission || false,
|
|
168
|
-
canDelete: results.find(r => r.operation === 'delete')?.hasPermission || false,
|
|
169
|
-
isLoading: false,
|
|
170
|
-
error: null
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Add debug info if enabled
|
|
174
|
-
if (enableDebug) {
|
|
175
|
-
const debugInfo = getDebugInfo();
|
|
176
|
-
newPermissions.debugInfo = {
|
|
177
|
-
cacheHits: debugInfo.cacheHits,
|
|
178
|
-
cacheMisses: debugInfo.cacheMisses,
|
|
179
|
-
averageResponseTime: debugInfo.averageResponseTime
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
setPermissions(newPermissions);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (!isMounted) return;
|
|
186
|
-
|
|
187
|
-
console.error(`[PagePermissionGuard] Error loading permissions for ${pageId}:`, error);
|
|
188
|
-
setPermissions({
|
|
189
|
-
canRead: false,
|
|
190
|
-
canCreate: false,
|
|
191
|
-
canUpdate: false,
|
|
192
|
-
canDelete: false,
|
|
193
|
-
isLoading: false,
|
|
194
|
-
error: error instanceof Error ? error : new Error('Unknown error loading permissions')
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
loadPermissions();
|
|
200
|
-
|
|
201
|
-
// Cleanup function
|
|
202
|
-
return () => {
|
|
203
|
-
isMounted = false;
|
|
204
|
-
};
|
|
205
|
-
}, [pageId, checkMultiplePermissions, getDebugInfo, enableDebug, cacheTTL]);
|
|
206
|
-
|
|
207
|
-
// Show loading state
|
|
208
|
-
if (permissions.isLoading) {
|
|
209
|
-
return (
|
|
210
|
-
<div className="page-permission-loading" role="status" aria-live="polite">
|
|
211
|
-
<span className="sr-only">Loading permissions...</span>
|
|
212
|
-
{fallback}
|
|
213
|
-
</div>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Show error state
|
|
218
|
-
if (permissions.error) {
|
|
219
|
-
return (
|
|
220
|
-
<div className="page-permission-error" role="alert">
|
|
221
|
-
<span className="sr-only">Permission check error</span>
|
|
222
|
-
{fallback}
|
|
223
|
-
</div>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Show fallback if no permissions
|
|
228
|
-
if (!permissions.canRead && !permissions.canCreate && !permissions.canUpdate && !permissions.canDelete) {
|
|
229
|
-
return <>{fallback}</>;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Render children with permissions
|
|
233
|
-
return <>{children(permissions)}</>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Convenience components for specific permission types
|
|
237
|
-
export function ReadPermissionGuard({
|
|
238
|
-
pageId,
|
|
239
|
-
children,
|
|
240
|
-
fallback = null,
|
|
241
|
-
...props
|
|
242
|
-
}: Omit<PagePermissionGuardProps, 'children'> & { children: React.ReactNode }) {
|
|
243
|
-
return (
|
|
244
|
-
<PagePermissionGuard pageId={pageId} fallback={fallback} {...props}>
|
|
245
|
-
{(permissions) => permissions.canRead ? <>{children}</> : <>{fallback}</>}
|
|
246
|
-
</PagePermissionGuard>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function CreatePermissionGuard({
|
|
251
|
-
pageId,
|
|
252
|
-
children,
|
|
253
|
-
fallback = null,
|
|
254
|
-
...props
|
|
255
|
-
}: Omit<PagePermissionGuardProps, 'children'> & { children: React.ReactNode }) {
|
|
256
|
-
return (
|
|
257
|
-
<PagePermissionGuard pageId={pageId} fallback={fallback} {...props}>
|
|
258
|
-
{(permissions) => permissions.canCreate ? <>{children}</> : <>{fallback}</>}
|
|
259
|
-
</PagePermissionGuard>
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function UpdatePermissionGuard({
|
|
264
|
-
pageId,
|
|
265
|
-
children,
|
|
266
|
-
fallback = null,
|
|
267
|
-
...props
|
|
268
|
-
}: Omit<PagePermissionGuardProps, 'children'> & { children: React.ReactNode }) {
|
|
269
|
-
return (
|
|
270
|
-
<PagePermissionGuard pageId={pageId} fallback={fallback} {...props}>
|
|
271
|
-
{(permissions) => permissions.canUpdate ? <>{children}</> : <>{fallback}</>}
|
|
272
|
-
</PagePermissionGuard>
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function DeletePermissionGuard({
|
|
277
|
-
pageId,
|
|
278
|
-
children,
|
|
279
|
-
fallback = null,
|
|
280
|
-
...props
|
|
281
|
-
}: Omit<PagePermissionGuardProps, 'children'> & { children: React.ReactNode }) {
|
|
282
|
-
return (
|
|
283
|
-
<PagePermissionGuard pageId={pageId} fallback={fallback} {...props}>
|
|
284
|
-
{(permissions) => permissions.canDelete ? <>{children}</> : <>{fallback}</>}
|
|
285
|
-
</PagePermissionGuard>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file RBAC Guard Component
|
|
3
|
-
* @package @jmruthers/pace-core
|
|
4
|
-
* @module Components/RBAC
|
|
5
|
-
* @since 0.3.0
|
|
6
|
-
*
|
|
7
|
-
* A component that conditionally renders children based on user permissions.
|
|
8
|
-
* This component integrates with the new RBAC system to provide secure,
|
|
9
|
-
* permission-based UI rendering.
|
|
10
|
-
*
|
|
11
|
-
* Features:
|
|
12
|
-
* - Permission-based conditional rendering
|
|
13
|
-
* - Database-backed permission validation
|
|
14
|
-
* - Loading states and error handling
|
|
15
|
-
* - Fallback content support
|
|
16
|
-
* - Type-safe permission checking
|
|
17
|
-
* - Automatic context detection
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```tsx
|
|
21
|
-
* import { RBACGuard } from '@jmruthers/pace-core';
|
|
22
|
-
*
|
|
23
|
-
* function MyComponent() {
|
|
24
|
-
* return (
|
|
25
|
-
* <div>
|
|
26
|
-
* <h1>Dashboard</h1>
|
|
27
|
-
*
|
|
28
|
-
* <RBACGuard operation="read" pageId="dashboard">
|
|
29
|
-
* <DashboardContent />
|
|
30
|
-
* </RBACGuard>
|
|
31
|
-
*
|
|
32
|
-
* <RBACGuard operation="create" pageId="events">
|
|
33
|
-
* <CreateEventButton />
|
|
34
|
-
* </RBACGuard>
|
|
35
|
-
*
|
|
36
|
-
* <RBACGuard
|
|
37
|
-
* operation="delete"
|
|
38
|
-
* pageId="users"
|
|
39
|
-
* fallback={<p>You don't have permission to delete users.</p>}
|
|
40
|
-
* >
|
|
41
|
-
* <DeleteUserButton />
|
|
42
|
-
* </RBACGuard>
|
|
43
|
-
* </div>
|
|
44
|
-
* );
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
* @accessibility
|
|
49
|
-
* - Supports screen reader friendly conditional content
|
|
50
|
-
* - Maintains focus management in conditional renders
|
|
51
|
-
* - Provides accessible fallback content
|
|
52
|
-
*
|
|
53
|
-
* @security
|
|
54
|
-
* - Database-backed permission validation
|
|
55
|
-
* - Secure permission checking
|
|
56
|
-
* - Organisation context enforcement
|
|
57
|
-
*
|
|
58
|
-
* @performance
|
|
59
|
-
* - Optimized with React.memo
|
|
60
|
-
* - Minimal re-renders
|
|
61
|
-
* - Efficient permission checking
|
|
62
|
-
*
|
|
63
|
-
* @dependencies
|
|
64
|
-
* - React 18+ - Components and hooks
|
|
65
|
-
* - useRBAC hook - Permission checking
|
|
66
|
-
* - RBAC types - Type definitions
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
70
|
-
import { useRBAC } from '../../hooks/useRBAC';
|
|
71
|
-
import type { RBACGuardProps, Operation } from '../../rbac/types';
|
|
72
|
-
|
|
73
|
-
export function RBACGuard({
|
|
74
|
-
children,
|
|
75
|
-
operation,
|
|
76
|
-
pageId,
|
|
77
|
-
fallback = null
|
|
78
|
-
}: RBACGuardProps) {
|
|
79
|
-
const { hasPermission, isLoading, error } = useRBAC(pageId);
|
|
80
|
-
const [hasAccess, setHasAccess] = useState(false);
|
|
81
|
-
const [isChecking, setIsChecking] = useState(true);
|
|
82
|
-
const [checkError, setCheckError] = useState<Error | null>(null);
|
|
83
|
-
|
|
84
|
-
const checkPermission = useCallback(async () => {
|
|
85
|
-
if (isLoading) return;
|
|
86
|
-
|
|
87
|
-
setIsChecking(true);
|
|
88
|
-
setCheckError(null);
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const hasPerm = await hasPermission(operation, pageId);
|
|
92
|
-
setHasAccess(hasPerm);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('Permission check failed:', err);
|
|
95
|
-
setHasAccess(false);
|
|
96
|
-
setCheckError(err instanceof Error ? err : new Error('Permission check failed'));
|
|
97
|
-
} finally {
|
|
98
|
-
setIsChecking(false);
|
|
99
|
-
}
|
|
100
|
-
}, [hasPermission, operation, pageId, isLoading]);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
checkPermission();
|
|
104
|
-
}, [checkPermission]);
|
|
105
|
-
|
|
106
|
-
// Show error state immediately if hook provides an error (bypass permission checking)
|
|
107
|
-
if (error) {
|
|
108
|
-
if (fallback) {
|
|
109
|
-
return (
|
|
110
|
-
<div className="rbac-error" role="alert">
|
|
111
|
-
<span className="sr-only">Permission check error</span>
|
|
112
|
-
{fallback}
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Show loading state while checking permissions
|
|
120
|
-
if (isLoading || isChecking) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="rbac-loading" role="status" aria-live="polite">
|
|
123
|
-
<span className="sr-only">Checking permissions...</span>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Show error state if permission check failed
|
|
129
|
-
if (checkError) {
|
|
130
|
-
if (fallback) {
|
|
131
|
-
return (
|
|
132
|
-
<div className="rbac-error" role="alert">
|
|
133
|
-
<span className="sr-only">Permission check error</span>
|
|
134
|
-
{fallback}
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Render children if user has permission, otherwise show fallback
|
|
142
|
-
return hasAccess ? <>{children}</> : <>{fallback}</>;
|
|
143
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file RBAC Provider Component (Legacy)
|
|
3
|
-
* @package @jmruthers/pace-core
|
|
4
|
-
* @module Components/RBAC
|
|
5
|
-
* @since 0.3.0
|
|
6
|
-
* @deprecated Use UnifiedAuthProvider instead for new applications
|
|
7
|
-
*
|
|
8
|
-
* A Role-Based Access Control (RBAC) provider that manages user permissions
|
|
9
|
-
* and access levels throughout the application. This component is maintained
|
|
10
|
-
* for backward compatibility but new applications should use UnifiedAuthProvider
|
|
11
|
-
* which provides integrated authentication and RBAC functionality.
|
|
12
|
-
*
|
|
13
|
-
* Features:
|
|
14
|
-
* - Role-based access control system
|
|
15
|
-
* - Permission-based authorization
|
|
16
|
-
* - Access level hierarchy management
|
|
17
|
-
* - Context-based state management
|
|
18
|
-
* - Supabase integration support
|
|
19
|
-
* - Loading state management
|
|
20
|
-
* - Type-safe permission checking
|
|
21
|
-
* - Flexible permission configuration
|
|
22
|
-
* - Access level validation
|
|
23
|
-
* - Provider pattern implementation
|
|
24
|
-
*
|
|
25
|
-
* @migration
|
|
26
|
-
* For new applications, use UnifiedAuthProvider instead:
|
|
27
|
-
* ```tsx
|
|
28
|
-
* // Old approach (deprecated)
|
|
29
|
-
* <RBACProvider permissions={permissions} accessLevel={accessLevel}>
|
|
30
|
-
* <App />
|
|
31
|
-
* </RBACProvider>
|
|
32
|
-
*
|
|
33
|
-
* // New approach (recommended)
|
|
34
|
-
* <UnifiedAuthProvider supabaseClient={supabase} appName="MyApp">
|
|
35
|
-
* <App />
|
|
36
|
-
* </UnifiedAuthProvider>
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```tsx
|
|
41
|
-
* // Basic RBAC provider setup (legacy)
|
|
42
|
-
* <RBACProvider
|
|
43
|
-
* permissions={{
|
|
44
|
-
* 'read:events': true,
|
|
45
|
-
* 'write:events': false,
|
|
46
|
-
* 'delete:events': false,
|
|
47
|
-
* 'admin:users': false
|
|
48
|
-
* }}
|
|
49
|
-
* accessLevel={AccessLevel.PARTICIPANT}
|
|
50
|
-
* >
|
|
51
|
-
* <App />
|
|
52
|
-
* </RBACProvider>
|
|
53
|
-
*
|
|
54
|
-
* // RBAC provider with Supabase integration (legacy)
|
|
55
|
-
* <RBACProvider
|
|
56
|
-
* supabaseClient={supabase}
|
|
57
|
-
* permissions={userPermissions}
|
|
58
|
-
* accessLevel={userAccessLevel}
|
|
59
|
-
* >
|
|
60
|
-
* <App />
|
|
61
|
-
* </RBACProvider>
|
|
62
|
-
*
|
|
63
|
-
* // Using RBAC in components (legacy)
|
|
64
|
-
* function EventList() {
|
|
65
|
-
* const { hasPermission, hasAccessLevel, accessLevel } = useRBAC();
|
|
66
|
-
*
|
|
67
|
-
* return (
|
|
68
|
-
* <div>
|
|
69
|
-
* <h1>Events</h1>
|
|
70
|
-
* {hasPermission('read:events') && (
|
|
71
|
-
* <EventList />
|
|
72
|
-
* )}
|
|
73
|
-
* {hasAccessLevel(AccessLevel.EDITOR) && (
|
|
74
|
-
* <CreateEventButton />
|
|
75
|
-
* )}
|
|
76
|
-
* {accessLevel === AccessLevel.ADMIN && (
|
|
77
|
-
* <AdminPanel />
|
|
78
|
-
* )}
|
|
79
|
-
* </div>
|
|
80
|
-
* );
|
|
81
|
-
* }
|
|
82
|
-
*
|
|
83
|
-
* // Conditional rendering based on permissions (legacy)
|
|
84
|
-
* function EventActions({ event }) {
|
|
85
|
-
* const { hasPermission } = useRBAC();
|
|
86
|
-
*
|
|
87
|
-
* return (
|
|
88
|
-
* <div className="flex gap-2">
|
|
89
|
-
* {hasPermission('read:events') && (
|
|
90
|
-
* <Button onClick={() => viewEvent(event.id)}>View</Button>
|
|
91
|
-
* )}
|
|
92
|
-
* {hasPermission('write:events') && (
|
|
93
|
-
* <Button onClick={() => editEvent(event.id)}>Edit</Button>
|
|
94
|
-
* )}
|
|
95
|
-
* {hasPermission('delete:events') && (
|
|
96
|
-
* <Button variant="destructive" onClick={() => deleteEvent(event.id)}>
|
|
97
|
-
* Delete
|
|
98
|
-
* </Button>
|
|
99
|
-
* )}
|
|
100
|
-
* </div>
|
|
101
|
-
* );
|
|
102
|
-
* }
|
|
103
|
-
* ```
|
|
104
|
-
*
|
|
105
|
-
* @accessibility
|
|
106
|
-
* - No direct accessibility concerns (context provider)
|
|
107
|
-
* - Enables accessible permission-based UI rendering
|
|
108
|
-
* - Supports screen reader friendly conditional content
|
|
109
|
-
* - Maintains focus management in conditional renders
|
|
110
|
-
*
|
|
111
|
-
* @security
|
|
112
|
-
* - Permission-based access control
|
|
113
|
-
* - Access level validation
|
|
114
|
-
* - Secure permission checking
|
|
115
|
-
* - Role-based authorization
|
|
116
|
-
* - Context isolation
|
|
117
|
-
* - Note: For enhanced security, use UnifiedAuthProvider
|
|
118
|
-
*
|
|
119
|
-
* @dependencies
|
|
120
|
-
* - React 18+ - Context and hooks
|
|
121
|
-
* - AccessLevel enum - Permission levels
|
|
122
|
-
* - Supabase client (optional) - Database integration
|
|
123
|
-
* - TypeScript - Type safety
|
|
124
|
-
*/
|
|
125
|
-
|
|
126
|
-
import React, { createContext, useContext, ReactNode, useEffect, useState } from 'react';
|
|
127
|
-
import { AccessLevel } from '../../types/unified';
|
|
128
|
-
|
|
129
|
-
interface RBACContextType {
|
|
130
|
-
permissions: Record<string, boolean>;
|
|
131
|
-
accessLevel: AccessLevel;
|
|
132
|
-
hasPermission: (permission: string) => boolean;
|
|
133
|
-
hasAccessLevel: (level: AccessLevel) => boolean;
|
|
134
|
-
isLoading: boolean;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const RBACContext = createContext<RBACContextType | null>(null);
|
|
138
|
-
|
|
139
|
-
interface RBACProviderProps {
|
|
140
|
-
children: ReactNode;
|
|
141
|
-
supabaseClient?: any;
|
|
142
|
-
permissions?: Record<string, boolean>;
|
|
143
|
-
accessLevel?: AccessLevel;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export const RBACProvider: React.FC<RBACProviderProps> = ({
|
|
147
|
-
children,
|
|
148
|
-
supabaseClient,
|
|
149
|
-
permissions = {},
|
|
150
|
-
accessLevel = AccessLevel.VIEWER
|
|
151
|
-
}) => {
|
|
152
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
153
|
-
|
|
154
|
-
const hasPermission = (permission: string): boolean => {
|
|
155
|
-
return permissions[permission] || false;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const hasAccessLevel = (level: AccessLevel): boolean => {
|
|
159
|
-
const levels = [AccessLevel.NONE, AccessLevel.VIEWER, AccessLevel.PARTICIPANT, AccessLevel.EDITOR, AccessLevel.PLANNER, AccessLevel.ADMIN, AccessLevel.SUPER_ADMIN, AccessLevel.SUPER];
|
|
160
|
-
const userLevelIndex = levels.indexOf(accessLevel);
|
|
161
|
-
const requiredLevelIndex = levels.indexOf(level);
|
|
162
|
-
return userLevelIndex >= requiredLevelIndex;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const contextValue: RBACContextType = {
|
|
166
|
-
permissions,
|
|
167
|
-
accessLevel,
|
|
168
|
-
hasPermission,
|
|
169
|
-
hasAccessLevel,
|
|
170
|
-
isLoading
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<RBACContext.Provider value={contextValue}>
|
|
175
|
-
{children}
|
|
176
|
-
</RBACContext.Provider>
|
|
177
|
-
);
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
export const useRBAC = (): RBACContextType => {
|
|
181
|
-
const context = useContext(RBACContext);
|
|
182
|
-
if (!context) {
|
|
183
|
-
throw new Error('useRBAC must be used within an RBACProvider');
|
|
184
|
-
}
|
|
185
|
-
return context;
|
|
186
|
-
};
|