@jmruthers/pace-core 0.5.189 → 0.5.190

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 (420) 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-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
  4. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-C4uxosp6.d.ts} +83 -24
  5. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  6. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
  7. package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
  8. package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
  9. package/dist/chunk-4QYC5L4K.js.map +1 -0
  10. package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
  11. package/dist/chunk-73HSNNOQ.js.map +1 -0
  12. package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
  13. package/dist/chunk-DZWK57KZ.js.map +1 -0
  14. package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
  15. package/dist/chunk-HQVPB5MZ.js.map +1 -0
  16. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  17. package/dist/chunk-HW3OVDUF.js.map +1 -0
  18. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  19. package/dist/chunk-I7PSE6JW.js.map +1 -0
  20. package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
  21. package/dist/chunk-J2XXC7R5.js.map +1 -0
  22. package/dist/{chunk-UCQSRW7Z.js → chunk-NIU6J6OX.js} +425 -378
  23. package/dist/chunk-NIU6J6OX.js.map +1 -0
  24. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  25. package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
  26. package/dist/chunk-RUYZKXOD.js.map +1 -0
  27. package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
  28. package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
  29. package/dist/{chunk-MX64ZF6I.js → chunk-STYK4OH2.js} +11 -11
  30. package/dist/chunk-STYK4OH2.js.map +1 -0
  31. package/dist/{chunk-YGPFYGA6.js → chunk-VVBAW5A5.js} +822 -498
  32. package/dist/chunk-VVBAW5A5.js.map +1 -0
  33. package/dist/chunk-Y4BUBBHD.js +614 -0
  34. package/dist/chunk-Y4BUBBHD.js.map +1 -0
  35. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  36. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  37. package/dist/components.d.ts +3 -4
  38. package/dist/components.js +19 -19
  39. package/dist/components.js.map +1 -1
  40. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  41. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  42. package/dist/hooks.d.ts +10 -5
  43. package/dist/hooks.js +14 -8
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.d.ts +13 -11
  46. package/dist/index.js +79 -69
  47. package/dist/index.js.map +1 -1
  48. package/dist/providers.d.ts +3 -3
  49. package/dist/providers.js +3 -1
  50. package/dist/rbac/index.d.ts +76 -12
  51. package/dist/rbac/index.js +12 -9
  52. package/dist/types.d.ts +1 -1
  53. package/dist/types.js +1 -1
  54. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
  55. package/dist/utils.js +16 -16
  56. package/docs/README.md +2 -2
  57. package/docs/api/classes/ColumnFactory.md +1 -1
  58. package/docs/api/classes/ErrorBoundary.md +1 -1
  59. package/docs/api/classes/InvalidScopeError.md +2 -2
  60. package/docs/api/classes/Logger.md +1 -1
  61. package/docs/api/classes/MissingUserContextError.md +2 -2
  62. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  63. package/docs/api/classes/PermissionDeniedError.md +1 -1
  64. package/docs/api/classes/RBACAuditManager.md +1 -1
  65. package/docs/api/classes/RBACCache.md +1 -1
  66. package/docs/api/classes/RBACEngine.md +4 -4
  67. package/docs/api/classes/RBACError.md +1 -1
  68. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  69. package/docs/api/classes/SecureSupabaseClient.md +21 -16
  70. package/docs/api/classes/StorageUtils.md +7 -4
  71. package/docs/api/enums/FileCategory.md +1 -1
  72. package/docs/api/enums/LogLevel.md +1 -1
  73. package/docs/api/enums/RBACErrorCode.md +1 -1
  74. package/docs/api/enums/RPCFunction.md +1 -1
  75. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  76. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  77. package/docs/api/interfaces/AggregateConfig.md +1 -1
  78. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  79. package/docs/api/interfaces/AvatarProps.md +1 -1
  80. package/docs/api/interfaces/BadgeProps.md +1 -1
  81. package/docs/api/interfaces/ButtonProps.md +1 -1
  82. package/docs/api/interfaces/CalendarProps.md +20 -6
  83. package/docs/api/interfaces/CardProps.md +1 -1
  84. package/docs/api/interfaces/ColorPalette.md +1 -1
  85. package/docs/api/interfaces/ColorShade.md +1 -1
  86. package/docs/api/interfaces/ComplianceResult.md +1 -1
  87. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  88. package/docs/api/interfaces/DataRecord.md +1 -1
  89. package/docs/api/interfaces/DataTableAction.md +1 -1
  90. package/docs/api/interfaces/DataTableColumn.md +1 -1
  91. package/docs/api/interfaces/DataTableProps.md +1 -1
  92. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  93. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  94. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  95. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  96. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  97. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  98. package/docs/api/interfaces/ExportColumn.md +1 -1
  99. package/docs/api/interfaces/ExportOptions.md +1 -1
  100. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  101. package/docs/api/interfaces/FileMetadata.md +1 -1
  102. package/docs/api/interfaces/FileReference.md +2 -2
  103. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  104. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  105. package/docs/api/interfaces/FileUploadProps.md +30 -19
  106. package/docs/api/interfaces/FooterProps.md +1 -1
  107. package/docs/api/interfaces/FormFieldProps.md +1 -1
  108. package/docs/api/interfaces/FormProps.md +1 -1
  109. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  110. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  111. package/docs/api/interfaces/InputProps.md +1 -1
  112. package/docs/api/interfaces/LabelProps.md +1 -1
  113. package/docs/api/interfaces/LoggerConfig.md +1 -1
  114. package/docs/api/interfaces/LoginFormProps.md +1 -1
  115. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  116. package/docs/api/interfaces/NavigationContextType.md +9 -9
  117. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  118. package/docs/api/interfaces/NavigationItem.md +1 -1
  119. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  120. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  121. package/docs/api/interfaces/Organisation.md +1 -1
  122. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  123. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  124. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  125. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  126. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  127. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  128. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  129. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  130. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  131. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  132. package/docs/api/interfaces/PaletteData.md +1 -1
  133. package/docs/api/interfaces/ParsedAddress.md +1 -1
  134. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  135. package/docs/api/interfaces/ProgressProps.md +3 -11
  136. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  138. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  139. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  140. package/docs/api/interfaces/QuickFix.md +1 -1
  141. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  142. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  143. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  144. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  145. package/docs/api/interfaces/RBACConfig.md +1 -1
  146. package/docs/api/interfaces/RBACContext.md +1 -1
  147. package/docs/api/interfaces/RBACLogger.md +1 -1
  148. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  149. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  151. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  152. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  153. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  154. package/docs/api/interfaces/RBACResult.md +1 -1
  155. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  156. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  157. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  158. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  161. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  162. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  163. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  164. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  165. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  166. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  167. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  168. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  169. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  170. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  171. package/docs/api/interfaces/RouteConfig.md +10 -10
  172. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  173. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  174. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  175. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  176. package/docs/api/interfaces/SetupIssue.md +1 -1
  177. package/docs/api/interfaces/StorageConfig.md +4 -4
  178. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  179. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  180. package/docs/api/interfaces/StorageListOptions.md +22 -9
  181. package/docs/api/interfaces/StorageListResult.md +4 -4
  182. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  183. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  184. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  185. package/docs/api/interfaces/StyleImport.md +1 -1
  186. package/docs/api/interfaces/SwitchProps.md +1 -1
  187. package/docs/api/interfaces/TabsContentProps.md +1 -1
  188. package/docs/api/interfaces/TabsListProps.md +1 -1
  189. package/docs/api/interfaces/TabsProps.md +1 -1
  190. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  191. package/docs/api/interfaces/TextareaProps.md +1 -1
  192. package/docs/api/interfaces/ToastActionElement.md +1 -1
  193. package/docs/api/interfaces/ToastProps.md +1 -1
  194. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  195. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  196. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  197. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  198. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  199. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  201. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  202. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  203. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  205. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  207. package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
  208. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  209. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  210. package/docs/api/interfaces/UserEventAccess.md +11 -11
  211. package/docs/api/interfaces/UserMenuProps.md +1 -1
  212. package/docs/api/interfaces/UserProfile.md +1 -1
  213. package/docs/api/modules.md +151 -92
  214. package/docs/api-reference/components.md +15 -7
  215. package/docs/api-reference/providers.md +2 -2
  216. package/docs/api-reference/rpc-functions.md +1 -0
  217. package/docs/best-practices/README.md +1 -1
  218. package/docs/best-practices/deployment.md +8 -8
  219. package/docs/getting-started/examples/README.md +2 -2
  220. package/docs/getting-started/installation-guide.md +4 -4
  221. package/docs/getting-started/quick-start.md +3 -3
  222. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  223. package/docs/rbac/compliance/compliance-guide.md +2 -2
  224. package/docs/rbac/event-based-apps.md +2 -2
  225. package/docs/rbac/getting-started.md +2 -2
  226. package/docs/rbac/quick-start.md +2 -2
  227. package/docs/security/README.md +4 -4
  228. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  229. package/docs/troubleshooting/README.md +2 -2
  230. package/docs/troubleshooting/migration.md +3 -3
  231. package/package.json +1 -3
  232. package/scripts/check-pace-core-compliance.cjs +1 -1
  233. package/scripts/check-pace-core-compliance.js +1 -1
  234. package/src/__tests__/fixtures/supabase.ts +301 -0
  235. package/src/__tests__/public-recipe-view.test.ts +9 -9
  236. package/src/__tests__/rls-policies.test.ts +197 -61
  237. package/src/components/AddressField/AddressField.test.tsx +42 -0
  238. package/src/components/AddressField/AddressField.tsx +71 -60
  239. package/src/components/AddressField/README.md +1 -0
  240. package/src/components/Alert/Alert.test.tsx +50 -10
  241. package/src/components/Alert/Alert.tsx +5 -3
  242. package/src/components/Avatar/Avatar.test.tsx +95 -43
  243. package/src/components/Avatar/Avatar.tsx +16 -16
  244. package/src/components/Button/Button.test.tsx +2 -1
  245. package/src/components/Button/Button.tsx +3 -3
  246. package/src/components/Calendar/Calendar.test.tsx +53 -37
  247. package/src/components/Calendar/Calendar.tsx +409 -82
  248. package/src/components/Card/Card.test.tsx +7 -4
  249. package/src/components/Card/Card.tsx +3 -6
  250. package/src/components/Checkbox/Checkbox.tsx +2 -2
  251. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  252. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  253. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  254. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  255. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  256. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  257. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  258. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  259. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  260. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  261. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  262. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  263. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  264. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  265. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  266. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  267. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  268. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  269. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  270. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  271. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  272. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  273. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  274. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  275. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  276. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  277. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  278. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  279. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  280. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  281. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  282. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  283. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  284. package/src/components/Dialog/Dialog.tsx +2 -2
  285. package/src/components/EventSelector/EventSelector.tsx +7 -7
  286. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  287. package/src/components/FileUpload/FileUpload.tsx +7 -4
  288. package/src/components/Header/Header.test.tsx +28 -0
  289. package/src/components/Header/Header.tsx +22 -9
  290. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  291. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  292. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  293. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  294. package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
  295. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  296. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  297. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  298. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  299. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  300. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  301. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  302. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  303. package/src/components/Progress/Progress.test.tsx +18 -19
  304. package/src/components/Progress/Progress.tsx +31 -32
  305. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  306. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  307. package/src/components/Select/Select.tsx +5 -5
  308. package/src/components/Switch/Switch.test.tsx +2 -1
  309. package/src/components/Switch/Switch.tsx +1 -1
  310. package/src/components/Toast/Toast.tsx +1 -1
  311. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  312. package/src/components/UserMenu/UserMenu.tsx +3 -3
  313. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  314. package/src/eslint-rules/pace-core-compliance.js +0 -2
  315. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  316. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  317. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  318. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  319. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  320. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  321. package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
  322. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  323. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
  324. package/src/hooks/index.ts +1 -1
  325. package/src/hooks/public/usePublicEvent.ts +2 -2
  326. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  327. package/src/hooks/useAppConfig.ts +24 -5
  328. package/src/hooks/useFileDisplay.ts +297 -34
  329. package/src/hooks/useFileReference.ts +56 -11
  330. package/src/hooks/useFileUrl.ts +1 -1
  331. package/src/hooks/useInactivityTracker.ts +16 -7
  332. package/src/hooks/usePermissionCache.test.ts +85 -8
  333. package/src/hooks/useQueryCache.ts +21 -0
  334. package/src/hooks/useSecureDataAccess.test.ts +80 -35
  335. package/src/hooks/useSecureDataAccess.ts +80 -37
  336. package/src/providers/services/EventServiceProvider.tsx +37 -17
  337. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  338. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  339. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  340. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  341. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  342. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  343. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  344. package/src/rbac/api.ts +240 -36
  345. package/src/rbac/cache-invalidation.ts +21 -7
  346. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  347. package/src/rbac/components/NavigationGuard.tsx +23 -63
  348. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  349. package/src/rbac/components/NavigationProvider.tsx +13 -11
  350. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  351. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  352. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  353. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  354. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  355. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  356. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  357. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  358. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  359. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  360. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  361. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  362. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  363. package/src/rbac/engine.ts +4 -2
  364. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  365. package/src/rbac/hooks/index.ts +3 -0
  366. package/src/rbac/hooks/useCan.test.ts +101 -53
  367. package/src/rbac/hooks/usePermissions.ts +108 -41
  368. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  369. package/src/rbac/hooks/useRBAC.ts +83 -40
  370. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  371. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  372. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  373. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  374. package/src/rbac/request-deduplication.ts +1 -1
  375. package/src/rbac/secureClient.ts +72 -12
  376. package/src/rbac/security.ts +29 -23
  377. package/src/rbac/types.ts +10 -0
  378. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  379. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  380. package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
  381. package/src/rbac/utils/contextValidator.ts +288 -0
  382. package/src/rbac/utils/eventContext.ts +48 -2
  383. package/src/services/EventService.ts +165 -21
  384. package/src/services/OrganisationService.ts +37 -2
  385. package/src/services/__tests__/EventService.test.ts +26 -21
  386. package/src/types/file-reference.ts +13 -10
  387. package/src/utils/app/appNameResolver.test.ts +346 -73
  388. package/src/utils/context/superAdminOverride.ts +58 -0
  389. package/src/utils/file-reference/index.ts +61 -33
  390. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  391. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  392. package/src/utils/storage/helpers.test.ts +1 -1
  393. package/src/utils/storage/helpers.ts +38 -19
  394. package/src/utils/storage/types.ts +15 -8
  395. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  396. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  397. package/src/vite-env.d.ts +2 -2
  398. package/dist/chunk-3GOZZZYH.js.map +0 -1
  399. package/dist/chunk-DDM4CCYT.js.map +0 -1
  400. package/dist/chunk-E7UAOUMY.js +0 -75
  401. package/dist/chunk-E7UAOUMY.js.map +0 -1
  402. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  403. package/dist/chunk-HEHYGYOX.js.map +0 -1
  404. package/dist/chunk-IM4QE42D.js.map +0 -1
  405. package/dist/chunk-MX64ZF6I.js.map +0 -1
  406. package/dist/chunk-SAUPYVLF.js.map +0 -1
  407. package/dist/chunk-THRPYOFK.js.map +0 -1
  408. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  409. package/dist/chunk-VGZZXKBR.js.map +0 -1
  410. package/dist/chunk-YGPFYGA6.js.map +0 -1
  411. package/dist/chunk-YHCN776L.js.map +0 -1
  412. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  413. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  414. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
  415. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  416. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
  417. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
  418. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
  419. /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
  420. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -69,7 +69,7 @@ describe('usePublicEvent', () => {
69
69
  Object.defineProperty(import.meta, 'env', {
70
70
  value: {
71
71
  VITE_SUPABASE_URL: 'https://test.supabase.co',
72
- VITE_SUPABASE_ANON_KEY: 'test-anon-key'
72
+ VITE_SUPABASE_PUBLISHABLE_KEY: 'test-publishable-key'
73
73
  },
74
74
  writable: true
75
75
  });
@@ -577,5 +577,32 @@ describe('usePublicEvent', () => {
577
577
  expect(result.current.event).toBe(null);
578
578
  expect(result.current.error).toEqual(new Error('Event not found'));
579
579
  });
580
+
581
+ it('warns when Supabase environment variables are missing', async () => {
582
+ vi.mocked(usePublicPageContext).mockReturnValue({
583
+ environment: { supabaseUrl: null, supabaseKey: null }
584
+ } as any);
585
+
586
+ Object.defineProperty(import.meta, 'env', {
587
+ value: {},
588
+ writable: true
589
+ });
590
+
591
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
592
+ const { createClient } = await import('@supabase/supabase-js');
593
+ vi.mocked(createClient).mockClear();
594
+
595
+ const { result } = renderHook(() => usePublicEvent('test-event'));
596
+
597
+ await waitFor(() => {
598
+ expect(result.current.isLoading).toBe(false);
599
+ }, { interval: 10 });
600
+
601
+ expect(result.current.error).toBeInstanceOf(Error);
602
+ expect(result.current.error?.message).toContain('Invalid event code or Supabase client not available');
603
+ expect(vi.mocked(createClient)).not.toHaveBeenCalled();
604
+
605
+ warnSpy.mockRestore();
606
+ });
580
607
  });
581
608
  });
@@ -0,0 +1,144 @@
1
+ import { renderHook, act } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { SupabaseClient } from '@supabase/supabase-js';
4
+ import { useQueryCache, queryCacheHelpers } from '../useQueryCache';
5
+
6
+ vi.mock('../../utils/core/logger', () => ({
7
+ createLogger: () => ({
8
+ debug: vi.fn(),
9
+ error: vi.fn(),
10
+ }),
11
+ }));
12
+
13
+ describe('useQueryCache', () => {
14
+ beforeEach(() => {
15
+ vi.useFakeTimers();
16
+ vi.setSystemTime(0);
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.useRealTimers();
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ const supabase = {} as SupabaseClient<any>;
25
+
26
+ it('caches query results and returns cached data', async () => {
27
+ const fetchFn = vi.fn().mockResolvedValue({ id: 1 });
28
+ const { result } = renderHook(() => useQueryCache(supabase));
29
+
30
+ const first = await result.current.getCachedQuery('table', 'id', '1', fetchFn, { ttl: 5 });
31
+ const second = await result.current.getCachedQuery('table', 'id', '1', fetchFn, { ttl: 5 });
32
+
33
+ expect(first).toEqual({ id: 1 });
34
+ expect(second).toEqual(first);
35
+ expect(fetchFn).toHaveBeenCalledTimes(1);
36
+
37
+ act(() => {
38
+ result.current.clearCache();
39
+ });
40
+ });
41
+
42
+ it('invalidates cache entries explicitly', async () => {
43
+ const fetchFn = vi.fn().mockResolvedValue({ id: 2 });
44
+ const { result } = renderHook(() => useQueryCache());
45
+
46
+ await result.current.getCachedQuery('table', 'id', '2', fetchFn);
47
+ act(() => {
48
+ result.current.invalidateQuery('table', 'id', '2');
49
+ });
50
+ await result.current.getCachedQuery('table', 'id', '2', fetchFn);
51
+
52
+ expect(fetchFn).toHaveBeenCalledTimes(2);
53
+
54
+ act(() => {
55
+ result.current.clearCache();
56
+ });
57
+ });
58
+
59
+ it('expires cache entries after ttl', async () => {
60
+ const fetchFn = vi.fn().mockResolvedValue('value');
61
+ const { result } = renderHook(() => useQueryCache());
62
+
63
+ await result.current.getCachedQuery('table', 'key', 'val', fetchFn, { ttl: 1 });
64
+
65
+ vi.setSystemTime(2000); // advance past ttl
66
+ await result.current.getCachedQuery('table', 'key', 'val', fetchFn, { ttl: 1 });
67
+
68
+ expect(fetchFn).toHaveBeenCalledTimes(2);
69
+
70
+ act(() => {
71
+ result.current.clearCache();
72
+ });
73
+ });
74
+
75
+ it('deduplicates in-flight requests', async () => {
76
+ let resolveFn: (value: string) => void;
77
+ const fetchPromise = new Promise<string>(resolve => {
78
+ resolveFn = resolve;
79
+ });
80
+ const fetchFn = vi.fn(() => fetchPromise);
81
+ const { result } = renderHook(() => useQueryCache());
82
+
83
+ const firstPromise = result.current.getCachedQuery('table', 'id', '3', fetchFn);
84
+ const secondPromise = result.current.getCachedQuery('table', 'id', '3', fetchFn);
85
+
86
+ expect(fetchFn).toHaveBeenCalledTimes(1);
87
+
88
+ resolveFn!('done');
89
+
90
+ await expect(firstPromise).resolves.toBe('done');
91
+ await expect(secondPromise).resolves.toBe('done');
92
+
93
+ act(() => {
94
+ result.current.clearCache();
95
+ });
96
+ });
97
+
98
+ it('bypasses cache when disabled', async () => {
99
+ const fetchFn = vi.fn().mockResolvedValue('fresh');
100
+ const { result } = renderHook(() => useQueryCache());
101
+
102
+ await result.current.getCachedQuery('table', 'id', '4', fetchFn, { enabled: false });
103
+ await result.current.getCachedQuery('table', 'id', '4', fetchFn, { enabled: false });
104
+
105
+ expect(fetchFn).toHaveBeenCalledTimes(2);
106
+ });
107
+ });
108
+
109
+ describe('queryCacheHelpers', () => {
110
+ beforeEach(() => {
111
+ vi.useFakeTimers();
112
+ vi.setSystemTime(0);
113
+ });
114
+
115
+ afterEach(() => {
116
+ vi.useRealTimers();
117
+ vi.clearAllMocks();
118
+ });
119
+
120
+ it('caches helper queries and reuses promises for in-flight requests', async () => {
121
+ let resolveFn: (value: string) => void;
122
+ const promise = new Promise<string>(resolve => {
123
+ resolveFn = resolve;
124
+ });
125
+ const fetchFn = vi.fn(() => promise);
126
+ const supabase = {} as SupabaseClient<any>;
127
+
128
+ const firstPromise = queryCacheHelpers.pacePersonByUserId(supabase, 'user', fetchFn);
129
+ const secondPromise = queryCacheHelpers.pacePersonByUserId(supabase, 'user', fetchFn);
130
+
131
+ expect(fetchFn).toHaveBeenCalledTimes(1);
132
+
133
+ resolveFn!('person');
134
+
135
+ await expect(firstPromise).resolves.toBe('person');
136
+ await expect(secondPromise).resolves.toBe('person');
137
+
138
+ // Subsequent call should return cached data immediately
139
+ const cached = await queryCacheHelpers.pacePersonByUserId(supabase, 'user', fetchFn);
140
+ expect(cached).toBe('person');
141
+ expect(fetchFn).toHaveBeenCalledTimes(1);
142
+ });
143
+ });
144
+
@@ -4,6 +4,8 @@ import { useSecureDataAccess } from '../useSecureDataAccess';
4
4
  import { useUnifiedAuth } from '../../providers';
5
5
  import { useOrganisations } from '../../hooks/useOrganisations';
6
6
  import { testDataGenerators } from '../../__tests__/helpers/test-utils';
7
+ import { useResolvedScope } from '../../rbac/hooks/useResolvedScope';
8
+ import { useSuperAdminBypass } from '../../rbac/hooks/useSuperAdminBypass';
7
9
 
8
10
  // Mock dependencies
9
11
  vi.mock('../../providers', () => ({
@@ -22,10 +24,20 @@ vi.mock('../../utils/context/organisationContext', () => ({
22
24
  setOrganisationContext: vi.fn().mockResolvedValue(undefined),
23
25
  }));
24
26
 
27
+ vi.mock('../../rbac/hooks/useResolvedScope', () => ({
28
+ useResolvedScope: vi.fn(),
29
+ }));
30
+
31
+ vi.mock('../../rbac/hooks/useSuperAdminBypass', () => ({
32
+ useSuperAdminBypass: vi.fn(),
33
+ }));
34
+
25
35
  const mockUseUnifiedAuth = {
26
36
  user: null,
27
37
  session: null,
28
- supabase: null
38
+ supabase: null,
39
+ selectedOrganisation: null,
40
+ selectedEvent: null,
29
41
  };
30
42
 
31
43
  const mockUseOrganisations = vi.fn(() => ({
@@ -54,7 +66,8 @@ const createAuthenticatedContext = (supabase = { auth: {} }, org = { id: 'org-12
54
66
  ...mockUseUnifiedAuth,
55
67
  user: mockUser,
56
68
  session: mockSession,
57
- supabase
69
+ supabase,
70
+ selectedOrganisation: org,
58
71
  } as any);
59
72
 
60
73
  // Create a fresh mock with ensureOrganisationContext that returns the org
@@ -76,6 +89,17 @@ const createAuthenticatedContext = (supabase = { auth: {} }, org = { id: 'org-12
76
89
  };
77
90
 
78
91
  vi.mocked(useOrganisations).mockReturnValue(mockOrgs as any);
92
+
93
+ // Mock resolved scope with organisationId
94
+ vi.mocked(useResolvedScope).mockReturnValue({
95
+ resolvedScope: {
96
+ organisationId: org.id,
97
+ eventId: undefined,
98
+ appId: undefined,
99
+ },
100
+ isLoading: false,
101
+ error: null,
102
+ });
79
103
  };
80
104
 
81
105
  describe('useSecureDataAccess', () => {
@@ -84,6 +108,16 @@ describe('useSecureDataAccess', () => {
84
108
  vi.mocked(useUnifiedAuth).mockReturnValue(mockUseUnifiedAuth as any);
85
109
  // Set up useOrganisations to return the default mock (which throws for org context)
86
110
  vi.mocked(useOrganisations).mockReturnValue(mockUseOrganisations() as any);
111
+ // Default mock for useResolvedScope - returns null scope (no context)
112
+ vi.mocked(useResolvedScope).mockReturnValue({
113
+ resolvedScope: null,
114
+ isLoading: false,
115
+ error: null,
116
+ });
117
+ // Default mock for useSuperAdminBypass - not super admin
118
+ vi.mocked(useSuperAdminBypass).mockReturnValue({
119
+ isSuperAdmin: false,
120
+ });
87
121
  });
88
122
 
89
123
  describe('validateContext', () => {
@@ -129,21 +163,23 @@ describe('useSecureDataAccess', () => {
129
163
  user: mockUser,
130
164
  session: mockSession,
131
165
  supabase: mockSupabase,
166
+ selectedOrganisation: { id: 'org-123' },
132
167
  } as any);
133
168
 
134
- const mockOrg = { id: 'org-123' };
135
- const ensureOrgContextMock = vi.fn(() => mockOrg);
136
-
137
- const mockOrgs = {
138
- ...mockUseOrganisations(),
139
- ensureOrganisationContext: ensureOrgContextMock,
140
- };
141
- vi.mocked(useOrganisations).mockReturnValue(mockOrgs as any);
169
+ // Mock useResolvedScope to return resolved scope with organisationId
170
+ vi.mocked(useResolvedScope).mockReturnValue({
171
+ resolvedScope: {
172
+ organisationId: 'org-123',
173
+ eventId: undefined,
174
+ appId: undefined,
175
+ },
176
+ isLoading: false,
177
+ error: null,
178
+ });
142
179
 
143
180
  const { result } = renderHook(() => useSecureDataAccess());
144
181
 
145
182
  expect(() => result.current.validateContext()).not.toThrow();
146
- expect(ensureOrgContextMock).toHaveBeenCalled();
147
183
  });
148
184
  });
149
185
 
@@ -158,13 +194,19 @@ describe('useSecureDataAccess', () => {
158
194
  user: mockUser,
159
195
  session: mockSession,
160
196
  supabase: mockSupabase,
197
+ selectedOrganisation: { id: 'org-123' },
161
198
  } as any);
162
199
 
163
- const mockOrgs = {
164
- ...mockUseOrganisations(),
165
- ensureOrganisationContext: vi.fn().mockReturnValue({ id: 'org-123' }),
166
- };
167
- vi.mocked(useOrganisations).mockReturnValue(mockOrgs as any);
200
+ // Mock resolved scope with organisationId
201
+ vi.mocked(useResolvedScope).mockReturnValue({
202
+ resolvedScope: {
203
+ organisationId: 'org-123',
204
+ eventId: undefined,
205
+ appId: undefined,
206
+ },
207
+ isLoading: false,
208
+ error: null,
209
+ });
168
210
 
169
211
  const { result } = renderHook(() => useSecureDataAccess());
170
212
 
@@ -50,7 +50,7 @@ export { useComponentPerformance } from './useComponentPerformance';
50
50
  export { useAppConfig } from './useAppConfig';
51
51
  export type { UseAppConfigReturn } from './useAppConfig';
52
52
  export { usePerformanceMonitor } from './usePerformanceMonitor';
53
- export { useQueryCache, queryCacheHelpers } from './useQueryCache';
53
+ export { useQueryCache, queryCacheHelpers, cleanupQueryCache } from './useQueryCache';
54
54
  export type { UseQueryCacheReturn, UseQueryCacheOptions } from './useQueryCache';
55
55
 
56
56
  // DataTable performance hook
@@ -120,7 +120,7 @@ export function usePublicEvent(
120
120
  // Fallback to direct environment variable access if not in PublicPageProvider
121
121
  environment = {
122
122
  supabaseUrl: (import.meta as any).env?.VITE_SUPABASE_URL || (import.meta as any).env?.NEXT_PUBLIC_SUPABASE_URL || null,
123
- supabaseKey: (import.meta as any).env?.VITE_SUPABASE_ANON_KEY || (import.meta as any).env?.NEXT_PUBLIC_SUPABASE_ANON_KEY || null
123
+ supabaseKey: (import.meta as any).env?.VITE_SUPABASE_PUBLISHABLE_KEY || (import.meta as any).env?.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || null
124
124
  };
125
125
  }
126
126
 
@@ -129,7 +129,7 @@ export function usePublicEvent(
129
129
  if (typeof window === 'undefined') return null;
130
130
 
131
131
  if (!environment.supabaseUrl || !environment.supabaseKey) {
132
- logger.warn('usePublicEvent', 'Missing Supabase environment variables. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment. Use publishable key if anon key is disabled.');
132
+ logger.warn('usePublicEvent', 'Missing Supabase environment variables. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY are set in your environment.');
133
133
  return null;
134
134
  }
135
135
 
@@ -109,7 +109,7 @@ export function usePublicFileDisplay(
109
109
  const [error, setError] = useState<Error | null>(null);
110
110
 
111
111
  const fetchFiles = useCallback(async (): Promise<void> => {
112
- if (!table_name || !record_id || !organisation_id || !supabase) {
112
+ if (!table_name || !record_id || !supabase) {
113
113
  setFileUrl(null);
114
114
  setFileReference(null);
115
115
  setFileReferences([]);
@@ -119,14 +119,17 @@ export function usePublicFileDisplay(
119
119
  return;
120
120
  }
121
121
 
122
- // Validate UUID format for organisationId to prevent database errors
123
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
124
- if (!uuidRegex.test(organisation_id)) {
125
- logger.warn('usePublicFileDisplay', 'Invalid organisationId format (not a valid UUID)', { organisation_id });
122
+ // Validate UUID format for organisationId to prevent database errors (only if provided)
123
+ if (organisation_id) {
124
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
125
+ if (!uuidRegex.test(organisation_id)) {
126
+ logger.warn('usePublicFileDisplay', 'Invalid organisationId format (not a valid UUID)', { organisation_id });
127
+ }
126
128
  }
127
129
 
128
130
  // Check cache first
129
- const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
131
+ // When organisation_id is undefined, use 'undefined' in cache key to distinguish from explicit null
132
+ const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
130
133
  if (enableCache) {
131
134
  const cached = publicFileCache.get(cacheKey);
132
135
  if (cached && Date.now() - cached.timestamp < cached.ttl) {
@@ -148,103 +151,186 @@ export function usePublicFileDisplay(
148
151
 
149
152
  let files: any[] = [];
150
153
 
151
- // CRITICAL: When category is provided, MUST use RPC function, not direct queries
152
- // Category is stored in file_metadata JSONB field, not a direct column
153
- if (category) {
154
- // Single file mode - use RPC to get files by category
155
- const rpcParams = {
156
- p_table_name: table_name,
157
- p_record_id: record_id,
158
- p_category: category,
159
- p_organisation_id: organisation_id
160
- };
161
-
162
- const { data, error: rpcError } = await (supabase as any)
163
- .rpc('data_file_reference_by_category_list', rpcParams);
164
-
165
- if (rpcError) {
166
- logger.error('usePublicFileDisplay', 'RPC function error', {
167
- function: 'data_file_reference_by_category_list',
168
- table_name,
169
- record_id,
170
- category,
171
- organisation_id,
172
- error: rpcError.message,
173
- errorCode: rpcError.code,
174
- errorDetails: rpcError.details,
175
- errorHint: rpcError.hint
176
- });
177
- throw new Error(rpcError.message || 'Failed to fetch file reference');
178
- }
154
+ // When organisation_id is undefined, search both user-scoped (null) and organisation-scoped files
155
+ // This allows FileDisplay to work without requiring the organisation_id prop
156
+ if (organisation_id === undefined) {
157
+ logger.debug('usePublicFileDisplay', 'organisation_id is undefined, searching both user-scoped and organisation-scoped files:', {
158
+ table_name,
159
+ record_id,
160
+ category
161
+ });
179
162
 
180
- // RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
181
- // We have table_name, record_id, organisation_id from function parameters
182
- // We can construct FileReference objects directly without another query (avoiding RLS issues)
183
- if (!data || data.length === 0) {
184
- files = [];
185
- } else {
186
- // Construct file reference objects from RPC response
187
- // This avoids RLS issues - the RPC already validated permissions and filtered public files
188
- files = data
189
- .filter((item: any) => {
190
- // RPC should only return public files for public context, but verify
191
- return item.is_public === true && item.id && item.file_path && item.file_metadata;
192
- })
193
- .map((item: any) => {
194
- // Construct complete file reference from RPC response + function parameters
195
- return {
163
+ // First, try user-scoped files (organisation_id = null)
164
+ let userScopedFiles: any[] = [];
165
+ let orgScopedFiles: any[] = [];
166
+
167
+ // Query user-scoped files
168
+ if (category) {
169
+ const { data: userData, error: userRpcError } = await (supabase as any)
170
+ .rpc('data_file_reference_by_category_list', {
171
+ p_table_name: table_name,
172
+ p_record_id: record_id,
173
+ p_category: category,
174
+ p_organisation_id: null
175
+ });
176
+
177
+ if (!userRpcError && userData) {
178
+ userScopedFiles = userData
179
+ .filter((item: any) => item.is_public === true && item.id && item.file_path && item.file_metadata)
180
+ .map((item: any) => ({
196
181
  id: item.id,
197
182
  table_name: table_name,
198
183
  record_id: record_id,
199
184
  file_path: item.file_path,
200
185
  file_metadata: item.file_metadata || {},
201
- organisation_id: organisation_id,
186
+ organisation_id: null,
202
187
  app_id: item.file_metadata?.app_id || null,
203
- is_public: true, // RPC already filtered for public files
188
+ is_public: true,
204
189
  created_at: item.created_at || new Date().toISOString(),
205
190
  updated_at: item.created_at || new Date().toISOString()
206
- };
191
+ }));
192
+ }
193
+ } else {
194
+ const { data: userData, error: userRpcError } = await (supabase as any)
195
+ .rpc('data_file_reference_list', {
196
+ p_table_name: table_name,
197
+ p_record_id: record_id,
198
+ p_organisation_id: null
207
199
  });
200
+
201
+ if (!userRpcError && userData) {
202
+ const ids = userData.map((item: any) => item.id);
203
+ if (ids.length > 0) {
204
+ const { data: fullData } = await supabase
205
+ .from('file_references')
206
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
207
+ .in('id', ids)
208
+ .eq('is_public', true);
209
+ userScopedFiles = fullData || [];
210
+ }
211
+ }
208
212
  }
213
+
214
+ // For public pages, we can't query user's organisations (user is anonymous)
215
+ // But we can try to find organisation-scoped files by querying the record's context
216
+ // For now, we'll just use user-scoped files. If needed, the page context can provide organisation_id
217
+ // Note: This is a limitation of public pages - they should provide organisation_id if needed
218
+
219
+ // Merge results
220
+ const allFiles = [...userScopedFiles, ...orgScopedFiles];
221
+ allFiles.sort((a, b) => {
222
+ const aTime = new Date(a.created_at).getTime();
223
+ const bTime = new Date(b.created_at).getTime();
224
+ return bTime - aTime;
225
+ });
226
+
227
+ files = allFiles;
228
+
229
+ logger.debug('usePublicFileDisplay', 'Found files with undefined organisation_id:', {
230
+ userScopedCount: userScopedFiles.length,
231
+ orgScopedCount: orgScopedFiles.length,
232
+ totalCount: files.length
233
+ });
209
234
  } else {
210
- // Multiple files mode - use RPC to get all files
211
- const { data: fileIds, error: rpcError } = await (supabase as any)
212
- .rpc('data_file_reference_list', {
235
+ // organisation_id is provided (or explicitly null) - use normal query
236
+ // CRITICAL: When category is provided, MUST use RPC function, not direct queries
237
+ // Category is stored in file_metadata JSONB field, not a direct column
238
+ if (category) {
239
+ // Single file mode - use RPC to get files by category
240
+ const rpcParams = {
213
241
  p_table_name: table_name,
214
242
  p_record_id: record_id,
215
- p_organisation_id: organisation_id
216
- });
243
+ p_category: category,
244
+ p_organisation_id: organisation_id ?? null
245
+ };
246
+
247
+ const { data, error: rpcError } = await (supabase as any)
248
+ .rpc('data_file_reference_by_category_list', rpcParams);
217
249
 
218
- if (rpcError) {
219
- logger.error('usePublicFileDisplay', 'RPC function error', {
220
- function: 'data_file_reference_list',
221
- table_name,
222
- record_id,
223
- organisation_id,
224
- error: rpcError.message,
225
- errorCode: rpcError.code,
226
- errorDetails: rpcError.details,
227
- errorHint: rpcError.hint
228
- });
229
- throw new Error(rpcError.message || 'Failed to fetch file references');
230
- }
250
+ if (rpcError) {
251
+ logger.error('usePublicFileDisplay', 'RPC function error', {
252
+ function: 'data_file_reference_by_category_list',
253
+ table_name,
254
+ record_id,
255
+ category,
256
+ organisation_id,
257
+ error: rpcError.message,
258
+ errorCode: rpcError.code,
259
+ errorDetails: rpcError.details,
260
+ errorHint: rpcError.hint
261
+ });
262
+ throw new Error(rpcError.message || 'Failed to fetch file reference');
263
+ }
231
264
 
232
- if (!fileIds || fileIds.length === 0) {
233
- files = [];
265
+ // RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
266
+ // We have table_name, record_id, organisation_id from function parameters
267
+ // We can construct FileReference objects directly without another query (avoiding RLS issues)
268
+ if (!data || data.length === 0) {
269
+ files = [];
270
+ } else {
271
+ // Construct file reference objects from RPC response
272
+ // This avoids RLS issues - the RPC already validated permissions and filtered public files
273
+ files = data
274
+ .filter((item: any) => {
275
+ // RPC should only return public files for public context, but verify
276
+ return item.is_public === true && item.id && item.file_path && item.file_metadata;
277
+ })
278
+ .map((item: any) => {
279
+ // Construct complete file reference from RPC response + function parameters
280
+ return {
281
+ id: item.id,
282
+ table_name: table_name,
283
+ record_id: record_id,
284
+ file_path: item.file_path,
285
+ file_metadata: item.file_metadata || {},
286
+ organisation_id: organisation_id ?? null,
287
+ app_id: item.file_metadata?.app_id || null,
288
+ is_public: true, // RPC already filtered for public files
289
+ created_at: item.created_at || new Date().toISOString(),
290
+ updated_at: item.created_at || new Date().toISOString()
291
+ };
292
+ });
293
+ }
234
294
  } else {
235
- // Fetch full file reference data for each ID, but only public files
236
- const ids = fileIds.map((item: any) => item.id);
237
- const { data: fullData, error: fetchError } = await supabase
238
- .from('file_references')
239
- .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
240
- .in('id', ids)
241
- .eq('is_public', true); // Only public files in public context
242
-
243
- if (fetchError) {
244
- throw new Error(fetchError.message || 'Failed to fetch file references');
295
+ // Multiple files mode - use RPC to get all files
296
+ const { data: fileIds, error: rpcError } = await (supabase as any)
297
+ .rpc('data_file_reference_list', {
298
+ p_table_name: table_name,
299
+ p_record_id: record_id,
300
+ p_organisation_id: organisation_id ?? null
301
+ });
302
+
303
+ if (rpcError) {
304
+ logger.error('usePublicFileDisplay', 'RPC function error', {
305
+ function: 'data_file_reference_list',
306
+ table_name,
307
+ record_id,
308
+ organisation_id,
309
+ error: rpcError.message,
310
+ errorCode: rpcError.code,
311
+ errorDetails: rpcError.details,
312
+ errorHint: rpcError.hint
313
+ });
314
+ throw new Error(rpcError.message || 'Failed to fetch file references');
245
315
  }
246
316
 
247
- files = fullData || [];
317
+ if (!fileIds || fileIds.length === 0) {
318
+ files = [];
319
+ } else {
320
+ // Fetch full file reference data for each ID, but only public files
321
+ const ids = fileIds.map((item: any) => item.id);
322
+ const { data: fullData, error: fetchError } = await supabase
323
+ .from('file_references')
324
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
325
+ .in('id', ids)
326
+ .eq('is_public', true); // Only public files in public context
327
+
328
+ if (fetchError) {
329
+ throw new Error(fetchError.message || 'Failed to fetch file references');
330
+ }
331
+
332
+ files = fullData || [];
333
+ }
248
334
  }
249
335
  }
250
336
 
@@ -353,7 +439,7 @@ export function usePublicFileDisplay(
353
439
 
354
440
  // Fetch files when parameters change
355
441
  useEffect(() => {
356
- if (table_name && record_id && organisation_id) {
442
+ if (table_name && record_id) {
357
443
  fetchFiles();
358
444
  } else {
359
445
  setFileUrl(null);
@@ -367,11 +453,11 @@ export function usePublicFileDisplay(
367
453
  }, [fetchFiles, table_name, record_id, organisation_id]);
368
454
 
369
455
  const refetch = useCallback(async (): Promise<void> => {
370
- if (!table_name || !record_id || !organisation_id) return;
456
+ if (!table_name || !record_id) return;
371
457
 
372
458
  // Clear cache for this file
373
459
  if (enableCache) {
374
- const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
460
+ const cacheKey = `public_file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
375
461
  publicFileCache.delete(cacheKey);
376
462
  }
377
463
  await fetchFiles();