@jmruthers/pace-core 0.5.115 → 0.5.116
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-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- 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/EventAppRoleData.md +71 -0
- 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/GrantEventAppRoleParams.md +122 -0
- 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 +1 -1
- 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/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- 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 +41 -14
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +1 -1
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RBAC Role Management Hook
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/Hooks
|
|
5
|
+
* @since 2.1.0
|
|
6
|
+
*
|
|
7
|
+
* React hook for managing RBAC roles safely using RPC functions.
|
|
8
|
+
* This hook provides a secure, type-safe interface for granting and revoking roles
|
|
9
|
+
* that ensures proper audit trails and security checks.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { useRoleManagement } from '@jmruthers/pace-core/rbac';
|
|
14
|
+
*
|
|
15
|
+
* function UserRolesComponent() {
|
|
16
|
+
* const { revokeEventAppRole, grantEventAppRole, isLoading, error } = useRoleManagement();
|
|
17
|
+
*
|
|
18
|
+
* const handleRevokeRole = async (roleId: string, roleData: EventAppRoleData) => {
|
|
19
|
+
* const result = await revokeEventAppRole({
|
|
20
|
+
* userId: roleData.user_id,
|
|
21
|
+
* organisationId: roleData.organisation_id,
|
|
22
|
+
* eventId: roleData.event_id,
|
|
23
|
+
* appId: roleData.app_id,
|
|
24
|
+
* role: roleData.role
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* if (result.success) {
|
|
28
|
+
* toast({ title: 'Role revoked successfully' });
|
|
29
|
+
* } else {
|
|
30
|
+
* toast({ title: 'Failed to revoke role', variant: 'destructive' });
|
|
31
|
+
* }
|
|
32
|
+
* };
|
|
33
|
+
*
|
|
34
|
+
* return <button onClick={() => handleRevokeRole(roleId, roleData)}>Revoke Role</button>;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { useState, useCallback } from 'react';
|
|
40
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
41
|
+
import type { UUID } from '../types';
|
|
42
|
+
|
|
43
|
+
export interface EventAppRoleData {
|
|
44
|
+
user_id: UUID;
|
|
45
|
+
organisation_id: UUID;
|
|
46
|
+
event_id: string;
|
|
47
|
+
app_id: UUID;
|
|
48
|
+
role: 'viewer' | 'participant' | 'planner' | 'event_admin';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RevokeEventAppRoleParams extends EventAppRoleData {
|
|
52
|
+
revoked_by?: UUID;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GrantEventAppRoleParams extends EventAppRoleData {
|
|
56
|
+
granted_by?: UUID;
|
|
57
|
+
valid_from?: string;
|
|
58
|
+
valid_to?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RoleManagementResult {
|
|
62
|
+
success: boolean;
|
|
63
|
+
message?: string;
|
|
64
|
+
error?: string;
|
|
65
|
+
roleId?: UUID;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useRoleManagement() {
|
|
69
|
+
const { user, supabase } = useUnifiedAuth();
|
|
70
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState<Error | null>(null);
|
|
72
|
+
|
|
73
|
+
if (!supabase) {
|
|
74
|
+
throw new Error('useRoleManagement requires a Supabase client. Ensure UnifiedAuthProvider is configured.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Revoke an event app role using the secure RPC function
|
|
79
|
+
*
|
|
80
|
+
* This function uses the `revoke_event_app_role` RPC which:
|
|
81
|
+
* - Runs with SECURITY DEFINER privileges
|
|
82
|
+
* - Includes proper permission checks
|
|
83
|
+
* - Automatically populates audit fields (revoked_by, timestamps)
|
|
84
|
+
* - Complies with Row-Level Security policies
|
|
85
|
+
*
|
|
86
|
+
* @param params - Role revocation parameters
|
|
87
|
+
* @returns Promise resolving to operation result
|
|
88
|
+
*/
|
|
89
|
+
const revokeEventAppRole = useCallback(async (
|
|
90
|
+
params: RevokeEventAppRoleParams
|
|
91
|
+
): Promise<RoleManagementResult> => {
|
|
92
|
+
setIsLoading(true);
|
|
93
|
+
setError(null);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const { data, error: rpcError } = await supabase.rpc('revoke_event_app_role', {
|
|
97
|
+
p_user_id: params.user_id,
|
|
98
|
+
p_organisation_id: params.organisation_id,
|
|
99
|
+
p_event_id: params.event_id,
|
|
100
|
+
p_app_id: params.app_id,
|
|
101
|
+
p_role: params.role,
|
|
102
|
+
p_revoked_by: params.revoked_by || user?.id || undefined
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (rpcError) {
|
|
106
|
+
throw new Error(rpcError.message || 'Failed to revoke role');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: data === true,
|
|
111
|
+
message: data === true ? 'Role revoked successfully' : 'No role found to revoke',
|
|
112
|
+
error: data === false ? 'No matching role found' : undefined
|
|
113
|
+
};
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
116
|
+
setError(err instanceof Error ? err : new Error(errorMessage));
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: errorMessage
|
|
120
|
+
};
|
|
121
|
+
} finally {
|
|
122
|
+
setIsLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}, [user?.id]);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Grant an event app role using the secure RPC function
|
|
128
|
+
*
|
|
129
|
+
* This function uses the `grant_event_app_role` RPC which:
|
|
130
|
+
* - Runs with SECURITY DEFINER privileges
|
|
131
|
+
* - Includes proper permission checks
|
|
132
|
+
* - Automatically populates audit fields (granted_by, timestamps)
|
|
133
|
+
* - Complies with Row-Level Security policies
|
|
134
|
+
*
|
|
135
|
+
* @param params - Role grant parameters
|
|
136
|
+
* @returns Promise resolving to operation result with role ID
|
|
137
|
+
*/
|
|
138
|
+
const grantEventAppRole = useCallback(async (
|
|
139
|
+
params: GrantEventAppRoleParams
|
|
140
|
+
): Promise<RoleManagementResult> => {
|
|
141
|
+
setIsLoading(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const { data, error: rpcError } = await supabase.rpc('grant_event_app_role', {
|
|
146
|
+
p_user_id: params.user_id,
|
|
147
|
+
p_organisation_id: params.organisation_id,
|
|
148
|
+
p_event_id: params.event_id,
|
|
149
|
+
p_app_id: params.app_id,
|
|
150
|
+
p_role: params.role,
|
|
151
|
+
p_granted_by: params.granted_by || user?.id || undefined,
|
|
152
|
+
p_valid_from: params.valid_from,
|
|
153
|
+
p_valid_to: params.valid_to
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (rpcError) {
|
|
157
|
+
throw new Error(rpcError.message || 'Failed to grant role');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!data) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: 'Failed to grant role - no role ID returned'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
message: 'Role granted successfully',
|
|
170
|
+
roleId: data as UUID
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
174
|
+
setError(err instanceof Error ? err : new Error(errorMessage));
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: errorMessage
|
|
178
|
+
};
|
|
179
|
+
} finally {
|
|
180
|
+
setIsLoading(false);
|
|
181
|
+
}
|
|
182
|
+
}, [user?.id]);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Revoke an event app role by role ID (alternative method)
|
|
186
|
+
*
|
|
187
|
+
* This fetches the role by ID first to get the required context (role name, event_id, app_id),
|
|
188
|
+
* then uses the unified `rbac_role_revoke` function to revoke it.
|
|
189
|
+
*
|
|
190
|
+
* @param roleId - The role ID to revoke
|
|
191
|
+
* @returns Promise resolving to operation result
|
|
192
|
+
*/
|
|
193
|
+
const revokeRoleById = useCallback(async (
|
|
194
|
+
roleId: UUID
|
|
195
|
+
): Promise<RoleManagementResult> => {
|
|
196
|
+
setIsLoading(true);
|
|
197
|
+
setError(null);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
// First, fetch the role by ID to get the required context
|
|
201
|
+
const { data: roleData, error: fetchError } = await supabase
|
|
202
|
+
.from('rbac_event_app_roles')
|
|
203
|
+
.select('user_id, role, event_id, app_id')
|
|
204
|
+
.eq('id', roleId)
|
|
205
|
+
.single();
|
|
206
|
+
|
|
207
|
+
if (fetchError || !roleData) {
|
|
208
|
+
throw new Error(fetchError?.message || 'Role not found');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Construct context_id in the format required by rbac_role_revoke: "event_id:app_id"
|
|
212
|
+
const contextId = `${roleData.event_id}:${roleData.app_id}`;
|
|
213
|
+
|
|
214
|
+
// Now call rbac_role_revoke with the required parameters
|
|
215
|
+
const { data, error: rpcError } = await supabase.rpc('rbac_role_revoke', {
|
|
216
|
+
p_user_id: roleData.user_id,
|
|
217
|
+
p_role_type: 'event_app',
|
|
218
|
+
p_role_name: roleData.role,
|
|
219
|
+
p_context_id: contextId,
|
|
220
|
+
p_revoked_by: user?.id || undefined
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (rpcError) {
|
|
224
|
+
throw new Error(rpcError.message || 'Failed to revoke role');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// rbac_role_revoke returns a table with success, message, revoked_count, error_code
|
|
228
|
+
const result = Array.isArray(data) && data.length > 0 ? data[0] : null;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: result?.success === true,
|
|
232
|
+
message: result?.message || undefined,
|
|
233
|
+
error: result?.success === false ? (result?.message || result?.error_code || 'Unknown error') : undefined
|
|
234
|
+
};
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
237
|
+
setError(err instanceof Error ? err : new Error(errorMessage));
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
error: errorMessage
|
|
241
|
+
};
|
|
242
|
+
} finally {
|
|
243
|
+
setIsLoading(false);
|
|
244
|
+
}
|
|
245
|
+
}, [user?.id, supabase]);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
revokeEventAppRole,
|
|
249
|
+
grantEventAppRole,
|
|
250
|
+
revokeRoleById,
|
|
251
|
+
isLoading,
|
|
252
|
+
error
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
@@ -416,6 +416,16 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
416
416
|
this.session = session;
|
|
417
417
|
this.user = session.user ?? null;
|
|
418
418
|
this.authError = null;
|
|
419
|
+
|
|
420
|
+
// Reset restoration state if valid session arrives after earlier failure
|
|
421
|
+
// This clears stale errors when session eventually succeeds
|
|
422
|
+
const hasTimeoutError = this.sessionRestorationState.restorationError?.name === 'SessionRestorationTimeoutError';
|
|
423
|
+
if (this.sessionRestorationState.isRestoring ||
|
|
424
|
+
this.sessionRestorationState.restorationError ||
|
|
425
|
+
(hasTimeoutError && session)) {
|
|
426
|
+
this.finishSessionRestoration();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
419
429
|
}
|
|
420
430
|
|
|
421
431
|
if (this.sessionRestorationState.isRestoring) {
|
|
@@ -15,6 +15,7 @@ import { Event } from '../types/unified';
|
|
|
15
15
|
import { Organisation } from '../types/organisation';
|
|
16
16
|
import { DebugLogger } from '../utils/debugLogger';
|
|
17
17
|
import { applyPalette, clearPalette } from '../theming/runtime';
|
|
18
|
+
import { secureStorage } from '../utils/secureStorage';
|
|
18
19
|
|
|
19
20
|
export class EventService extends BaseService implements IEventService {
|
|
20
21
|
private events: Event[] = [];
|
|
@@ -53,17 +54,40 @@ export class EventService extends BaseService implements IEventService {
|
|
|
53
54
|
this.setSelectedEventId = setSelectedEventId;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// Helper method to get user-scoped storage key
|
|
58
|
+
private getStorageKey(userId: string | null): string {
|
|
59
|
+
if (!userId) {
|
|
60
|
+
// Return a temporary key that won't match any user
|
|
61
|
+
return 'pace-core-selected-event-no-user';
|
|
62
|
+
}
|
|
63
|
+
return `pace-core-selected-event-${userId}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
// Update dependencies
|
|
57
|
-
updateDependencies(
|
|
67
|
+
async updateDependencies(
|
|
58
68
|
supabaseClient: SupabaseClient,
|
|
59
69
|
user: User | null,
|
|
60
70
|
session: Session | null,
|
|
61
71
|
appName: string,
|
|
62
72
|
selectedOrganisation: Organisation | null,
|
|
63
73
|
setSelectedEventId: (eventId: string | null) => void
|
|
64
|
-
): void {
|
|
74
|
+
): Promise<void> {
|
|
65
75
|
const previousOrgId = this.selectedOrganisation?.id;
|
|
66
76
|
const newOrgId = selectedOrganisation?.id;
|
|
77
|
+
const previousUserId = this.user?.id || null;
|
|
78
|
+
const newUserId = user?.id || null;
|
|
79
|
+
|
|
80
|
+
// If user changed, clear previous user's event selection from storage
|
|
81
|
+
if (previousUserId !== newUserId) {
|
|
82
|
+
if (previousUserId !== null) {
|
|
83
|
+
await this.clearEventSelectionForUser(previousUserId);
|
|
84
|
+
}
|
|
85
|
+
// If user is now null (logout), clear current selection state
|
|
86
|
+
if (newUserId === null) {
|
|
87
|
+
this.selectedEvent = null;
|
|
88
|
+
this.setSelectedEventId?.(null);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
67
91
|
|
|
68
92
|
this.supabaseClient = supabaseClient;
|
|
69
93
|
this.user = user;
|
|
@@ -133,23 +157,27 @@ export class EventService extends BaseService implements IEventService {
|
|
|
133
157
|
|
|
134
158
|
this.selectedEvent = event;
|
|
135
159
|
this.setSelectedEventId?.(event.event_id);
|
|
136
|
-
|
|
160
|
+
// Persist asynchronously (don't await to avoid blocking)
|
|
161
|
+
this.persistEventSelection(event.event_id).catch(error => {
|
|
162
|
+
console.warn('[EventService] Failed to persist event selection:', error);
|
|
163
|
+
});
|
|
137
164
|
// Reset the user cleared flag when selecting an event
|
|
138
165
|
this.userClearedEventRef = false;
|
|
139
|
-
//
|
|
140
|
-
|
|
166
|
+
// Theme application is now handled by useEventTheme() hook
|
|
167
|
+
// No need to call updateThemeForSelectedEvent() here
|
|
141
168
|
} else {
|
|
142
169
|
this.selectedEvent = null;
|
|
143
170
|
this.setSelectedEventId?.(null);
|
|
144
|
-
// Clear
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
// Clear from secure storage (don't await to avoid blocking)
|
|
172
|
+
this.clearEventSelection().catch(error => {
|
|
173
|
+
console.warn('[EventService] Failed to clear event selection:', error);
|
|
174
|
+
});
|
|
147
175
|
// Reset the auto-selection flag when clearing the event
|
|
148
176
|
this.hasAutoSelectedRef = false;
|
|
149
177
|
// Mark that user explicitly cleared the event to prevent auto-selection
|
|
150
178
|
this.userClearedEventRef = true;
|
|
151
|
-
//
|
|
152
|
-
|
|
179
|
+
// Theme clearing is now handled by useEventTheme() hook
|
|
180
|
+
// No need to call updateThemeForSelectedEvent() here
|
|
153
181
|
}
|
|
154
182
|
this.notify();
|
|
155
183
|
}
|
|
@@ -163,31 +191,31 @@ export class EventService extends BaseService implements IEventService {
|
|
|
163
191
|
|
|
164
192
|
async loadPersistedEvent(events: Event[]): Promise<boolean> {
|
|
165
193
|
try {
|
|
166
|
-
|
|
167
|
-
let persistedEventId = sessionStorage.getItem('pace-core-selected-event');
|
|
194
|
+
const userId = this.user?.id || null;
|
|
168
195
|
|
|
169
|
-
//
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
// If we found a value in localStorage, also store it in sessionStorage for this tab
|
|
173
|
-
if (persistedEventId) {
|
|
174
|
-
sessionStorage.setItem('pace-core-selected-event', persistedEventId);
|
|
175
|
-
}
|
|
196
|
+
// Don't load persisted event if no user is authenticated
|
|
197
|
+
if (!userId) {
|
|
198
|
+
return false;
|
|
176
199
|
}
|
|
177
200
|
|
|
201
|
+
const storageKey = this.getStorageKey(userId);
|
|
202
|
+
|
|
203
|
+
// Retrieve from secure storage (will automatically decrypt)
|
|
204
|
+
const persistedEventId = await secureStorage.getItem(storageKey);
|
|
205
|
+
|
|
178
206
|
if (persistedEventId && events.length > 0) {
|
|
207
|
+
// Validate that event exists in user's accessible events
|
|
179
208
|
const persistedEvent = events.find(event => event.event_id === persistedEventId);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
localStorage.removeItem('pace-core-selected-event');
|
|
209
|
+
|
|
210
|
+
if (persistedEvent) {
|
|
211
|
+
// Use setSelectedEvent() to go through same path as EventSelector
|
|
212
|
+
// This ensures consistent behavior and proper notification
|
|
213
|
+
// Theme will be applied by useEventTheme hook once user navigates away from login
|
|
214
|
+
this.setSelectedEvent(persistedEvent);
|
|
215
|
+
return true;
|
|
216
|
+
} else {
|
|
217
|
+
// Event no longer accessible to user, clear invalid persisted event
|
|
218
|
+
await secureStorage.removeItem(storageKey);
|
|
191
219
|
}
|
|
192
220
|
}
|
|
193
221
|
} catch (error) {
|
|
@@ -210,22 +238,26 @@ export class EventService extends BaseService implements IEventService {
|
|
|
210
238
|
return await this.loadPersistedEvent(this.events);
|
|
211
239
|
}
|
|
212
240
|
|
|
213
|
-
persistEventSelection(eventId: string): void {
|
|
241
|
+
async persistEventSelection(eventId: string): Promise<void> {
|
|
214
242
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
const userId = this.user?.id || null;
|
|
244
|
+
const storageKey = this.getStorageKey(userId);
|
|
245
|
+
|
|
246
|
+
// Store with encryption using secureStorage
|
|
247
|
+
await secureStorage.setItem(storageKey, eventId, { encrypt: true });
|
|
219
248
|
} catch (error) {
|
|
220
249
|
console.warn('[EventService] Failed to persist event selection:', error);
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
252
|
|
|
224
|
-
clearEventSelection(): void {
|
|
253
|
+
async clearEventSelection(): Promise<void> {
|
|
225
254
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
const userId = this.user?.id || null;
|
|
256
|
+
const storageKey = this.getStorageKey(userId);
|
|
257
|
+
|
|
258
|
+
// Clear from secure storage
|
|
259
|
+
await secureStorage.removeItem(storageKey);
|
|
260
|
+
|
|
229
261
|
// Clear the selected event
|
|
230
262
|
this.selectedEvent = null;
|
|
231
263
|
this.setSelectedEventId?.(null);
|
|
@@ -234,20 +266,49 @@ export class EventService extends BaseService implements IEventService {
|
|
|
234
266
|
}
|
|
235
267
|
}
|
|
236
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Clear event selection for a specific user (used when user logs out or changes)
|
|
271
|
+
*/
|
|
272
|
+
async clearEventSelectionForUser(userId: string | null): Promise<void> {
|
|
273
|
+
try {
|
|
274
|
+
if (!userId) return;
|
|
275
|
+
|
|
276
|
+
const storageKey = this.getStorageKey(userId);
|
|
277
|
+
await secureStorage.removeItem(storageKey);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn('[EventService] Failed to clear event selection for user:', error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
237
283
|
autoSelectNextEvent(events: Event[]): void {
|
|
238
284
|
const nextEvent = this.getNextEventByDate(events);
|
|
239
285
|
if (nextEvent) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
// Apply theme for auto-selected event
|
|
244
|
-
this.updateThemeForSelectedEvent();
|
|
286
|
+
// Use setSelectedEvent() to ensure consistent behavior
|
|
287
|
+
// Theme will be applied by useEventTheme() hook
|
|
288
|
+
this.setSelectedEvent(nextEvent);
|
|
245
289
|
}
|
|
246
290
|
}
|
|
247
291
|
|
|
248
292
|
// Lifecycle methods
|
|
249
293
|
async initialize(): Promise<void> {
|
|
250
294
|
await super.initialize();
|
|
295
|
+
|
|
296
|
+
// Clean up old global storage keys (backward compatibility)
|
|
297
|
+
// This ensures old data doesn't leak to new users
|
|
298
|
+
try {
|
|
299
|
+
// Remove old plain storage key
|
|
300
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
301
|
+
sessionStorage.removeItem('pace-core-selected-event');
|
|
302
|
+
}
|
|
303
|
+
if (typeof localStorage !== 'undefined') {
|
|
304
|
+
localStorage.removeItem('pace-core-selected-event');
|
|
305
|
+
// Also remove old encrypted format if it exists
|
|
306
|
+
localStorage.removeItem('_sec_pace-core-selected-event');
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn('[EventService] Failed to clean up old storage keys:', error);
|
|
310
|
+
}
|
|
311
|
+
|
|
251
312
|
// Load persisted event during initialization (don't skip)
|
|
252
313
|
// This ensures the last viewed event is restored before auto-selection happens
|
|
253
314
|
await this.fetchEvents(false);
|
|
@@ -342,9 +403,9 @@ export class EventService extends BaseService implements IEventService {
|
|
|
342
403
|
const nextEvent = this.getNextEventByDate(transformedEvents);
|
|
343
404
|
if (nextEvent) {
|
|
344
405
|
this.hasAutoSelectedRef = true;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.
|
|
406
|
+
// Use setSelectedEvent() to ensure consistent behavior
|
|
407
|
+
// Theme will be applied by useEventTheme() hook
|
|
408
|
+
this.setSelectedEvent(nextEvent);
|
|
348
409
|
}
|
|
349
410
|
}
|
|
350
411
|
} else {
|
|
@@ -353,9 +414,9 @@ export class EventService extends BaseService implements IEventService {
|
|
|
353
414
|
const nextEvent = this.getNextEventByDate(transformedEvents);
|
|
354
415
|
if (nextEvent) {
|
|
355
416
|
this.hasAutoSelectedRef = true;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
417
|
+
// Use setSelectedEvent() to ensure consistent behavior
|
|
418
|
+
// Theme will be applied by useEventTheme() hook
|
|
419
|
+
this.setSelectedEvent(nextEvent);
|
|
359
420
|
}
|
|
360
421
|
}
|
|
361
422
|
}
|
|
@@ -689,7 +689,7 @@ describe('AuthService', () => {
|
|
|
689
689
|
p_session_type: 'login',
|
|
690
690
|
p_event_id: null,
|
|
691
691
|
p_app_id: 'app-id-123', // Should be resolved from appName
|
|
692
|
-
p_user_agent
|
|
692
|
+
// p_user_agent, p_device_fingerprint, p_ip_address may be undefined in test environment
|
|
693
693
|
}));
|
|
694
694
|
}
|
|
695
695
|
|