@jmruthers/pace-core 0.5.185 → 0.5.186

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 (205) hide show
  1. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +4 -2
  2. package/dist/{chunk-STTZQK2I.js → chunk-DAGICKHT.js} +7 -5
  3. package/dist/chunk-DAGICKHT.js.map +1 -0
  4. package/dist/{chunk-AISXLWGZ.js → chunk-GRIQLQ52.js} +2 -2
  5. package/dist/{chunk-HC67NW5K.js → chunk-HDCUMOOI.js} +125 -47
  6. package/dist/chunk-HDCUMOOI.js.map +1 -0
  7. package/dist/{chunk-OKI34GZD.js → chunk-OALXJH4Y.js} +2 -2
  8. package/dist/{chunk-MX3EIJGQ.js → chunk-TC7D3CR3.js} +86 -7
  9. package/dist/chunk-TC7D3CR3.js.map +1 -0
  10. package/dist/{chunk-IXSNYUCT.js → chunk-UQWSHFVX.js} +1 -1
  11. package/dist/chunk-UQWSHFVX.js.map +1 -0
  12. package/dist/components.d.ts +2 -2
  13. package/dist/components.js +3 -3
  14. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  15. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-PRTSLxKx.d.ts} +3 -0
  16. package/dist/hooks.d.ts +49 -5
  17. package/dist/hooks.js +6 -4
  18. package/dist/hooks.js.map +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +9 -8
  21. package/dist/index.js.map +1 -1
  22. package/dist/rbac/index.d.ts +1 -1
  23. package/dist/rbac/index.js +2 -2
  24. package/dist/types.d.ts +2 -2
  25. package/dist/types.js +1 -1
  26. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +2 -2
  27. package/dist/utils.d.ts +1 -1
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/Logger.md +1 -1
  32. package/docs/api/classes/MissingUserContextError.md +1 -1
  33. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  34. package/docs/api/classes/PermissionDeniedError.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +2 -2
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +2 -2
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/enums/LogLevel.md +1 -1
  44. package/docs/api/enums/RBACErrorCode.md +1 -1
  45. package/docs/api/enums/RPCFunction.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/BadgeProps.md +1 -1
  48. package/docs/api/interfaces/ButtonProps.md +1 -1
  49. package/docs/api/interfaces/CalendarProps.md +1 -1
  50. package/docs/api/interfaces/CardProps.md +1 -1
  51. package/docs/api/interfaces/ColorPalette.md +1 -1
  52. package/docs/api/interfaces/ColorShade.md +1 -1
  53. package/docs/api/interfaces/ComplianceResult.md +1 -1
  54. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  55. package/docs/api/interfaces/DataRecord.md +1 -1
  56. package/docs/api/interfaces/DataTableAction.md +1 -1
  57. package/docs/api/interfaces/DataTableColumn.md +1 -1
  58. package/docs/api/interfaces/DataTableProps.md +1 -1
  59. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  60. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  61. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  62. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  63. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  64. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  65. package/docs/api/interfaces/ExportColumn.md +1 -1
  66. package/docs/api/interfaces/ExportOptions.md +1 -1
  67. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  68. package/docs/api/interfaces/FileMetadata.md +1 -1
  69. package/docs/api/interfaces/FileReference.md +1 -1
  70. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  71. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  72. package/docs/api/interfaces/FileUploadProps.md +36 -14
  73. package/docs/api/interfaces/FooterProps.md +1 -1
  74. package/docs/api/interfaces/FormFieldProps.md +1 -1
  75. package/docs/api/interfaces/FormProps.md +1 -1
  76. package/docs/api/interfaces/GrantEventAppRoleParams.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/LoggerConfig.md +1 -1
  81. package/docs/api/interfaces/LoginFormProps.md +1 -1
  82. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  83. package/docs/api/interfaces/NavigationContextType.md +1 -1
  84. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  85. package/docs/api/interfaces/NavigationItem.md +1 -1
  86. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  87. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  88. package/docs/api/interfaces/Organisation.md +1 -1
  89. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  90. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  91. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  92. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  93. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  94. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  95. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  96. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  97. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  98. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  99. package/docs/api/interfaces/PaletteData.md +1 -1
  100. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  101. package/docs/api/interfaces/ProgressProps.md +1 -1
  102. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  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/QuickFix.md +1 -1
  107. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  108. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  109. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  110. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  111. package/docs/api/interfaces/RBACConfig.md +2 -2
  112. package/docs/api/interfaces/RBACContext.md +1 -1
  113. package/docs/api/interfaces/RBACLogger.md +1 -1
  114. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  115. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  116. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  117. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  118. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  119. package/docs/api/interfaces/RBACResult.md +1 -1
  120. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  121. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  122. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  123. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  124. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  125. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  126. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  127. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  128. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  129. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  130. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  131. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  132. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  134. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  135. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  136. package/docs/api/interfaces/RouteConfig.md +1 -1
  137. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  138. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  139. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  140. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  141. package/docs/api/interfaces/SetupIssue.md +1 -1
  142. package/docs/api/interfaces/StorageConfig.md +1 -1
  143. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  144. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  145. package/docs/api/interfaces/StorageListOptions.md +1 -1
  146. package/docs/api/interfaces/StorageListResult.md +1 -1
  147. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  148. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  149. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  150. package/docs/api/interfaces/StyleImport.md +1 -1
  151. package/docs/api/interfaces/SwitchProps.md +1 -1
  152. package/docs/api/interfaces/TabsContentProps.md +1 -1
  153. package/docs/api/interfaces/TabsListProps.md +1 -1
  154. package/docs/api/interfaces/TabsProps.md +1 -1
  155. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  156. package/docs/api/interfaces/TextareaProps.md +1 -1
  157. package/docs/api/interfaces/ToastActionElement.md +1 -1
  158. package/docs/api/interfaces/ToastProps.md +1 -1
  159. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  160. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  161. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  162. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  163. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  164. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  165. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  166. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  167. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  168. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  169. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  170. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  171. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  172. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  173. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  174. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  175. package/docs/api/interfaces/UserEventAccess.md +1 -1
  176. package/docs/api/interfaces/UserMenuProps.md +1 -1
  177. package/docs/api/interfaces/UserProfile.md +1 -1
  178. package/docs/api/modules.md +17 -17
  179. package/docs/api-reference/components.md +26 -12
  180. package/docs/implementation-guides/file-reference-system.md +24 -2
  181. package/docs/implementation-guides/file-upload-storage.md +9 -1
  182. package/package.json +1 -1
  183. package/scripts/check-pace-core-compliance.js +512 -0
  184. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  185. package/src/components/FileUpload/FileUpload.tsx +7 -1
  186. package/src/components/Header/Header.tsx +2 -5
  187. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  188. package/src/hooks/index.ts +3 -0
  189. package/src/hooks/useFileReference.test.ts +1 -0
  190. package/src/hooks/usePreventTabReload.ts +106 -0
  191. package/src/hooks/useSecureDataAccess.ts +2 -2
  192. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  193. package/src/styles/core.css +5 -5
  194. package/src/types/database.generated.ts +63 -9
  195. package/src/types/file-reference.ts +3 -0
  196. package/src/utils/file-reference/__tests__/file-reference.test.ts +58 -4
  197. package/src/utils/file-reference/index.ts +12 -2
  198. package/src/utils/security/secureDataAccess.ts +1 -1
  199. package/src/utils/storage/helpers.ts +68 -0
  200. package/dist/chunk-HC67NW5K.js.map +0 -1
  201. package/dist/chunk-IXSNYUCT.js.map +0 -1
  202. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  203. package/dist/chunk-STTZQK2I.js.map +0 -1
  204. /package/dist/{chunk-AISXLWGZ.js.map → chunk-GRIQLQ52.js.map} +0 -0
  205. /package/dist/{chunk-OKI34GZD.js.map → chunk-OALXJH4Y.js.map} +0 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * @file RBAC Role Isolation Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Tests
5
+ * @since 2.0.0
6
+ *
7
+ * Regression tests for RBAC role isolation security fix.
8
+ *
9
+ * These tests verify that organisation roles (e.g., 'leader', 'member') do NOT
10
+ * implicitly grant event-app page permissions. Only the user's actual event-app
11
+ * role (e.g., 'planner', 'event_admin') should determine page permissions.
12
+ *
13
+ * Bug Reference: Organisation role bypasses event-app page permissions
14
+ * Security Impact: HIGH
15
+ */
16
+
17
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
18
+ import { RBACEngine } from '../engine';
19
+ import {
20
+ UUID,
21
+ Permission,
22
+ Scope,
23
+ PermissionCheck
24
+ } from '../types';
25
+ import { rbacCache } from '../cache';
26
+
27
+ // Mock Supabase client
28
+ const createMockSupabaseClient = () => ({
29
+ from: vi.fn(() => ({
30
+ select: vi.fn().mockReturnThis(),
31
+ eq: vi.fn().mockReturnThis(),
32
+ neq: vi.fn().mockReturnThis(),
33
+ in: vi.fn().mockReturnThis(),
34
+ is: vi.fn().mockReturnThis(),
35
+ lte: vi.fn().mockReturnThis(),
36
+ or: vi.fn().mockReturnThis(),
37
+ limit: vi.fn().mockResolvedValue({
38
+ data: [],
39
+ error: null
40
+ }),
41
+ single: vi.fn(),
42
+ maybeSingle: vi.fn(),
43
+ })),
44
+ rpc: vi.fn(),
45
+ });
46
+
47
+ // Test data matching the bug report scenario
48
+ const testData = {
49
+ userId: '00000000-0000-0000-0000-000000000001' as UUID,
50
+ organisationId: '00000000-0000-0000-0000-000000000002' as UUID, // scouts-victoria
51
+ eventId: 'baloo-bistro-event-123',
52
+ appId: '00000000-0000-0000-0000-000000000003' as UUID, // BASE app
53
+ pageId: '00000000-0000-0000-0000-000000000004' as UUID, // configuration page
54
+ pageName: 'configuration'
55
+ };
56
+
57
+ describe('RBAC Role Isolation Tests', () => {
58
+ let engine: RBACEngine;
59
+ let mockSupabase: any;
60
+
61
+ beforeEach(() => {
62
+ mockSupabase = createMockSupabaseClient();
63
+ engine = new RBACEngine(mockSupabase as any);
64
+ rbacCache.clear();
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.clearAllMocks();
69
+ rbacCache.clear();
70
+ });
71
+
72
+ describe('Organisation Role vs Event-App Role Isolation', () => {
73
+ /**
74
+ * Bug Scenario:
75
+ * - User has organisation role: 'leader' (for scouts-victoria)
76
+ * - User has event-app role: 'planner' (for BASE app)
77
+ * - Page permissions: 'event_admin' has full CRUD, 'planner' has only 'read'
78
+ * - User should NOT get 'update' permission just because they are a 'leader'
79
+ */
80
+ it('should deny update permission when user has leader org role but planner event-app role', async () => {
81
+ // Mock: rbac_check_permission_simplified should return FALSE
82
+ // because 'planner' only has 'read' permission, not 'update'
83
+ // The 'leader' org role should NOT grant implicit page permissions
84
+ mockSupabase.rpc.mockResolvedValue({
85
+ data: false,
86
+ error: null
87
+ });
88
+
89
+ const scope: Scope = {
90
+ organisationId: testData.organisationId,
91
+ eventId: testData.eventId,
92
+ appId: testData.appId
93
+ };
94
+
95
+ const permissionCheck: PermissionCheck = {
96
+ userId: testData.userId,
97
+ scope,
98
+ permission: 'update:page.configuration' as Permission,
99
+ pageId: testData.pageId
100
+ };
101
+
102
+ const securityContext = {
103
+ userId: testData.userId,
104
+ organisationId: testData.organisationId,
105
+ timestamp: new Date()
106
+ };
107
+
108
+ const result = await engine.isPermitted(permissionCheck, securityContext);
109
+
110
+ // CRITICAL: Permission must be denied
111
+ expect(result).toBe(false);
112
+
113
+ // Verify the RPC was called with correct parameters
114
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
115
+ 'rbac_check_permission_simplified',
116
+ expect.objectContaining({
117
+ p_user_id: testData.userId,
118
+ p_permission: 'update:page.configuration',
119
+ p_organisation_id: testData.organisationId,
120
+ p_event_id: testData.eventId,
121
+ p_app_id: testData.appId,
122
+ p_page_id: testData.pageId
123
+ })
124
+ );
125
+ });
126
+
127
+ it('should allow read permission when user has planner event-app role', async () => {
128
+ // Mock: rbac_check_permission_simplified should return TRUE
129
+ // because 'planner' has 'read' permission
130
+ mockSupabase.rpc.mockResolvedValue({
131
+ data: true,
132
+ error: null
133
+ });
134
+
135
+ const scope: Scope = {
136
+ organisationId: testData.organisationId,
137
+ eventId: testData.eventId,
138
+ appId: testData.appId
139
+ };
140
+
141
+ const permissionCheck: PermissionCheck = {
142
+ userId: testData.userId,
143
+ scope,
144
+ permission: 'read:page.configuration' as Permission,
145
+ pageId: testData.pageId
146
+ };
147
+
148
+ const securityContext = {
149
+ userId: testData.userId,
150
+ organisationId: testData.organisationId,
151
+ timestamp: new Date()
152
+ };
153
+
154
+ const result = await engine.isPermitted(permissionCheck, securityContext);
155
+
156
+ expect(result).toBe(true);
157
+ });
158
+
159
+ it('should deny delete permission when user has planner event-app role', async () => {
160
+ // 'planner' should NOT have delete permission
161
+ mockSupabase.rpc.mockResolvedValue({
162
+ data: false,
163
+ error: null
164
+ });
165
+
166
+ const scope: Scope = {
167
+ organisationId: testData.organisationId,
168
+ eventId: testData.eventId,
169
+ appId: testData.appId
170
+ };
171
+
172
+ const permissionCheck: PermissionCheck = {
173
+ userId: testData.userId,
174
+ scope,
175
+ permission: 'delete:page.configuration' as Permission,
176
+ pageId: testData.pageId
177
+ };
178
+
179
+ const securityContext = {
180
+ userId: testData.userId,
181
+ organisationId: testData.organisationId,
182
+ timestamp: new Date()
183
+ };
184
+
185
+ const result = await engine.isPermitted(permissionCheck, securityContext);
186
+
187
+ expect(result).toBe(false);
188
+ });
189
+
190
+ it('should deny create permission when user has planner event-app role', async () => {
191
+ // 'planner' should NOT have create permission
192
+ mockSupabase.rpc.mockResolvedValue({
193
+ data: false,
194
+ error: null
195
+ });
196
+
197
+ const scope: Scope = {
198
+ organisationId: testData.organisationId,
199
+ eventId: testData.eventId,
200
+ appId: testData.appId
201
+ };
202
+
203
+ const permissionCheck: PermissionCheck = {
204
+ userId: testData.userId,
205
+ scope,
206
+ permission: 'create:page.configuration' as Permission,
207
+ pageId: testData.pageId
208
+ };
209
+
210
+ const securityContext = {
211
+ userId: testData.userId,
212
+ organisationId: testData.organisationId,
213
+ timestamp: new Date()
214
+ };
215
+
216
+ const result = await engine.isPermitted(permissionCheck, securityContext);
217
+
218
+ expect(result).toBe(false);
219
+ });
220
+ });
221
+
222
+ describe('Event Admin Role Permissions', () => {
223
+ it('should allow full CRUD when user has event_admin role', async () => {
224
+ // event_admin should have all permissions
225
+ mockSupabase.rpc.mockResolvedValue({
226
+ data: true,
227
+ error: null
228
+ });
229
+
230
+ const scope: Scope = {
231
+ organisationId: testData.organisationId,
232
+ eventId: testData.eventId,
233
+ appId: testData.appId
234
+ };
235
+
236
+ const securityContext = {
237
+ userId: testData.userId,
238
+ organisationId: testData.organisationId,
239
+ timestamp: new Date()
240
+ };
241
+
242
+ const operations = ['read', 'create', 'update', 'delete'];
243
+
244
+ for (const operation of operations) {
245
+ const permissionCheck: PermissionCheck = {
246
+ userId: testData.userId,
247
+ scope,
248
+ permission: `${operation}:page.configuration` as Permission,
249
+ pageId: testData.pageId
250
+ };
251
+
252
+ const result = await engine.isPermitted(permissionCheck, securityContext);
253
+ expect(result).toBe(true);
254
+ }
255
+ });
256
+ });
257
+
258
+ describe('Super Admin Bypass', () => {
259
+ it('should allow all permissions for super_admin regardless of event-app role', async () => {
260
+ // Super admin bypasses all checks
261
+ mockSupabase.rpc.mockResolvedValue({
262
+ data: true,
263
+ error: null
264
+ });
265
+
266
+ const scope: Scope = {
267
+ organisationId: testData.organisationId,
268
+ eventId: testData.eventId,
269
+ appId: testData.appId
270
+ };
271
+
272
+ const securityContext = {
273
+ userId: testData.userId,
274
+ organisationId: testData.organisationId,
275
+ timestamp: new Date()
276
+ };
277
+
278
+ const permissionCheck: PermissionCheck = {
279
+ userId: testData.userId,
280
+ scope,
281
+ permission: 'delete:page.configuration' as Permission,
282
+ pageId: testData.pageId
283
+ };
284
+
285
+ const result = await engine.isPermitted(permissionCheck, securityContext);
286
+
287
+ expect(result).toBe(true);
288
+ });
289
+ });
290
+
291
+ describe('Org Admin Bypass', () => {
292
+ it('should allow org_admin to have all permissions within their organisation', async () => {
293
+ // org_admin has all permissions within their org (org-level bypass)
294
+ mockSupabase.rpc.mockResolvedValue({
295
+ data: true,
296
+ error: null
297
+ });
298
+
299
+ const scope: Scope = {
300
+ organisationId: testData.organisationId,
301
+ eventId: testData.eventId,
302
+ appId: testData.appId
303
+ };
304
+
305
+ const securityContext = {
306
+ userId: testData.userId,
307
+ organisationId: testData.organisationId,
308
+ timestamp: new Date()
309
+ };
310
+
311
+ const permissionCheck: PermissionCheck = {
312
+ userId: testData.userId,
313
+ scope,
314
+ permission: 'update:page.configuration' as Permission,
315
+ pageId: testData.pageId
316
+ };
317
+
318
+ const result = await engine.isPermitted(permissionCheck, securityContext);
319
+
320
+ expect(result).toBe(true);
321
+ });
322
+ });
323
+
324
+ describe('Role Isolation Edge Cases', () => {
325
+ it('should not leak permissions from organisation role to event-app context', async () => {
326
+ // Even if user has a high-level org role (like 'leader'), they should NOT
327
+ // get event-app page permissions unless their event-app role grants it
328
+ mockSupabase.rpc.mockResolvedValue({
329
+ data: false,
330
+ error: null
331
+ });
332
+
333
+ const scope: Scope = {
334
+ organisationId: testData.organisationId,
335
+ eventId: testData.eventId,
336
+ appId: testData.appId
337
+ };
338
+
339
+ const securityContext = {
340
+ userId: testData.userId,
341
+ organisationId: testData.organisationId,
342
+ timestamp: new Date()
343
+ };
344
+
345
+ // Test with page name instead of UUID (the bug could manifest differently)
346
+ const permissionCheck: PermissionCheck = {
347
+ userId: testData.userId,
348
+ scope,
349
+ permission: 'update:page.configuration' as Permission,
350
+ pageId: testData.pageName // Using page name
351
+ };
352
+
353
+ const result = await engine.isPermitted(permissionCheck, securityContext);
354
+
355
+ expect(result).toBe(false);
356
+ });
357
+
358
+ it('should deny permission when user has no event-app role at all', async () => {
359
+ // User with org membership but no event-app role should be denied
360
+ mockSupabase.rpc.mockResolvedValue({
361
+ data: false,
362
+ error: null
363
+ });
364
+
365
+ const scope: Scope = {
366
+ organisationId: testData.organisationId,
367
+ eventId: testData.eventId,
368
+ appId: testData.appId
369
+ };
370
+
371
+ const securityContext = {
372
+ userId: testData.userId,
373
+ organisationId: testData.organisationId,
374
+ timestamp: new Date()
375
+ };
376
+
377
+ const permissionCheck: PermissionCheck = {
378
+ userId: testData.userId,
379
+ scope,
380
+ permission: 'read:page.configuration' as Permission,
381
+ pageId: testData.pageId
382
+ };
383
+
384
+ const result = await engine.isPermitted(permissionCheck, securityContext);
385
+
386
+ expect(result).toBe(false);
387
+ });
388
+
389
+ it('should handle mixed permission checks correctly', async () => {
390
+ // Test scenario: user has 'planner' role which grants 'read' but not 'update'
391
+ // First call returns true (read), subsequent calls return false (update, create, delete)
392
+ const rpcResponses = [
393
+ { data: true, error: null }, // read: allowed
394
+ { data: false, error: null }, // update: denied
395
+ { data: false, error: null }, // create: denied
396
+ { data: false, error: null } // delete: denied
397
+ ];
398
+
399
+ let callIndex = 0;
400
+ mockSupabase.rpc.mockImplementation(() => {
401
+ const response = rpcResponses[callIndex];
402
+ callIndex++;
403
+ return Promise.resolve(response);
404
+ });
405
+
406
+ const scope: Scope = {
407
+ organisationId: testData.organisationId,
408
+ eventId: testData.eventId,
409
+ appId: testData.appId
410
+ };
411
+
412
+ const securityContext = {
413
+ userId: testData.userId,
414
+ organisationId: testData.organisationId,
415
+ timestamp: new Date()
416
+ };
417
+
418
+ // Test read permission (should be allowed for planner)
419
+ const readResult = await engine.isPermitted({
420
+ userId: testData.userId,
421
+ scope,
422
+ permission: 'read:page.configuration' as Permission,
423
+ pageId: testData.pageId
424
+ }, securityContext);
425
+ expect(readResult).toBe(true);
426
+
427
+ // Test update permission (should be denied for planner)
428
+ const updateResult = await engine.isPermitted({
429
+ userId: testData.userId,
430
+ scope,
431
+ permission: 'update:page.configuration' as Permission,
432
+ pageId: testData.pageId
433
+ }, securityContext);
434
+ expect(updateResult).toBe(false);
435
+
436
+ // Test create permission (should be denied for planner)
437
+ const createResult = await engine.isPermitted({
438
+ userId: testData.userId,
439
+ scope,
440
+ permission: 'create:page.configuration' as Permission,
441
+ pageId: testData.pageId
442
+ }, securityContext);
443
+ expect(createResult).toBe(false);
444
+
445
+ // Test delete permission (should be denied for planner)
446
+ const deleteResult = await engine.isPermitted({
447
+ userId: testData.userId,
448
+ scope,
449
+ permission: 'delete:page.configuration' as Permission,
450
+ pageId: testData.pageId
451
+ }, securityContext);
452
+ expect(deleteResult).toBe(false);
453
+ });
454
+ });
455
+ });
456
+
@@ -240,14 +240,14 @@
240
240
  /* Custom utility styles go here */
241
241
 
242
242
 
243
- /* Hide spinner arrows on number inputs in DataTable */
244
- .datatable-number-no-spinners::-webkit-inner-spin-button,
245
- .datatable-number-no-spinners::-webkit-outer-spin-button {
243
+ /* Hide spinner arrows on all number inputs (modern UX convention) */
244
+ input[type="number"]::-webkit-inner-spin-button,
245
+ input[type="number"]::-webkit-outer-spin-button {
246
246
  -webkit-appearance: none;
247
247
  margin: 0;
248
248
  }
249
-
250
- .datatable-number-no-spinners {
249
+
250
+ input[type="number"] {
251
251
  -moz-appearance: textfield;
252
252
  }
253
253
  }
@@ -3320,13 +3320,13 @@ export type Database = {
3320
3320
  },
3321
3321
  ]
3322
3322
  }
3323
- pace_id_documents: {
3323
+ pace_identification: {
3324
3324
  Row: {
3325
3325
  created_at: string | null
3326
3326
  document_number: string | null
3327
- document_type: string
3328
3327
  expiry_date: string | null
3329
3328
  id: string
3329
+ identification_type_id: number | null
3330
3330
  issue_city: string | null
3331
3331
  issue_country: string | null
3332
3332
  issue_date: string | null
@@ -3339,9 +3339,9 @@ export type Database = {
3339
3339
  Insert: {
3340
3340
  created_at?: string | null
3341
3341
  document_number?: string | null
3342
- document_type: string
3343
3342
  expiry_date?: string | null
3344
3343
  id?: string
3344
+ identification_type_id?: number | null
3345
3345
  issue_city?: string | null
3346
3346
  issue_country?: string | null
3347
3347
  issue_date?: string | null
@@ -3354,9 +3354,9 @@ export type Database = {
3354
3354
  Update: {
3355
3355
  created_at?: string | null
3356
3356
  document_number?: string | null
3357
- document_type?: string
3358
3357
  expiry_date?: string | null
3359
3358
  id?: string
3359
+ identification_type_id?: number | null
3360
3360
  issue_city?: string | null
3361
3361
  issue_country?: string | null
3362
3362
  issue_date?: string | null
@@ -3368,14 +3368,21 @@ export type Database = {
3368
3368
  }
3369
3369
  Relationships: [
3370
3370
  {
3371
- foreignKeyName: "fk_pace_id_documents_organisation_id"
3371
+ foreignKeyName: "fk_pace_identification_organisation_id"
3372
3372
  columns: ["organisation_id"]
3373
3373
  isOneToOne: false
3374
3374
  referencedRelation: "organisations"
3375
3375
  referencedColumns: ["id"]
3376
3376
  },
3377
3377
  {
3378
- foreignKeyName: "identification_documents_member_id_fkey"
3378
+ foreignKeyName: "fk_pace_identification_type_id"
3379
+ columns: ["identification_type_id"]
3380
+ isOneToOne: false
3381
+ referencedRelation: "pace_identification_type"
3382
+ referencedColumns: ["id"]
3383
+ },
3384
+ {
3385
+ foreignKeyName: "pace_identification_member_id_fkey"
3379
3386
  columns: ["member_id"]
3380
3387
  isOneToOne: false
3381
3388
  referencedRelation: "pace_member"
@@ -3383,6 +3390,53 @@ export type Database = {
3383
3390
  },
3384
3391
  ]
3385
3392
  }
3393
+ pace_identification_type: {
3394
+ Row: {
3395
+ created_at: string | null
3396
+ created_by: string | null
3397
+ description: string | null
3398
+ id: number
3399
+ is_active: boolean | null
3400
+ name: string
3401
+ organisation_id: string
3402
+ sort_order: number | null
3403
+ updated_at: string | null
3404
+ updated_by: string | null
3405
+ }
3406
+ Insert: {
3407
+ created_at?: string | null
3408
+ created_by?: string | null
3409
+ description?: string | null
3410
+ id?: never
3411
+ is_active?: boolean | null
3412
+ name: string
3413
+ organisation_id: string
3414
+ sort_order?: number | null
3415
+ updated_at?: string | null
3416
+ updated_by?: string | null
3417
+ }
3418
+ Update: {
3419
+ created_at?: string | null
3420
+ created_by?: string | null
3421
+ description?: string | null
3422
+ id?: never
3423
+ is_active?: boolean | null
3424
+ name?: string
3425
+ organisation_id?: string
3426
+ sort_order?: number | null
3427
+ updated_at?: string | null
3428
+ updated_by?: string | null
3429
+ }
3430
+ Relationships: [
3431
+ {
3432
+ foreignKeyName: "pace_identification_type_organisation_id_fkey"
3433
+ columns: ["organisation_id"]
3434
+ isOneToOne: false
3435
+ referencedRelation: "organisations"
3436
+ referencedColumns: ["id"]
3437
+ },
3438
+ ]
3439
+ }
3386
3440
  pace_member: {
3387
3441
  Row: {
3388
3442
  address_id: string | null
@@ -3858,7 +3912,7 @@ export type Database = {
3858
3912
  },
3859
3913
  ]
3860
3914
  }
3861
- pace_qualifications: {
3915
+ pace_qualification: {
3862
3916
  Row: {
3863
3917
  created_at: string | null
3864
3918
  credential_id: string | null
@@ -3900,14 +3954,14 @@ export type Database = {
3900
3954
  }
3901
3955
  Relationships: [
3902
3956
  {
3903
- foreignKeyName: "fk_pace_qualifications_organisation_id"
3957
+ foreignKeyName: "fk_pace_qualification_organisation_id"
3904
3958
  columns: ["organisation_id"]
3905
3959
  isOneToOne: false
3906
3960
  referencedRelation: "organisations"
3907
3961
  referencedColumns: ["id"]
3908
3962
  },
3909
3963
  {
3910
- foreignKeyName: "qualifications_member_id_fkey"
3964
+ foreignKeyName: "pace_qualification_member_id_fkey"
3911
3965
  columns: ["member_id"]
3912
3966
  isOneToOne: false
3913
3967
  referencedRelation: "pace_member"
@@ -55,6 +55,7 @@ export enum FileCategory {
55
55
  * Options for uploading a file with a file reference
56
56
  * @property pageContext - The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
57
57
  * Used for context-aware permission checks. Required to check appropriate page-level permissions.
58
+ * @property event_id - Optional event ID for event-scoped permission checks. Required for event-based apps.
58
59
  */
59
60
  export interface FileUploadOptions {
60
61
  table_name: string;
@@ -62,7 +63,9 @@ export interface FileUploadOptions {
62
63
  organisation_id: string;
63
64
  app_id: AppId;
64
65
  category: FileCategory;
66
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
65
67
  pageContext: string;
68
+ event_id?: string;
66
69
  is_public?: boolean;
67
70
  custom_metadata?: Record<string, unknown>;
68
71
  }