@jmruthers/pace-core 0.5.73 → 0.5.75

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 (283) hide show
  1. package/dist/{DataTable-INW5YIFV.js → DataTable-HWZQGASI.js} +8 -8
  2. package/dist/{PublicLoadingSpinner-DLpF5bbs.d.ts → PublicLoadingSpinner-BKNBT6b6.d.ts} +2 -2
  3. package/dist/RBACService-C4udt_Zp.d.ts +528 -0
  4. package/dist/{UnifiedAuthProvider-6SYT5WFN.js → UnifiedAuthProvider-3NKDOSOK.js} +6 -4
  5. package/dist/UnifiedAuthProvider-Bj6YCf7c.d.ts +113 -0
  6. package/dist/{chunk-2PRPDH66.js → chunk-2CHATWBF.js} +5 -7
  7. package/dist/chunk-2CHATWBF.js.map +1 -0
  8. package/dist/{chunk-43C63KLH.js → chunk-2DFZ432F.js} +496 -30
  9. package/dist/chunk-2DFZ432F.js.map +1 -0
  10. package/dist/{chunk-M4UMXYNK.js → chunk-33PHABLB.js} +36 -3
  11. package/dist/chunk-33PHABLB.js.map +1 -0
  12. package/dist/chunk-5F3NDPJV.js +232 -0
  13. package/dist/chunk-5F3NDPJV.js.map +1 -0
  14. package/dist/chunk-A4FUBC7B.js +17 -0
  15. package/dist/chunk-A4FUBC7B.js.map +1 -0
  16. package/dist/{chunk-SMJZMKYN.js → chunk-A6HBIY5P.js} +2 -11
  17. package/dist/{chunk-SMJZMKYN.js.map → chunk-A6HBIY5P.js.map} +1 -1
  18. package/dist/{chunk-GBC5PC3N.js → chunk-CY3AHGO4.js} +6256 -1937
  19. package/dist/chunk-CY3AHGO4.js.map +1 -0
  20. package/dist/{chunk-BYG6OSTC.js → chunk-DAXLNIDY.js} +48 -50
  21. package/dist/chunk-DAXLNIDY.js.map +1 -0
  22. package/dist/{chunk-VKOCWWVY.js → chunk-L3RV2ALE.js} +1 -6
  23. package/dist/{chunk-VKOCWWVY.js.map → chunk-L3RV2ALE.js.map} +1 -1
  24. package/dist/chunk-LW7MMEAQ.js +59 -0
  25. package/dist/chunk-LW7MMEAQ.js.map +1 -0
  26. package/dist/{chunk-LANO5IFV.js → chunk-NTNILOBC.js} +7 -9
  27. package/dist/chunk-NTNILOBC.js.map +1 -0
  28. package/dist/chunk-PYUXFQJ3.js +11 -0
  29. package/dist/chunk-PYUXFQJ3.js.map +1 -0
  30. package/dist/chunk-URUTVZ7N.js +27 -0
  31. package/dist/chunk-URUTVZ7N.js.map +1 -0
  32. package/dist/chunk-WN6XJWOS.js +2468 -0
  33. package/dist/chunk-WN6XJWOS.js.map +1 -0
  34. package/dist/{chunk-3SP4P7NS.js → chunk-XLZ7U46Z.js} +59 -1
  35. package/dist/chunk-XLZ7U46Z.js.map +1 -0
  36. package/dist/{chunk-UC2BWIK7.js → chunk-ZTT2AXMX.js} +9 -14
  37. package/dist/chunk-ZTT2AXMX.js.map +1 -0
  38. package/dist/components.d.ts +4 -5
  39. package/dist/components.js +32 -39
  40. package/dist/components.js.map +1 -1
  41. package/dist/hooks.d.ts +3 -3
  42. package/dist/hooks.js +9 -8
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +156 -10
  45. package/dist/index.js +188 -93
  46. package/dist/index.js.map +1 -1
  47. package/dist/{organisation-t-vvQC3g.d.ts → organisation-BtshODVF.d.ts} +4 -3
  48. package/dist/providers.d.ts +27 -38
  49. package/dist/providers.js +33 -23
  50. package/dist/rbac/index.d.ts +61 -5
  51. package/dist/rbac/index.js +13 -14
  52. package/dist/styles/index.js +2 -2
  53. package/dist/theming/runtime.js +1 -3
  54. package/dist/types.d.ts +3 -3
  55. package/dist/types.js +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/{unified-CMPjE_fv.d.ts → unified-CM7T0aTK.d.ts} +1 -1
  58. package/dist/useInactivityTracker-MRUU55XI.js +10 -0
  59. package/dist/useInactivityTracker-MRUU55XI.js.map +1 -0
  60. package/dist/{usePublicRouteParams-Ua1Vz-HG.d.ts → usePublicRouteParams-B-CumWRc.d.ts} +3 -3
  61. package/dist/utils.js +7 -9
  62. package/dist/utils.js.map +1 -1
  63. package/dist/validation.d.ts +1 -1
  64. package/docs/api/classes/ColumnFactory.md +1 -1
  65. package/docs/api/classes/ErrorBoundary.md +1 -1
  66. package/docs/api/classes/InvalidScopeError.md +1 -1
  67. package/docs/api/classes/MissingUserContextError.md +1 -1
  68. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  69. package/docs/api/classes/PermissionDeniedError.md +1 -1
  70. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  71. package/docs/api/classes/RBACAuditManager.md +1 -1
  72. package/docs/api/classes/RBACCache.md +1 -1
  73. package/docs/api/classes/RBACEngine.md +1 -1
  74. package/docs/api/classes/RBACError.md +1 -1
  75. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  76. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  77. package/docs/api/classes/StorageUtils.md +1 -1
  78. package/docs/api/enums/FileCategory.md +1 -1
  79. package/docs/api/interfaces/AggregateConfig.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +3 -3
  81. package/docs/api/interfaces/CardProps.md +2 -2
  82. package/docs/api/interfaces/ColorPalette.md +1 -1
  83. package/docs/api/interfaces/ColorShade.md +1 -1
  84. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  85. package/docs/api/interfaces/DataTableAction.md +1 -1
  86. package/docs/api/interfaces/DataTableColumn.md +1 -1
  87. package/docs/api/interfaces/DataTableProps.md +1 -1
  88. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventLogoProps.md +2 -2
  92. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  100. package/docs/api/interfaces/InputProps.md +2 -2
  101. package/docs/api/interfaces/LabelProps.md +1 -1
  102. package/docs/api/interfaces/LoginFormProps.md +1 -1
  103. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  104. package/docs/api/interfaces/NavigationContextType.md +1 -1
  105. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  106. package/docs/api/interfaces/NavigationItem.md +1 -1
  107. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  108. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  109. package/docs/api/interfaces/Organisation.md +1 -1
  110. package/docs/api/interfaces/OrganisationContextType.md +28 -17
  111. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  112. package/docs/api/interfaces/OrganisationProviderProps.md +2 -2
  113. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  114. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  115. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  116. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  117. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  118. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  119. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  120. package/docs/api/interfaces/PaletteData.md +1 -1
  121. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  122. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  123. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  124. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +2 -2
  125. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  126. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  127. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  128. package/docs/api/interfaces/RBACConfig.md +1 -1
  129. package/docs/api/interfaces/RBACContextType.md +5 -11
  130. package/docs/api/interfaces/RBACLogger.md +1 -1
  131. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  132. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  134. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  135. package/docs/api/interfaces/RouteConfig.md +1 -1
  136. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  137. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  138. package/docs/api/interfaces/StorageConfig.md +1 -1
  139. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  140. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  141. package/docs/api/interfaces/StorageListOptions.md +1 -1
  142. package/docs/api/interfaces/StorageListResult.md +1 -1
  143. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  144. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  145. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  146. package/docs/api/interfaces/StyleImport.md +1 -1
  147. package/docs/api/interfaces/SwitchProps.md +1 -1
  148. package/docs/api/interfaces/ToastActionElement.md +1 -1
  149. package/docs/api/interfaces/ToastProps.md +1 -1
  150. package/docs/api/interfaces/UnifiedAuthContextType.md +524 -440
  151. package/docs/api/interfaces/UnifiedAuthProviderProps.md +14 -14
  152. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  153. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  154. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  155. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  156. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  157. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  158. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  159. package/docs/api/interfaces/UserEventAccess.md +11 -11
  160. package/docs/api/interfaces/UserMenuProps.md +1 -1
  161. package/docs/api/interfaces/UserProfile.md +1 -1
  162. package/docs/api/modules.md +179 -52
  163. package/docs/architecture/services.md +30 -32
  164. package/docs/breaking-changes.md +2 -5
  165. package/docs/implementation-guides/data-tables.md +82 -1
  166. package/docs/migration/service-architecture.md +121 -260
  167. package/docs/rbac/README-rbac-rls-integration.md +48 -38
  168. package/{src/rbac/examples → examples/RBAC}/CompleteRBACExample.tsx +3 -2
  169. package/{src/rbac/examples → examples/RBAC}/EventBasedApp.tsx +5 -4
  170. package/{src/components/examples → examples/RBAC}/PermissionExample.tsx +7 -6
  171. package/examples/RBAC/__tests__/PermissionExample.test.tsx +150 -0
  172. package/examples/RBAC/index.ts +13 -0
  173. package/examples/README.md +37 -0
  174. package/examples/index.ts +22 -0
  175. package/{src/examples → examples/public-pages}/CorrectPublicPageImplementation.tsx +1 -1
  176. package/{src/examples → examples/public-pages}/PublicEventPage.tsx +1 -1
  177. package/{src/examples → examples/public-pages}/PublicPageApp.tsx +1 -1
  178. package/{src/examples → examples/public-pages}/PublicPageUsageExample.tsx +1 -1
  179. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +159 -0
  180. package/examples/public-pages/index.ts +14 -0
  181. package/package.json +22 -18
  182. package/src/__tests__/TEST_GUIDE_CURSOR.md +650 -9
  183. package/src/__tests__/helpers/README.md +255 -0
  184. package/src/__tests__/helpers/index.ts +62 -0
  185. package/src/__tests__/helpers/supabaseMock.ts +27 -3
  186. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -8
  187. package/src/components/DataTable/components/DataTableCore.tsx +37 -3
  188. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +55 -0
  189. package/src/components/DataTable/core/ColumnManager.ts +10 -0
  190. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +254 -0
  191. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +193 -0
  192. package/src/components/DataTable/examples/__tests__/HierarchicalExample.test.tsx +45 -0
  193. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +117 -0
  194. package/src/components/Dialog/Dialog.tsx +2 -2
  195. package/src/components/Dialog/examples/__tests__/HtmlDialogExample.test.tsx +71 -0
  196. package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +122 -0
  197. package/src/components/EventSelector/EventSelector.tsx +1 -1
  198. package/src/components/Header/Header.test.tsx +35 -1
  199. package/src/components/Header/Header.tsx +3 -1
  200. package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -3
  201. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +24 -4
  202. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +3 -2
  203. package/src/components/Toast/Toast.test.tsx +1 -1
  204. package/src/components/Toast/Toast.tsx +1 -1
  205. package/src/hooks/__tests__/useFocusManagement.unit.test.ts +220 -0
  206. package/src/hooks/__tests__/useIsMobile.unit.test.ts +117 -0
  207. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +295 -0
  208. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +29 -19
  209. package/src/hooks/__tests__/useRBAC.unit.test.ts +7 -3
  210. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +115 -19
  211. package/src/hooks/useEventTheme.test.ts +350 -0
  212. package/src/hooks/useEventTheme.ts +1 -1
  213. package/src/hooks/useEvents.ts +61 -0
  214. package/src/hooks/useOrganisationSecurity.test.ts +4 -4
  215. package/src/hooks/useOrganisationSecurity.ts +2 -2
  216. package/src/hooks/useOrganisations.ts +64 -0
  217. package/src/hooks/useSecureDataAccess.test.ts +9 -5
  218. package/src/hooks/useSecureDataAccess.ts +2 -2
  219. package/src/index.ts +18 -3
  220. package/src/providers/AuthProvider.tsx +8 -292
  221. package/src/providers/EventProvider.tsx +15 -425
  222. package/src/providers/InactivityProvider.tsx +8 -231
  223. package/src/providers/OrganisationProvider.test.simple.tsx +3 -2
  224. package/src/providers/OrganisationProvider.tsx +11 -890
  225. package/src/providers/UnifiedAuthProvider.tsx +8 -320
  226. package/src/providers/__tests__/AuthProvider.test.tsx +18 -17
  227. package/src/providers/__tests__/EventProvider.test.tsx +253 -2
  228. package/src/providers/__tests__/InactivityProvider.test-helper.tsx +65 -0
  229. package/src/providers/__tests__/InactivityProvider.test.tsx +46 -114
  230. package/src/providers/__tests__/OrganisationProvider.test.tsx +313 -3
  231. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +383 -2
  232. package/src/providers/index.ts +8 -7
  233. package/src/providers/services/EventServiceProvider.tsx +3 -0
  234. package/src/providers/services/UnifiedAuthProvider.tsx +3 -0
  235. package/src/rbac/hooks/usePermissions.test.ts +296 -0
  236. package/src/rbac/hooks/useRBAC.test.ts +9 -5
  237. package/src/rbac/hooks/useRBAC.ts +3 -3
  238. package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +688 -0
  239. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +507 -0
  240. package/src/services/AuthService.ts +19 -4
  241. package/src/services/__tests__/AuthService.test.ts +288 -0
  242. package/src/styles/core.css +2 -0
  243. package/src/types/__tests__/guards.test.ts +246 -0
  244. package/src/types/guards.ts +1 -0
  245. package/src/types/organisation.ts +3 -2
  246. package/src/validation/__tests__/sanitization.unit.test.ts +250 -0
  247. package/src/validation/__tests__/schemaUtils.unit.test.ts +451 -0
  248. package/src/validation/__tests__/user.unit.test.ts +440 -0
  249. package/dist/RBACProvider-BO4ilsQB.d.ts +0 -63
  250. package/dist/UnifiedAuthProvider-D02AMXgO.d.ts +0 -103
  251. package/dist/chunk-2PRPDH66.js.map +0 -1
  252. package/dist/chunk-3SP4P7NS.js.map +0 -1
  253. package/dist/chunk-43C63KLH.js.map +0 -1
  254. package/dist/chunk-5A4RL4BC.js +0 -5670
  255. package/dist/chunk-5A4RL4BC.js.map +0 -1
  256. package/dist/chunk-BYG6OSTC.js.map +0 -1
  257. package/dist/chunk-CDDYJCYU.js +0 -79
  258. package/dist/chunk-CDDYJCYU.js.map +0 -1
  259. package/dist/chunk-F24P24TZ.js +0 -17
  260. package/dist/chunk-F24P24TZ.js.map +0 -1
  261. package/dist/chunk-GBC5PC3N.js.map +0 -1
  262. package/dist/chunk-LANO5IFV.js.map +0 -1
  263. package/dist/chunk-M4UMXYNK.js.map +0 -1
  264. package/dist/chunk-RJNE764D.js +0 -953
  265. package/dist/chunk-RJNE764D.js.map +0 -1
  266. package/dist/chunk-UC2BWIK7.js.map +0 -1
  267. package/dist/rbac/cli/policy-manager.js +0 -278
  268. package/dist/rbac/cli/policy-manager.js.map +0 -1
  269. package/docs/api/interfaces/EventContextType.md +0 -96
  270. package/docs/api/interfaces/EventProviderProps.md +0 -19
  271. package/src/providers/OrganisationProvider.test.tsx +0 -164
  272. package/src/providers/UnifiedAuthProvider.test.tsx +0 -124
  273. package/src/providers/__tests__/AuthProvider.test.tsx.backup +0 -771
  274. package/src/providers/__tests__/EventProvider.test.tsx.backup +0 -824
  275. package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +0 -820
  276. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +0 -911
  277. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +0 -166
  278. package/src/rbac/cli/__tests__/policy-manager.test.ts +0 -339
  279. package/src/rbac/cli/policy-manager.ts +0 -443
  280. package/dist/{DataTable-INW5YIFV.js.map → DataTable-HWZQGASI.js.map} +0 -0
  281. package/dist/{UnifiedAuthProvider-6SYT5WFN.js.map → UnifiedAuthProvider-3NKDOSOK.js.map} +0 -0
  282. package/dist/{validation-PM_iOaTI.d.ts → validation-D8VcbTzC.d.ts} +2 -2
  283. /package/src/utils/{appNameResolver.test.ts.backup → appNameResolver.test 2.ts} +0 -0
@@ -1,898 +1,19 @@
1
1
  /**
2
- * @file Organisation Provider
2
+ * @file Re-export for OrganisationProvider
3
3
  * @package @jmruthers/pace-core
4
- * @module Providers/Organisation
5
- * @since 0.4.0
6
- *
7
- * Security-first organisation provider that enforces mandatory organisation context.
8
- * No data operations can proceed without valid organisation context.
4
+ * @module Providers
5
+ * @since 0.1.0
9
6
  *
10
- * Features:
11
- * - Mandatory organisation selection for all operations
12
- * - User organisation membership validation
13
- * - Role-based access within organisations
14
- * - Secure organisation switching
15
- * - Hierarchy support for parent/child organisations
16
- * - Error handling for security violations
17
- * - Persistent organisation selection
18
- *
19
- * @example
20
- * ```tsx
21
- * // Basic setup - organisation context is mandatory
22
- * import { UnifiedAuthProvider, OrganisationProvider } from '@jmruthers/pace-core';
23
- *
24
- * function App() {
25
- * return (
26
- * <UnifiedAuthProvider supabaseClient={supabase} appName="MY_APP">
27
- * <OrganisationProvider>
28
- * <YourAppContent />
29
- * </OrganisationProvider>
30
- * </UnifiedAuthProvider>
31
- * );
32
- * }
33
- *
34
- * // Using in components
35
- * function MyComponent() {
36
- * const {
37
- * selectedOrganisation,
38
- * getUserRole,
39
- * switchOrganisation
40
- * } = useOrganisations();
41
- *
42
- * // selectedOrganisation is guaranteed to be non-null when this renders
43
- * return (
44
- * <div>
45
- * <h1>{selectedOrganisation.display_name}</h1>
46
- * <p>Your role: {getUserRole()}</p>
47
- * </div>
48
- * );
49
- * }
50
- * ```
51
- *
52
- * @security
53
- * - All data access requires valid organisation context
54
- * - User membership validation on organisation load
55
- * - Role-based access control within organisations
56
- * - Secure organisation switching with validation
57
- * - Error states for security violations
58
- * - No fallback to default organisation - explicit selection required
59
- *
60
- * @dependencies
61
- * - React 18+ - Context, hooks, and effects
62
- * - UnifiedAuthProvider - Authentication context
63
- * - Supabase - Database operations
64
- * - Organisation types - Type definitions
65
- */
66
-
67
- import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef } from 'react';
68
- import { useNavigate } from 'react-router-dom';
69
- import { useUnifiedAuth } from './UnifiedAuthProvider';
70
- import { setOrganisationContext } from '../utils/organisationContext';
71
- import { DebugLogger } from '../utils/debugLogger';
72
- import type {
73
- Organisation,
74
- OrganisationMembership,
75
- OrganisationContextType,
76
- OrganisationProviderProps,
77
- OrganisationSecurityError,
78
- OrganisationHierarchy
79
- } from '../types/organisation';
80
-
81
- // Create the context
82
- const OrganisationContext = createContext<OrganisationContextType | undefined>(undefined);
83
-
84
- // Storage keys for persistence
85
- const STORAGE_KEYS = {
86
- SELECTED_ORGANISATION: 'pace-core-selected-organisation',
87
- ORGANISATION_CONTEXT: 'pace-core-organisation-context',
88
- } as const;
89
-
90
- /**
91
- * Organisation Provider component that enforces mandatory organisation context
92
- *
93
- * This provider:
94
- * - Loads user's organisation memberships on authentication
95
- * - Validates user has at least one active organisation
96
- * - Auto-selects primary organisation or first available
97
- * - Provides security helpers for organisation validation
98
- * - Handles organisation switching with validation
99
- * - Persists organisation selection across sessions
100
- *
101
- * SECURITY: No children are rendered without valid organisation context
7
+ * Re-exports the service-based OrganisationProvider for backward compatibility.
102
8
  */
103
- export function OrganisationProvider({ children }: OrganisationProviderProps) {
104
- const [selectedOrganisation, setSelectedOrganisation] = useState<Organisation | null>(null);
105
- const [organisations, setOrganisations] = useState<Organisation[]>([]);
106
- const [userMemberships, setUserMemberships] = useState<OrganisationMembership[]>([]);
107
- const [roleMapState, setRoleMapState] = useState<Map<string, string>>(new Map());
108
- const [isLoading, setIsLoading] = useState(true);
109
- const [error, setError] = useState<Error | null>(null);
110
- const [isContextReady, setIsContextReady] = useState(false);
111
- const [retryCount, setRetryCount] = useState(0);
112
- const isLoadingRef = useRef(false);
113
- const lastLoadTimeRef = useRef(0);
114
- const hasFailedRef = useRef(false);
115
- const abortControllerRef = useRef<AbortController | null>(null);
116
-
117
- const { user, session, supabase, signOut } = useUnifiedAuth();
118
-
119
- // Use navigate hook conditionally to avoid test failures
120
- let navigate: any = null;
121
- try {
122
- navigate = useNavigate();
123
- } catch (error) {
124
- // In test environment or when no router context, navigate will be null
125
- navigate = null;
126
- }
127
-
128
- // FIXED: Function to clear all cached data
129
- const clearAllCachedData = useCallback(() => {
130
- localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
131
- localStorage.removeItem(STORAGE_KEYS.ORGANISATION_CONTEXT);
132
- setSelectedOrganisation(null);
133
- setOrganisations([]);
134
- setUserMemberships([]);
135
- setRoleMapState(new Map());
136
- setError(null);
137
- setRetryCount(0);
138
- setIsContextReady(false);
139
- }, []);
140
-
141
- // Set organisation context in database session
142
- const setDatabaseOrganisationContext = useCallback(async (organisation: Organisation): Promise<void> => {
143
- if (!supabase || !session) {
144
- console.warn('[OrganisationProvider] No Supabase client or session available for setting organisation context');
145
- setIsContextReady(false);
146
- return;
147
- }
148
-
149
- try {
150
- await setOrganisationContext(supabase, organisation.id);
151
- DebugLogger.log('OrganisationProvider', 'Database organisation context set to:', organisation.display_name);
152
- setIsContextReady(true);
153
- } catch (error) {
154
- console.error('[OrganisationProvider] Failed to set database organisation context:', error);
155
- setIsContextReady(false);
156
- // Don't throw - this is a non-critical operation
157
- }
158
- }, [supabase, session]);
159
-
160
- // CRITICAL: Set database organisation context when organisation changes
161
- useEffect(() => {
162
- if (selectedOrganisation && supabase && session) {
163
- // Reset context ready state when organisation changes
164
- setIsContextReady(false);
165
-
166
- // Use an async IIFE to properly handle the async operation
167
- (async () => {
168
- await setDatabaseOrganisationContext(selectedOrganisation);
169
- })();
170
- } else {
171
- setIsContextReady(false);
172
- }
173
- }, [selectedOrganisation, setDatabaseOrganisationContext, supabase, session]);
174
-
175
- // CRITICAL: Load user organisations and validate access
176
- const loadUserOrganisations = useCallback(async () => {
177
- // Add call tracking to detect race conditions
178
- const callId = Math.random().toString(36).substr(2, 9);
179
- console.log(`[OrganisationProvider] Starting loadUserOrganisations call ${callId}`);
180
-
181
- if (!user || !session || !supabase) {
182
- // Clear state when no user, session, or supabase client
183
- DebugLogger.log('OrganisationProvider', 'Clearing organisation state - no user, session, or supabase client');
184
- setSelectedOrganisation(null);
185
- setOrganisations([]);
186
- setUserMemberships([]);
187
- setIsLoading(false);
188
- setError(null);
189
- return;
190
- }
191
-
192
- // FIXED: Additional check to prevent loading during auth state changes
193
- if (isLoadingRef.current) {
194
- console.log("OrganisationProvider", "Already loading, skipping duplicate load");
195
- return;
196
- }
197
-
198
- // FIXED: Prevent rapid retries - minimum 2 seconds between attempts
199
- const now = Date.now();
200
- if (now - lastLoadTimeRef.current < 2000) {
201
- console.log("OrganisationProvider", "Too soon since last load, skipping");
202
- return;
203
- }
204
-
205
- // FIXED: Cancel any existing request
206
- if (abortControllerRef.current) {
207
- abortControllerRef.current.abort();
208
- }
209
-
210
- // Create new abort controller for this request
211
- abortControllerRef.current = new AbortController();
212
- const abortSignal = abortControllerRef.current.signal;
213
-
214
- lastLoadTimeRef.current = now;
215
- isLoadingRef.current = true;
216
- setIsLoading(true);
217
- setError(null);
218
-
219
- try {
220
- DebugLogger.log("OrganisationProvider", "Loading organisations for user:", user.id);
221
-
222
- // Debug: Log Supabase client configuration
223
- console.log("[OrganisationProvider] Supabase client ready:", {
224
- isConnected: !!supabase,
225
- hasAuth: !!supabase.auth,
226
- hasRpc: !!supabase.rpc
227
- });
228
-
229
- // Get user's organisation memberships using secure RPC function
230
- // Only get actual members (org_admin, leader, member) - exclude supporters
231
- let memberships, membershipError;
232
- try {
233
- console.log("[OrganisationProvider] Making RPC call to data_user_organisation_roles_get...");
234
-
235
- // FIXED: Add timeout and abort signal to prevent hanging RPC calls
236
- const timeoutPromise = new Promise((_, reject) => {
237
- const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
238
- abortSignal.addEventListener('abort', () => {
239
- clearTimeout(timeoutId);
240
- reject(new Error('Request aborted'));
241
- });
242
- });
243
-
244
- const rpcPromise = supabase.rpc('data_user_organisation_roles_get', {
245
- p_user_id: user.id,
246
- p_organisation_id: null
247
- });
248
-
249
- // Check if request was aborted before making the call
250
- if (abortSignal.aborted) {
251
- throw new Error('Request aborted');
252
- }
253
-
254
- const result = await Promise.race([rpcPromise, timeoutPromise]) as any;
255
-
256
- console.log("[OrganisationProvider] RPC call completed:", {
257
- hasData: !!result.data,
258
- hasError: !!result.error,
259
- dataLength: result.data?.length || 0,
260
- errorMessage: result.error?.message || 'No error'
261
- });
262
-
263
- // Filter to only actual members (org_admin, leader, member) - exclude supporters
264
- memberships = result.data?.filter((role: any) =>
265
- ['org_admin', 'leader', 'member'].includes(role.role)
266
- ) || [];
267
- membershipError = result.error;
268
- } catch (queryError: any) {
269
- membershipError = queryError;
270
- }
271
-
272
- if (membershipError) {
273
- console.error("[OrganisationProvider] Error loading memberships:", membershipError);
274
-
275
- // If RPC fails with timeout, try direct database query as fallback
276
- if (membershipError.message?.includes('timeout')) {
277
- console.log("[OrganisationProvider] RPC timed out, trying direct database query as fallback...");
278
- try {
279
- // Check if request was aborted before making fallback query
280
- if (abortSignal.aborted) {
281
- throw new Error('Request aborted');
282
- }
283
-
284
- const { data: fallbackData, error: fallbackError } = await supabase
285
- .from('rbac_organisation_roles')
286
- .select(`
287
- id,
288
- user_id,
289
- organisation_id,
290
- role,
291
- status,
292
- granted_at,
293
- granted_by,
294
- revoked_at,
295
- revoked_by,
296
- notes,
297
- created_at,
298
- updated_at,
299
- organisations!inner(
300
- id,
301
- name,
302
- display_name,
303
- subscription_tier,
304
- settings,
305
- is_active,
306
- parent_id,
307
- created_at,
308
- updated_at
309
- )
310
- `)
311
- .eq('user_id', user.id)
312
- .eq('status', 'active')
313
- .is('revoked_at', null)
314
- .in('role', ['org_admin', 'leader', 'member']);
315
-
316
- if (fallbackError) {
317
- console.error("[OrganisationProvider] Fallback query also failed:", fallbackError);
318
- throw membershipError; // Throw original error
319
- }
320
-
321
- console.log("[OrganisationProvider] Fallback query successful, got", fallbackData?.length || 0, "memberships");
322
- memberships = fallbackData || [];
323
- membershipError = null;
324
- } catch (fallbackErr) {
325
- console.error("[OrganisationProvider] Fallback query failed:", fallbackErr);
326
- throw membershipError; // Throw original error
327
- }
328
- } else {
329
- throw membershipError;
330
- }
331
- }
332
-
333
- DebugLogger.log("OrganisationProvider", "Raw memberships data:", memberships);
334
-
335
- if (!memberships || memberships.length === 0) {
336
- throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
337
- }
338
-
339
- // FIXED: Debug log to identify any problematic membership data
340
- console.log("[OrganisationProvider] All memberships data:", memberships);
341
- memberships.forEach((membership: any, index: number) => {
342
- console.log(`[OrganisationProvider] Membership ${index}:`, {
343
- organisation_id: membership.organisation_id,
344
- type: typeof membership.organisation_id,
345
- length: membership.organisation_id ? membership.organisation_id.length : 'null/undefined',
346
- trimmed: membership.organisation_id ? membership.organisation_id.trim() : 'null/undefined'
347
- });
348
- if (!membership.organisation_id || membership.organisation_id.trim() === '') {
349
- console.warn(`[OrganisationProvider] Membership ${index} has invalid organisation_id:`, membership);
350
- }
351
- });
352
9
 
353
- // Get organisation details for the memberships
354
- const organisationIds = memberships
355
- .map((m: any) => m.organisation_id)
356
- .filter((id: string) => {
357
- // FIXED: Better validation to prevent empty string UUID errors
358
- if (!id || typeof id !== 'string') {
359
- console.warn("[OrganisationProvider] Invalid organisation ID (not string):", id);
360
- return false;
361
- }
362
- const trimmedId = id.trim();
363
- if (trimmedId === '') {
364
- console.warn("[OrganisationProvider] Empty organisation ID found");
365
- return false;
366
- }
367
- // Validate UUID format
368
- const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
369
- if (!isValidUuid) {
370
- console.warn("[OrganisationProvider] Invalid UUID format:", trimmedId);
371
- }
372
- return isValidUuid;
373
- });
374
-
375
- if (organisationIds.length === 0) {
376
- console.warn("[OrganisationProvider] No valid organisation IDs found in memberships:", memberships);
377
- throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
378
- }
379
-
380
- DebugLogger.log("OrganisationProvider", "Valid organisation IDs:", organisationIds);
381
-
382
- // FIXED: Additional validation to ensure no empty strings in the array
383
- console.log("[OrganisationProvider] Raw organisation IDs before cleaning:", organisationIds);
384
- console.log("[OrganisationProvider] Raw organisation IDs types:", organisationIds.map((id: any) => typeof id));
385
- console.log("[OrganisationProvider] Raw organisation IDs lengths:", organisationIds.map((id: any) => id ? id.length : 'null/undefined'));
386
-
387
- const cleanOrganisationIds = organisationIds.filter((id: any) => {
388
- const isValid = id && typeof id === 'string' && id.trim() !== '';
389
- if (!isValid) {
390
- console.warn("[OrganisationProvider] Filtering out invalid ID:", { id, type: typeof id, length: id ? id.length : 'null/undefined' });
391
- }
392
- return isValid;
393
- }).map((id: any) => id.trim()); // Ensure all IDs are trimmed
394
-
395
- console.log("[OrganisationProvider] Clean organisation IDs after filtering:", cleanOrganisationIds);
396
-
397
- if (cleanOrganisationIds.length === 0) {
398
- console.warn("[OrganisationProvider] No clean organisation IDs after filtering:", organisationIds);
399
- throw new Error('No valid organisation IDs found after cleaning') as OrganisationSecurityError;
400
- }
401
-
402
- DebugLogger.log("OrganisationProvider", "Clean organisation IDs for query:", cleanOrganisationIds);
403
-
404
- // Final validation: ensure no empty strings in the array
405
- const finalOrganisationIds = cleanOrganisationIds.filter((id: any) => {
406
- const isEmpty = !id || id.trim() === '';
407
- if (isEmpty) {
408
- console.error("[OrganisationProvider] CRITICAL: Empty string found in final array:", { id, type: typeof id });
409
- }
410
- return !isEmpty;
411
- });
412
-
413
- if (finalOrganisationIds.length !== cleanOrganisationIds.length) {
414
- console.error("[OrganisationProvider] CRITICAL: Empty strings were filtered out in final validation!");
415
- console.error("Original:", cleanOrganisationIds);
416
- console.error("Final:", finalOrganisationIds);
417
- }
418
-
419
- // CRITICAL: Log exactly what we're passing to the query
420
- console.log("[OrganisationProvider] FINAL QUERY ARRAY:", {
421
- array: finalOrganisationIds,
422
- length: finalOrganisationIds.length,
423
- types: finalOrganisationIds.map((id: any) => typeof id),
424
- hasEmpty: finalOrganisationIds.some((id: any) => !id || id.trim() === ''),
425
- stringified: JSON.stringify(finalOrganisationIds)
426
- });
427
-
428
- // Additional safety check - if we detect any empty strings, abort
429
- if (finalOrganisationIds.some((id: any) => !id || id.trim() === '')) {
430
- console.error("[OrganisationProvider] ABORTING QUERY - Empty string detected in final array!");
431
- throw new Error('Empty string detected in organisation IDs array - aborting query');
432
- }
433
-
434
- // Create a completely new array to avoid any reference issues
435
- const safeOrganisationIds = [...finalOrganisationIds].filter((id: any) => {
436
- const isValid = id && typeof id === 'string' && id.trim() !== '';
437
- if (!isValid) {
438
- console.error("[OrganisationProvider] SAFETY FILTER: Removing invalid ID:", { id, type: typeof id });
439
- }
440
- return isValid;
441
- });
442
-
443
- console.log("[OrganisationProvider] SAFE ARRAY FOR QUERY:", {
444
- original: finalOrganisationIds,
445
- safe: safeOrganisationIds,
446
- lengths: { original: finalOrganisationIds.length, safe: safeOrganisationIds.length }
447
- });
448
-
449
- // Try a different approach - use a simple select with manual filtering
450
- console.log("[OrganisationProvider] Using direct table query with manual filtering");
451
-
452
- // Check if request was aborted before making organisations query
453
- if (abortSignal.aborted) {
454
- throw new Error('Request aborted');
455
- }
456
-
457
- const { data: allOrganisations, error: orgError } = await supabase
458
- .from('organisations')
459
- .select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
460
-
461
- if (orgError) {
462
- console.error("[OrganisationProvider] Error loading organisations:", orgError);
463
- throw orgError;
464
- }
465
-
466
- // Filter manually on the client side
467
- const organisations = allOrganisations?.filter(org =>
468
- safeOrganisationIds.includes(org.id)
469
- ) || [];
10
+ export { OrganisationServiceProvider as OrganisationProvider } from './services/OrganisationServiceProvider';
11
+ export type { OrganisationServiceProviderProps as OrganisationProviderProps } from './services/OrganisationServiceProvider';
470
12
 
471
- // Create a map of organisation_id to role from the memberships data
472
- // Since we're now getting roles directly from the consolidated table
473
- const roleMap = new Map<string, string>();
474
- memberships?.forEach((membership: any) => {
475
- roleMap.set(membership.organisation_id, membership.role);
476
- });
13
+ // Re-export context and hook
14
+ export { OrganisationServiceContext, useOrganisationService } from './services/OrganisationServiceProvider';
15
+ export type { OrganisationServiceContextType } from './services/OrganisationServiceProvider';
477
16
 
478
- // Extract organisations and memberships
479
- const orgs = organisations as Organisation[];
480
- const activeOrgs = orgs.filter(org => org.is_active);
481
-
482
- if (activeOrgs.length === 0) {
483
- throw new Error('User has no access to active organisations') as OrganisationSecurityError;
484
- }
485
-
486
- DebugLogger.log("OrganisationProvider", "Active organisations:", activeOrgs);
487
-
488
- setOrganisations(activeOrgs);
489
- setUserMemberships(memberships as OrganisationMembership[]);
490
-
491
- // Store role map in component state for later use
492
- setRoleMapState(roleMap);
493
-
494
- // Auto-select organisation: try persisted, then primary, then first
495
- let initialOrg: Organisation | null = null;
496
-
497
- // 1. Try to restore from localStorage
498
- try {
499
- const persistedOrgString = localStorage.getItem(STORAGE_KEYS.SELECTED_ORGANISATION);
500
- if (persistedOrgString) {
501
- const persistedOrg = JSON.parse(persistedOrgString) as Organisation;
502
- // FIXED: Validate persisted org ID before using it
503
- if (persistedOrg.id && typeof persistedOrg.id === 'string' && persistedOrg.id.trim() !== '') {
504
- const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
505
- if (validPersistedOrg) {
506
- initialOrg = validPersistedOrg;
507
- DebugLogger.log("OrganisationProvider", "Restored persisted organisation:", initialOrg.display_name);
508
- } else {
509
- console.warn("[OrganisationProvider] Persisted organisation not found in active orgs, clearing cache");
510
- localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
511
- }
512
- } else {
513
- console.warn("[OrganisationProvider] Invalid persisted organisation ID, clearing cache");
514
- localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
515
- }
516
- }
517
- } catch (storageError) {
518
- console.warn("[OrganisationProvider] Failed to restore persisted organisation:", storageError);
519
- // Clear potentially corrupted cache
520
- localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
521
- }
522
-
523
- // 2. Fall back to org_admin role organisation (highest privilege)
524
- if (!initialOrg) {
525
- const adminMembership = memberships.find((m: any) => m.role === 'org_admin');
526
- if (adminMembership) {
527
- const foundOrg = organisations.find((org: any) => org.id === adminMembership.organisation_id);
528
- if (foundOrg) {
529
- initialOrg = foundOrg;
530
- DebugLogger.log("OrganisationProvider", "Selected org_admin organisation:", initialOrg.display_name);
531
- }
532
- }
533
- }
534
-
535
- // 3. Fall back to first organisation
536
- if (!initialOrg) {
537
- initialOrg = activeOrgs[0];
538
- DebugLogger.log("OrganisationProvider", "Selected first organisation:", initialOrg.display_name);
539
- }
540
-
541
- if (!initialOrg) {
542
- throw new Error('No valid organisation found for user') as OrganisationSecurityError;
543
- }
544
-
545
- setSelectedOrganisation(initialOrg);
546
-
547
- // Persist selection
548
- localStorage.setItem(STORAGE_KEYS.SELECTED_ORGANISATION, JSON.stringify(initialOrg));
549
-
550
- DebugLogger.log("OrganisationProvider", "Organisation context established:", {
551
- selectedOrganisation: initialOrg.display_name,
552
- totalOrganisations: activeOrgs.length,
553
- userRole: roleMap.get(initialOrg.id)
554
- });
555
-
556
- // FIXED: Reset retry count and failed flag on success
557
- setRetryCount(0);
558
- hasFailedRef.current = false;
559
-
560
- } catch (err) {
561
- console.error("[OrganisationProvider] Failed to load organisations:", err);
562
- setError(err as Error);
563
- // FIXED: Increment retry count on error
564
- setRetryCount(prev => prev + 1);
565
- // FIXED: Set failed flag to prevent further attempts
566
- hasFailedRef.current = true;
567
- // FIXED: Clear all cached data on error to prevent corruption
568
- clearAllCachedData();
569
- } finally {
570
- // FIXED: Always cleanup refs and abort controller
571
- isLoadingRef.current = false;
572
- setIsLoading(false);
573
- abortControllerRef.current = null;
574
- }
575
- }, [user, session, supabase, clearAllCachedData]);
576
-
577
- // FIXED: Load organisations only when authentication is complete and stable
578
- useEffect(() => {
579
- // Only load organizations if we have a valid user and session
580
- // and we're not in the middle of authentication loading
581
- if (user && session && supabase && !isLoading && !isLoadingRef.current) {
582
- // FIXED: Prevent infinite retry loops with stricter conditions
583
- if (retryCount >= 3 || hasFailedRef.current) {
584
- console.error("[OrganisationProvider] Max retry count reached or failed flag set, stopping organisation loading");
585
- setError(new Error('Failed to load organisations after multiple attempts'));
586
- setIsLoading(false);
587
- return;
588
- }
589
-
590
- // FIXED: Add circuit breaker - if we've failed multiple times, stop trying
591
- if (retryCount > 0 && Date.now() - lastLoadTimeRef.current < 5000) {
592
- console.log("[OrganisationProvider] Circuit breaker active - too soon since last attempt");
593
- return;
594
- }
595
-
596
- console.log("[OrganisationProvider] Authentication stable, loading organizations... (retry:", retryCount, ")");
597
- loadUserOrganisations();
598
- } else if (!user && !session) {
599
- // Clear state if no authentication
600
- console.log("[OrganisationProvider] No authentication, clearing organization state");
601
- setSelectedOrganisation(null);
602
- setOrganisations([]);
603
- setUserMemberships([]);
604
- setRoleMapState(new Map());
605
- setIsLoading(false);
606
- setError(null);
607
- setRetryCount(0); // Reset retry count
608
- isLoadingRef.current = false; // Reset loading ref
609
- hasFailedRef.current = false; // Reset failed flag
610
- // FIXED: Clear localStorage when no authentication to prevent stale data
611
- localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
612
- localStorage.removeItem(STORAGE_KEYS.ORGANISATION_CONTEXT);
613
- }
614
- }, [user, session, supabase, loadUserOrganisations]); // FIXED: Remove retryCount from dependencies to prevent infinite loop
615
-
616
- // FIXED: Add cleanup effect to prevent memory leaks
617
- useEffect(() => {
618
- return () => {
619
- // Cleanup on unmount
620
- isLoadingRef.current = false;
621
- hasFailedRef.current = false;
622
- lastLoadTimeRef.current = 0;
623
- // Abort any pending requests
624
- if (abortControllerRef.current) {
625
- abortControllerRef.current.abort();
626
- abortControllerRef.current = null;
627
- }
628
- };
629
- }, []);
630
-
631
- // Handle logout and redirect to login
632
- const handleLogoutAndRedirect = useCallback(async () => {
633
- try {
634
- await signOut();
635
- if (navigate) {
636
- navigate('/login', { replace: true });
637
- } else {
638
- // Fallback to window.location if navigate is not available
639
- window.location.href = '/login';
640
- }
641
- } catch (error) {
642
- console.error('[OrganisationProvider] Error during logout:', error);
643
- // Even if logout fails, redirect to login
644
- if (navigate) {
645
- navigate('/login', { replace: true });
646
- } else {
647
- // Fallback to window.location if navigate is not available
648
- window.location.href = '/login';
649
- }
650
- }
651
- }, [signOut, navigate]);
652
-
653
- // Security validation helper
654
- const ensureOrganisationContext = useCallback((): Organisation => {
655
- if (!selectedOrganisation) {
656
- throw new Error('Organisation context is required but not available') as OrganisationSecurityError;
657
- }
658
- return selectedOrganisation;
659
- }, [selectedOrganisation]);
660
-
661
- // Get user's role in specified organisation (defaults to current)
662
- const getUserRole = useCallback((orgId?: string): string => {
663
- const targetOrgId = orgId || selectedOrganisation?.id;
664
- if (!targetOrgId) return 'no_access';
665
-
666
- // Use roleMapState to get the role for this organisation
667
- return roleMapState.get(targetOrgId) || 'no_access';
668
- }, [roleMapState, selectedOrganisation]);
669
-
670
- // Validate user has access to organisation
671
- const validateOrganisationAccess = useCallback((orgId: string): boolean => {
672
- return userMemberships.some((m: any) =>
673
- m.organisation_id === orgId &&
674
- m.status === 'active' &&
675
- m.revoked_at === null
676
- );
677
- }, [userMemberships]);
678
-
679
- // Secure organisation switching
680
- const switchOrganisation = useCallback(async (orgId: string): Promise<void> => {
681
- DebugLogger.log("OrganisationProvider", "Switching to organisation:", orgId);
682
-
683
- // Validate access
684
- if (!validateOrganisationAccess(orgId)) {
685
- throw new Error(`User does not have access to organisation ${orgId}`) as OrganisationSecurityError;
686
- }
687
-
688
- const targetOrg = organisations.find(org => org.id === orgId);
689
- if (!targetOrg) {
690
- throw new Error(`Organisation ${orgId} not found in user's organisations`) as OrganisationSecurityError;
691
- }
692
-
693
- setSelectedOrganisation(targetOrg);
694
-
695
- // Persist selection
696
- localStorage.setItem(STORAGE_KEYS.SELECTED_ORGANISATION, JSON.stringify(targetOrg));
697
-
698
- // Set database organisation context
699
- await setDatabaseOrganisationContext(targetOrg);
700
-
701
- DebugLogger.log("OrganisationProvider", "Switched to organisation:", targetOrg.display_name);
702
- }, [organisations, validateOrganisationAccess, setDatabaseOrganisationContext]);
703
-
704
- // Refresh organisations data
705
- const refreshOrganisations = useCallback(async (): Promise<void> => {
706
- if (!user || !session || !supabase) return;
707
-
708
- // Force reload by triggering the effect
709
- setIsLoading(true);
710
- // The useEffect will handle the actual reload
711
- }, [user, session, supabase]);
712
-
713
- // Get primary organisation (highest privilege role)
714
- const getPrimaryOrganisation = useCallback((): Organisation | null => {
715
- // Look for org_admin role first, then leader, then member
716
- const rolePriority = ['org_admin', 'leader', 'member'];
717
-
718
- for (const role of rolePriority) {
719
- const membership = userMemberships.find((m: any) => m.role === role);
720
- if (membership) {
721
- return organisations.find((org: any) => org.id === membership.organisation_id) || null;
722
- }
723
- }
724
-
725
- return null;
726
- }, [userMemberships, organisations]);
727
-
728
- // Security status
729
- const isOrganisationSecure = useCallback((): boolean => {
730
- return !!(selectedOrganisation && user);
731
- }, [selectedOrganisation, user]);
732
-
733
- // Build organisation hierarchy (for future use)
734
- const buildOrganisationHierarchy = useCallback((orgs: Organisation[]): OrganisationHierarchy[] => {
735
- const orgMap = new Map<string, Organisation>();
736
- orgs.forEach(org => orgMap.set(org.id, org));
737
-
738
- const roots: OrganisationHierarchy[] = [];
739
-
740
- orgs.forEach(org => {
741
- if (!org.parent_id) {
742
- // Root organisation
743
- roots.push({
744
- organisation: org,
745
- children: [],
746
- depth: 0
747
- });
748
- }
749
- });
750
-
751
- // For now, return flat structure - hierarchy building can be added later
752
- return roots;
753
- }, []);
754
-
755
- // Computed values
756
- const hasValidOrganisationContext = useMemo(() => {
757
- return !!(selectedOrganisation && !isLoading && !error && isContextReady);
758
- }, [selectedOrganisation, isLoading, error, isContextReady]);
759
-
760
- // Memoized context value
761
- const contextValue = useMemo<OrganisationContextType>(() => {
762
- // SECURITY: Only provide full context if we have valid organisation
763
- if (!selectedOrganisation) {
764
- // This will never be accessed due to the render guards above,
765
- // but TypeScript requires the interface to be satisfied
766
- const placeholderOrg: Organisation = {
767
- id: '',
768
- name: '',
769
- display_name: '',
770
- subscription_tier: 'standard',
771
- settings: {},
772
- is_active: false,
773
- created_at: '',
774
- updated_at: ''
775
- };
776
-
777
- return {
778
- selectedOrganisation: placeholderOrg,
779
- organisations: [],
780
- userMemberships: [],
781
- isLoading,
782
- error,
783
- hasValidOrganisationContext: false,
784
- setSelectedOrganisation: () => {},
785
- switchOrganisation: async () => {},
786
- getUserRole: () => 'no_access',
787
- validateOrganisationAccess: () => false,
788
- refreshOrganisations: async () => {},
789
- ensureOrganisationContext: () => { throw new Error('No organisation context') as OrganisationSecurityError; },
790
- isOrganisationSecure: () => false,
791
- getPrimaryOrganisation: () => null
792
- };
793
- }
794
-
795
- return {
796
- selectedOrganisation,
797
- organisations,
798
- userMemberships,
799
- isLoading,
800
- error,
801
- hasValidOrganisationContext,
802
- setSelectedOrganisation,
803
- switchOrganisation,
804
- getUserRole,
805
- validateOrganisationAccess,
806
- refreshOrganisations,
807
- ensureOrganisationContext,
808
- isOrganisationSecure,
809
- getPrimaryOrganisation
810
- };
811
- }, [
812
- selectedOrganisation,
813
- organisations,
814
- userMemberships,
815
- isLoading,
816
- error,
817
- hasValidOrganisationContext,
818
- switchOrganisation,
819
- getUserRole,
820
- validateOrganisationAccess,
821
- refreshOrganisations,
822
- ensureOrganisationContext,
823
- isOrganisationSecure,
824
- getPrimaryOrganisation
825
- ]);
826
-
827
- // SECURITY: Only render children when we have valid organisation context
828
- if (isLoading || (selectedOrganisation && !isContextReady)) {
829
- return (
830
- <div className="organisation-loading" role="status" aria-label="Loading organisation context">
831
- <div className="flex items-center justify-center min-h-screen">
832
- <div className="text-center">
833
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
834
- <p className="text-muted-foreground">
835
- {isLoading ? 'Loading organisation context...' : 'Setting up organisation context...'}
836
- </p>
837
- </div>
838
- </div>
839
- </div>
840
- );
841
- }
842
-
843
- if (error || (user && !selectedOrganisation)) {
844
- return (
845
- <div className="organisation-error" role="alert">
846
- <div className="flex items-center justify-center min-h-screen">
847
- <div className="text-center max-w-md mx-auto p-6">
848
- <div className="text-destructive mb-4">
849
- <svg className="h-12 w-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
850
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
851
- </svg>
852
- </div>
853
- <h2 className="text-xl font-semibold text-foreground mb-2">
854
- Organisation Access Required
855
- </h2>
856
- <p className="text-muted-foreground mb-4">
857
- {error?.message || 'No valid organisation context available. Please contact your administrator to be added to an organisation.'}
858
- </p>
859
- <button
860
- onClick={handleLogoutAndRedirect}
861
- className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
862
- >
863
- Sign Out
864
- </button>
865
- </div>
866
- </div>
867
- </div>
868
- );
869
- }
870
-
871
- return (
872
- <OrganisationContext.Provider value={contextValue}>
873
- {children}
874
- </OrganisationContext.Provider>
875
- );
876
- }
877
-
878
- /**
879
- * Hook to access organisation context
880
- *
881
- * @returns Organisation context with guaranteed non-null selectedOrganisation
882
- * @throws {Error} If used outside OrganisationProvider
883
- */
884
- export const useOrganisations = (): OrganisationContextType => {
885
- const context = useContext(OrganisationContext);
886
- if (!context) {
887
- throw new Error('useOrganisations must be used within an OrganisationProvider');
888
- }
889
- return context;
890
- };
17
+ // Re-export convenience hook for backward compatibility
18
+ export { useOrganisations } from '../hooks/useOrganisations';
891
19
 
892
- // Re-export types for convenience
893
- export type {
894
- Organisation,
895
- OrganisationMembership,
896
- OrganisationContextType,
897
- OrganisationSecurityError
898
- };