@jmruthers/pace-core 0.5.108 → 0.5.109
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/CHANGELOG.md +75 -177
- package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
- package/dist/{DataTable-WFCHVWTY.js → DataTable-5HITILXS.js} +7 -7
- package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
- package/dist/{api-KG4A2X7P.js → api-5I3E47G2.js} +2 -2
- package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
- package/dist/{chunk-MOMYOQMC.js → chunk-3TKTL5AZ.js} +13 -13
- package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
- package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
- package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
- package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
- package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
- package/dist/{chunk-S63MFSY6.js → chunk-F6TSYCKP.js} +4 -2
- package/dist/{chunk-S63MFSY6.js.map → chunk-F6TSYCKP.js.map} +1 -1
- package/dist/chunk-GZRXOUBE.js +176 -0
- package/dist/chunk-GZRXOUBE.js.map +1 -0
- package/dist/{chunk-B3QX32P5.js → chunk-P72NKAT5.js} +41 -24
- package/dist/chunk-P72NKAT5.js.map +1 -0
- package/dist/{chunk-VJ7MPS2K.js → chunk-S4D3Z723.js} +6 -6
- package/dist/{chunk-IMZGJ2X7.js → chunk-UW2DE6JX.js} +4 -4
- package/dist/{chunk-QDDUU625.js → chunk-WWNOVFDC.js} +4 -4
- package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
- package/dist/components.js +9 -9
- package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
- package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +6 -6
- package/dist/index.js +16 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +4 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
- package/dist/utils.d.ts +2 -15
- package/dist/utils.js +4 -145
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +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 +3 -3
- 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/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/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/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/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 +37 -3
- package/docs/api-reference/hooks.md +53 -0
- package/docs/api-reference/providers.md +60 -0
- package/docs/core-concepts/authentication.md +2 -0
- package/docs/implementation-guides/authentication.md +1 -0
- package/docs/security/README.md +59 -0
- package/package.json +1 -1
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +48 -16
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
- package/src/index.ts +3 -0
- package/src/providers/services/AuthServiceProvider.tsx +4 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
- package/src/rbac/engine.ts +2 -0
- package/src/services/AuthService.ts +79 -1
- package/src/services/__tests__/AuthService.test.ts +184 -0
- package/src/types/database.ts +21 -9
- package/src/types/rbac-functions.ts +2 -1
- package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
- package/src/utils/sessionTracking.ts +7 -81
- package/dist/chunk-B3QX32P5.js.map +0 -1
- package/dist/chunk-NFPV7MRN.js +0 -94
- package/dist/chunk-NFPV7MRN.js.map +0 -1
- package/src/providers/AuthProvider.simplified.tsx +0 -974
- package/dist/{DataTable-WFCHVWTY.js.map → DataTable-5HITILXS.js.map} +0 -0
- package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
- package/dist/{api-KG4A2X7P.js.map → api-5I3E47G2.js.map} +0 -0
- package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
- package/dist/{chunk-MOMYOQMC.js.map → chunk-3TKTL5AZ.js.map} +0 -0
- package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
- package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
- package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
- package/dist/{chunk-VJ7MPS2K.js.map → chunk-S4D3Z723.js.map} +0 -0
- package/dist/{chunk-IMZGJ2X7.js.map → chunk-UW2DE6JX.js.map} +0 -0
- package/dist/{chunk-QDDUU625.js.map → chunk-WWNOVFDC.js.map} +0 -0
- package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
- package/dist/{validation-D8VcbTzC.d.ts → validation-DnhrNMju.d.ts} +2 -2
|
@@ -640,4 +640,188 @@ describe('AuthService', () => {
|
|
|
640
640
|
expect(authService.isAuthenticated()).toBe(false);
|
|
641
641
|
});
|
|
642
642
|
});
|
|
643
|
+
|
|
644
|
+
describe('Automatic Session Tracking', () => {
|
|
645
|
+
beforeEach(() => {
|
|
646
|
+
// Mock rpc function for session tracking
|
|
647
|
+
(mockSupabase as any).rpc = vi.fn();
|
|
648
|
+
// Mock from().select() chain for app ID resolution
|
|
649
|
+
(mockSupabase as any).from = vi.fn().mockReturnValue({
|
|
650
|
+
select: vi.fn().mockReturnValue({
|
|
651
|
+
eq: vi.fn().mockReturnValue({
|
|
652
|
+
eq: vi.fn().mockReturnValue({
|
|
653
|
+
single: vi.fn().mockResolvedValue({
|
|
654
|
+
data: { id: 'app-id-123' },
|
|
655
|
+
error: null
|
|
656
|
+
})
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('should track login session automatically on SIGNED_IN event', async () => {
|
|
664
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
665
|
+
const mockSession = { access_token: 'token', user: mockUser };
|
|
666
|
+
|
|
667
|
+
(mockSupabase as any).rpc.mockResolvedValue({ error: null });
|
|
668
|
+
|
|
669
|
+
let authStateCallback: any;
|
|
670
|
+
mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
|
|
671
|
+
authStateCallback = callback;
|
|
672
|
+
return { data: { subscription: { unsubscribe: vi.fn() } } };
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Initialize with appName to test app ID resolution
|
|
676
|
+
const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
|
|
677
|
+
await authServiceWithApp.initialize();
|
|
678
|
+
|
|
679
|
+
// Simulate SIGNED_IN event
|
|
680
|
+
if (authStateCallback) {
|
|
681
|
+
authStateCallback('SIGNED_IN', mockSession);
|
|
682
|
+
|
|
683
|
+
// Wait a bit for async tracking to complete
|
|
684
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
685
|
+
|
|
686
|
+
// Verify rbac_session_track was called with correct parameters
|
|
687
|
+
expect((mockSupabase as any).rpc).toHaveBeenCalledWith('rbac_session_track', expect.objectContaining({
|
|
688
|
+
p_user_id: 'user-123',
|
|
689
|
+
p_session_type: 'login',
|
|
690
|
+
p_event_id: null,
|
|
691
|
+
p_app_id: 'app-id-123', // Should be resolved from appName
|
|
692
|
+
p_user_agent: expect.any(String), // navigator.userAgent
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
authServiceWithApp.cleanup();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('should track logout session automatically on SIGNED_OUT event', async () => {
|
|
700
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
701
|
+
const mockSession = { access_token: 'token', user: mockUser };
|
|
702
|
+
|
|
703
|
+
(mockSupabase as any).rpc.mockResolvedValue({ error: null });
|
|
704
|
+
|
|
705
|
+
let authStateCallback: any;
|
|
706
|
+
mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
|
|
707
|
+
authStateCallback = callback;
|
|
708
|
+
return { data: { subscription: { unsubscribe: vi.fn() } } };
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
|
|
712
|
+
await authServiceWithApp.initialize();
|
|
713
|
+
|
|
714
|
+
// Simulate SIGNED_OUT event
|
|
715
|
+
if (authStateCallback) {
|
|
716
|
+
authStateCallback('SIGNED_OUT', mockSession);
|
|
717
|
+
|
|
718
|
+
// Wait a bit for async tracking to complete
|
|
719
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
720
|
+
|
|
721
|
+
// Verify rbac_session_track was called with logout type
|
|
722
|
+
expect((mockSupabase as any).rpc).toHaveBeenCalledWith('rbac_session_track', expect.objectContaining({
|
|
723
|
+
p_user_id: 'user-123',
|
|
724
|
+
p_session_type: 'logout',
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
authServiceWithApp.cleanup();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should NOT track session on TOKEN_REFRESHED event (to avoid duplicate login records)', async () => {
|
|
732
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
733
|
+
const mockSession = { access_token: 'new_token', user: mockUser };
|
|
734
|
+
|
|
735
|
+
(mockSupabase as any).rpc.mockResolvedValue({ error: null });
|
|
736
|
+
|
|
737
|
+
let authStateCallback: any;
|
|
738
|
+
mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
|
|
739
|
+
authStateCallback = callback;
|
|
740
|
+
return { data: { subscription: { unsubscribe: vi.fn() } } };
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
|
|
744
|
+
await authServiceWithApp.initialize();
|
|
745
|
+
|
|
746
|
+
// Simulate TOKEN_REFRESHED event
|
|
747
|
+
if (authStateCallback) {
|
|
748
|
+
authStateCallback('TOKEN_REFRESHED', mockSession);
|
|
749
|
+
|
|
750
|
+
// Wait a bit for any async operations
|
|
751
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
752
|
+
|
|
753
|
+
// Verify rbac_session_track was NOT called
|
|
754
|
+
expect((mockSupabase as any).rpc).not.toHaveBeenCalled();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
authServiceWithApp.cleanup();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('should handle tracking errors gracefully without breaking authentication', async () => {
|
|
761
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
762
|
+
const mockSession = { access_token: 'token', user: mockUser };
|
|
763
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
764
|
+
|
|
765
|
+
(mockSupabase as any).rpc.mockRejectedValue(new Error('Tracking failed'));
|
|
766
|
+
|
|
767
|
+
let authStateCallback: any;
|
|
768
|
+
mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
|
|
769
|
+
authStateCallback = callback;
|
|
770
|
+
return { data: { subscription: { unsubscribe: vi.fn() } } };
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const authServiceWithApp = new AuthService(mockSupabase as any, 'TEST_APP');
|
|
774
|
+
await authServiceWithApp.initialize();
|
|
775
|
+
|
|
776
|
+
// Simulate SIGNED_IN event
|
|
777
|
+
if (authStateCallback) {
|
|
778
|
+
authStateCallback('SIGNED_IN', mockSession);
|
|
779
|
+
|
|
780
|
+
// Wait a bit for async tracking to complete
|
|
781
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
782
|
+
|
|
783
|
+
// Verify error was logged but authentication still succeeded
|
|
784
|
+
// When rpc throws an exception, it goes to catch block which logs "Error tracking"
|
|
785
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
786
|
+
expect.stringContaining('Error tracking login session'),
|
|
787
|
+
expect.anything()
|
|
788
|
+
);
|
|
789
|
+
expect(authServiceWithApp.isAuthenticated()).toBe(true);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
consoleWarnSpy.mockRestore();
|
|
793
|
+
authServiceWithApp.cleanup();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should work without appName (app_id will be null)', async () => {
|
|
797
|
+
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
798
|
+
const mockSession = { access_token: 'token', user: mockUser };
|
|
799
|
+
|
|
800
|
+
(mockSupabase as any).rpc.mockResolvedValue({ error: null });
|
|
801
|
+
|
|
802
|
+
let authStateCallback: any;
|
|
803
|
+
mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
|
|
804
|
+
authStateCallback = callback;
|
|
805
|
+
return { data: { subscription: { unsubscribe: vi.fn() } } };
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Initialize without appName
|
|
809
|
+
await authService.initialize();
|
|
810
|
+
|
|
811
|
+
// Simulate SIGNED_IN event
|
|
812
|
+
if (authStateCallback) {
|
|
813
|
+
authStateCallback('SIGNED_IN', mockSession);
|
|
814
|
+
|
|
815
|
+
// Wait a bit for async tracking to complete
|
|
816
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
817
|
+
|
|
818
|
+
// Verify rbac_session_track was called with null app_id
|
|
819
|
+
expect((mockSupabase as any).rpc).toHaveBeenCalledWith('rbac_session_track', expect.objectContaining({
|
|
820
|
+
p_user_id: 'user-123',
|
|
821
|
+
p_session_type: 'login',
|
|
822
|
+
p_app_id: undefined, // Should be undefined when appName not provided
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
});
|
|
643
827
|
});
|
package/src/types/database.ts
CHANGED
|
@@ -387,26 +387,38 @@ export interface Database {
|
|
|
387
387
|
Row: {
|
|
388
388
|
id: string;
|
|
389
389
|
user_id: string;
|
|
390
|
-
email: string
|
|
391
|
-
app_id: string | null;
|
|
390
|
+
email: string; // NOT NULL in schema
|
|
392
391
|
event_id: string | null;
|
|
393
|
-
|
|
392
|
+
login_timestamp: string; // Changed from created_at to login_timestamp
|
|
393
|
+
session_id: string; // NOT NULL in schema
|
|
394
|
+
user_agent: string | null;
|
|
395
|
+
ip_address: string | null;
|
|
396
|
+
organisation_id: string; // NOT NULL in schema
|
|
397
|
+
app_id: string | null; // Added in migration
|
|
394
398
|
};
|
|
395
399
|
Insert: {
|
|
396
400
|
id?: string;
|
|
397
401
|
user_id: string;
|
|
398
|
-
email
|
|
399
|
-
app_id?: string | null;
|
|
402
|
+
email: string; // Required, NOT NULL
|
|
400
403
|
event_id?: string | null;
|
|
401
|
-
|
|
404
|
+
login_timestamp?: string;
|
|
405
|
+
session_id: string; // Required, NOT NULL
|
|
406
|
+
user_agent?: string | null;
|
|
407
|
+
ip_address?: string | null;
|
|
408
|
+
organisation_id: string; // Required, NOT NULL
|
|
409
|
+
app_id?: string | null;
|
|
402
410
|
};
|
|
403
411
|
Update: {
|
|
404
412
|
id?: string;
|
|
405
413
|
user_id?: string;
|
|
406
|
-
email?: string
|
|
407
|
-
app_id?: string | null;
|
|
414
|
+
email?: string;
|
|
408
415
|
event_id?: string | null;
|
|
409
|
-
|
|
416
|
+
login_timestamp?: string;
|
|
417
|
+
session_id?: string;
|
|
418
|
+
user_agent?: string | null;
|
|
419
|
+
ip_address?: string | null;
|
|
420
|
+
organisation_id?: string;
|
|
421
|
+
app_id?: string | null;
|
|
410
422
|
};
|
|
411
423
|
};
|
|
412
424
|
rbac_audit_events: {
|
|
@@ -30,10 +30,11 @@ export interface RBACPermissionCheckResult {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface RBACPermissionsGetParams {
|
|
33
|
-
p_user_id
|
|
33
|
+
p_user_id: UUID; // REQUIRED - no default, must be explicitly provided
|
|
34
34
|
p_organisation_id?: UUID;
|
|
35
35
|
p_event_id?: string;
|
|
36
36
|
p_app_id?: UUID;
|
|
37
|
+
p_page_id?: UUID;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export interface RBACPermissionsGetResult {
|
|
@@ -27,102 +27,6 @@ describe('sessionTracking', () => {
|
|
|
27
27
|
} as unknown as SupabaseClient;
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
describe('trackLogin', () => {
|
|
31
|
-
it('should track login successfully', async () => {
|
|
32
|
-
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
33
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: mockUser } });
|
|
34
|
-
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
35
|
-
|
|
36
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
37
|
-
mockSupabase.rpc = mockRpc;
|
|
38
|
-
|
|
39
|
-
// Import the module after setting up mocks
|
|
40
|
-
vi.resetModules();
|
|
41
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
42
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
43
|
-
|
|
44
|
-
await trackingFunctions.trackLogin('event-123');
|
|
45
|
-
|
|
46
|
-
expect(mockGetUser).toHaveBeenCalled();
|
|
47
|
-
expect(mockRpc).toHaveBeenCalledWith('rbac_session_track', {
|
|
48
|
-
p_user_id: 'user-123',
|
|
49
|
-
p_session_type: 'login',
|
|
50
|
-
p_event_id: 'event-123',
|
|
51
|
-
p_app_id: undefined,
|
|
52
|
-
p_ip_address: undefined,
|
|
53
|
-
p_user_agent: undefined
|
|
54
|
-
});
|
|
55
|
-
expect(consoleSpy.log).toHaveBeenCalledWith('Login session tracked successfully');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should track login without event ID', async () => {
|
|
59
|
-
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
60
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: mockUser } });
|
|
61
|
-
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
62
|
-
|
|
63
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
64
|
-
mockSupabase.rpc = mockRpc;
|
|
65
|
-
|
|
66
|
-
vi.resetModules();
|
|
67
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
68
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
69
|
-
|
|
70
|
-
await trackingFunctions.trackLogin();
|
|
71
|
-
|
|
72
|
-
expect(mockRpc).toHaveBeenCalledWith('rbac_session_track', {
|
|
73
|
-
p_user_id: 'user-123',
|
|
74
|
-
p_session_type: 'login',
|
|
75
|
-
p_event_id: undefined,
|
|
76
|
-
p_app_id: undefined,
|
|
77
|
-
p_ip_address: undefined,
|
|
78
|
-
p_user_agent: undefined
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should handle no authenticated user', async () => {
|
|
83
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: null } });
|
|
84
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
85
|
-
|
|
86
|
-
vi.resetModules();
|
|
87
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
88
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
89
|
-
|
|
90
|
-
await trackingFunctions.trackLogin();
|
|
91
|
-
|
|
92
|
-
expect(consoleSpy.warn).toHaveBeenCalledWith('No authenticated user found for session tracking');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should handle tracking error', async () => {
|
|
96
|
-
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
97
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: mockUser } });
|
|
98
|
-
const mockRpc = vi.fn().mockResolvedValue({ error: { message: 'Database error' } });
|
|
99
|
-
|
|
100
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
101
|
-
mockSupabase.rpc = mockRpc;
|
|
102
|
-
|
|
103
|
-
vi.resetModules();
|
|
104
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
105
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
106
|
-
|
|
107
|
-
await trackingFunctions.trackLogin();
|
|
108
|
-
|
|
109
|
-
expect(consoleSpy.error).toHaveBeenCalledWith('Failed to track login session:', { message: 'Database error' });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should handle unexpected errors', async () => {
|
|
113
|
-
const mockGetUser = vi.fn().mockRejectedValue(new Error('Auth error'));
|
|
114
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
115
|
-
|
|
116
|
-
vi.resetModules();
|
|
117
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
118
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
119
|
-
|
|
120
|
-
await trackingFunctions.trackLogin();
|
|
121
|
-
|
|
122
|
-
expect(consoleSpy.error).toHaveBeenCalledWith('Failed to track login:', expect.any(Error));
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
30
|
describe('trackEventSwitch', () => {
|
|
127
31
|
it('should track event switch successfully', async () => {
|
|
128
32
|
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
@@ -193,76 +97,6 @@ describe('sessionTracking', () => {
|
|
|
193
97
|
});
|
|
194
98
|
});
|
|
195
99
|
|
|
196
|
-
describe('trackLogout', () => {
|
|
197
|
-
it('should track logout successfully', async () => {
|
|
198
|
-
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
199
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: mockUser } });
|
|
200
|
-
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
201
|
-
|
|
202
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
203
|
-
mockSupabase.rpc = mockRpc;
|
|
204
|
-
|
|
205
|
-
vi.resetModules();
|
|
206
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
207
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
208
|
-
|
|
209
|
-
await trackingFunctions.trackLogout();
|
|
210
|
-
|
|
211
|
-
expect(mockRpc).toHaveBeenCalledWith('rbac_session_track', {
|
|
212
|
-
p_user_id: 'user-123',
|
|
213
|
-
p_session_type: 'logout',
|
|
214
|
-
p_event_id: undefined,
|
|
215
|
-
p_app_id: undefined,
|
|
216
|
-
p_ip_address: undefined,
|
|
217
|
-
p_user_agent: undefined
|
|
218
|
-
});
|
|
219
|
-
expect(consoleSpy.log).toHaveBeenCalledWith('Logout session tracked successfully');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should handle no authenticated user', async () => {
|
|
223
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: null } });
|
|
224
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
225
|
-
|
|
226
|
-
vi.resetModules();
|
|
227
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
228
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
229
|
-
|
|
230
|
-
await trackingFunctions.trackLogout();
|
|
231
|
-
|
|
232
|
-
expect(consoleSpy.warn).toHaveBeenCalledWith('No authenticated user found for session tracking');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('should handle tracking error', async () => {
|
|
236
|
-
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
237
|
-
const mockGetUser = vi.fn().mockResolvedValue({ data: { user: mockUser } });
|
|
238
|
-
const mockRpc = vi.fn().mockResolvedValue({ error: { message: 'Database error' } });
|
|
239
|
-
|
|
240
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
241
|
-
mockSupabase.rpc = mockRpc;
|
|
242
|
-
|
|
243
|
-
vi.resetModules();
|
|
244
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
245
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
246
|
-
|
|
247
|
-
await trackingFunctions.trackLogout();
|
|
248
|
-
|
|
249
|
-
expect(consoleSpy.error).toHaveBeenCalledWith('Failed to track logout session:', { message: 'Database error' });
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('should handle unexpected errors', async () => {
|
|
253
|
-
const mockGetUser = vi.fn().mockRejectedValue(new Error('Auth error'));
|
|
254
|
-
mockSupabase.auth.getUser = mockGetUser;
|
|
255
|
-
|
|
256
|
-
vi.resetModules();
|
|
257
|
-
const { useSessionTracking } = await import('../sessionTracking');
|
|
258
|
-
trackingFunctions = useSessionTracking(mockSupabase, 'test-app');
|
|
259
|
-
|
|
260
|
-
await trackingFunctions.trackLogout();
|
|
261
|
-
|
|
262
|
-
expect(consoleSpy.error).toHaveBeenCalledWith('Failed to track logout:', expect.any(Error));
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
100
|
describe('trackSessionExpired', () => {
|
|
267
101
|
it('should track session expiration successfully', async () => {
|
|
268
102
|
const mockUser = { id: 'user-123', email: 'test@example.com' };
|
|
@@ -339,10 +173,11 @@ describe('sessionTracking', () => {
|
|
|
339
173
|
const { useSessionTracking } = await import('../sessionTracking');
|
|
340
174
|
const trackingWithoutApp = useSessionTracking(mockSupabase);
|
|
341
175
|
|
|
342
|
-
expect(trackingWithoutApp).toHaveProperty('trackLogin');
|
|
343
176
|
expect(trackingWithoutApp).toHaveProperty('trackEventSwitch');
|
|
344
|
-
expect(trackingWithoutApp).toHaveProperty('trackLogout');
|
|
345
177
|
expect(trackingWithoutApp).toHaveProperty('trackSessionExpired');
|
|
178
|
+
// trackLogin and trackLogout are no longer available (auto-tracked by UnifiedAuthProvider)
|
|
179
|
+
expect(trackingWithoutApp).not.toHaveProperty('trackLogin');
|
|
180
|
+
expect(trackingWithoutApp).not.toHaveProperty('trackLogout');
|
|
346
181
|
});
|
|
347
182
|
|
|
348
183
|
it('should pass undefined app name to tracking calls', async () => {
|
|
@@ -357,12 +192,12 @@ describe('sessionTracking', () => {
|
|
|
357
192
|
const { useSessionTracking } = await import('../sessionTracking');
|
|
358
193
|
const trackingWithoutApp = useSessionTracking(mockSupabase);
|
|
359
194
|
|
|
360
|
-
await trackingWithoutApp.
|
|
195
|
+
await trackingWithoutApp.trackEventSwitch('event-123');
|
|
361
196
|
|
|
362
197
|
expect(mockRpc).toHaveBeenCalledWith('rbac_session_track', {
|
|
363
198
|
p_user_id: 'user-123',
|
|
364
|
-
p_session_type: '
|
|
365
|
-
p_event_id:
|
|
199
|
+
p_session_type: 'event_switch',
|
|
200
|
+
p_event_id: 'event-123',
|
|
366
201
|
p_app_id: undefined,
|
|
367
202
|
p_ip_address: undefined,
|
|
368
203
|
p_user_agent: undefined
|
|
@@ -2,7 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
2
2
|
|
|
3
3
|
// Define the tracking parameters locally since old RBAC types are removed
|
|
4
4
|
interface TrackUserSessionParams {
|
|
5
|
-
p_session_type: '
|
|
5
|
+
p_session_type: 'event_switch' | 'session_expired';
|
|
6
6
|
p_event_id?: string;
|
|
7
7
|
p_app_id?: string;
|
|
8
8
|
ip_address?: string;
|
|
@@ -10,10 +10,14 @@ interface TrackUserSessionParams {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Hook for
|
|
13
|
+
* Hook for manual session tracking (event switches and session expiration).
|
|
14
|
+
*
|
|
15
|
+
* Note: Login and logout tracking is automatically handled by UnifiedAuthProvider.
|
|
16
|
+
* You should only use this hook for tracking event switches or session expirations.
|
|
17
|
+
*
|
|
14
18
|
* @param supabaseClient - Supabase client instance
|
|
15
19
|
* @param appName - Optional application name for tracking
|
|
16
|
-
* @returns Object containing tracking functions
|
|
20
|
+
* @returns Object containing tracking functions for event switches and session expiration
|
|
17
21
|
*/
|
|
18
22
|
export function useSessionTracking(supabaseClient: SupabaseClient, appName?: string) {
|
|
19
23
|
// Resolve app name to app_id
|
|
@@ -39,45 +43,6 @@ export function useSessionTracking(supabaseClient: SupabaseClient, appName?: str
|
|
|
39
43
|
return undefined;
|
|
40
44
|
}
|
|
41
45
|
};
|
|
42
|
-
/**
|
|
43
|
-
* Track a user login event
|
|
44
|
-
* @param eventId - Optional event ID to associate with the login
|
|
45
|
-
*/
|
|
46
|
-
const trackLogin = async (eventId?: string) => {
|
|
47
|
-
try {
|
|
48
|
-
const { data: { user } } = await supabaseClient.auth.getUser();
|
|
49
|
-
if (!user) {
|
|
50
|
-
console.warn('No authenticated user found for session tracking');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const appId = await resolveAppId();
|
|
55
|
-
|
|
56
|
-
const params: TrackUserSessionParams = {
|
|
57
|
-
p_session_type: 'login',
|
|
58
|
-
p_event_id: eventId,
|
|
59
|
-
p_app_id: appId
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const { error } = await supabaseClient.rpc('rbac_session_track', {
|
|
63
|
-
p_user_id: user?.id,
|
|
64
|
-
p_session_type: params.p_session_type,
|
|
65
|
-
p_event_id: params.p_event_id,
|
|
66
|
-
p_app_id: params.p_app_id,
|
|
67
|
-
p_ip_address: params.ip_address,
|
|
68
|
-
p_user_agent: params.user_agent
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
if (error) {
|
|
72
|
-
console.error('Failed to track login session:', error);
|
|
73
|
-
} else {
|
|
74
|
-
console.log('Login session tracked successfully');
|
|
75
|
-
}
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error('Failed to track login:', error);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
46
|
/**
|
|
82
47
|
* Track an event switch
|
|
83
48
|
* @param eventId - ID of the event being switched to
|
|
@@ -117,43 +82,6 @@ export function useSessionTracking(supabaseClient: SupabaseClient, appName?: str
|
|
|
117
82
|
}
|
|
118
83
|
};
|
|
119
84
|
|
|
120
|
-
/**
|
|
121
|
-
* Track a user logout event
|
|
122
|
-
*/
|
|
123
|
-
const trackLogout = async () => {
|
|
124
|
-
try {
|
|
125
|
-
const { data: { user } } = await supabaseClient.auth.getUser();
|
|
126
|
-
if (!user) {
|
|
127
|
-
console.warn('No authenticated user found for session tracking');
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const appId = await resolveAppId();
|
|
132
|
-
|
|
133
|
-
const params: TrackUserSessionParams = {
|
|
134
|
-
p_session_type: 'logout',
|
|
135
|
-
p_app_id: appId
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const { error } = await supabaseClient.rpc('rbac_session_track', {
|
|
139
|
-
p_user_id: user?.id,
|
|
140
|
-
p_session_type: params.p_session_type,
|
|
141
|
-
p_event_id: params.p_event_id,
|
|
142
|
-
p_app_id: params.p_app_id,
|
|
143
|
-
p_ip_address: params.ip_address,
|
|
144
|
-
p_user_agent: params.user_agent
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (error) {
|
|
148
|
-
console.error('Failed to track logout session:', error);
|
|
149
|
-
} else {
|
|
150
|
-
console.log('Logout session tracked successfully');
|
|
151
|
-
}
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error('Failed to track logout:', error);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
85
|
/**
|
|
158
86
|
* Track a session expiration
|
|
159
87
|
*/
|
|
@@ -192,9 +120,7 @@ export function useSessionTracking(supabaseClient: SupabaseClient, appName?: str
|
|
|
192
120
|
};
|
|
193
121
|
|
|
194
122
|
return {
|
|
195
|
-
trackLogin,
|
|
196
123
|
trackEventSwitch,
|
|
197
|
-
trackLogout,
|
|
198
124
|
trackSessionExpired
|
|
199
125
|
};
|
|
200
126
|
}
|