@jmruthers/pace-core 0.5.108 → 0.5.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +93 -173
  2. package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
  3. package/dist/{DataTable-WFCHVWTY.js → DataTable-D3BK2FCN.js} +7 -7
  4. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
  5. package/dist/{api-KG4A2X7P.js → api-PIE4JRFS.js} +2 -2
  6. package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
  7. package/dist/{chunk-B3QX32P5.js → chunk-3J5N2T2N.js} +85 -28
  8. package/dist/chunk-3J5N2T2N.js.map +1 -0
  9. package/dist/{chunk-MOMYOQMC.js → chunk-7GBEBJLR.js} +29 -37
  10. package/dist/chunk-7GBEBJLR.js.map +1 -0
  11. package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
  12. package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
  13. package/dist/{chunk-VJ7MPS2K.js → chunk-AWK2FAUN.js} +6 -6
  14. package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
  15. package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
  16. package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
  17. package/dist/chunk-GZRXOUBE.js +176 -0
  18. package/dist/chunk-GZRXOUBE.js.map +1 -0
  19. package/dist/{chunk-QDDUU625.js → chunk-HADXAZT3.js} +4 -4
  20. package/dist/{chunk-IMZGJ2X7.js → chunk-HGZSO43Y.js} +4 -4
  21. package/dist/{chunk-S63MFSY6.js → chunk-XRSP3H52.js} +15 -8
  22. package/dist/chunk-XRSP3H52.js.map +1 -0
  23. package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
  24. package/dist/components.js +9 -9
  25. package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
  26. package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
  27. package/dist/hooks.d.ts +2 -2
  28. package/dist/hooks.js +7 -7
  29. package/dist/index.d.ts +6 -6
  30. package/dist/index.js +16 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/providers.d.ts +4 -3
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +35 -23
  35. package/dist/rbac/index.js +8 -8
  36. package/dist/types.d.ts +2 -2
  37. package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
  38. package/dist/utils.d.ts +2 -15
  39. package/dist/utils.js +4 -145
  40. package/dist/utils.js.map +1 -1
  41. package/dist/validation.d.ts +1 -1
  42. package/docs/api/classes/ColumnFactory.md +1 -1
  43. package/docs/api/classes/ErrorBoundary.md +1 -1
  44. package/docs/api/classes/InvalidScopeError.md +1 -1
  45. package/docs/api/classes/MissingUserContextError.md +1 -1
  46. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  47. package/docs/api/classes/PermissionDeniedError.md +1 -1
  48. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  49. package/docs/api/classes/RBACAuditManager.md +1 -1
  50. package/docs/api/classes/RBACCache.md +1 -1
  51. package/docs/api/classes/RBACEngine.md +9 -8
  52. package/docs/api/classes/RBACError.md +1 -1
  53. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  54. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  55. package/docs/api/classes/StorageUtils.md +1 -1
  56. package/docs/api/enums/FileCategory.md +1 -1
  57. package/docs/api/interfaces/AggregateConfig.md +1 -1
  58. package/docs/api/interfaces/ButtonProps.md +1 -1
  59. package/docs/api/interfaces/CardProps.md +1 -1
  60. package/docs/api/interfaces/ColorPalette.md +1 -1
  61. package/docs/api/interfaces/ColorShade.md +1 -1
  62. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  63. package/docs/api/interfaces/DataRecord.md +1 -1
  64. package/docs/api/interfaces/DataTableAction.md +1 -1
  65. package/docs/api/interfaces/DataTableColumn.md +3 -3
  66. package/docs/api/interfaces/DataTableProps.md +1 -1
  67. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  68. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  69. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  70. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  71. package/docs/api/interfaces/FileMetadata.md +1 -1
  72. package/docs/api/interfaces/FileReference.md +1 -1
  73. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  74. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  82. package/docs/api/interfaces/NavigationContextType.md +1 -1
  83. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  95. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  100. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  102. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  103. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  107. package/docs/api/interfaces/RBACConfig.md +19 -8
  108. package/docs/api/interfaces/RBACLogger.md +5 -5
  109. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  110. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  111. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  112. package/docs/api/interfaces/RouteConfig.md +1 -1
  113. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  114. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  115. package/docs/api/interfaces/StorageConfig.md +1 -1
  116. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  117. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  118. package/docs/api/interfaces/StorageListOptions.md +1 -1
  119. package/docs/api/interfaces/StorageListResult.md +1 -1
  120. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  121. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  122. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  123. package/docs/api/interfaces/StyleImport.md +1 -1
  124. package/docs/api/interfaces/SwitchProps.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  137. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  138. package/docs/api/interfaces/UserEventAccess.md +1 -1
  139. package/docs/api/interfaces/UserMenuProps.md +1 -1
  140. package/docs/api/interfaces/UserProfile.md +1 -1
  141. package/docs/api/modules.md +55 -20
  142. package/docs/api-reference/hooks.md +53 -0
  143. package/docs/api-reference/providers.md +60 -0
  144. package/docs/core-concepts/authentication.md +2 -0
  145. package/docs/documentation-index.md +0 -2
  146. package/docs/implementation-guides/authentication.md +1 -0
  147. package/docs/rbac/README.md +114 -38
  148. package/docs/rbac/api-reference.md +63 -16
  149. package/docs/rbac/getting-started.md +16 -16
  150. package/docs/rbac/quick-start.md +110 -35
  151. package/docs/rbac/troubleshooting.md +125 -2
  152. package/docs/security/README.md +59 -0
  153. package/package.json +1 -1
  154. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  155. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  156. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
  157. package/src/components/PaceAppLayout/PaceAppLayout.tsx +48 -16
  158. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
  159. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
  160. package/src/index.ts +3 -0
  161. package/src/providers/services/AuthServiceProvider.tsx +4 -3
  162. package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
  163. package/src/rbac/api.test.ts +2 -2
  164. package/src/rbac/api.ts +2 -1
  165. package/src/rbac/components/PagePermissionGuard.tsx +21 -38
  166. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  167. package/src/rbac/config.ts +2 -0
  168. package/src/rbac/engine.ts +17 -5
  169. package/src/rbac/security.ts +1 -1
  170. package/src/services/AuthService.ts +79 -1
  171. package/src/services/__tests__/AuthService.test.ts +184 -0
  172. package/src/types/database.ts +21 -9
  173. package/src/types/rbac-functions.ts +2 -1
  174. package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
  175. package/src/utils/sessionTracking.ts +7 -81
  176. package/dist/chunk-B3QX32P5.js.map +0 -1
  177. package/dist/chunk-MOMYOQMC.js.map +0 -1
  178. package/dist/chunk-NFPV7MRN.js +0 -94
  179. package/dist/chunk-NFPV7MRN.js.map +0 -1
  180. package/dist/chunk-S63MFSY6.js.map +0 -1
  181. package/docs/rbac/breaking-changes-v3.md +0 -222
  182. package/docs/rbac/migration-guide.md +0 -260
  183. package/src/providers/AuthProvider.simplified.tsx +0 -974
  184. package/dist/{DataTable-WFCHVWTY.js.map → DataTable-D3BK2FCN.js.map} +0 -0
  185. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
  186. package/dist/{api-KG4A2X7P.js.map → api-PIE4JRFS.js.map} +0 -0
  187. package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
  188. package/dist/{chunk-VJ7MPS2K.js.map → chunk-AWK2FAUN.js.map} +0 -0
  189. package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
  190. package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
  191. package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
  192. package/dist/{chunk-QDDUU625.js.map → chunk-HADXAZT3.js.map} +0 -0
  193. package/dist/{chunk-IMZGJ2X7.js.map → chunk-HGZSO43Y.js.map} +0 -0
  194. package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
  195. 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
  });
@@ -387,26 +387,38 @@ export interface Database {
387
387
  Row: {
388
388
  id: string;
389
389
  user_id: string;
390
- email: string | null;
391
- app_id: string | null;
390
+ email: string; // NOT NULL in schema
392
391
  event_id: string | null;
393
- created_at: string;
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?: string | null;
399
- app_id?: string | null;
402
+ email: string; // Required, NOT NULL
400
403
  event_id?: string | null;
401
- created_at?: string;
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 | null;
407
- app_id?: string | null;
414
+ email?: string;
408
415
  event_id?: string | null;
409
- created_at?: string;
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?: UUID;
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.trackLogin();
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: 'login',
365
- p_event_id: undefined,
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: 'login' | 'logout' | 'event_switch' | 'session_expired';
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 tracking user sessions and event interactions using the new RBAC system
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
  }