@jmruthers/pace-core 0.5.76 → 0.5.78

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 (447) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{RBACService-C4udt_Zp.d.ts → AuthService-Df3IozMG.d.ts} +10 -118
  3. package/dist/{DataTable-ntgmhO2W.d.ts → DataTable-BE0OXZKQ.d.ts} +9 -2
  4. package/dist/{DataTable-4GAVPIEG.js → DataTable-ETGVF4Y5.js} +50 -13
  5. package/dist/{PublicLoadingSpinner-BiNER8F5.d.ts → PublicLoadingSpinner-CnUaz0vG.d.ts} +5 -2
  6. package/dist/{UnifiedAuthProvider-Bj6YCf7c.d.ts → UnifiedAuthProvider-B391Aqum.d.ts} +42 -45
  7. package/dist/{UnifiedAuthProvider-3NKDOSOK.js → UnifiedAuthProvider-P5SOJAQ6.js} +4 -5
  8. package/dist/{api-DDMUKIUD.js → api-KG4A2X7P.js} +9 -3
  9. package/dist/{audit-6TOCAMKO.js → audit-65VNHEV2.js} +2 -2
  10. package/dist/{chunk-K34IM5CT.js → chunk-2OGV6IRV.js} +196 -626
  11. package/dist/chunk-2OGV6IRV.js.map +1 -0
  12. package/dist/{chunk-NTNILOBC.js → chunk-5BO3MI5Y.js} +4 -4
  13. package/dist/{chunk-XLZ7U46Z.js → chunk-CVMVPYAL.js} +9 -60
  14. package/dist/chunk-CVMVPYAL.js.map +1 -0
  15. package/dist/{chunk-URUTVZ7N.js → chunk-FL4ZCQLD.js} +2 -2
  16. package/dist/{chunk-LW7MMEAQ.js → chunk-FT2M4R4F.js} +2 -2
  17. package/dist/{chunk-5BSLGBYI.js → chunk-JCQZ6LA7.js} +2 -8
  18. package/dist/{chunk-5BSLGBYI.js.map → chunk-JCQZ6LA7.js.map} +1 -1
  19. package/dist/{chunk-KHJS6VIA.js → chunk-LRQ6RBJC.js} +157 -112
  20. package/dist/chunk-LRQ6RBJC.js.map +1 -0
  21. package/dist/{chunk-WN6XJWOS.js → chunk-MNJXXD6C.js} +274 -743
  22. package/dist/chunk-MNJXXD6C.js.map +1 -0
  23. package/dist/{chunk-KK73ZB4E.js → chunk-PTR5PMPE.js} +153 -132
  24. package/dist/chunk-PTR5PMPE.js.map +1 -0
  25. package/dist/{chunk-B2WTCLCV.js → chunk-Q7APDV6H.js} +18 -8
  26. package/dist/chunk-Q7APDV6H.js.map +1 -0
  27. package/dist/{chunk-A4FUBC7B.js → chunk-QGVSOUJ2.js} +2 -4
  28. package/dist/{chunk-A4FUBC7B.js.map → chunk-QGVSOUJ2.js.map} +1 -1
  29. package/dist/{chunk-FGMFQSHX.js → chunk-S63MFSY6.js} +500 -551
  30. package/dist/chunk-S63MFSY6.js.map +1 -0
  31. package/dist/{chunk-AFGTSUAD.js → chunk-VSOKOFRF.js} +4 -4
  32. package/dist/chunk-WUXCWRL6.js +20 -0
  33. package/dist/chunk-WUXCWRL6.js.map +1 -0
  34. package/dist/{chunk-Y6TXWPJO.js → chunk-YVVGHRGI.js} +105 -31
  35. package/dist/chunk-YVVGHRGI.js.map +1 -0
  36. package/dist/{chunk-M5IWZRBT.js → chunk-ZMNXIJP4.js} +2187 -981
  37. package/dist/chunk-ZMNXIJP4.js.map +1 -0
  38. package/dist/components.d.ts +6 -6
  39. package/dist/components.js +14 -18
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database-C3Szpi5J.d.ts → database-BXAfr2Y_.d.ts} +18 -0
  42. package/dist/hooks.d.ts +5 -5
  43. package/dist/hooks.js +8 -9
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.d.ts +19 -27
  46. package/dist/index.js +21 -29
  47. package/dist/index.js.map +1 -1
  48. package/dist/{organisation-BtshODVF.d.ts → organisation-D6qRDtbF.d.ts} +1 -1
  49. package/dist/providers.d.ts +7 -21
  50. package/dist/providers.js +3 -10
  51. package/dist/rbac/index.d.ts +71 -221
  52. package/dist/rbac/index.js +15 -16
  53. package/dist/{types-CGX9Vyf5.d.ts → types-BDg1mAGG.d.ts} +36 -6
  54. package/dist/types.d.ts +3 -3
  55. package/dist/types.js +61 -18
  56. package/dist/types.js.map +1 -1
  57. package/dist/{unified-CM7T0aTK.d.ts → unified-DQ4VcT7H.d.ts} +1 -1
  58. package/dist/{usePublicRouteParams-B-CumWRc.d.ts → usePublicRouteParams-BlgwXweB.d.ts} +3 -3
  59. package/dist/utils.d.ts +2 -2
  60. package/dist/utils.js +52 -9
  61. package/dist/utils.js.map +1 -1
  62. package/docs/CONTENT_AUDIT_REPORT.md +253 -0
  63. package/docs/DOCUMENTATION_AUDIT.md +172 -0
  64. package/docs/README.md +142 -147
  65. package/docs/STYLE_GUIDE.md +37 -0
  66. package/docs/api/classes/ColumnFactory.md +17 -17
  67. package/docs/api/classes/ErrorBoundary.md +1 -1
  68. package/docs/api/classes/InvalidScopeError.md +4 -4
  69. package/docs/api/classes/MissingUserContextError.md +4 -4
  70. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  71. package/docs/api/classes/PermissionDeniedError.md +5 -5
  72. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  73. package/docs/api/classes/RBACAuditManager.md +8 -8
  74. package/docs/api/classes/RBACCache.md +35 -5
  75. package/docs/api/classes/RBACEngine.md +49 -20
  76. package/docs/api/classes/RBACError.md +4 -4
  77. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  78. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  79. package/docs/api/classes/StorageUtils.md +1 -1
  80. package/docs/api/enums/FileCategory.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +4 -4
  82. package/docs/api/interfaces/ButtonProps.md +1 -1
  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/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +11 -0
  88. package/docs/api/interfaces/DataTableAction.md +65 -29
  89. package/docs/api/interfaces/DataTableColumn.md +36 -23
  90. package/docs/api/interfaces/DataTableProps.md +80 -38
  91. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  92. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  93. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  94. package/docs/api/interfaces/EventLogoProps.md +1 -1
  95. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  96. package/docs/api/interfaces/FileMetadata.md +1 -1
  97. package/docs/api/interfaces/FileReference.md +1 -1
  98. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  99. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  100. package/docs/api/interfaces/FileUploadProps.md +1 -1
  101. package/docs/api/interfaces/FooterProps.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoginFormProps.md +1 -1
  106. package/docs/api/interfaces/NavigationAccessRecord.md +11 -11
  107. package/docs/api/interfaces/NavigationContextType.md +9 -9
  108. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  109. package/docs/api/interfaces/NavigationItem.md +1 -1
  110. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  111. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  112. package/docs/api/interfaces/Organisation.md +1 -1
  113. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  114. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  115. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  116. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  117. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  118. package/docs/api/interfaces/PaceLoginPageProps.md +16 -3
  119. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  120. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  121. package/docs/api/interfaces/PagePermissionGuardProps.md +2 -2
  122. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  123. package/docs/api/interfaces/PaletteData.md +1 -1
  124. package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
  125. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  126. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  127. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  128. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  131. package/docs/api/interfaces/RBACConfig.md +1 -1
  132. package/docs/api/interfaces/RBACLogger.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  134. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  135. package/docs/api/interfaces/RouteAccessRecord.md +2 -2
  136. package/docs/api/interfaces/RouteConfig.md +2 -2
  137. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  138. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  139. package/docs/api/interfaces/StorageConfig.md +1 -1
  140. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  141. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  142. package/docs/api/interfaces/StorageListOptions.md +1 -1
  143. package/docs/api/interfaces/StorageListResult.md +1 -1
  144. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  145. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  146. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  147. package/docs/api/interfaces/StyleImport.md +1 -1
  148. package/docs/api/interfaces/SwitchProps.md +1 -1
  149. package/docs/api/interfaces/ToastActionElement.md +1 -1
  150. package/docs/api/interfaces/ToastProps.md +1 -1
  151. package/docs/api/interfaces/UnifiedAuthContextType.md +94 -521
  152. package/docs/api/interfaces/UnifiedAuthProviderProps.md +16 -16
  153. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  154. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  155. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  156. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  157. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  158. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  159. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  160. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  161. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  162. package/docs/api/interfaces/UserEventAccess.md +11 -11
  163. package/docs/api/interfaces/UserMenuProps.md +1 -1
  164. package/docs/api/interfaces/UserProfile.md +1 -1
  165. package/docs/api/modules.md +251 -269
  166. package/docs/api-reference/components.md +193 -0
  167. package/docs/api-reference/hooks.md +265 -0
  168. package/docs/api-reference/providers.md +6 -0
  169. package/docs/api-reference/types.md +6 -0
  170. package/docs/api-reference/utilities.md +207 -0
  171. package/docs/architecture/README.md +6 -0
  172. package/docs/{database-schema-requirements.md → architecture/database-schema-requirements.md} +6 -0
  173. package/docs/architecture/rbac-security-architecture.md +258 -0
  174. package/docs/architecture/services.md +9 -1
  175. package/docs/best-practices/README.md +6 -0
  176. package/docs/best-practices/accessibility.md +6 -0
  177. package/docs/{common-patterns.md → best-practices/common-patterns.md} +6 -0
  178. package/docs/best-practices/deployment.md +6 -0
  179. package/docs/best-practices/performance.md +475 -2
  180. package/docs/best-practices/security.md +6 -0
  181. package/docs/best-practices/testing.md +6 -0
  182. package/docs/core-concepts/authentication.md +6 -0
  183. package/docs/core-concepts/events.md +6 -0
  184. package/docs/core-concepts/organisations.md +6 -0
  185. package/docs/core-concepts/permissions.md +6 -0
  186. package/docs/core-concepts/rbac-system.md +8 -0
  187. package/docs/documentation-index.md +121 -182
  188. package/docs/{consuming-app-vite-config.md → getting-started/consuming-app-vite-config.md} +6 -0
  189. package/docs/getting-started/documentation-index.md +40 -0
  190. package/docs/getting-started/examples/README.md +878 -35
  191. package/docs/{faq.md → getting-started/faq.md} +7 -1
  192. package/docs/getting-started/installation-guide.md +6 -0
  193. package/docs/{quick-reference.md → getting-started/quick-reference.md} +6 -0
  194. package/docs/implementation-guides/app-layout.md +6 -0
  195. package/docs/implementation-guides/authentication.md +1021 -0
  196. package/docs/implementation-guides/component-styling.md +6 -0
  197. package/docs/implementation-guides/data-tables.md +1264 -2076
  198. package/docs/implementation-guides/dynamic-colors.md +6 -0
  199. package/docs/implementation-guides/event-theming-summary.md +6 -0
  200. package/docs/{file-reference-system.md → implementation-guides/file-reference-system.md} +6 -0
  201. package/docs/implementation-guides/file-upload-storage.md +6 -0
  202. package/docs/implementation-guides/forms.md +6 -0
  203. package/docs/implementation-guides/inactivity-tracking.md +6 -0
  204. package/docs/implementation-guides/navigation.md +6 -0
  205. package/docs/implementation-guides/organisation-security.md +6 -0
  206. package/docs/implementation-guides/permission-enforcement.md +6 -0
  207. package/docs/implementation-guides/public-pages-advanced.md +6 -0
  208. package/docs/implementation-guides/public-pages.md +6 -0
  209. package/docs/migration/MIGRATION_GUIDE.md +827 -351
  210. package/docs/migration/README.md +7 -1
  211. package/docs/migration/organisation-context-timing-fix.md +6 -0
  212. package/docs/migration/rbac-migration.md +44 -1
  213. package/docs/migration/service-architecture.md +6 -0
  214. package/docs/migration/v0.4.15-tailwind-scanning.md +6 -0
  215. package/docs/migration/v0.4.16-css-first-approach.md +6 -0
  216. package/docs/migration/v0.4.17-source-path-fix.md +6 -0
  217. package/docs/rbac/README-rbac-rls-integration.md +6 -0
  218. package/docs/rbac/README.md +6 -0
  219. package/docs/rbac/advanced-patterns.md +6 -0
  220. package/docs/rbac/api-reference.md +7 -1
  221. package/docs/rbac/breaking-changes-v3.md +222 -0
  222. package/docs/rbac/examples/rbac-rls-integration-example.md +6 -0
  223. package/docs/rbac/examples.md +6 -0
  224. package/docs/rbac/getting-started.md +6 -0
  225. package/docs/rbac/migration-guide.md +260 -0
  226. package/docs/rbac/quick-start.md +70 -13
  227. package/docs/rbac/rbac-rls-integration.md +6 -0
  228. package/docs/rbac/super-admin-guide.md +6 -0
  229. package/docs/rbac/troubleshooting.md +6 -0
  230. package/docs/security/README.md +6 -0
  231. package/docs/security/checklist.md +6 -0
  232. package/docs/styles/README.md +7 -1
  233. package/docs/{usage.md → styles/usage.md} +6 -0
  234. package/docs/testing/README.md +6 -0
  235. package/docs/{visual-testing.md → testing/visual-testing.md} +6 -0
  236. package/docs/troubleshooting/README.md +387 -5
  237. package/docs/troubleshooting/cake-page-permission-guard-issue-summary.md +6 -0
  238. package/docs/troubleshooting/common-issues.md +6 -0
  239. package/docs/troubleshooting/database-view-compatibility.md +6 -0
  240. package/docs/troubleshooting/organisation-context-setup.md +6 -0
  241. package/docs/troubleshooting/react-hooks-issue-analysis.md +6 -0
  242. package/docs/troubleshooting/styling-issues.md +6 -0
  243. package/docs/troubleshooting/tailwind-content-scanning.md +6 -0
  244. package/package.json +1 -1
  245. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -1
  246. package/src/__tests__/helpers/test-providers.tsx +3 -53
  247. package/src/components/DataTable/DataTable.test.tsx +319 -0
  248. package/src/components/DataTable/DataTable.tsx +32 -11
  249. package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx → DataTable.comprehensive.test.tsx.skip} +6 -4
  250. package/src/components/DataTable/__tests__/{DataTable.test.tsx → DataTable.test.tsx.skip} +6 -4
  251. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +31 -9
  252. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +601 -0
  253. package/src/components/DataTable/__tests__/keyboard.test.tsx +615 -0
  254. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +639 -0
  255. package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx.skip +330 -0
  256. package/src/components/DataTable/components/AccessDeniedPage.tsx +2 -2
  257. package/src/components/DataTable/components/ActionButtons.tsx +88 -104
  258. package/src/components/DataTable/components/DataTableCore.tsx +309 -337
  259. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +4 -2
  260. package/src/components/DataTable/components/DataTableModals.tsx +22 -1
  261. package/src/components/DataTable/components/EditableRow.tsx +69 -84
  262. package/src/components/DataTable/components/EmptyState.tsx +5 -1
  263. package/src/components/DataTable/components/ImportModal.tsx +65 -36
  264. package/src/components/DataTable/components/PaginationControls.tsx +40 -100
  265. package/src/components/DataTable/components/UnifiedTableBody.tsx +125 -148
  266. package/src/components/DataTable/context/DataTableContext.tsx +1 -1
  267. package/src/components/DataTable/core/ColumnFactory.ts +5 -0
  268. package/src/components/DataTable/examples/HierarchicalActionsExample.tsx +12 -10
  269. package/src/components/DataTable/examples/HierarchicalExample.tsx +1 -1
  270. package/src/components/DataTable/examples/InitialPageSizeExample.tsx +1 -0
  271. package/src/components/DataTable/examples/PerformanceExample.tsx +1 -0
  272. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +1 -5
  273. package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +167 -0
  274. package/src/components/DataTable/hooks/index.ts +7 -0
  275. package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +32 -15
  276. package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +102 -0
  277. package/src/components/DataTable/hooks/useDataTableConfiguration.ts +89 -0
  278. package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +117 -0
  279. package/src/components/DataTable/hooks/useDataTablePermissions.ts +71 -27
  280. package/src/components/DataTable/hooks/useDataTableState.ts +39 -11
  281. package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +33 -0
  282. package/src/components/DataTable/hooks/useHierarchicalState.ts +15 -1
  283. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +447 -0
  284. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +94 -0
  285. package/src/components/DataTable/hooks/useTableColumns.ts +10 -7
  286. package/src/components/DataTable/hooks/useTableHandlers.ts +174 -0
  287. package/src/components/DataTable/index.ts +12 -3
  288. package/src/components/DataTable/types.ts +129 -9
  289. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +159 -22
  290. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +111 -0
  291. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +15 -29
  292. package/src/components/DataTable/utils/a11yUtils.ts +244 -0
  293. package/src/components/DataTable/utils/debugTools.ts +609 -0
  294. package/src/components/DataTable/utils/exportUtils.ts +114 -16
  295. package/src/components/DataTable/utils/flexibleImport.ts +202 -32
  296. package/src/components/DataTable/utils/hierarchicalUtils.ts +1 -1
  297. package/src/components/DataTable/utils/index.ts +2 -0
  298. package/src/components/DataTable/utils/paginationUtils.ts +350 -0
  299. package/src/components/DataTable/utils/rowUtils.ts +6 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +19 -24
  301. package/src/components/NavigationMenu/NavigationMenu.tsx +19 -8
  302. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +1 -23
  303. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +56 -6
  304. package/src/components/PaceLoginPage/PaceLoginPage.tsx +137 -13
  305. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +1 -1
  306. package/src/components/Select/Select.tsx +1 -0
  307. package/src/components/examples/PermissionExample.tsx +173 -0
  308. package/src/examples/CorrectPublicPageImplementation.tsx +301 -0
  309. package/src/examples/PublicEventPage.tsx +274 -0
  310. package/src/examples/PublicPageApp.tsx +308 -0
  311. package/src/examples/PublicPageUsageExample.tsx +216 -0
  312. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +12 -1
  313. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +129 -17
  314. package/src/hooks/__tests__/useRBAC.unit.test.ts +151 -846
  315. package/src/hooks/useOrganisationPermissions.test.ts +42 -18
  316. package/src/hooks/useOrganisationPermissions.ts +12 -6
  317. package/src/hooks/useOrganisationSecurity.test.ts +138 -85
  318. package/src/hooks/useOrganisationSecurity.ts +41 -10
  319. package/src/index.ts +0 -1
  320. package/src/providers/AuthProvider.simplified.tsx +880 -0
  321. package/src/providers/UnifiedAuthProvider.test.simple.tsx +8 -8
  322. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +29 -19
  323. package/src/providers/index.ts +0 -1
  324. package/src/providers/services/EventServiceProvider.tsx +19 -15
  325. package/src/providers/services/InactivityServiceProvider.tsx +19 -15
  326. package/src/providers/services/OrganisationServiceProvider.tsx +19 -15
  327. package/src/providers/services/UnifiedAuthProvider.tsx +156 -127
  328. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +1 -1
  329. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -3
  330. package/src/rbac/README.md +1 -1
  331. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +25 -27
  332. package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +313 -0
  333. package/src/rbac/__tests__/engine.comprehensive.test.ts +114 -348
  334. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +28 -110
  335. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +33 -85
  336. package/src/rbac/__tests__/scenarios.user-role.test.tsx +2 -2
  337. package/src/rbac/adapters.tsx +26 -69
  338. package/src/rbac/api.test.ts +90 -27
  339. package/src/rbac/api.ts +61 -10
  340. package/src/rbac/audit.test.ts +33 -38
  341. package/src/rbac/audit.ts +21 -6
  342. package/src/rbac/cache.ts +33 -1
  343. package/src/rbac/components/NavigationGuard.tsx +11 -11
  344. package/src/rbac/components/NavigationProvider.test.tsx +11 -5
  345. package/src/rbac/components/NavigationProvider.tsx +37 -13
  346. package/src/rbac/components/PagePermissionGuard.tsx +111 -50
  347. package/src/rbac/components/PagePermissionProvider.tsx +5 -5
  348. package/src/rbac/components/PermissionEnforcer.tsx +11 -11
  349. package/src/rbac/components/RoleBasedRouter.tsx +5 -5
  350. package/src/rbac/components/SecureDataProvider.tsx +5 -5
  351. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +8 -8
  352. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +14 -14
  353. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +12 -12
  354. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +6 -6
  355. package/src/rbac/engine.test.simple.ts +19 -13
  356. package/src/rbac/engine.test.ts +1 -0
  357. package/src/rbac/engine.ts +330 -766
  358. package/src/rbac/errors.ts +156 -0
  359. package/src/rbac/hooks/usePermissions.ts +32 -10
  360. package/src/rbac/hooks/useRBAC.test.ts +126 -512
  361. package/src/rbac/hooks/useRBAC.ts +147 -193
  362. package/src/rbac/hooks/useResolvedScope.ts +12 -0
  363. package/src/rbac/index.ts +7 -4
  364. package/src/rbac/security.ts +109 -18
  365. package/src/rbac/types.ts +12 -1
  366. package/src/services/AuthService.ts +2 -15
  367. package/src/services/EventService.ts +43 -46
  368. package/src/services/OrganisationService.ts +51 -31
  369. package/src/services/__tests__/AuthService.test.ts +1 -1
  370. package/src/services/__tests__/EventService.test.ts +1 -1
  371. package/src/services/__tests__/OrganisationService.test.ts +1 -1
  372. package/src/services/base/BaseService.ts +8 -0
  373. package/src/styles/base.css +208 -0
  374. package/src/styles/semantic.css +24 -0
  375. package/src/types/database.generated.ts +7347 -0
  376. package/src/types/database.ts +20 -0
  377. package/src/utils/logger.ts +179 -0
  378. package/src/utils/organisationContext.ts +11 -4
  379. package/src/utils/storage/__tests__/helpers.unit.test.ts +6 -2
  380. package/dist/appNameResolver-UURKN7NF.js +0 -22
  381. package/dist/audit-6TOCAMKO.js.map +0 -1
  382. package/dist/chunk-B2WTCLCV.js.map +0 -1
  383. package/dist/chunk-FGMFQSHX.js.map +0 -1
  384. package/dist/chunk-K34IM5CT.js.map +0 -1
  385. package/dist/chunk-KHJS6VIA.js.map +0 -1
  386. package/dist/chunk-KK73ZB4E.js.map +0 -1
  387. package/dist/chunk-M5IWZRBT.js.map +0 -1
  388. package/dist/chunk-ULBI5JGB.js +0 -109
  389. package/dist/chunk-ULBI5JGB.js.map +0 -1
  390. package/dist/chunk-WN6XJWOS.js.map +0 -1
  391. package/dist/chunk-XLZ7U46Z.js.map +0 -1
  392. package/dist/chunk-Y6TXWPJO.js.map +0 -1
  393. package/docs/DOCUMENTATION_CHECKLIST.md +0 -281
  394. package/docs/TERMINOLOGY.md +0 -231
  395. package/docs/api/interfaces/RBACContextType.md +0 -468
  396. package/docs/api/interfaces/RBACProviderProps.md +0 -107
  397. package/docs/best-practices/performance-expansion.md +0 -473
  398. package/docs/breaking-changes.md +0 -179
  399. package/docs/consuming-app-example.md +0 -290
  400. package/docs/documentation-templates.md +0 -539
  401. package/docs/examples/navigation-menu-auth-fix.md +0 -344
  402. package/docs/getting-started/examples/basic-auth-app.md +0 -520
  403. package/docs/getting-started/examples/full-featured-app.md +0 -616
  404. package/docs/getting-started/quick-start.md +0 -376
  405. package/docs/implementation-guides/datatable-filtering.md +0 -313
  406. package/docs/implementation-guides/datatable-rbac-usage.md +0 -317
  407. package/docs/implementation-guides/hierarchical-datatable.md +0 -850
  408. package/docs/implementation-guides/large-datasets.md +0 -281
  409. package/docs/implementation-guides/performance.md +0 -403
  410. package/docs/migration/quick-migration-guide.md +0 -320
  411. package/docs/migration-guide.md +0 -193
  412. package/docs/migration-guides/unified-auth-provider-mandatory-timeouts.md +0 -226
  413. package/docs/performance/README.md +0 -551
  414. package/docs/style-guide.md +0 -964
  415. package/docs/troubleshooting/authentication-issues.md +0 -334
  416. package/docs/troubleshooting/debugging.md +0 -1117
  417. package/docs/troubleshooting/migration.md +0 -918
  418. package/src/__tests__/hooks/usePermissions.test.ts +0 -261
  419. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +0 -574
  420. package/src/hooks/__tests__/ServiceHooks.test.tsx +0 -613
  421. package/src/hooks/services/__tests__/useServiceHooks.test.tsx +0 -137
  422. package/src/hooks/services/usePermissions.ts +0 -70
  423. package/src/hooks/services/useRBACService.ts +0 -30
  424. package/src/hooks/usePermissionCheck.ts +0 -150
  425. package/src/providers/__tests__/ServiceProviders.test.tsx +0 -477
  426. package/src/providers/services/RBACServiceProvider.tsx +0 -79
  427. package/src/rbac/__tests__/integration.authflow.test.tsx +0 -119
  428. package/src/rbac/__tests__/integration.navigation.test.tsx +0 -69
  429. package/src/rbac/__tests__/integration.securedata.test.tsx +0 -92
  430. package/src/rbac/__tests__/integration.smoke.test.tsx +0 -73
  431. package/src/rbac/providers/RBACProvider.tsx +0 -645
  432. package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +0 -688
  433. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +0 -1186
  434. package/src/rbac/providers/index.ts +0 -11
  435. package/src/services/RBACService.ts +0 -522
  436. package/src/services/__tests__/RBACService.test.ts +0 -492
  437. package/src/services/interfaces/IRBACService.ts +0 -62
  438. package/src/utils/appNameResolver.test 2.ts +0 -494
  439. /package/dist/{DataTable-4GAVPIEG.js.map → DataTable-ETGVF4Y5.js.map} +0 -0
  440. /package/dist/{UnifiedAuthProvider-3NKDOSOK.js.map → UnifiedAuthProvider-P5SOJAQ6.js.map} +0 -0
  441. /package/dist/{api-DDMUKIUD.js.map → api-KG4A2X7P.js.map} +0 -0
  442. /package/dist/{appNameResolver-UURKN7NF.js.map → audit-65VNHEV2.js.map} +0 -0
  443. /package/dist/{chunk-NTNILOBC.js.map → chunk-5BO3MI5Y.js.map} +0 -0
  444. /package/dist/{chunk-URUTVZ7N.js.map → chunk-FL4ZCQLD.js.map} +0 -0
  445. /package/dist/{chunk-LW7MMEAQ.js.map → chunk-FT2M4R4F.js.map} +0 -0
  446. /package/dist/{chunk-AFGTSUAD.js.map → chunk-VSOKOFRF.js.map} +0 -0
  447. /package/docs/{app.css.example → styles/app.css.example} +0 -0
@@ -1,29 +1,37 @@
1
1
  /**
2
- * RBAC Core Engine
2
+ * RBAC Core Engine - Simplified Version
3
3
  * @package @jmruthers/pace-core
4
4
  * @module RBAC/Engine
5
- * @since 1.0.0
5
+ * @since 2.0.0
6
6
  *
7
- * This module implements the core RBAC permission algorithm with deny-overrides-allow precedence.
7
+ * This is a drastically simplified version that delegates permission checking to a single RPC function.
8
+ * All the complex grant collection logic has been moved to the database for better performance and security.
9
+ *
10
+ * BREAKING CHANGES FROM v1:
11
+ * - No more client-side grant collection
12
+ * - No more complex permission resolution algorithm
13
+ * - Single RPC call for all permission checks
14
+ * - Caching is still supported for performance
8
15
  */
9
16
 
10
17
  import { SupabaseClient } from '@supabase/supabase-js';
11
18
  import { Database } from '../types/database';
12
- import {
13
- UUID,
14
- Permission,
15
- Scope,
16
- PermissionCheck,
17
- AccessLevel,
19
+ import {
20
+ UUID,
21
+ Permission,
22
+ Scope,
23
+ PermissionCheck,
24
+ AccessLevel,
18
25
  PermissionMap,
19
26
  Operation,
20
- OrganisationRole,
21
- EventAppRole
27
+ RBACAppContext,
28
+ RBACRoleContext,
29
+ RBACPermission,
22
30
  } from './types';
23
31
  import { rbacCache, RBACCache } from './cache';
24
32
  import { emitAuditEvent } from './audit';
25
33
  import { initializeCacheInvalidation } from './cache-invalidation';
26
- import { getCurrentAppName } from '../utils/appNameResolver';
34
+ import { categorizeError, mapErrorCategoryToSecurityEventType } from './errors';
27
35
  import {
28
36
  RBACSecurityValidator,
29
37
  RBACSecurityMiddleware,
@@ -32,19 +40,10 @@ import {
32
40
  } from './security';
33
41
 
34
42
  /**
35
- * Grant type for permission resolution
36
- */
37
- interface Grant {
38
- type: 'allow' | 'deny';
39
- permission: Permission;
40
- scope: 'global' | 'organisation' | 'eventApp' | 'page';
41
- source: string;
42
- }
43
-
44
- /**
45
- * RBAC Engine
43
+ * Simplified RBAC Engine
46
44
  *
47
- * Implements the core permission algorithm with deny-overrides-allow precedence.
45
+ * Delegates all permission checks to the database via a single RPC function.
46
+ * This reduces complexity, improves performance, and enhances security.
48
47
  */
49
48
  export class RBACEngine {
50
49
  private supabase: SupabaseClient<Database>;
@@ -61,78 +60,99 @@ export class RBACEngine {
61
60
  /**
62
61
  * Check if a user has a specific permission
63
62
  *
63
+ * This method now delegates to the database RPC function for all the heavy lifting.
64
+ *
64
65
  * @param input - Permission check input
65
- * @param securityContext - Optional security context for enhanced validation
66
+ * @param securityContext - Security context for validation (required)
66
67
  * @returns Promise resolving to permission result
67
68
  */
68
- async isPermitted(input: PermissionCheck, securityContext?: SecurityContext): Promise<boolean> {
69
+ async isPermitted(input: PermissionCheck, securityContext: SecurityContext): Promise<boolean> {
69
70
  const startTime = Date.now();
70
71
  const { userId, permission, scope, pageId } = input;
71
72
 
72
73
  // Track cache usage for audit
73
74
  let cacheHit = false;
74
- let cacheSource: 'memory' | 'database' | 'rpc' = 'database';
75
+ let cacheSource: 'memory' | 'rpc' = 'rpc';
75
76
 
76
77
  try {
77
- // Security validation
78
- if (securityContext) {
79
- const validation = await this.securityMiddleware.validateInput(input, securityContext);
80
- if (!validation.isValid) {
81
- RBACSecurityValidator.logSecurityEvent({
82
- type: 'invalid_input',
83
- userId,
84
- details: { errors: validation.errors, input: JSON.stringify(input) },
85
- });
86
- return false;
87
- }
78
+ // ========================================================================
79
+ // STEP 1: Security Validation & Rate Limiting (MANDATORY)
80
+ // ========================================================================
81
+
82
+ // Validate input
83
+ const validation = await this.securityMiddleware.validateInput(input, securityContext);
84
+ if (!validation.isValid) {
85
+ RBACSecurityValidator.logSecurityEvent({
86
+ type: 'invalid_input',
87
+ userId,
88
+ details: { errors: validation.errors, input: JSON.stringify(input) },
89
+ });
90
+ return false;
91
+ }
88
92
 
89
- const rateLimit = await this.securityMiddleware.checkRateLimit(securityContext);
90
- if (!rateLimit.isAllowed) {
91
- RBACSecurityValidator.logSecurityEvent({
92
- type: 'rate_limit_exceeded',
93
- userId,
94
- details: { remaining: rateLimit.remaining },
95
- });
96
- return false;
97
- }
93
+ // Check rate limits
94
+ const rateLimit = await this.securityMiddleware.checkRateLimit(securityContext);
95
+ if (!rateLimit.isAllowed) {
96
+ RBACSecurityValidator.logSecurityEvent({
97
+ type: 'rate_limit_exceeded',
98
+ userId,
99
+ details: { remaining: rateLimit.remaining },
100
+ });
101
+ return false;
98
102
  }
99
103
 
100
- // Input validation (only when security context is provided)
101
- if (securityContext) {
102
- if (!RBACSecurityValidator.validateUserId(userId)) {
103
- RBACSecurityValidator.logSecurityEvent({
104
- type: 'invalid_input',
105
- userId,
106
- details: { error: 'Invalid user ID format' },
107
- });
108
- return false;
109
- }
104
+ // Validate user ID format
105
+ if (!RBACSecurityValidator.validateUserId(userId)) {
106
+ RBACSecurityValidator.logSecurityEvent({
107
+ type: 'invalid_input',
108
+ userId,
109
+ details: { error: 'Invalid user ID format' },
110
+ });
111
+ return false;
112
+ }
110
113
 
111
- if (!RBACSecurityValidator.validatePermission(permission)) {
112
- RBACSecurityValidator.logSecurityEvent({
113
- type: 'invalid_input',
114
- userId,
115
- details: { error: 'Invalid permission format', permission },
116
- });
117
- return false;
118
- }
114
+ // Validate permission format
115
+ if (!RBACSecurityValidator.validatePermission(permission)) {
116
+ RBACSecurityValidator.logSecurityEvent({
117
+ type: 'invalid_input',
118
+ userId,
119
+ details: { error: 'Invalid permission format', permission },
120
+ });
121
+ return false;
122
+ }
119
123
 
120
- if (!RBACSecurityValidator.validateScope(scope)) {
121
- RBACSecurityValidator.logSecurityEvent({
122
- type: 'invalid_input',
123
- userId,
124
- details: { error: 'Invalid scope format', scope },
125
- });
126
- return false;
127
- }
124
+ // Validate scope format
125
+ if (!RBACSecurityValidator.validateScope(scope)) {
126
+ RBACSecurityValidator.logSecurityEvent({
127
+ type: 'invalid_input',
128
+ userId,
129
+ details: { error: 'Invalid scope format', scope },
130
+ });
131
+ return false;
128
132
  }
129
- // 1) If GlobalRole == 'super_admin' → allow (log bypass:true)
130
- const isSuperAdmin = await this.checkSuperAdmin(userId);
131
- if (isSuperAdmin) {
133
+
134
+ // ========================================================================
135
+ // STEP 2: Check Cache (OPTIONAL - for performance)
136
+ // ========================================================================
137
+
138
+ const cacheKey = RBACCache.generateKey(
139
+ userId,
140
+ permission,
141
+ scope.organisationId,
142
+ scope.eventId,
143
+ scope.appId,
144
+ pageId
145
+ );
146
+
147
+ const cached = rbacCache.get<boolean>(cacheKey);
148
+ if (cached !== null) {
149
+ cacheHit = true;
150
+ cacheSource = 'memory';
151
+
132
152
  const duration = Date.now() - startTime;
133
- // Only emit audit event if we have a valid organisation ID
153
+
154
+ // Audit cache hit (if organisation context exists)
134
155
  if (scope.organisationId) {
135
- // Resolve pageId to UUID if it's a page name
136
156
  const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
137
157
  await emitAuditEvent({
138
158
  type: 'permission_check',
@@ -142,159 +162,111 @@ export class RBACEngine {
142
162
  appId: scope.appId,
143
163
  pageId: resolvedPageId,
144
164
  permission,
145
- decision: true,
165
+ decision: cached,
146
166
  source: 'api',
147
- bypass: true,
148
167
  duration_ms: duration,
168
+ cache_hit: true,
169
+ cache_source: 'memory',
149
170
  });
150
171
  }
151
- return true;
172
+
173
+ return cached;
152
174
  }
153
175
 
154
- // 2) Validate context requirements based on app configuration
155
- const validatedScope = await this.validateContextRequirements(scope, scope.appId);
156
- if (!validatedScope) {
157
- const duration = Date.now() - startTime;
158
- // Only emit audit event if we have a valid organisation ID
159
- if (scope.organisationId) {
160
- // Resolve pageId to UUID if it's a page name
161
- const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
162
- await emitAuditEvent({
163
- type: 'permission_denied',
164
- userId,
165
- organisationId: scope.organisationId,
166
- eventId: scope.eventId,
167
- appId: scope.appId,
168
- pageId: resolvedPageId,
169
- permission,
170
- source: 'api',
171
- metadata: {
172
- reason: 'invalid_context_requirements',
173
- app_requires_event: scope.appId ? await this.getAppConfig(scope.appId).then(config => config?.requires_event) : null
174
- }
175
- });
176
- }
177
- return false;
178
- }
176
+ // ========================================================================
177
+ // STEP 3: Call Simplified RPC Function (SINGLE DATABASE CALL)
178
+ // ========================================================================
179
+
180
+ // This single RPC call replaces hundreds of lines of complex client-side logic:
181
+ // - No more super admin checks here (RPC handles it)
182
+ // - No more grant collection (RPC handles it)
183
+ // - No more permission matching (RPC handles it)
184
+ // - No more deny-override-allow logic (RPC handles it)
185
+
186
+ const { data, error } = await (this.supabase as any).rpc('rbac_check_permission_simplified', {
187
+ p_user_id: userId,
188
+ p_permission: permission,
189
+ p_organisation_id: scope.organisationId || undefined,
190
+ p_event_id: scope.eventId || undefined,
191
+ p_app_id: scope.appId || undefined,
192
+ p_page_id: pageId || undefined,
193
+ });
194
+
195
+ if (error) {
196
+ console.error('[RBACEngine] RPC error:', error);
179
197
 
180
- // 3) Collect active grants at page → eventApp → organisation → global
181
- const grants = await this.collectActiveGrants(userId, validatedScope, pageId);
182
- console.log('[RBACEngine] Collected grants:', grants);
198
+ const category = categorizeError(error);
199
+ const eventType = mapErrorCategoryToSecurityEventType(category);
200
+ const errorDetails = error as { message?: string; code?: string; hint?: string; details?: string };
183
201
 
184
- // 4) Apply explicit denies at the closest scope first (deny overrides allow)
185
- const denies = grants.filter(g => g.type === 'deny');
186
- console.log('[RBACEngine] Deny grants:', denies);
187
- for (const deny of denies) {
188
- const matches = this.permissionMatches(deny.permission, permission);
189
- console.log('[RBACEngine] Checking deny:', {
190
- denyPermission: deny.permission,
191
- requestedPermission: permission,
192
- matches
202
+ RBACSecurityValidator.logSecurityEvent({
203
+ type: eventType,
204
+ userId,
205
+ details: {
206
+ error: errorDetails?.message || 'RPC call failed',
207
+ code: errorDetails?.code,
208
+ hint: errorDetails?.hint,
209
+ details: errorDetails?.details,
210
+ permission,
211
+ scope: JSON.stringify(scope),
212
+ category,
213
+ },
193
214
  });
194
- if (matches) {
195
- const duration = Date.now() - startTime;
196
- console.log('[RBACEngine] Permission DENIED by explicit deny rule');
197
- // Only emit audit event if we have a valid organisation ID
198
- if (scope.organisationId) {
199
- // Resolve pageId to UUID if it's a page name
200
- const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
201
- await emitAuditEvent({
202
- type: 'permission_denied',
203
- userId,
204
- organisationId: scope.organisationId,
205
- eventId: scope.eventId,
206
- appId: scope.appId,
207
- pageId: resolvedPageId,
208
- permission,
209
- source: 'api',
210
- });
211
- }
212
- return false;
213
- }
215
+
216
+ // Fail securely - deny on error
217
+ return false;
214
218
  }
215
219
 
216
- // 5) If no deny, check allows from closest to widest scope
217
- const allows = grants.filter(g => g.type === 'allow');
218
- console.log('[RBACEngine] Allow grants:', allows);
219
- let hasPermission = false;
220
+ const hasPermission = data === true;
220
221
 
221
- // Check allows in precedence order: page → eventApp → organisation → global
222
- const scopeOrder: Array<'page' | 'eventApp' | 'organisation' | 'global'> = ['page', 'eventApp', 'organisation', 'global'];
222
+ // ========================================================================
223
+ // STEP 4: Cache Result & Audit (COMPLETION)
224
+ // ========================================================================
223
225
 
224
- for (const scopeType of scopeOrder) {
225
- const scopeAllows = allows.filter(g => g.scope === scopeType);
226
- console.log(`[RBACEngine] Checking ${scopeType} allows:`, scopeAllows);
227
- for (const allow of scopeAllows) {
228
- console.log(`[RBACEngine] About to check permission match for ${scopeType}:`, {
229
- allowPermission: allow.permission,
230
- requestedPermission: permission,
231
- scopeType
232
- });
233
- const matches = this.permissionMatches(allow.permission, permission);
234
- console.log(`[RBACEngine] Permission match result:`, {
235
- scopeType,
236
- allowPermission: allow.permission,
237
- requestedPermission: permission,
238
- matches
239
- });
240
- if (matches) {
241
- console.log(`[RBACEngine] Permission GRANTED by ${scopeType} allow rule`);
242
- hasPermission = true;
243
- break;
244
- }
245
- }
246
- if (hasPermission) break;
247
- }
248
-
249
- // 6) Return final decision; emit audit event
250
- const finalDecision = hasPermission;
251
- const _duration = Date.now() - startTime;
226
+ // Cache the result for 60 seconds
227
+ rbacCache.set(cacheKey, hasPermission, 60000);
252
228
 
253
- console.log('[RBACEngine] Final decision:', {
254
- userId,
255
- permission,
256
- pageId,
257
- hasPermission,
258
- grantsCount: grants.length,
259
- allowsCount: allows.length,
260
- deniesCount: denies.length,
261
- duration: _duration
262
- });
263
-
264
- // Only emit audit event if we have a valid organisation ID
229
+ const duration = Date.now() - startTime;
230
+
231
+ // Emit audit event (if organisation context exists)
265
232
  if (scope.organisationId) {
266
- // Resolve pageId to UUID if it's a page name
267
233
  const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
268
234
  await emitAuditEvent({
269
- type: 'permission_check',
235
+ type: hasPermission ? 'permission_check' : 'permission_denied',
270
236
  userId,
271
237
  organisationId: scope.organisationId,
272
238
  eventId: scope.eventId,
273
239
  appId: scope.appId,
274
240
  pageId: resolvedPageId,
275
241
  permission,
276
- decision: finalDecision,
242
+ decision: hasPermission,
277
243
  source: 'api',
278
- duration_ms: _duration,
244
+ duration_ms: duration,
279
245
  cache_hit: cacheHit,
280
246
  cache_source: cacheSource,
281
247
  });
282
248
  }
283
249
 
284
- return finalDecision;
250
+ return hasPermission;
251
+
285
252
  } catch (error) {
286
- // Log security event for unexpected errors
253
+ const category = categorizeError(error);
254
+ const eventType = mapErrorCategoryToSecurityEventType(category);
255
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
256
+
287
257
  RBACSecurityValidator.logSecurityEvent({
288
- type: 'suspicious_activity',
258
+ type: eventType,
289
259
  userId,
290
- details: {
291
- error: error instanceof Error ? error.message : 'Unknown error',
260
+ details: {
261
+ error: errorMessage,
292
262
  permission,
293
- scope: JSON.stringify(scope)
263
+ scope: JSON.stringify(scope),
264
+ category,
294
265
  },
295
266
  });
296
267
 
297
- // Permission check failed - error logged via RBAC logger
268
+ // Fail securely - deny access on error
269
+ console.error('[RBACEngine] Permission check failed:', error);
298
270
  return false;
299
271
  }
300
272
  }
@@ -302,30 +274,20 @@ export class RBACEngine {
302
274
  /**
303
275
  * Get user's access level in a scope
304
276
  *
277
+ * This is derived from roles, not permissions.
278
+ *
305
279
  * @param input - Access level input
306
280
  * @returns Promise resolving to access level
307
281
  */
308
282
  async getAccessLevel(input: { userId: UUID; scope: Scope }): Promise<AccessLevel> {
309
283
  const { userId, scope } = input;
310
284
 
311
- // Check super admin first - super admins don't need context validation
312
- const isSuperAdmin = await this.checkSuperAdmin(userId);
313
- if (isSuperAdmin) {
314
- return 'super';
315
- }
316
-
317
- // Validate context requirements based on app configuration
318
- const validatedScope = await this.validateContextRequirements(scope, scope.appId);
319
- if (!validatedScope) {
320
- return 'viewer'; // Default to lowest access level for invalid context
321
- }
322
-
323
285
  // Check cache first
324
286
  const cacheKey = RBACCache.generateAccessLevelKey(
325
287
  userId,
326
- validatedScope.organisationId!,
327
- validatedScope.eventId,
328
- validatedScope.appId
288
+ scope.organisationId || '',
289
+ scope.eventId,
290
+ scope.appId
329
291
  );
330
292
 
331
293
  const cached = rbacCache.get<AccessLevel>(cacheKey);
@@ -333,63 +295,97 @@ export class RBACEngine {
333
295
  return cached;
334
296
  }
335
297
 
298
+ // Check super admin
299
+ const isSuperAdmin = await this.checkSuperAdmin(userId);
300
+ if (isSuperAdmin) {
301
+ rbacCache.set(cacheKey, 'super', 60000);
302
+ return 'super';
303
+ }
304
+
336
305
  // Check organisation role
337
- const orgRole = await this.getOrganisationRole(userId, validatedScope.organisationId!);
338
- if (orgRole === 'org_admin') {
339
- rbacCache.set(cacheKey, 'admin');
340
- return 'admin';
306
+ if (scope.organisationId) {
307
+ const { data: orgRole } = await this.supabase
308
+ .from('rbac_organisation_roles')
309
+ .select('role')
310
+ .eq('user_id', userId)
311
+ .eq('organisation_id', scope.organisationId)
312
+ .eq('status', 'active')
313
+ .is('revoked_at', null)
314
+ .single() as { data: { role: string } | null; error: any };
315
+
316
+ if (orgRole?.role === 'org_admin') {
317
+ rbacCache.set(cacheKey, 'admin', 60000);
318
+ return 'admin';
319
+ }
341
320
  }
342
321
 
343
322
  // Check event-app role
344
- if (validatedScope.eventId && validatedScope.appId) {
345
- const eventRole = await this.getEventAppRole(userId, validatedScope.eventId, validatedScope.appId);
346
- if (eventRole === 'event_admin') {
347
- rbacCache.set(cacheKey, 'admin');
323
+ if (scope.eventId && scope.appId) {
324
+ const now = new Date().toISOString();
325
+ const { data: eventRole } = await this.supabase
326
+ .from('rbac_event_app_roles')
327
+ .select('role')
328
+ .eq('user_id', userId)
329
+ .eq('event_id', scope.eventId)
330
+ .eq('app_id', scope.appId)
331
+ .eq('status', 'active')
332
+ .lte('valid_from', now)
333
+ .or(`valid_to.is.null,valid_to.gte.${now}`)
334
+ .single() as { data: { role: string } | null; error: any };
335
+
336
+ if (eventRole?.role === 'event_admin') {
337
+ rbacCache.set(cacheKey, 'admin', 60000);
348
338
  return 'admin';
349
339
  }
350
- if (eventRole === 'planner') {
351
- rbacCache.set(cacheKey, 'planner');
340
+ if (eventRole?.role === 'planner') {
341
+ rbacCache.set(cacheKey, 'planner', 60000);
352
342
  return 'planner';
353
343
  }
354
- if (eventRole === 'participant') {
355
- rbacCache.set(cacheKey, 'participant');
344
+ if (eventRole?.role === 'participant') {
345
+ rbacCache.set(cacheKey, 'participant', 60000);
356
346
  return 'participant';
357
347
  }
358
348
  }
359
349
 
360
350
  // Default to viewer
361
- rbacCache.set(cacheKey, 'viewer');
351
+ rbacCache.set(cacheKey, 'viewer', 60000);
362
352
  return 'viewer';
363
353
  }
364
354
 
365
355
  /**
366
356
  * Get user's permission map for a scope
367
357
  *
358
+ * This builds a map of page IDs to allowed operations.
359
+ * Uses the simplified RPC for each permission check.
360
+ *
368
361
  * @param input - Permission map input
369
362
  * @returns Promise resolving to permission map
370
363
  */
371
364
  async getPermissionMap(input: { userId: UUID; scope: Scope }): Promise<PermissionMap> {
372
365
  const { userId, scope } = input;
373
366
 
367
+ // Generate cache key early so it's available for super admin caching
368
+ const cacheKey = RBACCache.generatePermissionMapKey(
369
+ userId,
370
+ scope.organisationId || '',
371
+ scope.eventId,
372
+ scope.appId
373
+ );
374
+
374
375
  // Check super admin first - super admins have all permissions
375
376
  const isSuperAdmin = await this.checkSuperAdmin(userId);
376
377
  if (isSuperAdmin) {
377
- return {}; // Super admins have all permissions, return empty map
378
+ const wildcardMap: PermissionMap = { '*': true };
379
+ rbacCache.set(cacheKey, wildcardMap, 60000);
380
+ return wildcardMap;
378
381
  }
379
382
 
380
- // Validate context requirements based on app configuration
381
- const validatedScope = await this.validateContextRequirements(scope, scope.appId);
382
- if (!validatedScope) {
383
+ // Validate scope
384
+ if (!scope.organisationId) {
383
385
  return {}; // No permissions without valid context
384
386
  }
385
387
 
386
388
  // Check cache first
387
- const cacheKey = RBACCache.generatePermissionMapKey(
388
- userId,
389
- validatedScope.organisationId!,
390
- validatedScope.eventId,
391
- validatedScope.appId
392
- );
393
389
 
394
390
  const cached = rbacCache.get<PermissionMap>(cacheKey);
395
391
  if (cached) {
@@ -399,584 +395,152 @@ export class RBACEngine {
399
395
  const permissionMap: PermissionMap = {};
400
396
 
401
397
  // Get all pages for the app
402
- if (validatedScope.appId) {
398
+ if (scope.appId) {
403
399
  const { data: pages } = await this.supabase
404
400
  .from('rbac_app_pages')
405
401
  .select('id, page_name')
406
- .eq('app_id', validatedScope.appId) as { data: Array<{ id: string; page_name: string }> | null };
402
+ .eq('app_id', scope.appId) as { data: Array<{ id: string; page_name: string }> | null };
407
403
 
408
404
  if (pages) {
405
+ // Create a security context for permission checks
406
+ const securityContext: SecurityContext = {
407
+ userId,
408
+ organisationId: scope.organisationId,
409
+ timestamp: new Date(),
410
+ };
411
+
409
412
  for (const page of pages) {
410
- const operations: Operation[] = [];
411
-
412
- // Check each CRUD operation (read, create, update, delete only)
413
+ // Check each CRUD operation
413
414
  for (const operation of ['read', 'create', 'update', 'delete'] as Operation[]) {
414
- const hasPermission = await this.isPermitted({
415
- userId,
416
- scope: validatedScope,
417
- permission: `${operation}:${page.page_name}`,
418
- pageId: page.id,
419
- });
420
-
421
- if (hasPermission) {
422
- operations.push(operation);
423
- }
415
+ const hasPermission = await this.isPermitted(
416
+ {
417
+ userId,
418
+ scope,
419
+ permission: `${operation}:${page.page_name}`,
420
+ pageId: page.id,
421
+ },
422
+ securityContext
423
+ );
424
+
425
+ const permissionKey = `${operation}:${page.page_name}` as Permission;
426
+ permissionMap[permissionKey] = hasPermission;
424
427
  }
425
-
426
- permissionMap[page.id] = operations;
427
428
  }
428
429
  }
429
430
  }
430
431
 
431
- rbacCache.set(cacheKey, permissionMap);
432
+ rbacCache.set(cacheKey, permissionMap, 60000);
432
433
  return permissionMap;
433
434
  }
434
435
 
435
- /**
436
- * Check if user is super admin
437
- *
438
- * Directly queries the rbac_global_roles table to check for super_admin role.
439
- * This is consistent with how other RPC functions (rbac_permissions_get, etc.) check
440
- * for super admin status - they all use direct queries to the rbac_global_roles table.
441
- *
442
- * @param userId - User ID
443
- * @returns Promise resolving to super admin status
444
- */
445
- private async checkSuperAdmin(userId: UUID): Promise<boolean> {
446
- // Check cache first
447
- const cacheKey = `super_admin:${userId}`;
448
- const cached = rbacCache.get<boolean>(cacheKey);
449
- if (cached !== null) {
450
- return cached;
451
- }
436
+ async resolveAppContext(input: { userId: UUID; appName: string }): Promise<RBACAppContext | null> {
437
+ try {
438
+ const { userId, appName } = input;
439
+ const { data, error } = await (this.supabase as any).rpc('util_app_resolve', {
440
+ p_user_id: userId,
441
+ p_app_name: appName,
442
+ });
452
443
 
453
- // Directly query the rbac_global_roles table for super_admin role
454
- // This matches the pattern used in all RPC functions like rbac_permissions_get
455
- const now = new Date().toISOString();
456
- const { data, error } = await this.supabase
457
- .from('rbac_global_roles')
458
- .select('role')
459
- .eq('user_id', userId)
460
- .eq('role', 'super_admin')
461
- .lte('valid_from', now)
462
- .or(`valid_to.is.null,valid_to.gte.${now}`)
463
- .limit(1) as { data: Array<{ role: string }> | null; error: any };
444
+ if (error) {
445
+ console.error('[RBACEngine] Failed to resolve app context:', error);
446
+ return null;
447
+ }
464
448
 
465
- const isSuperAdmin = !error && data && data.length > 0;
466
-
467
- // Cache the result for 60 seconds
468
- rbacCache.set(cacheKey, isSuperAdmin, 60000);
469
-
470
- return Boolean(isSuperAdmin);
471
- }
449
+ if (!data || data.length === 0) {
450
+ return null;
451
+ }
472
452
 
473
- /**
474
- * Get app configuration including requires_event setting
475
- *
476
- * @param appId - App ID
477
- * @returns Promise resolving to app configuration
478
- */
479
- async getAppConfig(appId: UUID): Promise<{ requires_event: boolean } | null> {
480
- const { data, error } = await this.supabase
481
- .from('rbac_apps')
482
- .select('requires_event')
483
- .eq('id', appId)
484
- .eq('is_active', true)
485
- .single() as { data: { requires_event: boolean } | null; error: any };
453
+ const appData = data[0] as { app_id: UUID; has_access: boolean };
454
+ if (!appData?.app_id) {
455
+ return null;
456
+ }
486
457
 
487
- if (error || !data) {
458
+ return {
459
+ appId: appData.app_id,
460
+ hasAccess: appData.has_access !== false,
461
+ };
462
+ } catch (error) {
463
+ console.error('[RBACEngine] Unexpected error resolving app context:', error);
488
464
  return null;
489
465
  }
490
-
491
- return { requires_event: data.requires_event };
492
466
  }
493
467
 
494
- /**
495
- * Resolve organisation ID from event ID
496
- *
497
- * @param eventId - Event ID
498
- * @returns Promise resolving to organisation ID
499
- */
500
- private async resolveOrganisationFromEvent(eventId: string): Promise<UUID | null> {
501
- const { data, error } = await this.supabase
502
- .from('event')
503
- .select('organisation_id')
504
- .eq('id', eventId)
505
- .single() as { data: { organisation_id: string } | null; error: any };
468
+ async getRoleContext(input: { userId: UUID; scope: Scope }): Promise<RBACRoleContext> {
469
+ const result: RBACRoleContext = {
470
+ globalRole: null,
471
+ organisationRole: null,
472
+ eventAppRole: null,
473
+ };
506
474
 
507
- if (error || !data) {
508
- return null;
509
- }
475
+ try {
476
+ const { userId, scope } = input;
477
+ const { data, error } = await (this.supabase as any).rpc('rbac_permissions_get', {
478
+ p_user_id: userId,
479
+ p_organisation_id: scope.organisationId || null,
480
+ p_event_id: scope.eventId || null,
481
+ p_app_id: scope.appId || null,
482
+ });
510
483
 
511
- return data.organisation_id;
512
- }
484
+ if (error) {
485
+ console.error('[RBACEngine] Failed to load role context:', error);
486
+ return result;
487
+ }
513
488
 
514
- /**
515
- * Validate context requirements based on app configuration
516
- *
517
- * @param scope - Permission scope
518
- * @param appId - Optional app ID
519
- * @returns Promise resolving to validated scope with resolved organisation ID
520
- */
521
- private async validateContextRequirements(scope: Scope, appId?: UUID): Promise<Scope | null> {
522
- // If we have an app ID, check its requirements
523
- if (appId) {
524
- const appConfig = await this.getAppConfig(appId);
525
- if (!appConfig) {
526
- return null; // App not found or inactive
489
+ if (!Array.isArray(data)) {
490
+ return result;
527
491
  }
528
492
 
529
- if (appConfig.requires_event) {
530
- // Event-based app: requires eventId, organisationId is resolved from event
531
- if (!scope.eventId) {
532
- return null; // Event context required
493
+ for (const permission of data as RBACPermission[]) {
494
+ if (permission.permission_type === 'all_permissions') {
495
+ result.globalRole = 'super_admin';
533
496
  }
534
497
 
535
- // Resolve organisation from event if not provided
536
- if (!scope.organisationId) {
537
- const resolvedOrgId = await this.resolveOrganisationFromEvent(scope.eventId);
538
- if (!resolvedOrgId) {
539
- return null; // Could not resolve organisation from event
540
- }
541
- return {
542
- ...scope,
543
- organisationId: resolvedOrgId
544
- };
498
+ if (permission.permission_type === 'organisation_access') {
499
+ result.organisationRole = permission.role_name as any;
545
500
  }
546
501
 
547
- return scope;
548
- } else {
549
- // Organisation-based app: requires organisationId, eventId is optional
550
- if (!scope.organisationId) {
551
- return null; // Organisation context required
502
+ if (permission.permission_type === 'event_app_access') {
503
+ result.eventAppRole = permission.role_name as any;
552
504
  }
553
-
554
- return scope;
555
505
  }
556
- }
557
506
 
558
- // No app ID provided - use legacy validation (require organisation)
559
- if (!scope.organisationId) {
560
- return null; // Organisation context required for legacy behavior
507
+ return result;
508
+ } catch (error) {
509
+ console.error('[RBACEngine] Unexpected error loading role context:', error);
510
+ return result;
561
511
  }
562
-
563
- return scope;
564
512
  }
565
513
 
566
514
  /**
567
- * Collect active grants for a user in a scope
515
+ * Check if user is super admin
568
516
  *
569
517
  * @param userId - User ID
570
- * @param scope - Permission scope
571
- * @param pageId - Optional page ID
572
- * @returns Promise resolving to grants array
573
- *
574
- * PRECEDENCE ORDER (closest scope first): page → eventApp → organisation → global
518
+ * @returns Promise resolving to super admin status
575
519
  */
576
- private async collectActiveGrants(
577
- userId: UUID,
578
- scope: Scope,
579
- pageId?: UUID | string
580
- ): Promise<Grant[]> {
581
- const grants: Grant[] = [];
582
- const now = new Date().toISOString();
583
-
584
- // Pre-fetch user roles once (to avoid duplicate queries)
585
- const userRoles: string[] = [];
586
-
587
- // Get event-app roles
588
- if (scope.eventId && scope.appId) {
589
- const { data: eventRoles } = await this.supabase
590
- .from('rbac_event_app_roles')
591
- .select('role, status, valid_from, valid_to')
592
- .eq('user_id', userId)
593
- .eq('event_id', scope.eventId)
594
- .eq('app_id', scope.appId)
595
- .eq('status', 'active')
596
- .lte('valid_from', now)
597
- .or(`valid_to.is.null,valid_to.gte.${now}`) as { data: Array<{ role: string; status: string; valid_from: string; valid_to: string | null }> | null };
598
-
599
- if (eventRoles) {
600
- userRoles.push(...eventRoles.map(r => r.role));
601
- // Store event-app roles for later permission lookup
602
- // The actual permissions will be looked up from rbac_page_permissions table
603
- }
604
- }
605
-
606
- // Get organisation roles
607
- if (scope.organisationId) {
608
- const { data: orgRoles } = await this.supabase
609
- .from('rbac_organisation_roles')
610
- .select('role, status, valid_from, valid_to')
611
- .eq('user_id', userId)
612
- .eq('organisation_id', scope.organisationId)
613
- .eq('status', 'active')
614
- .lte('valid_from', now)
615
- .or(`valid_to.is.null,valid_to.gte.${now}`) as { data: Array<{ role: string; status: string; valid_from: string; valid_to: string | null }> | null };
616
-
617
- if (orgRoles) {
618
- userRoles.push(...orgRoles.map(r => r.role));
619
- // Store organisation roles for later permission lookup
620
- // The actual permissions will be looked up from rbac_page_permissions table
621
- }
622
- }
623
-
624
- console.log('[collectActiveGrants] User roles:', userRoles);
625
-
626
- // 1. PAGE GRANTS (closest scope first) - Use RPC to bypass RLS
627
- if (pageId) {
628
- // Resolve page ID if it's a page name (string)
629
- let resolvedPageId: UUID | null = null;
630
- if (typeof pageId === 'string') {
631
- // Check if it's already a UUID
632
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
633
- if (uuidRegex.test(pageId)) {
634
- resolvedPageId = pageId as UUID;
635
- } else {
636
- // It's a page name, resolve it to a page ID
637
- const appId = scope.appId;
638
- if (appId) {
639
- const { data: page } = await this.supabase
640
- .from('rbac_app_pages')
641
- .select('id')
642
- .eq('app_id', appId)
643
- .eq('page_name', pageId)
644
- .single() as { data: { id: UUID } | null };
645
- resolvedPageId = page?.id || null;
646
- }
647
- }
648
- } else {
649
- resolvedPageId = pageId;
650
- }
651
-
652
- if (resolvedPageId && scope.appId) {
653
- console.log('[collectActiveGrants] Fetching page permissions via RPC for page:', resolvedPageId);
654
-
655
- // Get the page name for building specific permission strings
656
- let pageName: string | null = null;
657
- if (typeof pageId === 'string' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(pageId)) {
658
- // pageId is already the page name
659
- pageName = pageId;
660
- } else {
661
- // Fetch page name from database
662
- const { data: page } = await this.supabase
663
- .from('rbac_app_pages')
664
- .select('page_name')
665
- .eq('id', resolvedPageId)
666
- .single() as { data: { page_name: string } | null };
667
- pageName = page?.page_name || null;
668
- }
669
-
670
- // Use RPC to get page permissions (bypasses RLS)
671
- // @ts-ignore - RPC type inference is incorrect
672
- const rpcResult = await this.supabase.rpc('rbac_permissions_get', {
673
- p_user_id: userId,
674
- p_app_id: scope.appId,
675
- p_event_id: scope.eventId || null,
676
- p_organisation_id: scope.organisationId || null,
677
- p_page_id: resolvedPageId
678
- });
679
-
680
- const { data: rpcPermissions } = rpcResult as { data: Array<{ permission_type: string; role_name: string; has_permission: boolean }> | null; error: any };
681
-
682
- console.log('[collectActiveGrants] RPC page permissions:', rpcPermissions);
683
-
684
- if (rpcPermissions && pageName) {
685
- // Filter to only page-level permissions (not org/event-app roles)
686
- const pagePerms = rpcPermissions.filter(p =>
687
- p.permission_type !== 'all_permissions' &&
688
- p.permission_type !== 'organisation_access' &&
689
- p.permission_type !== 'event_app_access'
690
- );
691
-
692
- for (const perm of pagePerms) {
693
- // Only add if user has this role
694
- if (userRoles.includes(perm.role_name)) {
695
- console.log('[collectActiveGrants] Adding page grant:', { operation: perm.permission_type, role: perm.role_name, allowed: perm.has_permission, pageName });
696
- grants.push({
697
- type: perm.has_permission ? 'allow' : 'deny',
698
- // Use permission_type directly as it already includes the page name
699
- permission: perm.permission_type as Permission,
700
- scope: 'page',
701
- source: 'rbac_page_permissions',
702
- });
703
- }
704
- }
705
- }
706
- }
520
+ private async checkSuperAdmin(userId: UUID): Promise<boolean> {
521
+ // Check cache first
522
+ const cacheKey = `super_admin:${userId}`;
523
+ const cached = rbacCache.get<boolean>(cacheKey);
524
+ if (cached !== null) {
525
+ return cached;
707
526
  }
708
527
 
709
- // 2. GLOBAL GRANTS (widest scope last)
710
- const { data: globalRoles } = await this.supabase
528
+ const now = new Date().toISOString();
529
+ const { data, error } = await this.supabase
711
530
  .from('rbac_global_roles')
712
- .select('role, valid_from, valid_to')
531
+ .select('role')
713
532
  .eq('user_id', userId)
533
+ .eq('role', 'super_admin')
714
534
  .lte('valid_from', now)
715
- .or(`valid_to.is.null,valid_to.gte.${now}`) as { data: Array<{ role: string; valid_from: string; valid_to: string | null }> | null };
535
+ .or(`valid_to.is.null,valid_to.gte.${now}`)
536
+ .limit(1) as { data: Array<{ role: string }> | null; error: any };
716
537
 
717
- if (globalRoles) {
718
- for (const role of globalRoles) {
719
- if (role.role === 'super_admin') {
720
- // Super admin gets all CRUD permissions
721
- grants.push(
722
- { type: 'allow', permission: 'read:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
723
- { type: 'allow', permission: 'create:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
724
- { type: 'allow', permission: 'update:*' as Permission, scope: 'global', source: 'rbac_global_roles' },
725
- { type: 'allow', permission: 'delete:*' as Permission, scope: 'global', source: 'rbac_global_roles' }
726
- );
727
- }
728
- }
729
- }
538
+ const isSuperAdmin = !error && data && data.length > 0;
730
539
 
731
- console.log('[collectActiveGrants] Final grants:', grants);
732
-
733
- return grants;
734
- }
735
-
736
- /**
737
- * Check page-specific permissions
738
- *
739
- * @param userId - User ID
740
- * @param pageId - Page ID
741
- * @param permission - Permission to check
742
- * @param scope - Permission scope
743
- * @returns Promise resolving to page permission result
744
- */
745
- private async checkPagePermissions(
746
- userId: UUID,
747
- pageId: UUID | string | undefined,
748
- permission: Permission,
749
- scope: Scope
750
- ): Promise<boolean> {
751
- if (!pageId) {
752
- return true; // No page restrictions
753
- }
754
-
755
- const [operation] = permission.split(':') as [Operation, string];
756
-
757
- // Get user's roles in this scope
758
- const userRoles: string[] = [];
759
-
760
- // Add organisation role
761
- if (scope.organisationId) {
762
- const orgRole = await this.getOrganisationRole(userId, scope.organisationId);
763
- if (orgRole) {
764
- userRoles.push(orgRole);
765
- }
766
- }
767
-
768
- // Add event-app role
769
- if (scope.eventId && scope.appId) {
770
- const eventRole = await this.getEventAppRole(userId, scope.eventId, scope.appId);
771
- if (eventRole) {
772
- userRoles.push(eventRole);
773
- }
774
- }
775
-
776
- // Resolve page ID if it's a page name (string)
777
- let resolvedPageId: UUID | null = null;
778
- if (typeof pageId === 'string') {
779
- // Check if it's already a UUID
780
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
781
- if (uuidRegex.test(pageId)) {
782
- resolvedPageId = pageId as UUID;
783
- } else {
784
- // It's a page name, resolve it to a page ID
785
- // First, get the app ID from scope or environment
786
- let appId = scope.appId;
787
- if (!appId) {
788
- // Try to get app ID from environment
789
- const appName = import.meta.env.VITE_APP_NAME || import.meta.env.REACT_APP_NAME;
790
- if (appName) {
791
- const { data: app } = await this.supabase
792
- .from('rbac_apps')
793
- .select('id')
794
- .eq('name', appName)
795
- .eq('is_active', true)
796
- .single() as { data: { id: UUID } | null };
797
- if (app) {
798
- appId = app.id;
799
- }
800
- }
801
- }
802
-
803
- if (appId) {
804
- const { data: page } = await this.supabase
805
- .from('rbac_app_pages')
806
- .select('id')
807
- .eq('app_id', appId)
808
- .eq('page_name', pageId)
809
- .single() as { data: { id: UUID } | null };
810
- resolvedPageId = page?.id || null;
811
- }
812
- }
813
- } else {
814
- resolvedPageId = pageId;
815
- }
816
-
817
- if (!resolvedPageId) {
818
- return false; // Page not found
819
- }
820
-
821
- // Check page permissions
822
- const { data: pagePermissions } = await this.supabase
823
- .from('rbac_page_permissions')
824
- .select('allowed')
825
- .eq('app_page_id', resolvedPageId)
826
- .eq('operation', operation)
827
- .in('role_name', userRoles)
828
- .single() as { data: { allowed: boolean } | null };
829
-
830
- return pagePermissions?.allowed ?? false;
831
- }
832
-
833
- /**
834
- * Get organisation role for a user
835
- *
836
- * @param userId - User ID
837
- * @param organisationId - Organisation ID
838
- * @returns Promise resolving to organisation role
839
- */
840
- private async getOrganisationRole(userId: UUID, organisationId: UUID): Promise<OrganisationRole | null> {
841
- const { data, error } = await this.supabase
842
- .from('rbac_organisation_roles')
843
- .select('role')
844
- .eq('user_id', userId)
845
- .eq('organisation_id', organisationId)
846
- .eq('status', 'active')
847
- .single() as { data: { role: string } | null; error: any };
848
-
849
- return error ? null : (data?.role as OrganisationRole) || null;
850
- }
851
-
852
- /**
853
- * Get event-app role for a user
854
- *
855
- * @param userId - User ID
856
- * @param eventId - Event ID
857
- * @param appId - App ID
858
- * @returns Promise resolving to event-app role
859
- */
860
- private async getEventAppRole(userId: UUID, eventId: string, appId: UUID): Promise<EventAppRole | null> {
861
- const { data, error } = await this.supabase
862
- .from('rbac_event_app_roles')
863
- .select('role, status, valid_from, valid_to')
864
- .eq('user_id', userId)
865
- .eq('event_id', eventId)
866
- .eq('app_id', appId)
867
- .eq('status', 'active')
868
- .lte('valid_from', new Date().toISOString())
869
- .or(`valid_to.is.null,valid_to.gte.${new Date().toISOString()}`)
870
- .single() as { data: { role: string; status: string; valid_from: string; valid_to: string | null } | null; error: any };
871
-
872
- return error ? null : (data?.role as EventAppRole) || null;
873
- }
874
-
875
- /**
876
- * Get permission for organisation role
877
- *
878
- * @param role - Organisation role
879
- * @returns Permission string
880
- */
881
- private getPermissionForOrgRole(role: OrganisationRole): Permission {
882
- switch (role) {
883
- case 'org_admin':
884
- return 'read:*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
885
- case 'leader':
886
- return 'read:organisation.*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
887
- case 'member':
888
- return 'read:organisation.*' as Permission;
889
- case 'supporter':
890
- return 'read:organisation.public' as Permission;
891
- default:
892
- return 'read:organisation.public' as Permission;
893
- }
894
- }
895
-
896
- /**
897
- * Get permission for event-app role
898
- *
899
- * @param role - Event-app role
900
- * @returns Permission string
901
- */
902
- private getPermissionForEventRole(role: EventAppRole): Permission {
903
- switch (role) {
904
- case 'event_admin':
905
- return 'read:event.*' as Permission; // Will be expanded to all CRUD in collectActiveGrants
906
- case 'planner':
907
- return 'read:event.planning' as Permission; // Will be expanded to all CRUD in collectActiveGrants
908
- case 'participant':
909
- return 'read:event.*' as Permission;
910
- case 'viewer':
911
- return 'read:event.public' as Permission;
912
- default:
913
- return 'read:event.public' as Permission;
914
- }
915
- }
916
-
917
- /**
918
- * Check if a permission matches another permission
919
- *
920
- * @param grantPermission - Permission from grant
921
- * @param requestedPermission - Requested permission
922
- * @returns True if permissions match
923
- */
924
- private permissionMatches(grantPermission: Permission, requestedPermission: Permission): boolean {
925
- console.log('[permissionMatches] Checking:', { grantPermission, requestedPermission });
540
+ // Cache for 60 seconds
541
+ rbacCache.set(cacheKey, isSuperAdmin, 60000);
926
542
 
927
- // Exact match
928
- if (grantPermission === requestedPermission) {
929
- console.log('[permissionMatches] Exact match found');
930
- return true;
931
- }
932
-
933
- // Wildcard match
934
- if (grantPermission.endsWith(':*') || grantPermission.endsWith('.*')) {
935
- const [grantOp, grantResource] = grantPermission.split(':');
936
- const [requestedOp, requestedResource] = requestedPermission.split(':');
937
-
938
- console.log('[permissionMatches] Wildcard check:', {
939
- grantOp,
940
- grantResource,
941
- requestedOp,
942
- requestedResource,
943
- operationsMatch: grantOp === requestedOp
944
- });
945
-
946
- if (grantOp === requestedOp) {
947
- // For wildcard permissions like "read:*" or "read:organisation.*", grantResource is "*" or "organisation.*"
948
- // We need to check if the requested resource starts with the prefix before "*"
949
- const prefix = grantResource.slice(0, -1); // Remove the "*"
950
- const matches = prefix === '' || requestedResource.startsWith(prefix);
951
- console.log('[permissionMatches] Wildcard match result:', { prefix, matches });
952
- return matches;
953
- }
954
- }
955
-
956
- // Check for other wildcard patterns
957
- if (grantPermission.includes('*')) {
958
- const [grantOp, grantResource] = grantPermission.split(':');
959
- const [requestedOp, requestedResource] = requestedPermission.split(':');
960
-
961
- console.log('[permissionMatches] Other wildcard check:', {
962
- grantOp,
963
- grantResource,
964
- requestedOp,
965
- requestedResource,
966
- operationsMatch: grantOp === requestedOp
967
- });
968
-
969
- if (grantOp === requestedOp) {
970
- // For wildcard permissions like "read:event*", grantResource is "event*"
971
- const prefix = grantResource.replace('*', '');
972
- const matches = requestedResource.startsWith(prefix);
973
- console.log('[permissionMatches] Other wildcard match result:', { prefix, matches });
974
- return matches;
975
- }
976
- }
977
-
978
- console.log('[permissionMatches] No match found');
979
- return false;
543
+ return Boolean(isSuperAdmin);
980
544
  }
981
545
 
982
546
  /**
@@ -984,7 +548,7 @@ export class RBACEngine {
984
548
  *
985
549
  * @param pageId - Page ID (UUID) or page name (string)
986
550
  * @param appId - App ID to look up the page
987
- * @returns Resolved page ID (UUID) or original pageId if it's already a UUID or can't be resolved
551
+ * @returns Resolved page ID (UUID) or original pageId
988
552
  */
989
553
  private async resolvePageId(pageId?: UUID | string, appId?: UUID): Promise<UUID | string | undefined> {
990
554
  if (!pageId) {
@@ -999,11 +563,10 @@ export class RBACEngine {
999
563
 
1000
564
  // It's a page name, but we need appId to resolve it
1001
565
  if (!appId) {
1002
- // If we can't resolve it, return the original value
1003
566
  return pageId;
1004
567
  }
1005
568
 
1006
- // It's a page name, resolve it to a page ID
569
+ // Resolve page name to UUID
1007
570
  try {
1008
571
  const { data: page } = await this.supabase
1009
572
  .from('rbac_app_pages')
@@ -1012,10 +575,10 @@ export class RBACEngine {
1012
575
  .eq('page_name', pageId)
1013
576
  .single() as { data: { id: UUID } | null };
1014
577
 
1015
- return page?.id || pageId; // Return original if not found
578
+ return page?.id || pageId;
1016
579
  } catch (error) {
1017
580
  console.warn('[RBAC Engine] Failed to resolve page name to UUID:', { pageId, appId, error });
1018
- return pageId; // Return original on error
581
+ return pageId;
1019
582
  }
1020
583
  }
1021
584
  }
@@ -1029,3 +592,4 @@ export class RBACEngine {
1029
592
  export function createRBACEngine(supabase: SupabaseClient<Database>): RBACEngine {
1030
593
  return new RBACEngine(supabase);
1031
594
  }
595
+