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