@jmruthers/pace-core 0.5.110 → 0.5.112
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.
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-D3BK2FCN.js → DataTable-3D3BUZDV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-KZZUO27W.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-QPMBZZUZ.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-H4YJJF7R.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-3OGQLOJM.js} +23 -7
- package/dist/chunk-3OGQLOJM.js.map +1 -0
- package/dist/{chunk-EYSXQ756.js → chunk-7H75SHXZ.js} +2 -2
- package/dist/{chunk-D6MEKC27.js → chunk-BUN7NMV7.js} +2 -2
- package/dist/{chunk-AWK2FAUN.js → chunk-C5RN4TE5.js} +7 -7
- package/dist/{chunk-3J5N2T2N.js → chunk-EKVVTPIF.js} +183 -127
- package/dist/chunk-EKVVTPIF.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-F6QB26OS.js} +290 -255
- package/dist/chunk-F6QB26OS.js.map +1 -0
- package/dist/{chunk-HADXAZT3.js → chunk-I7JC7PTJ.js} +54 -92
- package/dist/chunk-I7JC7PTJ.js.map +1 -0
- package/dist/{chunk-EZ64QG2I.js → chunk-L36JW4KV.js} +2 -2
- package/dist/{chunk-7GBEBJLR.js → chunk-MNSGWRPB.js} +45 -37
- package/dist/chunk-MNSGWRPB.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-NEONKMTU.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-OO3V7W4H.js} +35 -11
- package/dist/chunk-OO3V7W4H.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-TAJRS6YB.js} +57 -23
- package/dist/chunk-TAJRS6YB.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-WMPZY26G.js} +8 -4
- package/dist/{chunk-HGZSO43Y.js.map → chunk-WMPZY26G.js.map} +1 -1
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +13 -8
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +36 -36
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +3 -3
- package/docs/rbac/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.test.tsx +405 -154
- package/src/components/DataTable/components/DataTableCore.tsx +6 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/EventSelector/EventSelector.tsx +32 -2
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +56 -8
- package/src/components/NavigationMenu/NavigationMenu.tsx +75 -12
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +37 -13
- package/src/rbac/api.ts +25 -8
- package/src/rbac/audit-enhanced.ts +14 -2
- package/src/rbac/audit.test.ts +18 -8
- package/src/rbac/audit.ts +25 -6
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +4 -3
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +12 -2
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +65 -25
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +22 -7
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +42 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3J5N2T2N.js.map +0 -1
- package/dist/chunk-7GBEBJLR.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-HADXAZT3.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-XRSP3H52.js.map +0 -1
- /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-3D3BUZDV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-KZZUO27W.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-QPMBZZUZ.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-H4YJJF7R.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-7H75SHXZ.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-BUN7NMV7.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-C5RN4TE5.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-L36JW4KV.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-NEONKMTU.js.map} +0 -0
|
@@ -163,19 +163,31 @@ export class EnhancedRBACAuditManager {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
try {
|
|
166
|
+
// Validate pageId: only include in page_id column if it's a valid UUID
|
|
167
|
+
// Otherwise, store it in metadata to avoid database errors
|
|
168
|
+
const rawPageId = 'pageId' in event ? event.pageId : undefined;
|
|
169
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
170
|
+
const isValidPageIdUuid = rawPageId && uuidRegex.test(rawPageId);
|
|
171
|
+
const pageIdUuid: UUID | undefined = isValidPageIdUuid ? (rawPageId as UUID) : undefined;
|
|
172
|
+
const pageIdName: string | undefined = rawPageId && !isValidPageIdUuid ? rawPageId : undefined;
|
|
173
|
+
|
|
166
174
|
const auditEvent: Omit<RBACAuditEvent, 'id' | 'created_at'> = {
|
|
167
175
|
event_type: event.type,
|
|
168
176
|
user_id: event.userId,
|
|
169
177
|
organisation_id: event.organisationId,
|
|
170
178
|
event_id: 'eventId' in event ? event.eventId : undefined,
|
|
171
179
|
app_id: 'appId' in event ? event.appId : undefined,
|
|
172
|
-
page_id:
|
|
180
|
+
page_id: pageIdUuid, // Only set if it's a valid UUID
|
|
173
181
|
permission: 'permission' in event ? event.permission : undefined,
|
|
174
182
|
decision: 'decision' in event ? event.decision : undefined,
|
|
175
183
|
source: 'source' in event ? event.source : 'api', // Default to 'api' if not provided
|
|
176
184
|
bypass: 'bypass' in event ? event.bypass : undefined,
|
|
177
185
|
duration_ms: 'duration_ms' in event ? event.duration_ms : undefined,
|
|
178
|
-
metadata:
|
|
186
|
+
metadata: {
|
|
187
|
+
...(event.metadata || {}),
|
|
188
|
+
// Store page name/identifier in metadata if it's not a UUID
|
|
189
|
+
page_name: pageIdName,
|
|
190
|
+
},
|
|
179
191
|
};
|
|
180
192
|
|
|
181
193
|
const { error } = await (this.supabase as any)
|
package/src/rbac/audit.test.ts
CHANGED
|
@@ -68,13 +68,15 @@ describe('RBACAuditManager', () => {
|
|
|
68
68
|
|
|
69
69
|
describe('Permission Check Events', () => {
|
|
70
70
|
it('emits permission check events correctly', async () => {
|
|
71
|
+
// Use a valid UUID format for pageId
|
|
72
|
+
const validPageId = '01234567-89ab-cdef-0123-456789abcdef' as UUID;
|
|
71
73
|
const event: PermissionCheckAuditEvent = {
|
|
72
74
|
type: 'permission_check',
|
|
73
75
|
userId: 'user-123' as UUID,
|
|
74
76
|
organisationId: 'org-456' as UUID,
|
|
75
77
|
eventId: 'event-789',
|
|
76
78
|
appId: 'app-101' as UUID,
|
|
77
|
-
pageId:
|
|
79
|
+
pageId: validPageId,
|
|
78
80
|
permission: 'read:users',
|
|
79
81
|
decision: true,
|
|
80
82
|
source: 'api' as AuditEventSource,
|
|
@@ -93,13 +95,16 @@ describe('RBACAuditManager', () => {
|
|
|
93
95
|
organisation_id: 'org-456',
|
|
94
96
|
event_id: 'event-789',
|
|
95
97
|
app_id: 'app-101',
|
|
96
|
-
page_id:
|
|
98
|
+
page_id: validPageId,
|
|
97
99
|
permission: 'read:users',
|
|
98
100
|
decision: true,
|
|
99
101
|
source: 'api',
|
|
100
102
|
bypass: false,
|
|
101
103
|
duration_ms: 150,
|
|
102
|
-
metadata: expect.objectContaining({
|
|
104
|
+
metadata: expect.objectContaining({
|
|
105
|
+
ip: '192.168.1.1',
|
|
106
|
+
no_organisation_context: false
|
|
107
|
+
})
|
|
103
108
|
})
|
|
104
109
|
]
|
|
105
110
|
);
|
|
@@ -136,14 +141,16 @@ describe('RBACAuditManager', () => {
|
|
|
136
141
|
|
|
137
142
|
describe('Permission Denied Events', () => {
|
|
138
143
|
it('emits permission denied events correctly', async () => {
|
|
144
|
+
// Use a valid UUID format for pageId
|
|
145
|
+
const validPageId = '01234567-89ab-cdef-0123-456789abcdef' as UUID;
|
|
139
146
|
const event: PermissionDeniedAuditEvent = {
|
|
140
147
|
type: 'permission_denied',
|
|
141
148
|
userId: 'user-123' as UUID,
|
|
142
149
|
organisationId: 'org-456' as UUID,
|
|
143
150
|
eventId: 'event-789',
|
|
144
151
|
appId: 'app-101' as UUID,
|
|
145
|
-
pageId:
|
|
146
|
-
permission: '
|
|
152
|
+
pageId: validPageId,
|
|
153
|
+
permission: 'update:users',
|
|
147
154
|
source: 'api' as AuditEventSource,
|
|
148
155
|
metadata: { reason: 'Insufficient role' }
|
|
149
156
|
};
|
|
@@ -158,10 +165,13 @@ describe('RBACAuditManager', () => {
|
|
|
158
165
|
organisation_id: 'org-456',
|
|
159
166
|
event_id: 'event-789',
|
|
160
167
|
app_id: 'app-101',
|
|
161
|
-
page_id:
|
|
162
|
-
permission: '
|
|
168
|
+
page_id: validPageId,
|
|
169
|
+
permission: 'update:users',
|
|
163
170
|
source: 'api',
|
|
164
|
-
metadata: expect.objectContaining({
|
|
171
|
+
metadata: expect.objectContaining({
|
|
172
|
+
reason: 'Insufficient role',
|
|
173
|
+
no_organisation_context: false
|
|
174
|
+
})
|
|
165
175
|
})
|
|
166
176
|
]
|
|
167
177
|
);
|
package/src/rbac/audit.ts
CHANGED
|
@@ -163,16 +163,33 @@ export class RBACAuditManager {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
try {
|
|
166
|
-
//
|
|
166
|
+
// Since organisationId is now required in SecurityContext, this should rarely happen
|
|
167
|
+
// But we still handle the edge case properly without masking it
|
|
168
|
+
if (!event.organisationId) {
|
|
169
|
+
console.warn('[RBAC Audit] Audit event without organisation context - this should be investigated:', {
|
|
170
|
+
userId: event.userId,
|
|
171
|
+
eventType: event.type,
|
|
172
|
+
note: 'Organisation context is required for RBAC operations. This may indicate a security issue or missing context derivation.'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate pageId: only include in page_id column if it's a valid UUID
|
|
177
|
+
// Otherwise, store it in metadata to avoid database errors
|
|
178
|
+
const rawPageId = 'pageId' in event ? event.pageId : undefined;
|
|
179
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
180
|
+
const isValidPageIdUuid = rawPageId && uuidRegex.test(rawPageId);
|
|
181
|
+
const pageIdUuid: UUID | undefined = isValidPageIdUuid ? (rawPageId as UUID) : undefined;
|
|
182
|
+
const pageIdName: string | undefined = rawPageId && !isValidPageIdUuid ? rawPageId : undefined;
|
|
183
|
+
|
|
167
184
|
const auditEvent: Omit<RBACAuditEvent, 'id' | 'created_at'> = {
|
|
168
185
|
event_type: event.type,
|
|
169
186
|
user_id: event.userId,
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
organisation_id: event.organisationId ||
|
|
187
|
+
// Store organisationId - nullable to properly track missing context cases
|
|
188
|
+
// Do NOT use fallback UUID as it masks security issues
|
|
189
|
+
organisation_id: event.organisationId || null, // Explicitly null if missing
|
|
173
190
|
event_id: 'eventId' in event ? event.eventId : undefined,
|
|
174
191
|
app_id: 'appId' in event ? event.appId : undefined,
|
|
175
|
-
page_id:
|
|
192
|
+
page_id: pageIdUuid, // Only set if it's a valid UUID
|
|
176
193
|
permission: 'permission' in event ? event.permission : undefined,
|
|
177
194
|
decision: 'decision' in event ? event.decision : undefined,
|
|
178
195
|
source: 'source' in event ? event.source : 'api', // Default to 'api' if not provided
|
|
@@ -182,8 +199,10 @@ export class RBACAuditManager {
|
|
|
182
199
|
...event.metadata,
|
|
183
200
|
cache_hit: 'cache_hit' in event ? event.cache_hit : undefined,
|
|
184
201
|
cache_source: 'cache_source' in event ? event.cache_source : undefined,
|
|
185
|
-
//
|
|
202
|
+
// Explicit flag indicating this event had no organisation context
|
|
186
203
|
no_organisation_context: !event.organisationId,
|
|
204
|
+
// Store page name/identifier in metadata if it's not a UUID
|
|
205
|
+
page_name: pageIdName,
|
|
187
206
|
},
|
|
188
207
|
};
|
|
189
208
|
|
package/src/rbac/cache.test.ts
CHANGED
|
@@ -129,6 +129,18 @@ describe('RBACCache', () => {
|
|
|
129
129
|
expect(cache.get('org-789-data')).not.toBeNull();
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it('supports wildcard patterns spanning cache segments', () => {
|
|
133
|
+
cache.set('perm:user-123:org-789:null:null:view:dashboard', true);
|
|
134
|
+
cache.set('perm:user-456:org-789:null:null:view:dashboard', true);
|
|
135
|
+
cache.set('perm:user-123:org-000:null:null:view:dashboard', true);
|
|
136
|
+
|
|
137
|
+
cache.invalidate('perm:*:org-789:*');
|
|
138
|
+
|
|
139
|
+
expect(cache.get('perm:user-123:org-789:null:null:view:dashboard')).toBeNull();
|
|
140
|
+
expect(cache.get('perm:user-456:org-789:null:null:view:dashboard')).toBeNull();
|
|
141
|
+
expect(cache.get('perm:user-123:org-000:null:null:view:dashboard')).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
132
144
|
it('handles empty pattern gracefully', () => {
|
|
133
145
|
expect(() => cache.invalidate('')).not.toThrow();
|
|
134
146
|
});
|
package/src/rbac/cache.ts
CHANGED
|
@@ -72,18 +72,38 @@ export class RBACCache {
|
|
|
72
72
|
* @param pattern - Pattern to match against cache keys
|
|
73
73
|
*/
|
|
74
74
|
invalidate(pattern: string): void {
|
|
75
|
+
const trimmedPattern = pattern?.trim();
|
|
76
|
+
|
|
77
|
+
if (!trimmedPattern) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const matcher = this.createMatcher(trimmedPattern);
|
|
75
82
|
const keysToDelete: string[] = [];
|
|
76
|
-
|
|
83
|
+
|
|
77
84
|
for (const key of this.cache.keys()) {
|
|
78
|
-
if (key
|
|
85
|
+
if (matcher(key)) {
|
|
79
86
|
keysToDelete.push(key);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
keysToDelete.forEach(key => this.cache.delete(key));
|
|
84
|
-
|
|
91
|
+
|
|
85
92
|
// Notify invalidation callbacks
|
|
86
|
-
this.invalidationCallbacks.forEach(callback => callback(
|
|
93
|
+
this.invalidationCallbacks.forEach(callback => callback(trimmedPattern));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private createMatcher(pattern: string): (key: string) => boolean {
|
|
97
|
+
if (pattern.includes('*')) {
|
|
98
|
+
const escapedSegments = pattern
|
|
99
|
+
.split('*')
|
|
100
|
+
.map(segment => segment.replace(/[|\\{}()[\]^$+?.-]/g, '\\$&'));
|
|
101
|
+
const regexPattern = escapedSegments.join('.*');
|
|
102
|
+
const regex = new RegExp(regexPattern);
|
|
103
|
+
return (key: string) => regex.test(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (key: string) => key.includes(pattern);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
/**
|
|
@@ -239,9 +259,9 @@ export const rbacCache = new RBACCache();
|
|
|
239
259
|
* Cache key patterns for invalidation
|
|
240
260
|
*/
|
|
241
261
|
export const CACHE_PATTERNS = {
|
|
242
|
-
USER: (userId: UUID) =>
|
|
243
|
-
ORGANISATION: (organisationId: UUID) =>
|
|
244
|
-
EVENT: (eventId: string) =>
|
|
245
|
-
APP: (appId: UUID) =>
|
|
246
|
-
PERMISSION: (userId: UUID, organisationId: UUID) => `perm:${userId}:${organisationId}
|
|
262
|
+
USER: (userId: UUID) => `:${userId}:`,
|
|
263
|
+
ORGANISATION: (organisationId: UUID) => `:${organisationId}:`,
|
|
264
|
+
EVENT: (eventId: string) => `:${eventId}:`,
|
|
265
|
+
APP: (appId: UUID) => `:${appId}`,
|
|
266
|
+
PERMISSION: (userId: UUID, organisationId: UUID) => `perm:${userId}:${organisationId}:`,
|
|
247
267
|
} as const;
|
|
@@ -74,7 +74,7 @@ const mockNavigationItems: NavigationItem[] = [
|
|
|
74
74
|
id: 'admin',
|
|
75
75
|
label: 'Admin',
|
|
76
76
|
path: '/admin',
|
|
77
|
-
permissions: ['
|
|
77
|
+
permissions: ['update:admin'] as Permission[],
|
|
78
78
|
pageId: 'page-admin',
|
|
79
79
|
accessLevel: 'admin',
|
|
80
80
|
meta: {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
67
|
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
|
68
|
-
import {
|
|
68
|
+
import { useMultiplePermissions } from '../hooks/usePermissions';
|
|
69
69
|
import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
|
|
70
70
|
import { UUID, Permission, Scope } from '../types';
|
|
71
71
|
import { createScopeFromEvent } from '../utils/eventContext';
|
|
@@ -176,26 +176,26 @@ export function NavigationGuard({
|
|
|
176
176
|
resolveScope();
|
|
177
177
|
}, [scope, selectedOrganisation, selectedEvent, supabase]);
|
|
178
178
|
|
|
179
|
-
// Check permissions using
|
|
180
|
-
|
|
181
|
-
const representativePermission = navigationItem.permissions[0];
|
|
182
|
-
const { can, isLoading, error } = useCan(
|
|
179
|
+
// Check all permissions using useMultiplePermissions hook
|
|
180
|
+
const { results: permissionResults, isLoading, error } = useMultiplePermissions(
|
|
183
181
|
user?.id || '',
|
|
184
182
|
resolvedScope || { eventId: selectedEvent?.event_id || undefined },
|
|
185
|
-
|
|
186
|
-
navigationItem.pageId,
|
|
183
|
+
navigationItem.permissions || [],
|
|
187
184
|
true // Use cache
|
|
188
185
|
);
|
|
189
186
|
|
|
190
|
-
// Determine if user has required permissions
|
|
187
|
+
// Determine if user has required permissions based on requireAll prop
|
|
191
188
|
const hasRequiredPermissions = useMemo((): boolean => {
|
|
192
|
-
if (navigationItem.permissions.length === 0) return true;
|
|
189
|
+
if (!navigationItem.permissions || navigationItem.permissions.length === 0) return true;
|
|
193
190
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
if (requireAll) {
|
|
192
|
+
// User must have ALL permissions
|
|
193
|
+
return Object.values(permissionResults).every(result => result === true);
|
|
194
|
+
} else {
|
|
195
|
+
// User must have ANY permission (default behavior)
|
|
196
|
+
return Object.values(permissionResults).some(result => result === true);
|
|
197
|
+
}
|
|
198
|
+
}, [navigationItem.permissions, permissionResults, requireAll]);
|
|
199
199
|
|
|
200
200
|
// Handle permission check completion
|
|
201
201
|
useEffect(() => {
|
|
@@ -378,7 +378,7 @@ const PagePermissionGuardComponent = ({
|
|
|
378
378
|
}, [operation, pageName]);
|
|
379
379
|
|
|
380
380
|
// Create a stable scope that only includes valid values
|
|
381
|
-
//
|
|
381
|
+
// OrganisationId is required - use undefined if not available, useCan will handle loading state
|
|
382
382
|
const stableScope = useMemo(() => {
|
|
383
383
|
if (resolvedScope && resolvedScope.organisationId) {
|
|
384
384
|
return {
|
|
@@ -387,8 +387,9 @@ const PagePermissionGuardComponent = ({
|
|
|
387
387
|
eventId: resolvedScope.eventId || undefined
|
|
388
388
|
};
|
|
389
389
|
}
|
|
390
|
-
// Return
|
|
391
|
-
|
|
390
|
+
// Return scope without organisationId - useCan will keep loading state until resolved
|
|
391
|
+
// Scope.organisationId is optional, so undefined is valid
|
|
392
|
+
return { organisationId: undefined, appId: undefined, eventId: undefined };
|
|
392
393
|
}, [resolvedScope]);
|
|
393
394
|
|
|
394
395
|
// Check if user has permission - only call useCan when we have a resolved scope with valid organisationId
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ```tsx
|
|
20
20
|
* // Basic permission enforcement
|
|
21
21
|
* <PermissionEnforcer
|
|
22
|
-
* permissions={['read:events', '
|
|
22
|
+
* permissions={['read:events', 'update:events']}
|
|
23
23
|
* operation="event-management"
|
|
24
24
|
* fallback={<AccessDeniedPage />}
|
|
25
25
|
* >
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
69
|
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
|
70
|
-
import {
|
|
70
|
+
import { useMultiplePermissions } from '../hooks/usePermissions';
|
|
71
71
|
import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
|
|
72
72
|
import { UUID, Permission, Scope } from '../types';
|
|
73
73
|
import { createScopeFromEvent } from '../utils/eventContext';
|
|
@@ -129,7 +129,6 @@ export function PermissionEnforcer({
|
|
|
129
129
|
const { user, selectedOrganisation, selectedEvent, supabase } = useUnifiedAuth();
|
|
130
130
|
const [hasChecked, setHasChecked] = useState(false);
|
|
131
131
|
const [checkError, setCheckError] = useState<Error | null>(null);
|
|
132
|
-
const [permissionResults, setPermissionResults] = useState<Record<string, boolean>>({});
|
|
133
132
|
const [resolvedScope, setResolvedScope] = useState<Scope | null>(null);
|
|
134
133
|
|
|
135
134
|
// Resolve scope - either use provided scope or resolve from context
|
|
@@ -182,26 +181,31 @@ export function PermissionEnforcer({
|
|
|
182
181
|
resolveScope();
|
|
183
182
|
}, [scope, selectedOrganisation, selectedEvent, supabase]);
|
|
184
183
|
|
|
185
|
-
// Check permissions using
|
|
186
|
-
|
|
187
|
-
const representativePermission = permissions[0];
|
|
188
|
-
const { can, isLoading, error } = useCan(
|
|
184
|
+
// Check all permissions using useMultiplePermissions hook
|
|
185
|
+
const { results: permissionResults, isLoading, error } = useMultiplePermissions(
|
|
189
186
|
user?.id || '',
|
|
190
187
|
resolvedScope || { eventId: selectedEvent?.event_id || undefined },
|
|
191
|
-
|
|
192
|
-
undefined,
|
|
188
|
+
permissions,
|
|
193
189
|
true // Use cache
|
|
194
190
|
);
|
|
195
191
|
|
|
196
|
-
// Determine if user has required permissions
|
|
192
|
+
// Determine if user has required permissions based on requireAll prop
|
|
197
193
|
const hasRequiredPermissions = useMemo((): boolean => {
|
|
198
194
|
if (permissions.length === 0) return true;
|
|
199
195
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
// If permissionResults is not yet available or empty, deny access
|
|
197
|
+
if (!permissionResults || Object.keys(permissionResults).length === 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (requireAll) {
|
|
202
|
+
// User must have ALL permissions
|
|
203
|
+
return Object.values(permissionResults).every(result => result === true);
|
|
204
|
+
} else {
|
|
205
|
+
// User must have ANY permission (default behavior)
|
|
206
|
+
return Object.values(permissionResults).some(result => result === true);
|
|
207
|
+
}
|
|
208
|
+
}, [permissions, permissionResults, requireAll]);
|
|
205
209
|
|
|
206
210
|
// Handle permission check completion
|
|
207
211
|
useEffect(() => {
|
|
@@ -79,6 +79,9 @@ export interface RouteConfig {
|
|
|
79
79
|
/** Permissions required for this route */
|
|
80
80
|
permissions: Permission[];
|
|
81
81
|
|
|
82
|
+
/** If true, this route is public and doesn't require permission checks */
|
|
83
|
+
public?: boolean;
|
|
84
|
+
|
|
82
85
|
/** Roles that can access this route */
|
|
83
86
|
roles?: string[];
|
|
84
87
|
|
|
@@ -232,10 +235,13 @@ export function RoleBasedRouter({
|
|
|
232
235
|
currentRouteConfig?.pageId
|
|
233
236
|
);
|
|
234
237
|
|
|
235
|
-
//
|
|
238
|
+
// Check if route is public
|
|
239
|
+
const isPublicRoute = currentRouteConfig?.public === true;
|
|
240
|
+
|
|
241
|
+
// If route has no permissions and is not public, deny access (secure by default)
|
|
236
242
|
const hasPermissions = currentRouteConfig?.permissions && currentRouteConfig.permissions.length > 0;
|
|
237
|
-
const finalCanAccess = hasPermissions ? canAccessCurrentRoute : false;
|
|
238
|
-
const finalLoading = hasPermissions ? permissionLoading : false;
|
|
243
|
+
const finalCanAccess = isPublicRoute ? true : (hasPermissions ? canAccessCurrentRoute : false);
|
|
244
|
+
const finalLoading = isPublicRoute ? false : (hasPermissions ? permissionLoading : false);
|
|
239
245
|
|
|
240
246
|
// Get all accessible routes for current user
|
|
241
247
|
const getAccessibleRoutes = useCallback((): RouteConfig[] => {
|
|
@@ -323,13 +329,14 @@ export function RoleBasedRouter({
|
|
|
323
329
|
|
|
324
330
|
// Use the actual permission check result
|
|
325
331
|
const allowed = finalCanAccess;
|
|
332
|
+
// Log route access (including public routes for audit monitoring)
|
|
326
333
|
recordRouteAccess(currentPath, allowed, currentRouteConfig);
|
|
327
334
|
|
|
328
|
-
if (!allowed) {
|
|
329
|
-
// Redirect to fallback route
|
|
335
|
+
if (!allowed && !isPublicRoute) {
|
|
336
|
+
// Redirect to fallback route (skip for public routes)
|
|
330
337
|
navigate(fallbackRoute, { replace: true });
|
|
331
338
|
}
|
|
332
|
-
}, [location.pathname, currentRouteConfig, canAccessCurrentRoute, recordRouteAccess, strictMode, user?.id, currentScope, onStrictModeViolation, navigate, fallbackRoute]);
|
|
339
|
+
}, [location.pathname, currentRouteConfig, canAccessCurrentRoute, recordRouteAccess, strictMode, user?.id, currentScope, onStrictModeViolation, navigate, fallbackRoute, isPublicRoute]);
|
|
333
340
|
|
|
334
341
|
// Context value
|
|
335
342
|
const contextValue = useMemo((): RoleBasedRouterContextType => ({
|
|
@@ -350,8 +357,8 @@ export function RoleBasedRouter({
|
|
|
350
357
|
auditLog
|
|
351
358
|
]);
|
|
352
359
|
|
|
353
|
-
// Show loading state while checking permissions
|
|
354
|
-
if (finalLoading) {
|
|
360
|
+
// Show loading state while checking permissions (skip for public routes)
|
|
361
|
+
if (finalLoading && !isPublicRoute) {
|
|
355
362
|
return (
|
|
356
363
|
<div className="flex items-center justify-center min-h-screen">
|
|
357
364
|
<div className="text-center">
|
|
@@ -363,7 +370,7 @@ export function RoleBasedRouter({
|
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
// Show unauthorized component if user can't access current route
|
|
366
|
-
if (currentRouteConfig && !finalCanAccess) {
|
|
373
|
+
if (currentRouteConfig && !finalCanAccess && !isPublicRoute) {
|
|
367
374
|
return (
|
|
368
375
|
<UnauthorizedComponent
|
|
369
376
|
route={currentRoute}
|