@jmruthers/pace-core 0.5.1 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -55
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
@@ -1,129 +0,0 @@
1
- /**
2
- * @file Role-Based Content Component
3
- * @package @jmruthers/pace-core
4
- * @module Components/RBAC
5
- * @since 0.3.0
6
- *
7
- * A component that conditionally renders children based on user roles.
8
- * This component integrates with the new RBAC system to provide role-based
9
- * UI rendering with support for multiple role types.
10
- *
11
- * Features:
12
- * - Role-based conditional rendering
13
- * - Support for global, organisation, and event-app roles
14
- * - Multiple role checking
15
- * - Fallback content support
16
- * - Type-safe role checking
17
- * - Automatic context detection
18
- *
19
- * @example
20
- * ```tsx
21
- * import { RoleBasedContent } from '@jmruthers/pace-core';
22
- *
23
- * function MyComponent() {
24
- * return (
25
- * <div>
26
- * <h1>Admin Panel</h1>
27
- *
28
- * <RoleBasedContent globalRoles={['super_admin']}>
29
- * <SuperAdminPanel />
30
- * </RoleBasedContent>
31
- *
32
- * <RoleBasedContent organisationRoles={['org_admin', 'leader']}>
33
- * <OrganisationManagementPanel />
34
- * </RoleBasedContent>
35
- *
36
- * <RoleBasedContent eventAppRoles={['event_admin', 'planner']}>
37
- * <EventManagementPanel />
38
- * </RoleBasedContent>
39
- *
40
- * <RoleBasedContent
41
- * globalRoles={['super_admin']}
42
- * organisationRoles={['org_admin']}
43
- * fallback={<p>You need admin privileges to access this content.</p>}
44
- * >
45
- * <AdminOnlyContent />
46
- * </RoleBasedContent>
47
- * </div>
48
- * );
49
- * }
50
- * ```
51
- *
52
- * @accessibility
53
- * - Supports screen reader friendly conditional content
54
- * - Maintains focus management in conditional renders
55
- * - Provides accessible fallback content
56
- *
57
- * @security
58
- * - Role-based access control
59
- * - Secure role checking
60
- * - Organisation context enforcement
61
- *
62
- * @performance
63
- * - Optimized with React.memo
64
- * - Minimal re-renders
65
- * - Efficient role checking
66
- *
67
- * @dependencies
68
- * - React 18+ - Components and hooks
69
- * - useRBAC hook - Role checking
70
- * - RBAC types - Type definitions
71
- */
72
-
73
- import React from 'react';
74
- import { useRBAC } from '../../hooks/useRBAC';
75
- import type {
76
- RoleBasedContentProps,
77
- GlobalRole,
78
- OrganisationRole,
79
- EventAppRole
80
- } from '../../rbac/types';
81
-
82
- export function RoleBasedContent({
83
- children,
84
- globalRoles = [],
85
- organisationRoles = [],
86
- eventAppRoles = [],
87
- fallback = null
88
- }: RoleBasedContentProps) {
89
- const {
90
- globalRole,
91
- organisationRole,
92
- eventAppRole,
93
- isLoading,
94
- error
95
- } = useRBAC();
96
-
97
- // Show loading state while checking roles
98
- if (isLoading) {
99
- return (
100
- <div className="rbac-loading" role="status" aria-live="polite">
101
- <span className="sr-only">Checking roles...</span>
102
- </div>
103
- );
104
- }
105
-
106
- // Show error state - render fallback if provided, otherwise null
107
- if (error) {
108
- if (fallback) {
109
- return (
110
- <div className="rbac-error" role="alert">
111
- <span className="sr-only">Role check error</span>
112
- {fallback}
113
- </div>
114
- );
115
- }
116
- return null;
117
- }
118
-
119
- // Check if user has any of the required roles
120
- const hasGlobalRole = globalRoles.length > 0 && globalRole && globalRoles.includes(globalRole as GlobalRole);
121
- const hasOrgRole = organisationRoles.length > 0 && organisationRole && organisationRoles.includes(organisationRole as OrganisationRole);
122
- const hasEventRole = eventAppRoles.length > 0 && eventAppRole && eventAppRoles.includes(eventAppRole as EventAppRole);
123
-
124
- // User has access if they have any of the required roles
125
- const hasAccess = hasGlobalRole || hasOrgRole || hasEventRole;
126
-
127
- // Render children if user has required role, otherwise show fallback
128
- return hasAccess ? <>{children}</> : <>{fallback}</>;
129
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * RBAC Components Index
3
- * @package @jmruthers/pace-core
4
- * @module Components/RBAC
5
- * @since 0.3.0
6
- */
7
-
8
- // Export RBAC components
9
- export { RBACGuard } from './RBACGuard';
10
- export { RoleBasedContent } from './RoleBasedContent';
11
- export { RBACProvider } from './RBACProvider';
12
- export {
13
- PagePermissionGuard,
14
- ReadPermissionGuard,
15
- CreatePermissionGuard,
16
- UpdatePermissionGuard,
17
- DeletePermissionGuard,
18
- PAGE_IDS
19
- } from './PagePermissionGuard';
20
-
21
- // Export types
22
- export type { RBACGuardProps } from '../../rbac/types';
23
- export type { RoleBasedContentProps } from '../../rbac/types';
package/src/rbac/hooks.ts DELETED
@@ -1,570 +0,0 @@
1
- /**
2
- * RBAC React Hooks
3
- * @package @jmruthers/pace-core
4
- * @module RBAC/Hooks
5
- * @since 1.0.0
6
- *
7
- * This module provides React hooks for RBAC functionality.
8
- */
9
-
10
- import { useState, useEffect, useCallback, useMemo } from 'react';
11
- import {
12
- UUID,
13
- Scope,
14
- Permission,
15
- AccessLevel,
16
- PermissionMap,
17
- UsePermissionsReturn,
18
- UseCanReturn
19
- } from './types';
20
- import {
21
- getAccessLevel,
22
- getPermissionMap,
23
- isPermitted,
24
- isPermittedCached
25
- } from './api';
26
-
27
- /**
28
- * Hook to get user's permissions in a scope
29
- *
30
- * @param userId - User ID
31
- * @param scope - Permission scope
32
- * @returns Permission data and loading state
33
- *
34
- * @example
35
- * ```tsx
36
- * function MyComponent() {
37
- * const { permissions, isLoading, error } = usePermissions(
38
- * 'user-123',
39
- * { organisationId: 'org-456' }
40
- * );
41
- *
42
- * if (isLoading) return <div>Loading...</div>;
43
- * if (error) return <div>Error: {error.message}</div>;
44
- *
45
- * return (
46
- * <div>
47
- * {permissions['page-1']?.includes('read') && <ReadButton />}
48
- * {permissions['page-1']?.includes('manage') && <ManageButton />}
49
- * </div>
50
- * );
51
- * }
52
- * ```
53
- */
54
- export function usePermissions(userId: UUID, scope: Scope): UsePermissionsReturn {
55
- const [permissions, setPermissions] = useState<PermissionMap>({});
56
- const [isLoading, setIsLoading] = useState(true);
57
- const [error, setError] = useState<Error | null>(null);
58
-
59
- const fetchPermissions = useCallback(async () => {
60
- if (!userId) {
61
- setPermissions({});
62
- setIsLoading(false);
63
- return;
64
- }
65
-
66
- try {
67
- setIsLoading(true);
68
- setError(null);
69
-
70
- const result = await getPermissionMap({ userId, scope });
71
- setPermissions(result);
72
- } catch (err) {
73
- setError(err instanceof Error ? err : new Error('Failed to fetch permissions'));
74
- } finally {
75
- setIsLoading(false);
76
- }
77
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
78
-
79
- useEffect(() => {
80
- fetchPermissions();
81
- }, [fetchPermissions]);
82
-
83
- return {
84
- permissions,
85
- isLoading,
86
- error,
87
- refetch: fetchPermissions,
88
- };
89
- }
90
-
91
- /**
92
- * Hook to check if user has a specific permission
93
- *
94
- * @param userId - User ID
95
- * @param scope - Permission scope
96
- * @param permission - Permission to check
97
- * @param pageId - Optional page ID
98
- * @param useCache - Whether to use cached results (default: true)
99
- * @returns Permission check result and loading state
100
- *
101
- * @example
102
- * ```tsx
103
- * function MyComponent() {
104
- * const { can, isLoading } = useCan(
105
- * 'user-123',
106
- * { organisationId: 'org-456' },
107
- * 'manage:events',
108
- * 'page-789'
109
- * );
110
- *
111
- * if (isLoading) return <div>Checking permission...</div>;
112
- *
113
- * return (
114
- * <div>
115
- * {can ? <AdminPanel /> : <AccessDenied />}
116
- * </div>
117
- * );
118
- * }
119
- * ```
120
- */
121
- export function useCan(
122
- userId: UUID,
123
- scope: Scope,
124
- permission: Permission,
125
- pageId?: UUID,
126
- useCache: boolean = true
127
- ): UseCanReturn {
128
- const [can, setCan] = useState(false);
129
- const [isLoading, setIsLoading] = useState(true);
130
- const [error, setError] = useState<Error | null>(null);
131
-
132
- const check = useCallback(async () => {
133
- console.log('[useCan] check() called with:', { userId, scope, permission, pageId });
134
- console.log('[useCan] Hook parameters:', { userId, scope, permission, pageId, useCache });
135
-
136
- if (!userId) {
137
- console.log('[useCan] No userId, denying access');
138
- setCan(false);
139
- setIsLoading(false);
140
- return;
141
- }
142
-
143
- // Check for super admin status first - super admins bypass all scope requirements
144
- try {
145
- const { isSuperAdmin } = await import('./api');
146
- const isSuper = await isSuperAdmin(userId);
147
- if (isSuper) {
148
- console.log('[useCan] User is super admin, granting access');
149
- setCan(true);
150
- setIsLoading(false);
151
- return;
152
- }
153
- } catch (error) {
154
- console.error('[useCan] Error checking super admin status:', error);
155
- // Continue with normal permission check if super admin check fails
156
- }
157
-
158
- // Check if scope is incomplete (missing required fields)
159
- if (!scope || !scope.organisationId || !scope.appId) {
160
- console.log('[useCan] Incomplete scope, waiting for resolution:', scope);
161
- setCan(false);
162
- setIsLoading(true); // Keep loading until scope is complete
163
- return;
164
- }
165
-
166
- console.log('[useCan] Scope is complete, checking permission...');
167
- console.log('[useCan] Detailed scope info:', {
168
- organisationId: scope.organisationId,
169
- eventId: scope.eventId,
170
- appId: scope.appId,
171
- permission,
172
- pageId
173
- });
174
-
175
- try {
176
- setIsLoading(true);
177
- setError(null);
178
-
179
- console.log('[useCan] About to call isPermitted/isPermittedCached...');
180
- const result = useCache
181
- ? await isPermittedCached({ userId, scope, permission, pageId })
182
- : await isPermitted({ userId, scope, permission, pageId });
183
-
184
- console.log('[useCan] Permission check result:', result);
185
- console.log('[useCan] Permission check details:', {
186
- userId,
187
- scope,
188
- permission,
189
- pageId,
190
- result,
191
- timestamp: new Date().toISOString()
192
- });
193
- setCan(result);
194
- } catch (err) {
195
- console.error('[useCan] Permission check error:', err);
196
- console.error('[useCan] Error details:', {
197
- userId,
198
- scope,
199
- permission,
200
- pageId,
201
- error: err instanceof Error ? err.message : 'Unknown error',
202
- timestamp: new Date().toISOString()
203
- });
204
- setError(err instanceof Error ? err : new Error('Failed to check permission'));
205
- setCan(false);
206
- } finally {
207
- setIsLoading(false);
208
- }
209
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permission, pageId, useCache]);
210
-
211
- useEffect(() => {
212
- check();
213
- }, [check]);
214
-
215
- return {
216
- can,
217
- isLoading,
218
- error,
219
- check,
220
- };
221
- }
222
-
223
- /**
224
- * Hook to get user's access level in a scope
225
- *
226
- * @param userId - User ID
227
- * @param scope - Permission scope
228
- * @returns Access level and loading state
229
- *
230
- * @example
231
- * ```tsx
232
- * function MyComponent() {
233
- * const { accessLevel, isLoading } = useAccessLevel(
234
- * 'user-123',
235
- * { organisationId: 'org-456' }
236
- * );
237
- *
238
- * if (isLoading) return <div>Loading...</div>;
239
- *
240
- * return (
241
- * <div>
242
- * {accessLevel === 'super' && <SuperAdminPanel />}
243
- * {accessLevel === 'admin' && <AdminPanel />}
244
- * {accessLevel === 'planner' && <PlannerPanel />}
245
- * </div>
246
- * );
247
- * }
248
- * ```
249
- */
250
- export function useAccessLevel(userId: UUID, scope: Scope): {
251
- accessLevel: AccessLevel | null;
252
- isLoading: boolean;
253
- error: Error | null;
254
- refetch: () => Promise<void>;
255
- } {
256
- const [accessLevel, setAccessLevel] = useState<AccessLevel | null>(null);
257
- const [isLoading, setIsLoading] = useState(true);
258
- const [error, setError] = useState<Error | null>(null);
259
-
260
- const fetchAccessLevel = useCallback(async () => {
261
- if (!userId) {
262
- setAccessLevel(null);
263
- setIsLoading(false);
264
- return;
265
- }
266
-
267
- try {
268
- setIsLoading(true);
269
- setError(null);
270
-
271
- const result = await getAccessLevel({ userId, scope });
272
- setAccessLevel(result);
273
- } catch (err) {
274
- setError(err instanceof Error ? err : new Error('Failed to fetch access level'));
275
- setAccessLevel(null);
276
- } finally {
277
- setIsLoading(false);
278
- }
279
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
280
-
281
- useEffect(() => {
282
- fetchAccessLevel();
283
- }, [fetchAccessLevel]);
284
-
285
- return {
286
- accessLevel,
287
- isLoading,
288
- error,
289
- refetch: fetchAccessLevel,
290
- };
291
- }
292
-
293
- /**
294
- * Hook to check multiple permissions at once
295
- *
296
- * @param userId - User ID
297
- * @param scope - Permission scope
298
- * @param permissions - Array of permissions to check
299
- * @param pageId - Optional page ID
300
- * @param useCache - Whether to use cached results (default: true)
301
- * @returns Object with permission results and loading state
302
- *
303
- * @example
304
- * ```tsx
305
- * function MyComponent() {
306
- * const { permissions, isLoading } = useMultiplePermissions(
307
- * 'user-123',
308
- * { organisationId: 'org-456' },
309
- * ['read:events', 'manage:events', 'delete:events']
310
- * );
311
- *
312
- * return (
313
- * <div>
314
- * {permissions['read:events'] && <ReadButton />}
315
- * {permissions['manage:events'] && <ManageButton />}
316
- * {permissions['delete:events'] && <DeleteButton />}
317
- * </div>
318
- * );
319
- * }
320
- * ```
321
- */
322
- export function useMultiplePermissions(
323
- userId: UUID,
324
- scope: Scope,
325
- permissions: Permission[],
326
- pageId?: UUID,
327
- useCache: boolean = true
328
- ): {
329
- permissions: Record<Permission, boolean>;
330
- isLoading: boolean;
331
- error: Error | null;
332
- refetch: () => Promise<void>;
333
- } {
334
- const [permissionResults, setPermissionResults] = useState<Record<Permission, boolean>>({} as Record<Permission, boolean>);
335
- const [isLoading, setIsLoading] = useState(true);
336
- const [error, setError] = useState<Error | null>(null);
337
-
338
- const fetchPermissions = useCallback(async () => {
339
- if (!userId || permissions.length === 0) {
340
- setPermissionResults({} as Record<Permission, boolean>);
341
- setIsLoading(false);
342
- return;
343
- }
344
-
345
- try {
346
- setIsLoading(true);
347
- setError(null);
348
-
349
- const results: Record<Permission, boolean> = {} as Record<Permission, boolean>;
350
-
351
- // Check all permissions in parallel
352
- const promises = permissions.map(async (permission) => {
353
- const result = useCache
354
- ? await isPermittedCached({ userId, scope, permission, pageId })
355
- : await isPermitted({ userId, scope, permission, pageId });
356
-
357
- return { permission, result };
358
- });
359
-
360
- const resolved = await Promise.all(promises);
361
-
362
- resolved.forEach(({ permission, result }) => {
363
- results[permission] = result;
364
- });
365
-
366
- setPermissionResults(results);
367
- } catch (err) {
368
- setError(err instanceof Error ? err : new Error('Failed to check permissions'));
369
- setPermissionResults({} as Record<Permission, boolean>);
370
- } finally {
371
- setIsLoading(false);
372
- }
373
- }, [userId, scope.organisationId, scope.eventId, scope.appId, permissions, pageId, useCache]);
374
-
375
- useEffect(() => {
376
- fetchPermissions();
377
- }, [fetchPermissions]);
378
-
379
- return {
380
- permissions: permissionResults,
381
- isLoading,
382
- error,
383
- refetch: fetchPermissions,
384
- };
385
- }
386
-
387
- /**
388
- * Hook to check if user has any of the specified permissions
389
- *
390
- * @param userId - User ID
391
- * @param scope - Permission scope
392
- * @param permissions - Array of permissions to check
393
- * @param pageId - Optional page ID
394
- * @returns True if user has any permission and loading state
395
- *
396
- * @example
397
- * ```tsx
398
- * function MyComponent() {
399
- * const { hasAny, isLoading } = useHasAnyPermission(
400
- * 'user-123',
401
- * { organisationId: 'org-456' },
402
- * ['read:events', 'manage:events']
403
- * );
404
- *
405
- * return (
406
- * <div>
407
- * {hasAny ? <EventContent /> : <AccessDenied />}
408
- * </div>
409
- * );
410
- * }
411
- * ```
412
- */
413
- export function useHasAnyPermission(
414
- userId: UUID,
415
- scope: Scope,
416
- permissions: Permission[],
417
- pageId?: UUID
418
- ): {
419
- hasAny: boolean;
420
- isLoading: boolean;
421
- error: Error | null;
422
- refetch: () => Promise<void>;
423
- } {
424
- const { permissions: permissionResults, isLoading, error, refetch } = useMultiplePermissions(
425
- userId,
426
- scope,
427
- permissions,
428
- pageId
429
- );
430
-
431
- const hasAny = useMemo(() => {
432
- return Object.values(permissionResults).some(Boolean);
433
- }, [permissionResults]);
434
-
435
- return {
436
- hasAny,
437
- isLoading,
438
- error,
439
- refetch,
440
- };
441
- }
442
-
443
- /**
444
- * Hook to check if user has all of the specified permissions
445
- *
446
- * @param userId - User ID
447
- * @param scope - Permission scope
448
- * @param permissions - Array of permissions to check
449
- * @param pageId - Optional page ID
450
- * @returns True if user has all permissions and loading state
451
- *
452
- * @example
453
- * ```tsx
454
- * function MyComponent() {
455
- * const { hasAll, isLoading } = useHasAllPermissions(
456
- * 'user-123',
457
- * { organisationId: 'org-456' },
458
- * ['read:events', 'manage:events']
459
- * );
460
- *
461
- * return (
462
- * <div>
463
- * {hasAll ? <FullAccessPanel /> : <LimitedAccessPanel />}
464
- * </div>
465
- * );
466
- * }
467
- * ```
468
- */
469
- export function useHasAllPermissions(
470
- userId: UUID,
471
- scope: Scope,
472
- permissions: Permission[],
473
- pageId?: UUID
474
- ): {
475
- hasAll: boolean;
476
- isLoading: boolean;
477
- error: Error | null;
478
- refetch: () => Promise<void>;
479
- } {
480
- const { permissions: permissionResults, isLoading, error, refetch } = useMultiplePermissions(
481
- userId,
482
- scope,
483
- permissions,
484
- pageId
485
- );
486
-
487
- const hasAll = useMemo(() => {
488
- return Object.values(permissionResults).every(Boolean);
489
- }, [permissionResults]);
490
-
491
- return {
492
- hasAll,
493
- isLoading,
494
- error,
495
- refetch,
496
- };
497
- }
498
-
499
- /**
500
- * Hook to read cached permissions (contract requirement)
501
- *
502
- * This hook only reads from the core cache and does not perform
503
- * any bespoke caching as per the contract requirements.
504
- *
505
- * @param userId - User ID
506
- * @param scope - Permission scope
507
- * @returns Cached permission data and loading state
508
- *
509
- * @example
510
- * ```tsx
511
- * function MyComponent() {
512
- * const { permissions, isLoading, error } = useCachedPermissions(
513
- * 'user-123',
514
- * { organisationId: 'org-456' }
515
- * );
516
- *
517
- * if (isLoading) return <div>Loading cached permissions...</div>;
518
- * if (error) return <div>Error: {error.message}</div>;
519
- *
520
- * return (
521
- * <div>
522
- * {permissions['page-1']?.includes('read') && <ReadButton />}
523
- * {permissions['page-1']?.includes('manage') && <ManageButton />}
524
- * </div>
525
- * );
526
- * }
527
- * ```
528
- */
529
- export function useCachedPermissions(userId: UUID, scope: Scope): {
530
- permissions: PermissionMap;
531
- isLoading: boolean;
532
- error: Error | null;
533
- refetch: () => Promise<void>;
534
- } {
535
- const [permissions, setPermissions] = useState<PermissionMap>({});
536
- const [isLoading, setIsLoading] = useState(true);
537
- const [error, setError] = useState<Error | null>(null);
538
-
539
- const fetchCachedPermissions = useCallback(async () => {
540
- if (!userId) {
541
- setPermissions({});
542
- setIsLoading(false);
543
- return;
544
- }
545
-
546
- try {
547
- setIsLoading(true);
548
- setError(null);
549
-
550
- // Use cached version of getPermissionMap
551
- const result = await getPermissionMap({ userId, scope });
552
- setPermissions(result);
553
- } catch (err) {
554
- setError(err instanceof Error ? err : new Error('Failed to fetch cached permissions'));
555
- } finally {
556
- setIsLoading(false);
557
- }
558
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
559
-
560
- useEffect(() => {
561
- fetchCachedPermissions();
562
- }, [fetchCachedPermissions]);
563
-
564
- return {
565
- permissions,
566
- isLoading,
567
- error,
568
- refetch: fetchCachedPermissions,
569
- };
570
- }