@jmruthers/pace-core 0.5.110 → 0.5.111
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-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
- package/dist/chunk-ZL45MG76.js.map +1 -0
- 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/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/FileUpload/FileUpload.tsx +2 -8
- 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.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- 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 +47 -23
- 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 +41 -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-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
package/src/rbac/audit.test.ts
CHANGED
|
@@ -143,7 +143,7 @@ describe('RBACAuditManager', () => {
|
|
|
143
143
|
eventId: 'event-789',
|
|
144
144
|
appId: 'app-101' as UUID,
|
|
145
145
|
pageId: 'page-202' as UUID,
|
|
146
|
-
permission: '
|
|
146
|
+
permission: 'update:users',
|
|
147
147
|
source: 'api' as AuditEventSource,
|
|
148
148
|
metadata: { reason: 'Insufficient role' }
|
|
149
149
|
};
|
|
@@ -159,7 +159,7 @@ describe('RBACAuditManager', () => {
|
|
|
159
159
|
event_id: 'event-789',
|
|
160
160
|
app_id: 'app-101',
|
|
161
161
|
page_id: 'page-202',
|
|
162
|
-
permission: '
|
|
162
|
+
permission: 'update:users',
|
|
163
163
|
source: 'api',
|
|
164
164
|
metadata: expect.objectContaining({ reason: 'Insufficient role' })
|
|
165
165
|
})
|
package/src/rbac/audit.ts
CHANGED
|
@@ -163,13 +163,22 @@ 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
|
+
|
|
167
176
|
const auditEvent: Omit<RBACAuditEvent, 'id' | 'created_at'> = {
|
|
168
177
|
event_type: event.type,
|
|
169
178
|
user_id: event.userId,
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
organisation_id: event.organisationId ||
|
|
179
|
+
// Store organisationId - nullable to properly track missing context cases
|
|
180
|
+
// Do NOT use fallback UUID as it masks security issues
|
|
181
|
+
organisation_id: event.organisationId || null, // Explicitly null if missing
|
|
173
182
|
event_id: 'eventId' in event ? event.eventId : undefined,
|
|
174
183
|
app_id: 'appId' in event ? event.appId : undefined,
|
|
175
184
|
page_id: 'pageId' in event ? event.pageId : undefined,
|
|
@@ -182,7 +191,7 @@ export class RBACAuditManager {
|
|
|
182
191
|
...event.metadata,
|
|
183
192
|
cache_hit: 'cache_hit' in event ? event.cache_hit : undefined,
|
|
184
193
|
cache_source: 'cache_source' in event ? event.cache_source : undefined,
|
|
185
|
-
//
|
|
194
|
+
// Explicit flag indicating this event had no organisation context
|
|
186
195
|
no_organisation_context: !event.organisationId,
|
|
187
196
|
},
|
|
188
197
|
};
|
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}
|