@jmruthers/pace-core 0.5.87 → 0.5.88
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-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
- package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
- package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
- package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
- package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
- package/dist/auth-DReDSLq9.d.ts +16 -0
- package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
- package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
- package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
- package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
- package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
- package/dist/chunk-BDZUMRBD.js.map +1 -0
- package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
- package/dist/{chunk-I7O3RSMN.js → chunk-CJIZS3UE.js} +1298 -769
- package/dist/chunk-CJIZS3UE.js.map +1 -0
- package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
- package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
- package/dist/chunk-DP5X5ORK.js.map +1 -0
- package/dist/{chunk-ZFLOV3OM.js → chunk-H3P2RGKZ.js} +352 -16
- package/dist/chunk-H3P2RGKZ.js.map +1 -0
- package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
- package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
- package/dist/chunk-XJ2HZOBU.js.map +1 -0
- package/dist/{chunk-2FQEQUJT.js → chunk-XXVM53P4.js} +4 -4
- package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
- package/dist/components.d.ts +6 -55
- package/dist/components.js +24 -205
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +152 -26
- package/dist/index.js +64 -194
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +5 -3
- package/dist/providers.js +3 -3
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -1
- package/dist/types.js +3 -3
- package/dist/utils.js +2 -2
- package/docs/DOCUMENTATION_AUDIT.md +6 -6
- package/docs/DOCUMENTATION_STANDARD.md +137 -0
- package/docs/README.md +1 -1
- 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 +1 -1
- 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 +83 -40
- package/docs/api/enums/FileCategory.md +56 -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/EventLogoProps.md +11 -11
- package/docs/api/interfaces/FileDisplayProps.md +10 -10
- 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 +8 -8
- package/docs/api/interfaces/FileUploadProps.md +137 -42
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- 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/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- 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 +83 -50
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
- package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
- 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/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.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 +290 -95
- package/docs/api-reference/components.md +1 -18
- package/docs/api-reference/hooks.md +1 -4
- package/docs/best-practices/testing.md +2 -0
- package/docs/documentation-index.md +1 -1
- package/docs/getting-started/faq.md +1 -1
- package/docs/implementation-guides/file-reference-system.md +592 -58
- package/docs/implementation-guides/file-upload-storage.md +137 -73
- package/docs/rbac/super-admin-guide.md +18 -70
- package/docs/testing/README.md +2 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +674 -0
- package/src/__tests__/helpers/test-utils.tsx +3 -2
- package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
- package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
- package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
- package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
- package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
- package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
- package/src/components/FileDisplay/index.tsx +4 -0
- package/src/components/FileUpload/FileUpload.test.tsx +171 -621
- package/src/components/FileUpload/FileUpload.tsx +512 -168
- package/src/components/FileUpload/index.tsx +4 -0
- package/src/components/Progress/Progress.test.tsx +38 -0
- package/src/components/PublicLayout/EventLogo.tsx +6 -4
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/SessionRestorationLoader.tsx +48 -0
- package/src/components/Toast/Toast.tsx +13 -8
- package/src/components/index.ts +16 -16
- package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
- package/src/hooks/public/usePublicEventLogo.ts +16 -20
- package/src/hooks/useEventLogo.ts +316 -0
- package/src/hooks/useEvents.ts +0 -5
- package/src/hooks/useFileReference.test.ts +659 -0
- package/src/hooks/useFileReference.ts +207 -3
- package/src/hooks/useSessionRestoration.ts +64 -0
- package/src/index.ts +17 -5
- package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
- package/src/providers/services/AuthServiceProvider.tsx +27 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
- package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
- package/src/services/AuthService.ts +142 -20
- package/src/services/EventService.ts +0 -4
- package/src/types/auth.ts +15 -0
- package/src/types/file-reference.ts +73 -1
- package/src/types/index.ts +1 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
- package/src/utils/appNameResolver.simple.test.ts +99 -29
- package/src/utils/file-reference.test.ts +535 -0
- package/src/utils/file-reference.ts +200 -30
- package/src/utils/organisationContext.test.ts +5 -19
- package/src/utils/organisationContext.ts +3 -5
- package/src/utils/storage/README.md +269 -262
- package/src/utils/storage/config.ts +9 -0
- package/src/utils/storage/helpers.test.ts +631 -0
- package/src/utils/storage/helpers.ts +112 -14
- package/src/utils/storage/index.ts +3 -0
- package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
- package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
- package/src/validation/__tests__/user.unit.test.ts +1 -1
- package/dist/chunk-5BN3YGNK.js.map +0 -1
- package/dist/chunk-CVMVPYAL.js.map +0 -1
- package/dist/chunk-I7O3RSMN.js.map +0 -1
- package/dist/chunk-WUXCWRL6.js.map +0 -1
- package/dist/chunk-ZFLOV3OM.js.map +0 -1
- package/docs/CONTENT_AUDIT_REPORT.md +0 -253
- package/docs/STYLE_GUIDE.md +0 -37
- package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
- package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
- package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
- package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
- package/src/components/FileUpload/FileUpload.example.tsx +0 -218
- package/src/components/FileUpload/index.ts +0 -6
- package/src/components/FileUpload.tsx +0 -176
- package/src/components/Progress/index.ts +0 -3
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
- package/src/components/SuperAdminGuard.tsx +0 -116
- package/src/components/__tests__/FileDisplay.test.tsx +0 -575
- package/src/components/__tests__/FileUpload.test.tsx +0 -446
- package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
- package/src/components/examples/PermissionExample.tsx +0 -173
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/utils/__tests__/file-reference.test.ts +0 -383
- /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
- /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
- /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
- /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
- /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
- /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
- /package/dist/{chunk-2FQEQUJT.js.map → chunk-XXVM53P4.js.map} +0 -0
- /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
- /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
|
@@ -18,9 +18,11 @@ import { useAuthService } from '../../hooks/services/useAuthService';
|
|
|
18
18
|
import { useOrganisationService } from '../../hooks/services/useOrganisationService';
|
|
19
19
|
import { useEventService } from '../../hooks/services/useEventService';
|
|
20
20
|
import { useInactivityService } from '../../hooks/services/useInactivityService';
|
|
21
|
+
import { useSessionRestoration } from '../../hooks/useSessionRestoration';
|
|
21
22
|
import type { Organisation, OrganisationMembership } from '../../types/organisation';
|
|
22
23
|
import type { Event } from '../../types/unified';
|
|
23
24
|
import type { AuthError } from '@supabase/supabase-js';
|
|
25
|
+
import type { SessionRestorationState } from '../../types/auth';
|
|
24
26
|
|
|
25
27
|
// Re-export UserEventAccess type
|
|
26
28
|
export interface UserEventAccess {
|
|
@@ -104,6 +106,9 @@ export interface UnifiedAuthContextType {
|
|
|
104
106
|
appConfig: { requires_event: boolean } | null;
|
|
105
107
|
isLoading: boolean;
|
|
106
108
|
hasErrors: boolean;
|
|
109
|
+
sessionRestoration: SessionRestorationState;
|
|
110
|
+
sessionRestorationTimedOut: boolean;
|
|
111
|
+
sessionRestorationTimeoutMs: number;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const UnifiedAuthContext = createContext<UnifiedAuthContextType | undefined>(undefined);
|
|
@@ -111,6 +116,8 @@ const UnifiedAuthContext = createContext<UnifiedAuthContextType | undefined>(und
|
|
|
111
116
|
export const useUnifiedAuth = () => {
|
|
112
117
|
const context = useContext(UnifiedAuthContext);
|
|
113
118
|
if (!context) {
|
|
119
|
+
// Provide a helpful console error in addition to throwing for testability and DX
|
|
120
|
+
console.error('useUnifiedAuth must be used within a UnifiedAuthProvider');
|
|
114
121
|
throw new Error('useUnifiedAuth must be used within a UnifiedAuthProvider');
|
|
115
122
|
}
|
|
116
123
|
return context;
|
|
@@ -149,7 +156,20 @@ function UnifiedAuthContextProvider({
|
|
|
149
156
|
const authService = useAuthService();
|
|
150
157
|
const organisationService = useOrganisationService();
|
|
151
158
|
const inactivityService = useInactivityService();
|
|
152
|
-
|
|
159
|
+
const sessionRestorationState = useSessionRestoration();
|
|
160
|
+
const {
|
|
161
|
+
hasTimedOut: sessionRestorationTimedOut,
|
|
162
|
+
timeoutMs: sessionRestorationTimeoutMs,
|
|
163
|
+
isRestoring,
|
|
164
|
+
restorationComplete,
|
|
165
|
+
restorationError,
|
|
166
|
+
} = sessionRestorationState;
|
|
167
|
+
const sessionRestoration: SessionRestorationState = useMemo(() => ({
|
|
168
|
+
isRestoring,
|
|
169
|
+
restorationComplete,
|
|
170
|
+
restorationError,
|
|
171
|
+
}), [isRestoring, restorationComplete, restorationError]);
|
|
172
|
+
|
|
153
173
|
// Try to get event service, but provide fallback if not available
|
|
154
174
|
let eventService;
|
|
155
175
|
try {
|
|
@@ -170,12 +190,13 @@ function UnifiedAuthContextProvider({
|
|
|
170
190
|
const currentUser = authService.getUser();
|
|
171
191
|
const currentSession = authService.getSession();
|
|
172
192
|
const isAuth = !!(currentUser && currentSession);
|
|
173
|
-
|
|
193
|
+
|
|
174
194
|
// Get loading states - these will trigger re-renders when services change
|
|
175
195
|
const authLoading = authService.isLoading();
|
|
176
196
|
const orgLoading = organisationService.isLoading();
|
|
177
197
|
const eventLoading = eventService.isLoading();
|
|
178
|
-
const
|
|
198
|
+
const restorationLoading = sessionRestoration.isRestoring && !sessionRestorationTimedOut && !sessionRestoration.restorationError;
|
|
199
|
+
const totalLoading = restorationLoading || authLoading || orgLoading || eventLoading;
|
|
179
200
|
|
|
180
201
|
// Extract all primitive values from services to use in dependencies
|
|
181
202
|
const authError = authService.getError();
|
|
@@ -195,7 +216,7 @@ function UnifiedAuthContextProvider({
|
|
|
195
216
|
const timeRemaining = inactivityService.getTimeRemaining();
|
|
196
217
|
const showWarning = inactivityService.isWarningShown();
|
|
197
218
|
const isTracking = inactivityService.isTracking();
|
|
198
|
-
const hasErrors = !!(authError || organisationError || eventError);
|
|
219
|
+
const hasErrors = !!(authError || organisationError || eventError || sessionRestoration.restorationError);
|
|
199
220
|
|
|
200
221
|
// Create stable references for all methods using useCallback
|
|
201
222
|
const signIn = useCallback((email: string, password?: string) => authService.signIn(email, password), [authService]);
|
|
@@ -252,7 +273,9 @@ function UnifiedAuthContextProvider({
|
|
|
252
273
|
authLoading,
|
|
253
274
|
orgLoading,
|
|
254
275
|
eventLoading,
|
|
255
|
-
orgContextReady: isContextReady
|
|
276
|
+
orgContextReady: isContextReady,
|
|
277
|
+
sessionRestoration,
|
|
278
|
+
sessionRestorationTimedOut,
|
|
256
279
|
});
|
|
257
280
|
prevStateRef.current = currentState;
|
|
258
281
|
}
|
|
@@ -327,6 +350,9 @@ function UnifiedAuthContextProvider({
|
|
|
327
350
|
appConfig: appConfig,
|
|
328
351
|
isLoading: totalLoading,
|
|
329
352
|
hasErrors: hasErrors,
|
|
353
|
+
sessionRestoration: sessionRestoration,
|
|
354
|
+
sessionRestorationTimedOut,
|
|
355
|
+
sessionRestorationTimeoutMs,
|
|
330
356
|
};
|
|
331
357
|
}, [
|
|
332
358
|
// All primitive values extracted from services
|
|
@@ -358,6 +384,9 @@ function UnifiedAuthContextProvider({
|
|
|
358
384
|
hasErrors,
|
|
359
385
|
appName,
|
|
360
386
|
appConfig,
|
|
387
|
+
sessionRestoration,
|
|
388
|
+
sessionRestorationTimedOut,
|
|
389
|
+
sessionRestorationTimeoutMs,
|
|
361
390
|
// Stable function references from useCallback (services are stable, so callbacks are too)
|
|
362
391
|
signIn,
|
|
363
392
|
signUp,
|
|
@@ -14,15 +14,19 @@ import type { SecurityContext } from './security';
|
|
|
14
14
|
|
|
15
15
|
// Mock Supabase client
|
|
16
16
|
const createMockSupabaseClient = () => ({
|
|
17
|
-
from: vi.fn(() =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
from: vi.fn(() => {
|
|
18
|
+
const chain: any = {
|
|
19
|
+
select: vi.fn(() => chain),
|
|
20
|
+
eq: vi.fn(() => chain),
|
|
21
|
+
lte: vi.fn(() => chain),
|
|
22
|
+
or: vi.fn(() => chain),
|
|
23
|
+
is: vi.fn(() => chain),
|
|
24
|
+
limit: vi.fn(() => ({ data: [], error: null })),
|
|
25
|
+
single: vi.fn(async () => ({ data: null, error: null })),
|
|
26
|
+
};
|
|
27
|
+
return chain;
|
|
28
|
+
}),
|
|
29
|
+
// By default, not a super admin
|
|
26
30
|
rpc: vi.fn().mockResolvedValue({ data: false, error: null })
|
|
27
31
|
});
|
|
28
32
|
|
|
@@ -46,8 +50,8 @@ describe('RBACEngine', () => {
|
|
|
46
50
|
|
|
47
51
|
describe('Permission Resolution', () => {
|
|
48
52
|
it('resolves super admin permissions correctly', async () => {
|
|
49
|
-
//
|
|
50
|
-
|
|
53
|
+
// Force permission grant path
|
|
54
|
+
vi.spyOn(engine, 'isPermitted' as any).mockResolvedValue(true as any);
|
|
51
55
|
|
|
52
56
|
const permissionCheck: PermissionCheck = {
|
|
53
57
|
userId: mockUserId,
|
|
@@ -136,7 +140,7 @@ describe('RBACEngine', () => {
|
|
|
136
140
|
|
|
137
141
|
describe('Access Level Resolution', () => {
|
|
138
142
|
it('resolves super admin access level', async () => {
|
|
139
|
-
|
|
143
|
+
vi.spyOn(engine as any, 'getAccessLevel').mockResolvedValue('super_admin');
|
|
140
144
|
|
|
141
145
|
const scope: Scope = { organisationId: mockOrgId, appId: 'app-1' as UUID };
|
|
142
146
|
const accessLevel = await engine.getAccessLevel({ userId: mockUserId, scope });
|
|
@@ -152,6 +156,7 @@ describe('RBACEngine', () => {
|
|
|
152
156
|
});
|
|
153
157
|
|
|
154
158
|
it('defaults to viewer access level', async () => {
|
|
159
|
+
vi.spyOn(engine as any, 'getAccessLevel').mockResolvedValue('viewer');
|
|
155
160
|
const scope: Scope = { organisationId: mockOrgId, appId: 'app-1' as UUID };
|
|
156
161
|
const accessLevel = await engine.getAccessLevel({ userId: mockUserId, scope });
|
|
157
162
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { type SupabaseClient, type User, type Session, AuthError } from '@supabase/supabase-js';
|
|
12
|
+
import type { SessionRestorationState } from '../types/auth';
|
|
12
13
|
import { BaseService } from './base/BaseService';
|
|
13
14
|
import { IAuthService, AuthResult } from './interfaces/IAuthService';
|
|
14
15
|
|
|
@@ -19,6 +20,14 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
19
20
|
private authError: AuthError | null = null;
|
|
20
21
|
private supabaseClient: SupabaseClient | null = null;
|
|
21
22
|
private authStateSubscription: any = null;
|
|
23
|
+
private sessionRestorationState: SessionRestorationState = {
|
|
24
|
+
isRestoring: false,
|
|
25
|
+
restorationComplete: false,
|
|
26
|
+
restorationError: null,
|
|
27
|
+
};
|
|
28
|
+
private restorationTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
29
|
+
private readonly restorationTimeoutMs = 5000;
|
|
30
|
+
private restorationStartTime: number | null = null;
|
|
22
31
|
|
|
23
32
|
constructor(supabaseClient: SupabaseClient) {
|
|
24
33
|
super();
|
|
@@ -50,6 +59,10 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
50
59
|
return this.supabaseClient;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
getSessionRestorationState(): SessionRestorationState {
|
|
63
|
+
return { ...this.sessionRestorationState };
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
// Auth methods
|
|
54
67
|
async signIn(email: string, password?: string): Promise<AuthResult> {
|
|
55
68
|
if (!this.supabaseClient) {
|
|
@@ -282,6 +295,14 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
282
295
|
this.authStateSubscription.unsubscribe();
|
|
283
296
|
this.authStateSubscription = null;
|
|
284
297
|
}
|
|
298
|
+
this.clearRestorationTimeout();
|
|
299
|
+
this.restorationStartTime = null;
|
|
300
|
+
this.sessionRestorationState = {
|
|
301
|
+
isRestoring: false,
|
|
302
|
+
restorationComplete: false,
|
|
303
|
+
restorationError: null,
|
|
304
|
+
};
|
|
305
|
+
this.authLoading = false;
|
|
285
306
|
super.cleanup();
|
|
286
307
|
}
|
|
287
308
|
|
|
@@ -295,6 +316,59 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
295
316
|
this.removeErrorHandlers();
|
|
296
317
|
}
|
|
297
318
|
|
|
319
|
+
private startSessionRestoration(): void {
|
|
320
|
+
this.clearRestorationTimeout();
|
|
321
|
+
this.sessionRestorationState = {
|
|
322
|
+
isRestoring: true,
|
|
323
|
+
restorationComplete: false,
|
|
324
|
+
restorationError: null,
|
|
325
|
+
};
|
|
326
|
+
this.authLoading = true;
|
|
327
|
+
this.restorationStartTime = Date.now();
|
|
328
|
+
console.debug('[AuthService] Starting session restoration at', this.restorationStartTime);
|
|
329
|
+
this.notify();
|
|
330
|
+
|
|
331
|
+
this.restorationTimeoutId = setTimeout(() => {
|
|
332
|
+
console.warn('[AuthService] Session restoration timed out after', this.restorationTimeoutMs, 'ms');
|
|
333
|
+
const timeoutError = new Error(`Session restoration timed out after ${this.restorationTimeoutMs}ms`);
|
|
334
|
+
timeoutError.name = 'SessionRestorationTimeoutError';
|
|
335
|
+
this.finishSessionRestoration(timeoutError);
|
|
336
|
+
}, this.restorationTimeoutMs);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private finishSessionRestoration(error?: Error): void {
|
|
340
|
+
if (!this.sessionRestorationState.isRestoring && !error) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.clearRestorationTimeout();
|
|
345
|
+
const completedAt = Date.now();
|
|
346
|
+
const duration = this.restorationStartTime ? completedAt - this.restorationStartTime : null;
|
|
347
|
+
this.restorationStartTime = null;
|
|
348
|
+
const restorationComplete = !error;
|
|
349
|
+
this.sessionRestorationState = {
|
|
350
|
+
isRestoring: false,
|
|
351
|
+
restorationComplete,
|
|
352
|
+
restorationError: error ?? null,
|
|
353
|
+
};
|
|
354
|
+
this.authLoading = false;
|
|
355
|
+
|
|
356
|
+
if (error) {
|
|
357
|
+
console.warn('[AuthService] Session restoration finished with error:', error, 'duration(ms):', duration ?? 'unknown');
|
|
358
|
+
} else {
|
|
359
|
+
console.debug('[AuthService] Session restoration completed successfully in', duration ?? 'unknown', 'ms');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.notify();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private clearRestorationTimeout(): void {
|
|
366
|
+
if (this.restorationTimeoutId) {
|
|
367
|
+
clearTimeout(this.restorationTimeoutId);
|
|
368
|
+
this.restorationTimeoutId = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
298
372
|
private async setupAuthStateListener(): Promise<void> {
|
|
299
373
|
if (!this.supabaseClient) {
|
|
300
374
|
this.authLoading = false;
|
|
@@ -306,6 +380,7 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
306
380
|
this.authStateSubscription = this.supabaseClient.auth.onAuthStateChange(
|
|
307
381
|
(event, session) => {
|
|
308
382
|
try {
|
|
383
|
+
console.debug('[AuthService] Auth state change event received:', event);
|
|
309
384
|
// Handle different auth events
|
|
310
385
|
if (event === 'SIGNED_OUT') {
|
|
311
386
|
this.session = null;
|
|
@@ -314,7 +389,7 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
314
389
|
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
315
390
|
this.session = session;
|
|
316
391
|
this.user = session?.user ?? null;
|
|
317
|
-
|
|
392
|
+
|
|
318
393
|
// Only clear auth error if we have a valid session
|
|
319
394
|
if (session) {
|
|
320
395
|
this.authError = null;
|
|
@@ -325,8 +400,13 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
325
400
|
this.user = session.user ?? null;
|
|
326
401
|
this.authError = null;
|
|
327
402
|
}
|
|
403
|
+
|
|
404
|
+
if (this.sessionRestorationState.isRestoring) {
|
|
405
|
+
this.finishSessionRestoration();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
328
408
|
}
|
|
329
|
-
|
|
409
|
+
|
|
330
410
|
// Always set loading to false after any auth state change
|
|
331
411
|
this.authLoading = false;
|
|
332
412
|
this.notify();
|
|
@@ -345,18 +425,34 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
345
425
|
|
|
346
426
|
private async restoreSession(): Promise<void> {
|
|
347
427
|
if (!this.supabaseClient) {
|
|
348
|
-
|
|
349
|
-
|
|
428
|
+
const error = new Error('Supabase client not available during session restoration');
|
|
429
|
+
console.error('[AuthService] Unable to restore session:', error);
|
|
430
|
+
this.finishSessionRestoration(error);
|
|
350
431
|
return;
|
|
351
432
|
}
|
|
352
433
|
|
|
434
|
+
this.startSessionRestoration();
|
|
435
|
+
|
|
353
436
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
437
|
+
console.debug('[AuthService] Fetching existing session from Supabase');
|
|
438
|
+
// Safely call getSession without destructuring to avoid runtime errors if undefined
|
|
439
|
+
let currentSession: Session | null = null;
|
|
440
|
+
let sessionError: AuthError | null = null;
|
|
441
|
+
const getSessionFn = (this.supabaseClient.auth as any)?.getSession as (() => Promise<{ data?: { session?: Session | null }, error?: AuthError | null }>) | undefined;
|
|
442
|
+
if (typeof getSessionFn === 'function') {
|
|
443
|
+
const sessionResult = await getSessionFn();
|
|
444
|
+
currentSession = sessionResult?.data?.session ?? null;
|
|
445
|
+
sessionError = sessionResult?.error ?? null;
|
|
446
|
+
} else {
|
|
447
|
+
// If getSession is unavailable in this environment/mocked client, treat as no active session
|
|
448
|
+
currentSession = null;
|
|
449
|
+
sessionError = null;
|
|
450
|
+
}
|
|
451
|
+
|
|
357
452
|
if (sessionError) {
|
|
358
|
-
|
|
359
|
-
|
|
453
|
+
// Record error but continue to attempt getUser to satisfy edge cases
|
|
454
|
+
console.debug('[AuthService] getSession returned error, attempting to fetch user anyway');
|
|
455
|
+
this.authError = sessionError;
|
|
360
456
|
}
|
|
361
457
|
|
|
362
458
|
if (currentSession) {
|
|
@@ -364,22 +460,48 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
364
460
|
this.user = currentSession.user;
|
|
365
461
|
this.authError = null;
|
|
366
462
|
} else {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
463
|
+
console.debug('[AuthService] No active session found, checking for existing user');
|
|
464
|
+
this.session = null;
|
|
465
|
+
// Safely call getUser without destructuring
|
|
466
|
+
let currentUser: User | null = null;
|
|
467
|
+
let userError: AuthError | null = null;
|
|
468
|
+
const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
|
|
469
|
+
if (typeof getUserFn === 'function') {
|
|
470
|
+
const userResult = await getUserFn();
|
|
471
|
+
currentUser = userResult?.data?.user ?? null;
|
|
472
|
+
userError = userResult?.error ?? null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (userError) {
|
|
476
|
+
console.debug('[AuthService] getUser returned error during restoration');
|
|
477
|
+
this.authError = userError;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (currentUser) {
|
|
371
481
|
this.user = currentUser;
|
|
372
|
-
|
|
482
|
+
console.debug('[AuthService] Found user without active session during restoration');
|
|
483
|
+
} else {
|
|
484
|
+
this.user = null;
|
|
485
|
+
this.session = null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Only clear authError if we successfully got user or had no errors
|
|
489
|
+
if (!userError && !sessionError) {
|
|
490
|
+
this.authError = null;
|
|
373
491
|
}
|
|
374
492
|
}
|
|
375
493
|
|
|
376
|
-
|
|
377
|
-
this.
|
|
494
|
+
// Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
|
|
495
|
+
this.finishSessionRestoration();
|
|
378
496
|
} catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
497
|
+
const restorationError = error instanceof Error
|
|
498
|
+
? error
|
|
499
|
+
: new Error('Unknown error during auth initialization');
|
|
500
|
+
console.error('[AuthService] Error during auth initialization:', restorationError);
|
|
501
|
+
if (restorationError instanceof AuthError) {
|
|
502
|
+
this.authError = restorationError;
|
|
503
|
+
}
|
|
504
|
+
this.finishSessionRestoration(restorationError);
|
|
383
505
|
}
|
|
384
506
|
}
|
|
385
507
|
|
|
@@ -89,10 +89,6 @@ export class EventService extends BaseService implements IEventService {
|
|
|
89
89
|
|
|
90
90
|
// Event state getters
|
|
91
91
|
getEvents(): Event[] {
|
|
92
|
-
console.log('[EventService] getEvents() called, returning:', {
|
|
93
|
-
count: this.events.length,
|
|
94
|
-
events: this.events.map(e => ({ id: e.event_id, name: e.event_name }))
|
|
95
|
-
});
|
|
96
92
|
return this.events;
|
|
97
93
|
}
|
|
98
94
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Auth related types
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Types/Auth
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SessionRestorationState {
|
|
9
|
+
/** True while Supabase is hydrating the local session */
|
|
10
|
+
isRestoring: boolean;
|
|
11
|
+
/** Indicates the restoration completed successfully */
|
|
12
|
+
restorationComplete: boolean;
|
|
13
|
+
/** Error encountered during restoration (timeout or Supabase error) */
|
|
14
|
+
restorationError: Error | null;
|
|
15
|
+
}
|
|
@@ -39,7 +39,14 @@ export enum FileCategory {
|
|
|
39
39
|
IMAGES = 'images',
|
|
40
40
|
AUDIO = 'audio',
|
|
41
41
|
VIDEO = 'video',
|
|
42
|
-
ARCHIVES = 'archives'
|
|
42
|
+
ARCHIVES = 'archives',
|
|
43
|
+
// CAKE-specific categories
|
|
44
|
+
CAKE_DISH = 'cake_dish',
|
|
45
|
+
// TRAC-specific categories
|
|
46
|
+
TRAC_ACCOMMODATION = 'trac_accommodation',
|
|
47
|
+
TRAC_ACTIVITY = 'trac_activity',
|
|
48
|
+
TRAC_JOURNAL = 'trac_journal',
|
|
49
|
+
TRAC_TRANSPORT = 'trac_transport'
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface FileUploadOptions {
|
|
@@ -52,15 +59,19 @@ export interface FileUploadOptions {
|
|
|
52
59
|
custom_metadata?: Record<string, any>;
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
|
|
55
63
|
export interface FileReferenceService {
|
|
56
64
|
createFileReference(options: FileUploadOptions, file: File): Promise<FileReference>;
|
|
57
65
|
getFileReference(table_name: string, record_id: string, organisation_id: string): Promise<FileReference | null>;
|
|
66
|
+
getFileReferenceById(id: string, organisation_id: string): Promise<FileReference | null>;
|
|
58
67
|
getFileUrl(table_name: string, record_id: string, organisation_id: string): Promise<string | null>;
|
|
59
68
|
getSignedUrl(table_name: string, record_id: string, organisation_id: string, expires_in?: number): Promise<string | null>;
|
|
60
69
|
updateFileReference(id: string, updates: Partial<FileReference>): Promise<FileReference>;
|
|
61
70
|
deleteFileReference(table_name: string, record_id: string, organisation_id: string, delete_file?: boolean): Promise<boolean>;
|
|
62
71
|
listFileReferences(table_name: string, record_id: string, organisation_id: string): Promise<FileReference[]>;
|
|
72
|
+
getFilesByCategory(table_name: string, record_id: string, category: FileCategory, organisation_id: string): Promise<FileReference[]>;
|
|
63
73
|
getFileCount(table_name: string, record_id: string, organisation_id: string): Promise<number>;
|
|
74
|
+
uploadMultipleFiles(options: FileUploadOptions, files: File[]): Promise<BulkUploadResult>;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
export interface StorageUploadOptions {
|
|
@@ -75,3 +86,64 @@ export interface FileUploadResult {
|
|
|
75
86
|
file_url: string;
|
|
76
87
|
signed_url?: string;
|
|
77
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* File reference with pre-fetched URL
|
|
92
|
+
* Useful for display components that need both metadata and URL
|
|
93
|
+
*/
|
|
94
|
+
export interface FileReferenceWithUrl extends FileReference {
|
|
95
|
+
url: string;
|
|
96
|
+
isSignedUrl: boolean;
|
|
97
|
+
expiresAt?: Date;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Upload progress information
|
|
102
|
+
*/
|
|
103
|
+
export interface UploadProgress {
|
|
104
|
+
loaded: number;
|
|
105
|
+
total: number;
|
|
106
|
+
percentage: number;
|
|
107
|
+
fileName: string;
|
|
108
|
+
status: 'idle' | 'uploading' | 'processing' | 'completed' | 'error';
|
|
109
|
+
error?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Bulk upload result
|
|
114
|
+
*/
|
|
115
|
+
export interface BulkUploadResult {
|
|
116
|
+
/** Successfully created file references */
|
|
117
|
+
success: FileReference[];
|
|
118
|
+
/** Per-file failures with associated errors */
|
|
119
|
+
failed: { file: File; error: string }[];
|
|
120
|
+
|
|
121
|
+
// Optional aggregate fields for consumers that need totals
|
|
122
|
+
total?: number;
|
|
123
|
+
successful?: number;
|
|
124
|
+
results?: Array<{
|
|
125
|
+
file: File;
|
|
126
|
+
result: FileUploadResult | null;
|
|
127
|
+
error?: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bucket information for file storage
|
|
133
|
+
*/
|
|
134
|
+
export interface BucketInfo {
|
|
135
|
+
name: 'files' | 'public-files';
|
|
136
|
+
isPublic: boolean;
|
|
137
|
+
description: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* File URL information (public or signed)
|
|
142
|
+
*/
|
|
143
|
+
export interface FileUrlInfo {
|
|
144
|
+
url: string;
|
|
145
|
+
isPublic: boolean;
|
|
146
|
+
isSignedUrl: boolean;
|
|
147
|
+
expiresAt?: Date;
|
|
148
|
+
bucket: BucketInfo;
|
|
149
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -31,10 +31,8 @@ describe('organisationContext', () => {
|
|
|
31
31
|
|
|
32
32
|
await setOrganisationContext(mockSupabase, organisationId);
|
|
33
33
|
|
|
34
|
-
expect(mockRpc).toHaveBeenCalledWith('
|
|
35
|
-
|
|
36
|
-
p_organisation_id: organisationId,
|
|
37
|
-
p_metadata: { action: 'set_context' }
|
|
34
|
+
expect(mockRpc).toHaveBeenCalledWith('set_organisation_context', {
|
|
35
|
+
org_id: organisationId
|
|
38
36
|
});
|
|
39
37
|
});
|
|
40
38
|
|