@jmruthers/pace-core 0.5.1 → 0.5.4

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 (210) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -55
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * @file useRBAC Hook Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/useRBAC
5
+ * @since 0.3.0
6
+ *
7
+ * Comprehensive tests for the useRBAC hook covering all critical functionality.
8
+ */
9
+
10
+ import { renderHook, waitFor } from '@testing-library/react';
11
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
12
+ import { useRBAC } from './useRBAC';
13
+ import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
14
+ import { useOrganisations } from '../../providers/OrganisationProvider';
15
+ import { useEvents } from '../../providers/EventProvider';
16
+
17
+ // Mock the providers
18
+ vi.mock('../../providers/UnifiedAuthProvider', () => ({
19
+ useUnifiedAuth: vi.fn()
20
+ }));
21
+
22
+ vi.mock('../../providers/OrganisationProvider', () => ({
23
+ useOrganisations: vi.fn()
24
+ }));
25
+
26
+ vi.mock('../../providers/EventProvider', () => ({
27
+ useEvents: vi.fn()
28
+ }));
29
+
30
+ // Mock Supabase client
31
+ const mockSupabase = {
32
+ from: vi.fn(() => ({
33
+ select: vi.fn(() => ({
34
+ eq: vi.fn(() => ({
35
+ eq: vi.fn(() => ({
36
+ single: vi.fn().mockResolvedValue({ data: { global_role: 'user' }, error: null })
37
+ }))
38
+ }))
39
+ }))
40
+ })),
41
+ rpc: vi.fn().mockResolvedValue({
42
+ data: [
43
+ {
44
+ permission_type: 'organisation_access',
45
+ role_name: 'user',
46
+ operation: 'read',
47
+ resource: 'users'
48
+ }
49
+ ],
50
+ error: null
51
+ })
52
+ };
53
+
54
+ // Mock data
55
+ const mockUser = {
56
+ id: 'user-123',
57
+ email: 'test@example.com',
58
+ user_metadata: { global_role: 'user' }
59
+ };
60
+
61
+ const mockSession = {
62
+ access_token: 'mock-token',
63
+ user: mockUser
64
+ };
65
+
66
+ const mockOrganisation = {
67
+ id: 'org-123',
68
+ name: 'Test Organisation'
69
+ };
70
+
71
+ const mockEvent = {
72
+ event_id: 'event-123',
73
+ name: 'Test Event'
74
+ };
75
+
76
+ describe('useRBAC Hook', () => {
77
+ const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
78
+ const mockUseOrganisations = vi.mocked(useOrganisations);
79
+ const mockUseEvents = vi.mocked(useEvents);
80
+
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+
84
+ // Default mock implementations
85
+ mockUseUnifiedAuth.mockReturnValue({
86
+ user: mockUser,
87
+ session: mockSession,
88
+ supabase: mockSupabase,
89
+ appName: 'test-app'
90
+ });
91
+
92
+ mockUseOrganisations.mockReturnValue({
93
+ selectedOrganisation: mockOrganisation
94
+ });
95
+
96
+ mockUseEvents.mockReturnValue({
97
+ selectedEvent: mockEvent
98
+ });
99
+ });
100
+
101
+ afterEach(() => {
102
+ vi.restoreAllMocks();
103
+ });
104
+
105
+ describe('Initialization', () => {
106
+ it('initializes with loading state', async () => {
107
+ const { result } = renderHook(() => useRBAC());
108
+
109
+ // Wait for loading to complete
110
+ await waitFor(() => {
111
+ expect(result.current.isLoading).toBe(false);
112
+ });
113
+
114
+ expect(result.current.globalRole).toBeNull();
115
+ expect(result.current.organisationRole).toBe('user');
116
+ expect(result.current.eventAppRole).toBeNull();
117
+ expect(result.current.permissions).toEqual([
118
+ {
119
+ permission_type: 'organisation_access',
120
+ role_name: 'user',
121
+ operation: 'read',
122
+ resource: 'users'
123
+ }
124
+ ]);
125
+ expect(result.current.error).toBeNull();
126
+ });
127
+
128
+ it('loads RBAC context on mount', async () => {
129
+ // Mock successful role detection
130
+ mockSupabase.from.mockReturnValue({
131
+ select: vi.fn(() => ({
132
+ eq: vi.fn(() => ({
133
+ eq: vi.fn(() => ({
134
+ single: vi.fn().mockResolvedValue({
135
+ data: { global_role: 'super_admin' },
136
+ error: null
137
+ })
138
+ }))
139
+ }))
140
+ }))
141
+ });
142
+
143
+ const { result } = renderHook(() => useRBAC());
144
+
145
+ await waitFor(() => {
146
+ expect(result.current.isLoading).toBe(false);
147
+ });
148
+ });
149
+
150
+ it('handles missing auth context gracefully', () => {
151
+ mockUseUnifiedAuth.mockReturnValue({
152
+ user: null,
153
+ session: null,
154
+ supabase: null,
155
+ appName: null
156
+ });
157
+
158
+ const { result } = renderHook(() => useRBAC());
159
+
160
+ expect(result.current.globalRole).toBeNull();
161
+ expect(result.current.organisationRole).toBeNull();
162
+ expect(result.current.eventAppRole).toBeNull();
163
+ });
164
+ });
165
+
166
+ describe('Role Detection', () => {
167
+ it('detects super admin role', async () => {
168
+ // Override the rpc mock for this test
169
+ mockSupabase.rpc.mockResolvedValue({
170
+ data: [
171
+ {
172
+ permission_type: 'all_permissions',
173
+ role_name: 'super_admin',
174
+ operation: 'read',
175
+ resource: 'users'
176
+ }
177
+ ],
178
+ error: null
179
+ });
180
+
181
+ const { result } = renderHook(() => useRBAC());
182
+
183
+ await waitFor(() => {
184
+ expect(result.current.globalRole).toBe('super_admin');
185
+ expect(result.current.isSuperAdmin).toBe(true);
186
+ });
187
+ });
188
+
189
+ it('detects organisation roles', async () => {
190
+ // Override the rpc mock for this test
191
+ mockSupabase.rpc.mockResolvedValue({
192
+ data: [
193
+ {
194
+ permission_type: 'organisation_access',
195
+ role_name: 'org_admin',
196
+ operation: 'read',
197
+ resource: 'users'
198
+ }
199
+ ],
200
+ error: null
201
+ });
202
+
203
+ const { result } = renderHook(() => useRBAC());
204
+
205
+ await waitFor(() => {
206
+ expect(result.current.organisationRole).toBe('org_admin');
207
+ expect(result.current.isOrgAdmin).toBe(true);
208
+ });
209
+ });
210
+
211
+ it('detects event app roles', async () => {
212
+ // Override the rpc mock for this test
213
+ mockSupabase.rpc.mockResolvedValue({
214
+ data: [
215
+ {
216
+ permission_type: 'event_app_access',
217
+ role_name: 'planner',
218
+ operation: 'read',
219
+ resource: 'users'
220
+ }
221
+ ],
222
+ error: null
223
+ });
224
+
225
+ const { result } = renderHook(() => useRBAC());
226
+
227
+ await waitFor(() => {
228
+ expect(result.current.eventAppRole).toBe('planner');
229
+ });
230
+ });
231
+
232
+ it('handles role hierarchy correctly', async () => {
233
+ // Override the rpc mock for this test
234
+ mockSupabase.rpc.mockResolvedValue({
235
+ data: [
236
+ {
237
+ permission_type: 'all_permissions',
238
+ role_name: 'super_admin',
239
+ operation: 'read',
240
+ resource: 'users'
241
+ },
242
+ {
243
+ permission_type: 'organisation_access',
244
+ role_name: 'org_admin',
245
+ operation: 'read',
246
+ resource: 'users'
247
+ }
248
+ ],
249
+ error: null
250
+ });
251
+
252
+ const { result } = renderHook(() => useRBAC());
253
+
254
+ await waitFor(() => {
255
+ expect(result.current.isSuperAdmin).toBe(true);
256
+ expect(result.current.isOrgAdmin).toBe(true);
257
+ });
258
+ });
259
+ });
260
+
261
+ describe('Permission Checking', () => {
262
+ it('hasPermission returns true for super admin', async () => {
263
+ // Override the rpc mock for this test
264
+ mockSupabase.rpc.mockResolvedValue({
265
+ data: [
266
+ {
267
+ permission_type: 'all_permissions',
268
+ role_name: 'super_admin',
269
+ operation: 'read',
270
+ resource: 'users'
271
+ }
272
+ ],
273
+ error: null
274
+ });
275
+
276
+ const { result } = renderHook(() => useRBAC());
277
+
278
+ await waitFor(() => {
279
+ expect(result.current.isSuperAdmin).toBe(true);
280
+ });
281
+
282
+ // Super admin should have all permissions
283
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
284
+ expect(hasPermission).toBe(true);
285
+ });
286
+
287
+ it('hasPermission checks database for regular users', async () => {
288
+ // Override the rpc mock for this test
289
+ mockSupabase.rpc.mockResolvedValue({
290
+ data: [
291
+ {
292
+ permission_type: 'organisation_access',
293
+ role_name: 'user',
294
+ operation: 'read',
295
+ resource: 'users'
296
+ }
297
+ ],
298
+ error: null
299
+ });
300
+
301
+ const { result } = renderHook(() => useRBAC());
302
+
303
+ await waitFor(() => {
304
+ expect(result.current.organisationRole).toBe('user');
305
+ });
306
+
307
+ // Mock the app lookup
308
+ mockSupabase.from.mockReturnValue({
309
+ select: vi.fn(() => ({
310
+ eq: vi.fn(() => ({
311
+ eq: vi.fn(() => ({
312
+ single: vi.fn().mockResolvedValue({
313
+ data: { id: 'app-123' },
314
+ error: null
315
+ })
316
+ }))
317
+ }))
318
+ }))
319
+ });
320
+
321
+ // Mock the permission check
322
+ mockSupabase.rpc.mockResolvedValue({
323
+ data: true,
324
+ error: null
325
+ });
326
+
327
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
328
+ expect(hasPermission).toBe(true);
329
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('check_page_permission', {
330
+ p_user_id: 'user-123',
331
+ p_app_id: 'app-123',
332
+ p_page_id: 'dashboard',
333
+ p_operation: 'read',
334
+ p_event_id: 'event-123',
335
+ p_organisation_id: 'org-123'
336
+ });
337
+ });
338
+
339
+ it('hasPermission handles errors gracefully', async () => {
340
+ // Override the rpc mock for this test
341
+ mockSupabase.rpc.mockResolvedValue({
342
+ data: [
343
+ {
344
+ permission_type: 'organisation_access',
345
+ role_name: 'user',
346
+ operation: 'read',
347
+ resource: 'users'
348
+ }
349
+ ],
350
+ error: null
351
+ });
352
+
353
+ const { result } = renderHook(() => useRBAC());
354
+
355
+ await waitFor(() => {
356
+ expect(result.current.organisationRole).toBe('user');
357
+ });
358
+
359
+ // Mock the app lookup
360
+ mockSupabase.from.mockReturnValue({
361
+ select: vi.fn(() => ({
362
+ eq: vi.fn(() => ({
363
+ eq: vi.fn(() => ({
364
+ single: vi.fn().mockResolvedValue({
365
+ data: { id: 'app-123' },
366
+ error: null
367
+ })
368
+ }))
369
+ }))
370
+ }))
371
+ });
372
+
373
+ // Mock the permission check to fail
374
+ mockSupabase.rpc.mockRejectedValue(new Error('Database error'));
375
+
376
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
377
+ expect(hasPermission).toBe(false);
378
+ });
379
+
380
+ it('hasGlobalPermission works for global roles', () => {
381
+ mockSupabase.from.mockReturnValue({
382
+ select: vi.fn(() => ({
383
+ eq: vi.fn(() => ({
384
+ eq: vi.fn(() => ({
385
+ single: vi.fn().mockResolvedValue({
386
+ data: { global_role: 'super_admin' },
387
+ error: null
388
+ })
389
+ }))
390
+ }))
391
+ }))
392
+ });
393
+
394
+ const { result } = renderHook(() => useRBAC());
395
+
396
+ // Test after role is loaded
397
+ waitFor(() => {
398
+ expect(result.current.hasGlobalPermission('super_admin')).toBe(true);
399
+ expect(result.current.hasGlobalPermission('org_admin')).toBe(true);
400
+ expect(result.current.hasGlobalPermission('invalid_permission')).toBe(false);
401
+ });
402
+ });
403
+ });
404
+
405
+ describe('Context Integration', () => {
406
+ it('integrates with organisation context', async () => {
407
+ const { result } = renderHook(() => useRBAC());
408
+
409
+ await waitFor(() => {
410
+ expect(result.current.organisationRole).toBeDefined();
411
+ });
412
+ });
413
+
414
+ it('integrates with event context', async () => {
415
+ const { result } = renderHook(() => useRBAC());
416
+
417
+ await waitFor(() => {
418
+ expect(result.current.eventAppRole).toBeDefined();
419
+ });
420
+ });
421
+
422
+ it('handles missing event context gracefully', () => {
423
+ mockUseEvents.mockImplementation(() => {
424
+ throw new Error('EventProvider not available');
425
+ });
426
+
427
+ const { result } = renderHook(() => useRBAC());
428
+
429
+ // Should not throw error and continue without event context
430
+ expect(result.current.eventAppRole).toBeNull();
431
+ });
432
+
433
+ it('handles missing organisation context gracefully', () => {
434
+ mockUseOrganisations.mockReturnValue({
435
+ selectedOrganisation: null
436
+ });
437
+
438
+ const { result } = renderHook(() => useRBAC());
439
+
440
+ expect(result.current.organisationRole).toBeNull();
441
+ });
442
+ });
443
+
444
+ describe('Error Handling', () => {
445
+ it('handles database errors during role detection', async () => {
446
+ mockSupabase.from.mockReturnValue({
447
+ select: vi.fn(() => ({
448
+ eq: vi.fn(() => ({
449
+ eq: vi.fn(() => ({
450
+ single: vi.fn().mockRejectedValue(new Error('Database error'))
451
+ }))
452
+ }))
453
+ }))
454
+ });
455
+
456
+ const { result } = renderHook(() => useRBAC());
457
+
458
+ await waitFor(() => {
459
+ expect(result.current.error).toBeDefined();
460
+ expect(result.current.isLoading).toBe(false);
461
+ });
462
+ });
463
+
464
+ it('handles missing app data gracefully', async () => {
465
+ mockSupabase.from.mockReturnValue({
466
+ select: vi.fn(() => ({
467
+ eq: vi.fn(() => ({
468
+ eq: vi.fn(() => ({
469
+ single: vi.fn().mockResolvedValue({
470
+ data: null,
471
+ error: { message: 'App not found' }
472
+ })
473
+ }))
474
+ }))
475
+ }))
476
+ });
477
+
478
+ const { result } = renderHook(() => useRBAC());
479
+
480
+ await waitFor(() => {
481
+ expect(result.current.globalRole).toBeNull();
482
+ });
483
+ });
484
+ });
485
+
486
+ describe('Performance', () => {
487
+ it('maintains stable references for same inputs', () => {
488
+ const { result, rerender } = renderHook(() => useRBAC());
489
+
490
+ const firstRender = result.current;
491
+ rerender();
492
+ const secondRender = result.current;
493
+
494
+ // Functions should be stable
495
+ expect(firstRender.hasPermission).toBe(secondRender.hasPermission);
496
+ expect(firstRender.hasGlobalPermission).toBe(secondRender.hasGlobalPermission);
497
+ });
498
+
499
+ it('handles rapid re-renders efficiently', () => {
500
+ const { result, rerender } = renderHook(() => useRBAC());
501
+
502
+ // Rapid re-renders should not cause issues
503
+ for (let i = 0; i < 10; i++) {
504
+ rerender();
505
+ }
506
+
507
+ expect(result.current).toBeDefined();
508
+ });
509
+ });
510
+
511
+ describe('Edge Cases', () => {
512
+ it('handles null user gracefully', () => {
513
+ mockUseUnifiedAuth.mockReturnValue({
514
+ user: null,
515
+ session: null,
516
+ supabase: mockSupabase,
517
+ appName: 'test-app'
518
+ });
519
+
520
+ const { result } = renderHook(() => useRBAC());
521
+
522
+ expect(result.current.globalRole).toBeNull();
523
+ });
524
+
525
+ it('handles missing session gracefully', () => {
526
+ mockUseUnifiedAuth.mockReturnValue({
527
+ user: mockUser,
528
+ session: null,
529
+ supabase: mockSupabase,
530
+ appName: 'test-app'
531
+ });
532
+
533
+ const { result } = renderHook(() => useRBAC());
534
+
535
+ expect(result.current.globalRole).toBeNull();
536
+ });
537
+
538
+ it('handles missing supabase client gracefully', () => {
539
+ mockUseUnifiedAuth.mockReturnValue({
540
+ user: mockUser,
541
+ session: mockSession,
542
+ supabase: null,
543
+ appName: 'test-app'
544
+ });
545
+
546
+ const { result } = renderHook(() => useRBAC());
547
+
548
+ expect(result.current.globalRole).toBeNull();
549
+ });
550
+ });
551
+ });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file RBAC Hook
3
3
  * @package @jmruthers/pace-core
4
- * @module Hooks/RBAC
4
+ * @module RBAC/Hooks
5
5
  * @since 0.3.0
6
6
  *
7
7
  * A React hook that provides access to the new RBAC (Role-Based Access Control) system.
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * @example
19
19
  * ```tsx
20
- * import { useRBAC } from '@jmruthers/pace-core';
20
+ * import { useRBAC } from '@jmruthers/pace-core/rbac';
21
21
  *
22
22
  * function MyComponent() {
23
23
  * const {
@@ -67,9 +67,9 @@
67
67
  */
68
68
 
69
69
  import { useState, useEffect, useCallback, useMemo } from 'react';
70
- import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
71
- import { useOrganisations } from '../providers/OrganisationProvider';
72
- import { useEvents } from '../providers/EventProvider';
70
+ import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
71
+ import { useOrganisations } from '../../providers/OrganisationProvider';
72
+ import { useEvents } from '../../providers/EventProvider';
73
73
  import type {
74
74
  UserRBACContext,
75
75
  GlobalRole,
@@ -77,7 +77,7 @@ import type {
77
77
  EventAppRole,
78
78
  Operation,
79
79
  RBACPermission
80
- } from '../rbac/types';
80
+ } from '../types';
81
81
 
82
82
  export function useRBAC(pageId?: string): UserRBACContext {
83
83
  const { user, session, supabase, appName } = useUnifiedAuth();
@@ -259,4 +259,4 @@ export function useRBAC(pageId?: string): UserRBACContext {
259
259
  isLoading,
260
260
  error
261
261
  };
262
- }
262
+ }
package/src/rbac/index.ts CHANGED
@@ -74,16 +74,11 @@ export {
74
74
  // Components (NEW - Phase 1 & 2)
75
75
  export * from './components';
76
76
 
77
- // Hooks
78
- export {
79
- usePermissions,
80
- useCan,
81
- useAccessLevel,
82
- useMultiplePermissions,
83
- useHasAnyPermission,
84
- useHasAllPermissions,
85
- useCachedPermissions,
86
- } from './hooks';
77
+ // Hooks - Consolidated from rbac/hooks/
78
+ export * from './hooks';
79
+
80
+ // Providers - Consolidated from rbac/providers/
81
+ export * from './providers';
87
82
 
88
83
  // Adapters
89
84
  export {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file RBAC Provider
3
3
  * @package @jmruthers/pace-core
4
- * @module Providers/RBAC
4
+ * @module RBAC/Providers
5
5
  * @since 0.1.0
6
6
  *
7
7
  * Handles role-based access control, permissions, and event access management.
@@ -10,8 +10,8 @@
10
10
 
11
11
  import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
12
12
  import { type SupabaseClient, type User, type Session } from '@supabase/supabase-js';
13
- import { AccessLevel } from '../types/unified';
14
- import { DebugLogger } from '../utils/debugLogger';
13
+ import { AccessLevel } from '../../types/unified';
14
+ import { DebugLogger } from '../../utils/debugLogger';
15
15
 
16
16
  // App configuration type
17
17
  interface AppConfig {
@@ -215,7 +215,7 @@ export function RBACProvider({
215
215
  const loadAppConfig = async () => {
216
216
  try {
217
217
  // Use the same app name resolution as PagePermissionGuard
218
- const { getCurrentAppName } = await import('../utils/appNameResolver');
218
+ const { getCurrentAppName } = await import('../../utils/appNameResolver');
219
219
  const resolvedAppName = getCurrentAppName() || appName;
220
220
 
221
221
  // First resolve app name to app_id
@@ -371,7 +371,7 @@ export function RBACProvider({
371
371
 
372
372
  try {
373
373
  // Use the same app name resolution as PagePermissionGuard
374
- const { getCurrentAppName } = await import('../utils/appNameResolver');
374
+ const { getCurrentAppName } = await import('../../utils/appNameResolver');
375
375
  const resolvedAppName = getCurrentAppName() || appName;
376
376
 
377
377
  // First resolve app name to app_id
@@ -422,7 +422,7 @@ export function RBACProvider({
422
422
  setEventAccessLoading(true);
423
423
  try {
424
424
  // Use the same app name resolution as PagePermissionGuard
425
- const { getCurrentAppName } = await import('../utils/appNameResolver');
425
+ const { getCurrentAppName } = await import('../../utils/appNameResolver');
426
426
  const resolvedAppName = getCurrentAppName() || appName;
427
427
 
428
428
  // First resolve app name to app_id