@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.
Files changed (206) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -58
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +441 -0
  174. package/src/rbac/hooks/index.ts +21 -0
  175. package/src/rbac/hooks/useCan.test.ts +461 -0
  176. package/src/rbac/hooks/usePermissions.test.ts +359 -0
  177. package/src/rbac/hooks/usePermissions.ts +567 -0
  178. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  179. package/src/rbac/hooks/useRBAC.test.ts +503 -0
  180. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  181. package/src/rbac/index.ts +5 -10
  182. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  183. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  184. package/src/rbac/providers/index.ts +11 -0
  185. package/src/styles/core.css +0 -58
  186. package/src/utils/formatDate.test.ts +241 -0
  187. package/dist/chunk-AUE24LVR.js +0 -268
  188. package/dist/chunk-AUE24LVR.js.map +0 -1
  189. package/dist/chunk-COBPIXXQ.js +0 -379
  190. package/dist/chunk-COBPIXXQ.js.map +0 -1
  191. package/dist/chunk-OEGRKULD.js.map +0 -1
  192. package/dist/chunk-OYRY44Q2.js +0 -62
  193. package/dist/chunk-OYRY44Q2.js.map +0 -1
  194. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  195. package/dist/chunk-TGDCLPP2.js.map +0 -1
  196. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  197. package/src/components/RBAC/RBACGuard.tsx +0 -143
  198. package/src/components/RBAC/RBACProvider.tsx +0 -186
  199. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  200. package/src/components/RBAC/index.ts +0 -23
  201. package/src/rbac/hooks.ts +0 -570
  202. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  203. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  204. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  205. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  206. /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
- };