@jmruthers/pace-core 0.5.189 → 0.5.191

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 (438) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  4. package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
  5. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
  6. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  7. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
  8. package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
  9. package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
  10. package/dist/chunk-6LTQQAT6.js.map +1 -0
  11. package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
  12. package/dist/chunk-6TQDD426.js.map +1 -0
  13. package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
  14. package/dist/chunk-G37KK66H.js.map +1 -0
  15. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  16. package/dist/chunk-HW3OVDUF.js.map +1 -0
  17. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  18. package/dist/chunk-I7PSE6JW.js.map +1 -0
  19. package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
  20. package/dist/chunk-LOMZXPSN.js.map +1 -0
  21. package/dist/chunk-OETXORNB.js +614 -0
  22. package/dist/chunk-OETXORNB.js.map +1 -0
  23. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  24. package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
  25. package/dist/chunk-ROXMHMY2.js.map +1 -0
  26. package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
  27. package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
  28. package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
  29. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  30. package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
  31. package/dist/chunk-VRGWKHDB.js.map +1 -0
  32. package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
  33. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  34. package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
  35. package/dist/chunk-XYXSXPUK.js.map +1 -0
  36. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  37. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  38. package/dist/components.d.ts +5 -6
  39. package/dist/components.js +19 -19
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  42. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  43. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  44. package/dist/hooks.d.ts +20 -15
  45. package/dist/hooks.js +14 -8
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/index.d.ts +17 -15
  48. package/dist/index.js +86 -81
  49. package/dist/index.js.map +1 -1
  50. package/dist/providers.d.ts +3 -3
  51. package/dist/providers.js +3 -1
  52. package/dist/rbac/index.d.ts +77 -13
  53. package/dist/rbac/index.js +12 -9
  54. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  55. package/dist/types.d.ts +3 -3
  56. package/dist/types.js +1 -1
  57. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
  58. package/dist/utils.d.ts +8 -8
  59. package/dist/utils.js +16 -16
  60. package/docs/README.md +2 -2
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +2 -2
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +2 -2
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +1 -1
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +5 -5
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  73. package/docs/api/classes/SecureSupabaseClient.md +25 -20
  74. package/docs/api/classes/StorageUtils.md +7 -4
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +20 -6
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +2 -2
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  109. package/docs/api/interfaces/FileUploadProps.md +30 -19
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  120. package/docs/api/interfaces/NavigationContextType.md +9 -9
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  133. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +2 -2
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  139. package/docs/api/interfaces/ProgressProps.md +3 -11
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  175. package/docs/api/interfaces/RouteConfig.md +10 -10
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  178. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +4 -4
  182. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  183. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  184. package/docs/api/interfaces/StorageListOptions.md +22 -9
  185. package/docs/api/interfaces/StorageListResult.md +4 -4
  186. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  187. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  188. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +11 -11
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +165 -106
  218. package/docs/api-reference/components.md +15 -7
  219. package/docs/api-reference/providers.md +2 -2
  220. package/docs/api-reference/rpc-functions.md +1 -0
  221. package/docs/best-practices/README.md +1 -1
  222. package/docs/best-practices/deployment.md +8 -8
  223. package/docs/getting-started/examples/README.md +2 -2
  224. package/docs/getting-started/installation-guide.md +4 -4
  225. package/docs/getting-started/quick-start.md +3 -3
  226. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  227. package/docs/migration/README.md +18 -0
  228. package/docs/migration/database-changes-december-2025.md +767 -0
  229. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  230. package/docs/rbac/compliance/compliance-guide.md +2 -2
  231. package/docs/rbac/event-based-apps.md +2 -2
  232. package/docs/rbac/getting-started.md +2 -2
  233. package/docs/rbac/quick-start.md +2 -2
  234. package/docs/security/README.md +4 -4
  235. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  236. package/docs/troubleshooting/README.md +2 -2
  237. package/docs/troubleshooting/migration.md +3 -3
  238. package/package.json +1 -3
  239. package/scripts/check-pace-core-compliance.cjs +1 -1
  240. package/scripts/check-pace-core-compliance.js +1 -1
  241. package/src/__tests__/fixtures/supabase.ts +301 -0
  242. package/src/__tests__/public-recipe-view.test.ts +19 -19
  243. package/src/__tests__/rls-policies.test.ts +210 -74
  244. package/src/components/AddressField/AddressField.test.tsx +42 -0
  245. package/src/components/AddressField/AddressField.tsx +71 -60
  246. package/src/components/AddressField/README.md +7 -6
  247. package/src/components/Alert/Alert.test.tsx +50 -10
  248. package/src/components/Alert/Alert.tsx +5 -3
  249. package/src/components/Avatar/Avatar.test.tsx +95 -43
  250. package/src/components/Avatar/Avatar.tsx +16 -16
  251. package/src/components/Button/Button.test.tsx +2 -1
  252. package/src/components/Button/Button.tsx +3 -3
  253. package/src/components/Calendar/Calendar.test.tsx +53 -37
  254. package/src/components/Calendar/Calendar.tsx +409 -82
  255. package/src/components/Card/Card.test.tsx +7 -4
  256. package/src/components/Card/Card.tsx +3 -6
  257. package/src/components/Checkbox/Checkbox.tsx +2 -2
  258. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  259. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  260. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  261. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  262. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  263. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  264. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  265. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  266. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  267. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  268. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  269. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  270. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  271. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  272. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  273. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  274. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  275. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  276. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  277. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  278. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  279. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  280. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  281. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  282. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  284. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  285. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  286. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  287. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  288. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  289. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  290. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  291. package/src/components/Dialog/Dialog.tsx +2 -2
  292. package/src/components/EventSelector/EventSelector.tsx +7 -7
  293. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  294. package/src/components/FileUpload/FileUpload.tsx +7 -4
  295. package/src/components/Header/Header.test.tsx +28 -0
  296. package/src/components/Header/Header.tsx +22 -9
  297. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  298. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  299. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  301. package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
  302. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  303. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  304. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  305. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  306. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  307. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  308. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  309. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  310. package/src/components/Progress/Progress.test.tsx +18 -19
  311. package/src/components/Progress/Progress.tsx +31 -32
  312. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  313. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  314. package/src/components/Select/Select.test.tsx +4 -1
  315. package/src/components/Select/Select.tsx +65 -20
  316. package/src/components/Switch/Switch.test.tsx +2 -1
  317. package/src/components/Switch/Switch.tsx +1 -1
  318. package/src/components/Toast/Toast.tsx +1 -1
  319. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  320. package/src/components/UserMenu/UserMenu.tsx +3 -3
  321. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  322. package/src/eslint-rules/pace-core-compliance.js +0 -2
  323. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  324. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  325. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  326. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  327. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  328. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  329. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
  330. package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
  331. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  332. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
  333. package/src/hooks/index.ts +1 -1
  334. package/src/hooks/public/usePublicEvent.ts +10 -10
  335. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  336. package/src/hooks/useAppConfig.ts +24 -5
  337. package/src/hooks/useFileDisplay.ts +298 -36
  338. package/src/hooks/useFileReference.ts +56 -11
  339. package/src/hooks/useFileUrl.ts +1 -1
  340. package/src/hooks/useInactivityTracker.ts +16 -7
  341. package/src/hooks/usePermissionCache.test.ts +85 -8
  342. package/src/hooks/useQueryCache.ts +27 -6
  343. package/src/hooks/useSecureDataAccess.test.ts +87 -42
  344. package/src/hooks/useSecureDataAccess.ts +95 -48
  345. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  346. package/src/providers/services/EventServiceProvider.tsx +37 -17
  347. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  348. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  349. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  350. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  351. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  352. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  353. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  354. package/src/rbac/api.ts +240 -36
  355. package/src/rbac/cache-invalidation.ts +21 -7
  356. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  357. package/src/rbac/components/NavigationGuard.tsx +23 -63
  358. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  359. package/src/rbac/components/NavigationProvider.tsx +13 -11
  360. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  361. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  362. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  363. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  364. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  365. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  366. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  367. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  368. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  369. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  370. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  371. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  372. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  373. package/src/rbac/engine.ts +4 -2
  374. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  375. package/src/rbac/hooks/index.ts +3 -0
  376. package/src/rbac/hooks/useCan.test.ts +101 -53
  377. package/src/rbac/hooks/usePermissions.ts +108 -41
  378. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  379. package/src/rbac/hooks/useRBAC.ts +83 -40
  380. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  381. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  382. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  383. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  384. package/src/rbac/request-deduplication.ts +1 -1
  385. package/src/rbac/secureClient.ts +72 -12
  386. package/src/rbac/security.ts +29 -23
  387. package/src/rbac/types.ts +10 -0
  388. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  389. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  390. package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
  391. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
  392. package/src/rbac/utils/contextValidator.ts +288 -0
  393. package/src/rbac/utils/eventContext.ts +52 -3
  394. package/src/services/AuthService.ts +37 -8
  395. package/src/services/EventService.ts +165 -21
  396. package/src/services/OrganisationService.ts +125 -137
  397. package/src/services/__tests__/EventService.test.ts +26 -21
  398. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  399. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  400. package/src/types/database.generated.ts +166 -201
  401. package/src/types/file-reference.ts +13 -10
  402. package/src/types/supabase.ts +2 -2
  403. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  404. package/src/utils/app/appNameResolver.test.ts +346 -73
  405. package/src/utils/context/superAdminOverride.ts +58 -0
  406. package/src/utils/file-reference/index.ts +65 -37
  407. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  408. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  409. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  410. package/src/utils/google-places/types.ts +1 -1
  411. package/src/utils/request-deduplication.ts +4 -4
  412. package/src/utils/security/secureDataAccess.test.ts +1 -1
  413. package/src/utils/security/secureDataAccess.ts +7 -4
  414. package/src/utils/storage/README.md +1 -1
  415. package/src/utils/storage/helpers.test.ts +1 -1
  416. package/src/utils/storage/helpers.ts +38 -19
  417. package/src/utils/storage/types.ts +15 -8
  418. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  419. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  420. package/src/vite-env.d.ts +2 -2
  421. package/dist/chunk-3GOZZZYH.js.map +0 -1
  422. package/dist/chunk-DDM4CCYT.js.map +0 -1
  423. package/dist/chunk-E7UAOUMY.js +0 -75
  424. package/dist/chunk-E7UAOUMY.js.map +0 -1
  425. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  426. package/dist/chunk-HEHYGYOX.js.map +0 -1
  427. package/dist/chunk-IM4QE42D.js.map +0 -1
  428. package/dist/chunk-MX64ZF6I.js.map +0 -1
  429. package/dist/chunk-SAUPYVLF.js.map +0 -1
  430. package/dist/chunk-THRPYOFK.js.map +0 -1
  431. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  432. package/dist/chunk-VGZZXKBR.js.map +0 -1
  433. package/dist/chunk-YGPFYGA6.js.map +0 -1
  434. package/dist/chunk-YHCN776L.js.map +0 -1
  435. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
  436. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  437. /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
  438. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -16,6 +16,9 @@ import { Organisation } from '../types/organisation';
16
16
  import { assertOrganisationId } from '../types/core';
17
17
  import { logger } from '../utils/core/logger';
18
18
  import { secureStorage } from '../utils/security/secureStorage';
19
+ import { isSuperAdmin, getAppConfigByName } from '../rbac/api';
20
+ import type { UUID } from '../rbac/types';
21
+ import type { AppConfig } from '../rbac/utils/contextValidator';
19
22
 
20
23
  export class EventService extends BaseService implements IEventService {
21
24
  private events: Event[] = [];
@@ -30,6 +33,8 @@ export class EventService extends BaseService implements IEventService {
30
33
  private appName: string = '';
31
34
  private selectedOrganisation: Organisation | null = null;
32
35
  private setSelectedEventId: ((eventId: string | null) => void) | null = null;
36
+ private isSuperAdmin: boolean = false; // Track super admin status for conditional validation
37
+ private appConfig: AppConfig | null = null; // Cache app config to avoid repeated lookups
33
38
 
34
39
  // Internal state management
35
40
  private isInitializedRef = false;
@@ -76,6 +81,7 @@ export class EventService extends BaseService implements IEventService {
76
81
  const newOrgId = selectedOrganisation?.id;
77
82
  const previousUserId = this.user?.id || null;
78
83
  const newUserId = user?.id || null;
84
+ const previousAppName = this.appName;
79
85
 
80
86
  // If user changed, clear previous user's event selection from storage
81
87
  if (previousUserId !== newUserId) {
@@ -87,6 +93,10 @@ export class EventService extends BaseService implements IEventService {
87
93
  this.selectedEvent = null;
88
94
  this.setSelectedEventId?.(null);
89
95
  }
96
+ // Reset initialization when user changes
97
+ this.resetInitialization();
98
+ this.isInitializedRef = false;
99
+ this.isFetchingRef = false;
90
100
  }
91
101
 
92
102
  this.supabaseClient = supabaseClient;
@@ -96,8 +106,31 @@ export class EventService extends BaseService implements IEventService {
96
106
  this.selectedOrganisation = selectedOrganisation;
97
107
  this.setSelectedEventId = setSelectedEventId;
98
108
 
99
- // If organisation changed (from null to value, or different org), reset initialization
100
- // This ensures events are re-fetched when organisation context becomes available
109
+ // Clear app config cache when app name changes
110
+ if (previousAppName !== appName) {
111
+ this.appConfig = null;
112
+ }
113
+
114
+ // Update super admin status when user changes
115
+ // This allows super admins to select events from any organisation
116
+ if (user?.id) {
117
+ try {
118
+ this.isSuperAdmin = await isSuperAdmin(user.id as UUID);
119
+ logger.debug('EventService', 'Updated super admin status', {
120
+ userId: user.id,
121
+ isSuperAdmin: this.isSuperAdmin
122
+ });
123
+ } catch (error) {
124
+ logger.warn('EventService', 'Failed to check super admin status', { error });
125
+ this.isSuperAdmin = false; // Default to false on error
126
+ }
127
+ } else {
128
+ this.isSuperAdmin = false;
129
+ }
130
+
131
+ // If organisation changed (from null to value, or different org, or value to null), reset initialization
132
+ // This ensures events are re-fetched when organisation context changes
133
+ // For event-required apps, selectedOrganisation will be null, so we need to reset when it changes from undefined/null to null
101
134
  if (previousOrgId !== newOrgId) {
102
135
  this.resetInitialization(); // Reset BaseService's isInitialized flag
103
136
  this.isInitializedRef = false;
@@ -138,20 +171,9 @@ export class EventService extends BaseService implements IEventService {
138
171
  // Event methods
139
172
  setSelectedEvent(event: Event | null): void {
140
173
  if (event) {
141
- // SECURITY: Validate event belongs to current organisation
142
- try {
143
- if (this.selectedOrganisation && event.organisation_id !== this.selectedOrganisation.id) {
144
- logger.error('EventService', 'Event organisation_id does not match selected organisation', {
145
- eventOrganisationId: event.organisation_id,
146
- selectedOrganisationId: this.selectedOrganisation.id,
147
- eventName: event.event_name
148
- });
149
- return;
150
- }
151
- } catch (error) {
152
- logger.error('EventService', 'Error during event validation:', error);
153
- }
154
-
174
+ // No validation needed: For event-required apps, org is derived from event
175
+ // For org-required apps, event is optional and doesn't need validation
176
+ // RLS policies handle security at the database level
155
177
  this.selectedEvent = event;
156
178
  this.setSelectedEventId?.(event.event_id);
157
179
  // Persist asynchronously (don't await to avoid blocking)
@@ -286,6 +308,12 @@ export class EventService extends BaseService implements IEventService {
286
308
  async initialize(): Promise<void> {
287
309
  // Only call super.initialize() which will call doInitialize() and fetchEvents()
288
310
  // Don't call fetchEvents() again here to avoid double-fetching
311
+ logger.debug('EventService', 'initialize() called', {
312
+ isInitializedRef: this.isInitializedRef,
313
+ hasUser: !!this.user,
314
+ hasSession: !!this.session,
315
+ appName: this.appName
316
+ });
289
317
  await super.initialize();
290
318
  }
291
319
 
@@ -294,13 +322,23 @@ export class EventService extends BaseService implements IEventService {
294
322
  }
295
323
 
296
324
  protected async doInitialize(): Promise<void> {
325
+ logger.debug('EventService', 'doInitialize() called', {
326
+ isInitializedRef: this.isInitializedRef,
327
+ isFetchingRef: this.isFetchingRef,
328
+ hasUser: !!this.user,
329
+ hasSession: !!this.session,
330
+ appName: this.appName
331
+ });
332
+
297
333
  // Skip if already initialized
298
334
  if (this.isInitializedRef) {
335
+ logger.debug('EventService', 'Skipping initialization - already initialized');
299
336
  return;
300
337
  }
301
338
 
302
339
  // Skip if already fetching
303
340
  if (this.isFetchingRef) {
341
+ logger.debug('EventService', 'Skipping initialization - already fetching');
304
342
  return;
305
343
  }
306
344
 
@@ -313,13 +351,25 @@ export class EventService extends BaseService implements IEventService {
313
351
  logger.warn('EventService', 'Failed to clean up old storage keys:', error);
314
352
  }
315
353
 
316
- // Skip if no user or organisation
317
- if (!this.user || !this.selectedOrganisation) {
354
+ // Skip if no user
355
+ // For event-required apps, selectedOrganisation may be null (org derived from event)
356
+ // For org-required apps, selectedOrganisation is required
357
+ if (!this.user) {
358
+ logger.debug('EventService', 'Skipping initialization - missing user');
318
359
  return;
319
360
  }
320
361
 
362
+ logger.debug('EventService', 'Initializing - fetching events', {
363
+ userId: this.user.id,
364
+ organisationId: this.selectedOrganisation?.id || 'derived-from-event',
365
+ appName: this.appName
366
+ });
367
+
321
368
  // Initial setup - fetch events on initialization
322
369
  await this.fetchEvents(false);
370
+
371
+ // Mark as initialized after successful fetch
372
+ this.isInitializedRef = true;
323
373
  }
324
374
 
325
375
  protected doCleanup(): void {
@@ -327,7 +377,15 @@ export class EventService extends BaseService implements IEventService {
327
377
  }
328
378
 
329
379
  private async fetchEvents(skipLoadPersisted: boolean = false): Promise<void> {
330
- if (!this.user || !this.session || !this.supabaseClient || !this.appName || !this.selectedOrganisation) {
380
+ // For event-required apps, selectedOrganisation may be null (org derived from event)
381
+ // For org-required apps, selectedOrganisation is required
382
+ if (!this.user || !this.session || !this.supabaseClient || !this.appName) {
383
+ logger.debug('EventService', 'Skipping fetchEvents - missing dependencies', {
384
+ hasUser: !!this.user,
385
+ hasSession: !!this.session,
386
+ hasSupabaseClient: !!this.supabaseClient,
387
+ appName: this.appName
388
+ });
331
389
  // Already false from initialization, just notify
332
390
  this.notify();
333
391
  return;
@@ -339,6 +397,7 @@ export class EventService extends BaseService implements IEventService {
339
397
 
340
398
  // Prevent multiple simultaneous fetches
341
399
  if (this.isFetchingRef) {
400
+ logger.debug('EventService', 'Skipping fetchEvents - already fetching');
342
401
  return;
343
402
  }
344
403
 
@@ -346,12 +405,97 @@ export class EventService extends BaseService implements IEventService {
346
405
  let isMounted = true;
347
406
 
348
407
  try {
408
+ // Load app config if not already cached (only once per app)
409
+ if (!this.appConfig && this.appName) {
410
+ try {
411
+ this.appConfig = await getAppConfigByName(this.appName);
412
+ } catch (configError) {
413
+ logger.warn('EventService', 'Failed to load app config, defaulting to event-required', {
414
+ error: configError
415
+ });
416
+ // Default to event-required for safety
417
+ this.appConfig = { requires_event: true };
418
+ }
419
+ }
420
+
421
+ // Determine organisationId for RPC call
422
+ // For event-required apps: org is derived from selectedEvent (if available), or null to get all accessible events
423
+ // For org-required apps: org comes from selectedOrganisation
424
+ // Super admins: pass null to see all events
425
+ let organisationIdForRpc: string | null = null;
426
+
427
+ // Check if user is super admin first
428
+ let userIsSuperAdmin = false;
429
+ try {
430
+ userIsSuperAdmin = await isSuperAdmin(this.user.id as UUID);
431
+ if (userIsSuperAdmin) {
432
+ // Super admin: Pass null to see all events across all organisations
433
+ organisationIdForRpc = null;
434
+ logger.debug('EventService', 'Super admin detected - fetching all events', {
435
+ userId: this.user.id
436
+ });
437
+ } else {
438
+ // Not super admin: determine org from context based on app type
439
+ if (this.selectedEvent) {
440
+ // If event is already selected, use its organisation
441
+ organisationIdForRpc = this.selectedEvent.organisation_id;
442
+ } else if (this.appConfig?.requires_event === true) {
443
+ // Event-required app with no selected event yet: pass null to get all accessible events
444
+ // The RPC will filter by event app roles, returning all events the user has access to
445
+ organisationIdForRpc = null;
446
+ logger.debug('EventService', 'Event-required app: fetching all accessible events (no event selected yet)', {
447
+ userId: this.user.id,
448
+ appName: this.appName
449
+ });
450
+ } else if (this.selectedOrganisation) {
451
+ // Org-required app: use selected organisation
452
+ organisationIdForRpc = this.selectedOrganisation.id;
453
+ } else {
454
+ // No context available - this shouldn't happen for authenticated users
455
+ logger.warn('EventService', 'No organisation context available for event fetch', {
456
+ hasSelectedEvent: !!this.selectedEvent,
457
+ hasSelectedOrganisation: !!this.selectedOrganisation,
458
+ appRequiresEvent: this.appConfig?.requires_event
459
+ });
460
+ organisationIdForRpc = null; // Will return empty list
461
+ }
462
+ }
463
+ } catch (superAdminCheckError) {
464
+ // If super admin check fails, fall back to organisation-scoped query
465
+ logger.warn('EventService', 'Failed to check super admin status, using organisation-scoped query', {
466
+ error: superAdminCheckError
467
+ });
468
+ // Fallback: use available context
469
+ if (this.selectedEvent) {
470
+ organisationIdForRpc = this.selectedEvent.organisation_id;
471
+ } else if (this.appConfig?.requires_event === true) {
472
+ // Event-required app: pass null to get all accessible events
473
+ organisationIdForRpc = null;
474
+ } else if (this.selectedOrganisation) {
475
+ organisationIdForRpc = this.selectedOrganisation.id;
476
+ }
477
+ }
478
+
479
+ logger.debug('EventService', 'Fetching events via RPC', {
480
+ userId: this.user.id,
481
+ organisationId: organisationIdForRpc,
482
+ appName: this.appName
483
+ });
484
+
349
485
  // Call the RPC function following the established pattern
350
- const { data, error: rpcError } = await this.supabaseClient.rpc('data_user_events_get', {
486
+ // For super admins, pass null for p_organisation_id to see all events
487
+ let { data, error: rpcError } = await this.supabaseClient.rpc('data_user_events_get', {
351
488
  p_user_id: this.user.id,
352
- p_organisation_id: this.selectedOrganisation.id,
489
+ p_organisation_id: organisationIdForRpc,
353
490
  p_app_name: this.appName
354
491
  });
492
+
493
+ logger.debug('EventService', 'RPC response received', {
494
+ hasData: !!data,
495
+ dataLength: Array.isArray(data) ? data.length : 'not array',
496
+ hasError: !!rpcError,
497
+ error: rpcError
498
+ });
355
499
 
356
500
  if (rpcError) {
357
501
  logger.error('EventService', 'RPC error fetching events:', rpcError);
@@ -27,6 +27,15 @@ interface OrganisationRoleRpcResponse {
27
27
  organisation_id: string;
28
28
  role: 'org_admin' | 'leader' | 'member' | 'supporter';
29
29
  status: 'active' | 'inactive' | 'suspended';
30
+ // Organisation fields from RPC
31
+ name?: string;
32
+ display_name?: string;
33
+ subscription_tier?: string;
34
+ settings?: unknown;
35
+ is_active?: boolean;
36
+ parent_id?: string;
37
+ organisation_created_at?: string;
38
+ organisation_updated_at?: string;
30
39
  [key: string]: unknown;
31
40
  }
32
41
 
@@ -71,6 +80,21 @@ export class OrganisationService extends BaseService implements IOrganisationSer
71
80
 
72
81
  // Additional methods for testing
73
82
  setSelectedOrganisation(organisation: Organisation | null): void {
83
+ // SECURITY: Validate organisation is in user's accessible organisations (only if orgs are loaded)
84
+ if (organisation && this._organisations.length > 0) {
85
+ const isValidOrg = this._organisations.some(org => org.id === organisation.id);
86
+ if (!isValidOrg) {
87
+ logger.warn('OrganisationService', 'Attempted to set invalid organisation - not in user\'s accessible organisations', {
88
+ organisationId: organisation.id,
89
+ organisationName: organisation.name,
90
+ accessibleOrgIds: this._organisations.map(o => o.id)
91
+ });
92
+ // Don't set invalid organisation - this prevents security issues
93
+ // If organisations haven't loaded yet, validation will happen in loadUserOrganisations()
94
+ return;
95
+ }
96
+ }
97
+
74
98
  this._selectedOrganisation = organisation;
75
99
  if (organisation) {
76
100
  localStorage.setItem('pace-core-selected-organisation', JSON.stringify(organisation));
@@ -343,159 +367,106 @@ export class OrganisationService extends BaseService implements IOrganisationSer
343
367
  this.notify();
344
368
 
345
369
  try {
346
- // Get user's organisation memberships using secure RPC function
347
- // Include all roles (org_admin, leader, member, supporter) - supporters can access PORTAL
348
- let memberships, membershipError;
370
+ // Get user's organisation roles directly from rbac_organisation_roles table
371
+ // This queries the source table directly instead of using the RPC which filters to match core_organisation_memberships view
372
+ // We still filter to active, non-revoked roles for the org selector
373
+ // The join includes organisation data, so we don't need a separate query that might be filtered by RLS
374
+ let memberships, membershipError, organisations: Organisation[] = [];
349
375
  try {
350
- // Add timeout and abort signal to prevent hanging RPC calls
351
- const timeoutPromise = new Promise((_, reject) => {
352
- const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
353
- abortSignal.addEventListener('abort', () => {
354
- clearTimeout(timeoutId);
355
- reject(new Error('Request aborted'));
356
- });
357
- });
358
-
359
- const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
360
- p_user_id: this.user.id,
361
- p_organisation_id: null
362
- });
363
-
364
- // Check if request was aborted before making the call
376
+ // Check if request was aborted before making query
365
377
  if (abortSignal.aborted) {
366
378
  throw new Error('Request aborted');
367
379
  }
380
+
381
+ const { data: rolesData, error: rolesError } = await this.supabaseClient
382
+ .from('rbac_organisation_roles')
383
+ .select(`
384
+ id,
385
+ user_id,
386
+ organisation_id,
387
+ role,
388
+ status,
389
+ granted_at,
390
+ granted_by,
391
+ revoked_at,
392
+ revoked_by,
393
+ notes,
394
+ created_at,
395
+ updated_at,
396
+ core_organisations!inner(
397
+ id,
398
+ name,
399
+ display_name,
400
+ subscription_tier,
401
+ settings,
402
+ is_active,
403
+ parent_id,
404
+ created_at,
405
+ updated_at
406
+ )
407
+ `)
408
+ .eq('user_id', this.user.id)
409
+ .eq('status', 'active')
410
+ .is('revoked_at', null);
368
411
 
369
- const result = await Promise.race([rpcPromise, timeoutPromise]) as { data: OrganisationRoleRpcResponse[] | null; error: Error | null };
412
+ if (rolesError) {
413
+ logger.error("OrganisationService", "Error loading organisation roles:", rolesError);
414
+ throw rolesError;
415
+ }
370
416
 
371
- // Filter to organisation members (org_admin, leader, member, supporter)
372
- // Supporters are included to allow PORTAL access
373
- // Map to branded types when filtering
374
- memberships = result.data?.filter((role) =>
375
- ['org_admin', 'leader', 'member', 'supporter'].includes(role.role)
376
- ).map((m) => ({
417
+ // Map to branded types and extract organisation data from the join
418
+ // The join already includes organisation data, so we don't need a separate query
419
+ memberships = rolesData?.map((m) => ({
377
420
  ...m,
378
421
  user_id: assertUserId(m.user_id),
379
422
  organisation_id: assertOrganisationId(m.organisation_id),
380
423
  })) || [];
381
- membershipError = result.error;
382
- } catch (queryError) {
383
- membershipError = queryError instanceof Error ? queryError : new Error(String(queryError));
384
- }
385
-
386
- if (membershipError) {
387
- logger.error("OrganisationService", "Error loading memberships:", membershipError);
388
424
 
389
- // If RPC fails with timeout, try direct database query as fallback
390
- if (membershipError.message?.includes('timeout')) {
391
- try {
392
- // Check if request was aborted before making fallback query
393
- if (abortSignal.aborted) {
394
- throw new Error('Request aborted');
395
- }
396
-
397
- const { data: fallbackData, error: fallbackError } = await this.supabaseClient
398
- .from('rbac_organisation_roles')
399
- .select(`
400
- id,
401
- user_id,
402
- organisation_id,
403
- role,
404
- status,
405
- granted_at,
406
- granted_by,
407
- revoked_at,
408
- revoked_by,
409
- notes,
410
- created_at,
411
- updated_at,
412
- organisations!inner(
413
- id,
414
- name,
415
- display_name,
416
- subscription_tier,
417
- settings,
418
- is_active,
419
- parent_id,
420
- created_at,
421
- updated_at
422
- )
423
- `)
424
- .eq('user_id', this.user.id)
425
- .eq('status', 'active')
426
- .is('revoked_at', null)
427
- .in('role', ['org_admin', 'leader', 'member', 'supporter']);
428
-
429
- if (fallbackError) {
430
- logger.error("OrganisationService", "Fallback query also failed:", fallbackError);
431
- throw membershipError; // Throw original error
432
- }
433
-
434
- // Map to branded types
435
- memberships = fallbackData?.map((m) => ({
436
- ...m,
437
- user_id: assertUserId(m.user_id),
438
- organisation_id: assertOrganisationId(m.organisation_id),
439
- })) || [];
440
- membershipError = null;
441
- } catch (fallbackErr) {
442
- logger.error("OrganisationService", "Fallback query failed:", fallbackErr);
443
- throw membershipError; // Throw original error
425
+ // Extract unique organisations from the join results
426
+ // Use a Map to deduplicate by organisation ID
427
+ // Supabase returns joined data nested under the relation name
428
+ const organisationsMap = new Map<string, Organisation>();
429
+ rolesData?.forEach((role: any) => {
430
+ // The join returns organisation data nested under 'core_organisations' key
431
+ const orgData = role.core_organisations;
432
+ if (orgData && role.organisation_id && !organisationsMap.has(role.organisation_id)) {
433
+ organisationsMap.set(role.organisation_id, {
434
+ id: orgData.id,
435
+ name: orgData.name,
436
+ display_name: orgData.display_name,
437
+ subscription_tier: orgData.subscription_tier,
438
+ settings: orgData.settings,
439
+ is_active: orgData.is_active,
440
+ parent_id: orgData.parent_id,
441
+ created_at: orgData.created_at,
442
+ updated_at: orgData.updated_at,
443
+ } as Organisation);
444
444
  }
445
+ });
446
+
447
+ organisations = Array.from(organisationsMap.values());
448
+
449
+ // Extract organisations from join results
450
+ } catch (queryError) {
451
+ // Extract error message properly from Supabase error objects
452
+ if (queryError instanceof Error) {
453
+ membershipError = queryError;
454
+ } else if (queryError && typeof queryError === 'object' && 'message' in queryError) {
455
+ membershipError = new Error(String((queryError as any).message));
445
456
  } else {
446
- throw membershipError;
457
+ membershipError = new Error(String(queryError));
447
458
  }
459
+ logger.error("OrganisationService", "Error loading organisation roles:", membershipError);
460
+ throw membershipError;
448
461
  }
449
462
 
450
463
  if (!memberships || memberships.length === 0) {
451
464
  throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
452
465
  }
453
466
 
454
- // Get organisation details for the memberships
455
- const organisationIds = memberships
456
- .map((m) => m.organisation_id)
457
- .filter((id: string) => {
458
- // Better validation to prevent empty string UUID errors
459
- if (!id || typeof id !== 'string') {
460
- logger.warn("OrganisationService", "Invalid organisation ID (not string):", id);
461
- return false;
462
- }
463
- const trimmedId = id.trim();
464
- if (trimmedId === '') {
465
- logger.warn("OrganisationService", "Empty organisation ID found");
466
- return false;
467
- }
468
- // Validate UUID format
469
- 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);
470
- if (!isValidUuid) {
471
- logger.warn("OrganisationService", "Invalid UUID format:", trimmedId);
472
- }
473
- return isValidUuid;
474
- });
475
-
476
- if (organisationIds.length === 0) {
477
- logger.warn("OrganisationService", "No valid organisation IDs found in memberships:", memberships);
478
- throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
479
- }
480
-
481
- // Check if request was aborted before making organisations query
482
- if (abortSignal.aborted) {
483
- throw new Error('Request aborted');
484
- }
485
-
486
- const { data: allOrganisations, error: orgError } = await this.supabaseClient
487
- .from('organisations')
488
- .select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
489
-
490
- if (orgError) {
491
- logger.error("OrganisationService", "Error loading organisations:", orgError);
492
- throw orgError;
467
+ if (!organisations || organisations.length === 0) {
468
+ throw new Error('No organisations found in role data') as OrganisationSecurityError;
493
469
  }
494
-
495
- // Filter manually on the client side
496
- const organisations = allOrganisations?.filter(org =>
497
- organisationIds.includes(org.id)
498
- ) || [];
499
470
 
500
471
  // Create a map of organisation_id to role from the memberships data
501
472
  const roleMap = new Map<string, string>();
@@ -507,6 +478,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
507
478
  const orgs = organisations as Organisation[];
508
479
  const activeOrgs = orgs.filter(org => org.is_active);
509
480
 
481
+ // Filter to active organisations only
482
+
510
483
  if (activeOrgs.length === 0) {
511
484
  throw new Error('User has no access to active organisations') as OrganisationSecurityError;
512
485
  }
@@ -534,16 +507,13 @@ export class OrganisationService extends BaseService implements IOrganisationSer
534
507
  initialOrg = validPersistedOrg;
535
508
  selectionMethod = 'persisted';
536
509
  } else {
537
- logger.warn("OrganisationService", "Persisted organisation not found in active orgs, clearing cache");
538
510
  localStorage.removeItem('pace-core-selected-organisation');
539
511
  }
540
512
  } else {
541
- logger.warn("OrganisationService", "Invalid persisted organisation ID, clearing cache");
542
513
  localStorage.removeItem('pace-core-selected-organisation');
543
514
  }
544
515
  }
545
516
  } catch (storageError) {
546
- logger.warn("OrganisationService", "Failed to restore persisted organisation:", storageError);
547
517
  // Clear potentially corrupted cache
548
518
  localStorage.removeItem('pace-core-selected-organisation');
549
519
  }
@@ -570,6 +540,16 @@ export class OrganisationService extends BaseService implements IOrganisationSer
570
540
  throw new Error('No valid organisation found for user') as OrganisationSecurityError;
571
541
  }
572
542
 
543
+ // SECURITY: Validate current selected organisation is still valid (in case it was set before orgs loaded)
544
+ const currentSelectedOrg = this._selectedOrganisation;
545
+ if (currentSelectedOrg && !activeOrgs.some(org => org.id === currentSelectedOrg.id)) {
546
+ logger.warn('OrganisationService', 'Current selected organisation is no longer valid, resetting', {
547
+ invalidOrgId: currentSelectedOrg.id,
548
+ validOrgIds: activeOrgs.map(o => o.id)
549
+ });
550
+ this._selectedOrganisation = null;
551
+ }
552
+
573
553
  this._selectedOrganisation = initialOrg;
574
554
 
575
555
  // Persist selection
@@ -583,14 +563,22 @@ export class OrganisationService extends BaseService implements IOrganisationSer
583
563
  this.hasFailedRef = false;
584
564
 
585
565
  } catch (err) {
586
- logger.error("OrganisationService", "Failed to load organisations:", err);
587
- this._error = err as Error;
566
+ const error = err as Error;
567
+ // "User has no access to active organisations" is a valid state for users without orgs (e.g., profile pages)
568
+ // Only log actual errors, not expected states
569
+ if (error.message !== 'User has no access to active organisations') {
570
+ logger.error("OrganisationService", "Failed to load organisations:", err);
571
+ }
572
+ this._error = error;
588
573
  // Increment retry count on error
589
574
  this.retryCount = this.retryCount + 1;
590
575
  // Set failed flag to prevent further attempts
591
576
  this.hasFailedRef = true;
592
577
  // Clear all cached data on error to prevent corruption
593
578
  this.clearAllCachedData();
579
+ // Mark context as ready even on error - this allows the app to proceed
580
+ // The app can check hasValidOrganisationContext() to determine if org context is available
581
+ this._isContextReady = true;
594
582
  } finally {
595
583
  // Always cleanup refs and abort controller
596
584
  this.isLoadingRef = false;