@jmruthers/pace-core 0.5.88 → 0.5.90
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-DcTI5Ov4.d.ts → AuthService-DX15wM6y.d.ts} +8 -0
- package/dist/{DataTable-PWBMKMOG.js → DataTable-VIP44OB6.js} +7 -6
- package/dist/{UnifiedAuthProvider-5D3HEQND.js → UnifiedAuthProvider-6JRTOFPS.js} +4 -3
- package/dist/{chunk-BNXBJOGL.js → chunk-4DYK5KCK.js} +4 -4
- package/dist/{chunk-XXVM53P4.js → chunk-7NIERLC6.js} +8 -8
- package/dist/{chunk-XXVM53P4.js.map → chunk-7NIERLC6.js.map} +1 -1
- package/dist/{chunk-KTPG5VCH.js → chunk-7XBW2P7B.js} +2 -2
- package/dist/{chunk-DP5X5ORK.js → chunk-AIV3VYBQ.js} +82 -25
- package/dist/chunk-AIV3VYBQ.js.map +1 -0
- package/dist/{chunk-CJIZS3UE.js → chunk-EWMXLDIX.js} +19 -15
- package/dist/chunk-EWMXLDIX.js.map +1 -0
- package/dist/{chunk-YY4YYM3E.js → chunk-G2SCPUKC.js} +2 -2
- package/dist/{chunk-CXKMRKRF.js → chunk-G2YT64FA.js} +3 -3
- package/dist/{chunk-AQGF5OG7.js → chunk-GD3ENUKD.js} +3 -3
- package/dist/{chunk-H3P2RGKZ.js → chunk-JDPFQV3V.js} +55 -6
- package/dist/chunk-JDPFQV3V.js.map +1 -0
- package/dist/{chunk-3RZBKQ5Y.js → chunk-JQWSAYZC.js} +2 -2
- package/dist/{chunk-A6HBIY5P.js → chunk-SMJZMKYN.js} +11 -2
- package/dist/{chunk-A6HBIY5P.js.map → chunk-SMJZMKYN.js.map} +1 -1
- package/dist/{chunk-L3RV2ALE.js → chunk-VKOCWWVY.js} +6 -1
- package/dist/{chunk-L3RV2ALE.js.map → chunk-VKOCWWVY.js.map} +1 -1
- package/dist/{chunk-6UHXQH7P.js → chunk-XZHZYSAK.js} +5 -5
- package/dist/components.js +9 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.js +8 -7
- package/dist/styles/index.js +2 -2
- package/dist/theming/runtime.js +3 -1
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +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 +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/EventLogoProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +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 +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/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 +9 -9
- package/docs/implementation-guides/public-pages-advanced.md +10 -0
- package/package.json +1 -1
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +10 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +20 -14
- package/src/hooks/public/usePublicEvent.ts +0 -2
- package/src/services/AuthService.ts +29 -29
- package/src/services/EventService.ts +65 -0
- package/src/services/__tests__/AuthService.restoreSession.test.ts +35 -0
- package/src/services/__tests__/AuthService.test.ts +6 -7
- package/src/services/__tests__/EventService.eventColours.test.ts +72 -0
- package/src/utils/storage/helpers.test.ts +107 -3
- package/src/utils/storage/helpers.ts +80 -5
- package/dist/chunk-CJIZS3UE.js.map +0 -1
- package/dist/chunk-DP5X5ORK.js.map +0 -1
- package/dist/chunk-H3P2RGKZ.js.map +0 -1
- /package/dist/{DataTable-PWBMKMOG.js.map → DataTable-VIP44OB6.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5D3HEQND.js.map → UnifiedAuthProvider-6JRTOFPS.js.map} +0 -0
- /package/dist/{chunk-BNXBJOGL.js.map → chunk-4DYK5KCK.js.map} +0 -0
- /package/dist/{chunk-KTPG5VCH.js.map → chunk-7XBW2P7B.js.map} +0 -0
- /package/dist/{chunk-YY4YYM3E.js.map → chunk-G2SCPUKC.js.map} +0 -0
- /package/dist/{chunk-CXKMRKRF.js.map → chunk-G2YT64FA.js.map} +0 -0
- /package/dist/{chunk-AQGF5OG7.js.map → chunk-GD3ENUKD.js.map} +0 -0
- /package/dist/{chunk-3RZBKQ5Y.js.map → chunk-JQWSAYZC.js.map} +0 -0
- /package/dist/{chunk-6UHXQH7P.js.map → chunk-XZHZYSAK.js.map} +0 -0
package/docs/api/modules.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
[@jmruthers/pace-core - v0.5.
|
|
1
|
+
[@jmruthers/pace-core - v0.5.90](README.md) / Exports
|
|
2
2
|
|
|
3
|
-
# @jmruthers/pace-core - v0.5.
|
|
3
|
+
# @jmruthers/pace-core - v0.5.90
|
|
4
4
|
|
|
5
5
|
**`File`**
|
|
6
6
|
|
|
@@ -3800,7 +3800,7 @@ Useful for testing or when you need to force refresh all data
|
|
|
3800
3800
|
|
|
3801
3801
|
#### Defined in
|
|
3802
3802
|
|
|
3803
|
-
[packages/core/src/hooks/public/usePublicEvent.ts:
|
|
3803
|
+
[packages/core/src/hooks/public/usePublicEvent.ts:410](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L410)
|
|
3804
3804
|
|
|
3805
3805
|
___
|
|
3806
3806
|
|
|
@@ -3821,7 +3821,7 @@ Get cache statistics for debugging
|
|
|
3821
3821
|
|
|
3822
3822
|
#### Defined in
|
|
3823
3823
|
|
|
3824
|
-
[packages/core/src/hooks/public/usePublicEvent.ts:
|
|
3824
|
+
[packages/core/src/hooks/public/usePublicEvent.ts:421](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L421)
|
|
3825
3825
|
|
|
3826
3826
|
___
|
|
3827
3827
|
|
|
@@ -6988,7 +6988,7 @@ Signed URL with expiration timestamp, or null if failed
|
|
|
6988
6988
|
|
|
6989
6989
|
#### Defined in
|
|
6990
6990
|
|
|
6991
|
-
[packages/core/src/utils/storage/helpers.ts:
|
|
6991
|
+
[packages/core/src/utils/storage/helpers.ts:309](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L309)
|
|
6992
6992
|
|
|
6993
6993
|
___
|
|
6994
6994
|
|
|
@@ -7014,7 +7014,7 @@ Success status and optional error message
|
|
|
7014
7014
|
|
|
7015
7015
|
#### Defined in
|
|
7016
7016
|
|
|
7017
|
-
[packages/core/src/utils/storage/helpers.ts:
|
|
7017
|
+
[packages/core/src/utils/storage/helpers.ts:344](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L344)
|
|
7018
7018
|
|
|
7019
7019
|
___
|
|
7020
7020
|
|
|
@@ -7039,7 +7039,7 @@ List of files with metadata
|
|
|
7039
7039
|
|
|
7040
7040
|
#### Defined in
|
|
7041
7041
|
|
|
7042
|
-
[packages/core/src/utils/storage/helpers.ts:
|
|
7042
|
+
[packages/core/src/utils/storage/helpers.ts:378](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L378)
|
|
7043
7043
|
|
|
7044
7044
|
___
|
|
7045
7045
|
|
|
@@ -7065,7 +7065,7 @@ File blob with metadata, or null if failed
|
|
|
7065
7065
|
|
|
7066
7066
|
#### Defined in
|
|
7067
7067
|
|
|
7068
|
-
[packages/core/src/utils/storage/helpers.ts:
|
|
7068
|
+
[packages/core/src/utils/storage/helpers.ts:438](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L438)
|
|
7069
7069
|
|
|
7070
7070
|
___
|
|
7071
7071
|
|
|
@@ -7092,4 +7092,4 @@ Move a file to archived location (soft delete)
|
|
|
7092
7092
|
|
|
7093
7093
|
#### Defined in
|
|
7094
7094
|
|
|
7095
|
-
[packages/core/src/utils/storage/helpers.ts:
|
|
7095
|
+
[packages/core/src/utils/storage/helpers.ts:491](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L491)
|
|
@@ -407,6 +407,16 @@ interface LogoSize {
|
|
|
407
407
|
|
|
408
408
|
#### Usage Examples
|
|
409
409
|
|
|
410
|
+
> **Storage path formats**
|
|
411
|
+
>
|
|
412
|
+
> When configuring `event_logo` for an event, Pace supports multiple formats:
|
|
413
|
+
>
|
|
414
|
+
> - A fully qualified `https://` URL will be used directly.
|
|
415
|
+
> - A storage path like `public-files/org-123/logo.png` automatically resolves against the standard buckets configured via `pace-core`.
|
|
416
|
+
> - If your deployment stores logos in a different bucket, prefix the path with the bucket name using `custombucket/org-123/logo.png` (alphanumeric names) or `custom-bucket::org-123/logo.png` (bucket names containing hyphens).
|
|
417
|
+
>
|
|
418
|
+
> The double colon form helps when your folders start with numbers (for example `2024/`), ensuring they are not mistaken for bucket names.
|
|
419
|
+
|
|
410
420
|
**Basic Logo Loading:**
|
|
411
421
|
```tsx
|
|
412
422
|
import { usePublicEventLogo } from '@jmruthers/pace-core';
|
package/package.json
CHANGED
|
@@ -174,6 +174,16 @@ describe('PaceLoginPage Component', () => {
|
|
|
174
174
|
expect(errorElement).toHaveClass('mt-4', 'text-destructive', 'text-center');
|
|
175
175
|
expect(errorElement.tagName).toBe('EM');
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
it('does not show benign AuthSessionMissingError on login page', () => {
|
|
179
|
+
const authErr = new Error('Auth session missing!');
|
|
180
|
+
authErr.name = 'AuthSessionMissingError';
|
|
181
|
+
mockAuthContext.authError = authErr as any;
|
|
182
|
+
|
|
183
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
184
|
+
|
|
185
|
+
expect(screen.queryByText(/Auth session missing/i)).toBeNull();
|
|
186
|
+
});
|
|
177
187
|
});
|
|
178
188
|
|
|
179
189
|
// Role-based redirection tests
|
|
@@ -292,15 +292,18 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
292
292
|
try {
|
|
293
293
|
const { error } = await signIn(data.email, data.password);
|
|
294
294
|
|
|
295
|
-
if (
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
295
|
+
if (error) {
|
|
296
|
+
// Throw error so LoginForm can catch and display it
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Navigation will be handled by the useEffect that checks app access
|
|
301
|
+
// Don't navigate here if requireAppAccess is true
|
|
302
|
+
if (!requireAppAccess) {
|
|
303
|
+
try {
|
|
304
|
+
navigate(onSuccessRedirectPath, { replace: true });
|
|
305
|
+
} catch (navError) {
|
|
306
|
+
console.error('Navigation error after sign-in:', navError);
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
} finally {
|
|
@@ -325,11 +328,14 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
|
|
|
325
328
|
console.error('Login error:', error);
|
|
326
329
|
}}
|
|
327
330
|
/>
|
|
328
|
-
{
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
{(() => {
|
|
332
|
+
const benign = !!(authError && (
|
|
333
|
+
authError.name === 'AuthSessionMissingError' || /Auth session missing/i.test(authError.message)
|
|
334
|
+
));
|
|
335
|
+
return authError && !benign ? (
|
|
336
|
+
<em className="mt-4 text-destructive text-center">{authError.message}</em>
|
|
337
|
+
) : null;
|
|
338
|
+
})()}
|
|
333
339
|
{accessError && (
|
|
334
340
|
<em className="mt-4 text-destructive text-center">
|
|
335
341
|
{accessError}
|
|
@@ -224,7 +224,6 @@ export function usePublicEvent(
|
|
|
224
224
|
event_catering_email,
|
|
225
225
|
event_news,
|
|
226
226
|
event_billing,
|
|
227
|
-
event_footer,
|
|
228
227
|
event_email
|
|
229
228
|
`)
|
|
230
229
|
.eq('event_code', eventCode)
|
|
@@ -301,7 +300,6 @@ export function usePublicEvent(
|
|
|
301
300
|
event_catering_email,
|
|
302
301
|
event_news,
|
|
303
302
|
event_billing,
|
|
304
|
-
event_footer,
|
|
305
303
|
event_email
|
|
306
304
|
`)
|
|
307
305
|
.eq('event_code', eventCode)
|
|
@@ -453,42 +453,42 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
453
453
|
// Record error but continue to attempt getUser to satisfy edge cases
|
|
454
454
|
console.debug('[AuthService] getSession returned error, attempting to fetch user anyway');
|
|
455
455
|
this.authError = sessionError;
|
|
456
|
+
|
|
457
|
+
// Attempt getUser as fallback when getSession fails
|
|
458
|
+
try {
|
|
459
|
+
const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
|
|
460
|
+
if (typeof getUserFn === 'function') {
|
|
461
|
+
const userResult = await getUserFn();
|
|
462
|
+
const currentUser = userResult?.data?.user ?? null;
|
|
463
|
+
const userError = userResult?.error ?? null;
|
|
464
|
+
|
|
465
|
+
if (currentUser) {
|
|
466
|
+
this.user = currentUser;
|
|
467
|
+
// If we got a user but no session, we still don't have a valid session
|
|
468
|
+
this.session = null;
|
|
469
|
+
}
|
|
470
|
+
if (userError && !this.authError) {
|
|
471
|
+
this.authError = userError;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (getUserError) {
|
|
475
|
+
// If getUser also fails, we've already recorded the sessionError
|
|
476
|
+
console.debug('[AuthService] getUser also failed:', getUserError);
|
|
477
|
+
}
|
|
456
478
|
}
|
|
457
479
|
|
|
458
480
|
if (currentSession) {
|
|
459
481
|
this.session = currentSession;
|
|
460
482
|
this.user = currentSession.user;
|
|
461
483
|
this.authError = null;
|
|
462
|
-
} else {
|
|
463
|
-
|
|
484
|
+
} else if (!sessionError) {
|
|
485
|
+
// Only skip getUser if we didn't already attempt it due to a sessionError
|
|
486
|
+
// Treat missing session as a normal cold-start state on public pages (e.g., login)
|
|
487
|
+
// Do not call getUser() which can raise AuthSessionMissingError and surface a noisy banner
|
|
488
|
+
console.debug('[AuthService] No active session found; treating as normal unauthenticated state');
|
|
464
489
|
this.session = null;
|
|
465
|
-
|
|
466
|
-
|
|
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) {
|
|
481
|
-
this.user = currentUser;
|
|
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;
|
|
491
|
-
}
|
|
490
|
+
this.user = null;
|
|
491
|
+
this.authError = null;
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
|
|
@@ -14,6 +14,7 @@ import { IEventService } from './interfaces/IEventService';
|
|
|
14
14
|
import { Event } from '../types/unified';
|
|
15
15
|
import { Organisation } from '../types/organisation';
|
|
16
16
|
import { DebugLogger } from '../utils/debugLogger';
|
|
17
|
+
import { applyPalette, clearPalette } from '../theming/runtime';
|
|
17
18
|
|
|
18
19
|
export class EventService extends BaseService implements IEventService {
|
|
19
20
|
private events: Event[] = [];
|
|
@@ -135,6 +136,8 @@ export class EventService extends BaseService implements IEventService {
|
|
|
135
136
|
this.persistEventSelection(event.event_id);
|
|
136
137
|
// Reset the user cleared flag when selecting an event
|
|
137
138
|
this.userClearedEventRef = false;
|
|
139
|
+
// Apply theme for the newly selected event
|
|
140
|
+
this.updateThemeForSelectedEvent();
|
|
138
141
|
} else {
|
|
139
142
|
this.selectedEvent = null;
|
|
140
143
|
this.setSelectedEventId?.(null);
|
|
@@ -145,6 +148,8 @@ export class EventService extends BaseService implements IEventService {
|
|
|
145
148
|
this.hasAutoSelectedRef = false;
|
|
146
149
|
// Mark that user explicitly cleared the event to prevent auto-selection
|
|
147
150
|
this.userClearedEventRef = true;
|
|
151
|
+
// Clear theme when event is cleared
|
|
152
|
+
this.updateThemeForSelectedEvent();
|
|
148
153
|
}
|
|
149
154
|
this.notify();
|
|
150
155
|
}
|
|
@@ -175,6 +180,8 @@ export class EventService extends BaseService implements IEventService {
|
|
|
175
180
|
if (persistedEvent) {
|
|
176
181
|
this.selectedEvent = persistedEvent;
|
|
177
182
|
this.setSelectedEventId?.(persistedEventId);
|
|
183
|
+
// Apply theme for persisted event
|
|
184
|
+
this.updateThemeForSelectedEvent();
|
|
178
185
|
return true;
|
|
179
186
|
} else {
|
|
180
187
|
// Clear invalid persisted event
|
|
@@ -218,6 +225,8 @@ export class EventService extends BaseService implements IEventService {
|
|
|
218
225
|
this.selectedEvent = nextEvent;
|
|
219
226
|
this.setSelectedEventId?.(nextEvent.event_id);
|
|
220
227
|
this.persistEventSelection(nextEvent.event_id);
|
|
228
|
+
// Apply theme for auto-selected event
|
|
229
|
+
this.updateThemeForSelectedEvent();
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
|
|
@@ -338,6 +347,62 @@ export class EventService extends BaseService implements IEventService {
|
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
|
|
352
|
+
*/
|
|
353
|
+
private parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
|
|
354
|
+
try {
|
|
355
|
+
if (!input) return null;
|
|
356
|
+
let obj: any = input;
|
|
357
|
+
if (typeof input === 'string') {
|
|
358
|
+
try {
|
|
359
|
+
obj = JSON.parse(input);
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
} else if (typeof input !== 'object') {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
|
|
368
|
+
const main = pick(obj, 'ev-main', 'main');
|
|
369
|
+
const sec = pick(obj, 'ev-sec', 'sec');
|
|
370
|
+
const acc = pick(obj, 'ev-acc', 'acc');
|
|
371
|
+
if (!main && !sec && !acc) return null;
|
|
372
|
+
|
|
373
|
+
// Fill helper: ensure all TW shades exist using raw or 500 as fallback
|
|
374
|
+
const shades = ['50','100','200','300','400','500','600','700','800','900','950'];
|
|
375
|
+
const fill = (p: any) => {
|
|
376
|
+
if (!p) return {} as any;
|
|
377
|
+
const out: any = {};
|
|
378
|
+
for (const s of shades) out[s] = p[s] || p?.raw || p?.['500'];
|
|
379
|
+
if (p?.raw) out.raw = p.raw;
|
|
380
|
+
return out;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return { main: fill(main), sec: fill(sec), acc: fill(acc) };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn('[EventService] Failed to parse/normalize event colours:', error);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Apply or clear runtime theme variables based on the current selected event's colours
|
|
392
|
+
*/
|
|
393
|
+
private updateThemeForSelectedEvent(): void {
|
|
394
|
+
try {
|
|
395
|
+
const normalized = this.parseAndNormalizeEventColours(this.selectedEvent?.event_colours);
|
|
396
|
+
if (normalized) {
|
|
397
|
+
applyPalette(normalized);
|
|
398
|
+
} else {
|
|
399
|
+
clearPalette();
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn('[EventService] Failed to update theme from event colours:', error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
341
406
|
getNextEventByDate(events?: Event[]): Event | null {
|
|
342
407
|
const eventsToUse = events || this.events;
|
|
343
408
|
if (!eventsToUse || eventsToUse.length === 0) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { AuthService } from '../../services/AuthService';
|
|
3
|
+
|
|
4
|
+
// Minimal Supabase auth mock sufficient for restoreSession/initialize paths
|
|
5
|
+
function createSupabaseMock() {
|
|
6
|
+
const auth = {
|
|
7
|
+
getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
|
|
8
|
+
getUser: vi.fn(), // should not be called when session is null
|
|
9
|
+
onAuthStateChange: vi.fn().mockImplementation((cb: any) => {
|
|
10
|
+
// Provide a subscription-like object
|
|
11
|
+
return { unsubscribe: () => {} } as any;
|
|
12
|
+
}),
|
|
13
|
+
} as any;
|
|
14
|
+
|
|
15
|
+
return { auth } as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('AuthService.restoreSession (no session startup)', () => {
|
|
19
|
+
it('does not call getUser() and does not set authError when session is null', async () => {
|
|
20
|
+
const supabase = createSupabaseMock();
|
|
21
|
+
const service = new AuthService(supabase);
|
|
22
|
+
|
|
23
|
+
// initialize() triggers setup listener and restoreSession()
|
|
24
|
+
await service.initialize();
|
|
25
|
+
|
|
26
|
+
// Assertions
|
|
27
|
+
expect(supabase.auth.getSession).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(supabase.auth.getUser).not.toHaveBeenCalled();
|
|
29
|
+
expect(service.getError()).toBeNull();
|
|
30
|
+
expect(service.getUser()).toBeNull();
|
|
31
|
+
expect(service.getSession()).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
@@ -518,23 +518,22 @@ describe('AuthService', () => {
|
|
|
518
518
|
expect(mockSupabase.auth.getUser).toHaveBeenCalled();
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
-
it('should
|
|
522
|
-
const userError = new AuthError('User error');
|
|
521
|
+
it('should not call getUser when getSession succeeds with no session (prevents AuthSessionMissingError)', async () => {
|
|
523
522
|
mockSupabase.auth.getSession.mockResolvedValue({
|
|
524
523
|
data: { session: null },
|
|
525
524
|
error: null
|
|
526
525
|
});
|
|
527
|
-
mockSupabase.auth.getUser.mockResolvedValue({
|
|
528
|
-
data: { user: null },
|
|
529
|
-
error: userError
|
|
530
|
-
});
|
|
531
526
|
mockSupabase.auth.onAuthStateChange.mockReturnValue({
|
|
532
527
|
data: { subscription: { unsubscribe: vi.fn() } }
|
|
533
528
|
});
|
|
534
529
|
|
|
535
530
|
await authService.initialize();
|
|
536
531
|
|
|
537
|
-
|
|
532
|
+
// When getSession succeeds with no session, getUser is NOT called to prevent
|
|
533
|
+
// AuthSessionMissingError from being raised on public pages (e.g., login page)
|
|
534
|
+
// This is intentional behavior to avoid noisy error banners for benign unauthenticated states
|
|
535
|
+
expect(mockSupabase.auth.getSession).toHaveBeenCalled();
|
|
536
|
+
expect(mockSupabase.auth.getUser).not.toHaveBeenCalled();
|
|
538
537
|
});
|
|
539
538
|
|
|
540
539
|
it('should restore session from storage during initialization', async () => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { EventService } from '../../services/EventService';
|
|
3
|
+
|
|
4
|
+
function createSupabaseMock() {
|
|
5
|
+
const auth = {
|
|
6
|
+
onAuthStateChange: vi.fn().mockReturnValue({ unsubscribe: () => {} }),
|
|
7
|
+
} as any;
|
|
8
|
+
return { auth } as any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const baseEvent = {
|
|
12
|
+
event_id: 'e1',
|
|
13
|
+
event_name: 'Test',
|
|
14
|
+
organisation_id: 'org1',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('EventService event_colours normalization', () => {
|
|
18
|
+
it('applies palettes when event_colours uses ev-* keys (object)', async () => {
|
|
19
|
+
const supabase = createSupabaseMock();
|
|
20
|
+
const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
|
|
21
|
+
// @ts-ignore access private for test by casting
|
|
22
|
+
(svc as any).selectedEvent = {
|
|
23
|
+
...baseEvent,
|
|
24
|
+
event_colours: {
|
|
25
|
+
'ev-main': { raw: { L: 0.75, C: 0.1, H: 200 }, '500': { L: 0.75, C: 0.1, H: 200 } },
|
|
26
|
+
'ev-sec': { raw: { L: 0.7, C: 0.12, H: 260 }, '500': { L: 0.7, C: 0.12, H: 260 } },
|
|
27
|
+
'ev-acc': { raw: { L: 0.65, C: 0.14, H: 20 }, '500': { L: 0.65, C: 0.14, H: 20 } },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Spy on document style updates by calling the method and ensuring no throw
|
|
32
|
+
expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parses string JSON with ev-* keys', () => {
|
|
36
|
+
const supabase = createSupabaseMock();
|
|
37
|
+
const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
|
|
38
|
+
const payload = JSON.stringify({
|
|
39
|
+
'ev-main': { '500': { L: 0.75, C: 0.1, H: 200 }, raw: { L: 0.75, C: 0.1, H: 200 } },
|
|
40
|
+
'ev-sec': { '500': { L: 0.7, C: 0.12, H: 260 }, raw: { L: 0.7, C: 0.12, H: 260 } },
|
|
41
|
+
'ev-acc': { '500': { L: 0.65, C: 0.14, H: 20 }, raw: { L: 0.65, C: 0.14, H: 20 } },
|
|
42
|
+
});
|
|
43
|
+
// @ts-ignore private
|
|
44
|
+
(svc as any).selectedEvent = { ...baseEvent, event_colours: payload };
|
|
45
|
+
expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts plain main/sec/acc', () => {
|
|
49
|
+
const supabase = createSupabaseMock();
|
|
50
|
+
const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
|
|
51
|
+
// @ts-ignore private
|
|
52
|
+
(svc as any).selectedEvent = {
|
|
53
|
+
...baseEvent,
|
|
54
|
+
event_colours: {
|
|
55
|
+
main: { '500': { L: 0.75, C: 0.1, H: 200 } },
|
|
56
|
+
sec: { '500': { L: 0.7, C: 0.12, H: 260 } },
|
|
57
|
+
acc: { '500': { L: 0.65, C: 0.14, H: 20 } },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('clears when invalid structure', () => {
|
|
64
|
+
const supabase = createSupabaseMock();
|
|
65
|
+
const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
|
|
66
|
+
// @ts-ignore private
|
|
67
|
+
(svc as any).selectedEvent = { ...baseEvent, event_colours: { something: {} } };
|
|
68
|
+
expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
@@ -354,7 +354,7 @@ describe('[utility] Storage Helpers', () => {
|
|
|
354
354
|
describe('getPublicUrl', () => {
|
|
355
355
|
it('generates public URL for public bucket', () => {
|
|
356
356
|
const filePath = 'org-123/logos/logo.png';
|
|
357
|
-
|
|
357
|
+
|
|
358
358
|
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
359
359
|
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
360
360
|
});
|
|
@@ -370,9 +370,23 @@ describe('[utility] Storage Helpers', () => {
|
|
|
370
370
|
expect(result).toBe('https://example.com/public/logo.png');
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
+
it('returns direct URLs without calling storage', () => {
|
|
374
|
+
const directUrl = 'https://cdn.example.com/logo.png';
|
|
375
|
+
|
|
376
|
+
const localSupabase = createMockSupabaseClient();
|
|
377
|
+
localSupabase.storage = {
|
|
378
|
+
from: vi.fn()
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const result = getPublicUrl(localSupabase as any, directUrl, true);
|
|
382
|
+
|
|
383
|
+
expect(localSupabase.storage.from).not.toHaveBeenCalled();
|
|
384
|
+
expect(result).toBe(directUrl);
|
|
385
|
+
});
|
|
386
|
+
|
|
373
387
|
it('generates public URL for private bucket', () => {
|
|
374
388
|
const filePath = 'org-123/documents/test.pdf';
|
|
375
|
-
|
|
389
|
+
|
|
376
390
|
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
377
391
|
data: { publicUrl: 'https://example.com/files/test.pdf' }
|
|
378
392
|
});
|
|
@@ -387,10 +401,100 @@ describe('[utility] Storage Helpers', () => {
|
|
|
387
401
|
expect(result).toBe('https://example.com/files/test.pdf');
|
|
388
402
|
});
|
|
389
403
|
|
|
404
|
+
it('uses explicit bucket prefix when provided', () => {
|
|
405
|
+
const filePath = 'public-files/org-123/logo.png';
|
|
406
|
+
|
|
407
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
408
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
mockSupabase.storage = {
|
|
412
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
416
|
+
|
|
417
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
418
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
419
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('supports custom bucket hints without hyphen characters', () => {
|
|
423
|
+
const filePath = 'branding/org-123/logo.png';
|
|
424
|
+
|
|
425
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
426
|
+
data: { publicUrl: 'https://example.com/branding/logo.png' }
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
mockSupabase.storage = {
|
|
430
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
434
|
+
|
|
435
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('branding');
|
|
436
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
437
|
+
expect(result).toBe('https://example.com/branding/logo.png');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('supports double colon bucket hints to avoid path ambiguity', () => {
|
|
441
|
+
const filePath = 'brand-assets::org-123/logo.png';
|
|
442
|
+
|
|
443
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
444
|
+
data: { publicUrl: 'https://example.com/brand/logo.png' }
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
mockSupabase.storage = {
|
|
448
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
452
|
+
|
|
453
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('brand-assets');
|
|
454
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
|
|
455
|
+
expect(result).toBe('https://example.com/brand/logo.png');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('treats numeric leading path segments as directories rather than buckets', () => {
|
|
459
|
+
const filePath = '2024/05/logo.png';
|
|
460
|
+
|
|
461
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
462
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
mockSupabase.storage = {
|
|
466
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
470
|
+
|
|
471
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
472
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
|
|
473
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('ignores bucket prefix when path starts with UUID', () => {
|
|
477
|
+
const filePath = '123e4567-e89b-12d3-a456-426614174000/event_logos/logo.png';
|
|
478
|
+
|
|
479
|
+
const mockGetPublicUrl = vi.fn().mockReturnValue({
|
|
480
|
+
data: { publicUrl: 'https://example.com/public/logo.png' }
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
mockSupabase.storage = {
|
|
484
|
+
from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const result = getPublicUrl(mockSupabase, filePath, true);
|
|
488
|
+
|
|
489
|
+
expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
|
|
490
|
+
expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
|
|
491
|
+
expect(result).toBe('https://example.com/public/logo.png');
|
|
492
|
+
});
|
|
493
|
+
|
|
390
494
|
it('validates required parameters', () => {
|
|
391
495
|
expect(() => getPublicUrl(null as any, 'path', false))
|
|
392
496
|
.toThrow();
|
|
393
|
-
|
|
497
|
+
|
|
394
498
|
expect(() => getPublicUrl(mockSupabase, '', false))
|
|
395
499
|
.toThrow();
|
|
396
500
|
});
|