@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +75 -177
  2. package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
  3. package/dist/{DataTable-WFCHVWTY.js → DataTable-5HITILXS.js} +7 -7
  4. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
  5. package/dist/{api-KG4A2X7P.js → api-5I3E47G2.js} +2 -2
  6. package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
  7. package/dist/{chunk-MOMYOQMC.js → chunk-3TKTL5AZ.js} +13 -13
  8. package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
  9. package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
  10. package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
  11. package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
  12. package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
  13. package/dist/{chunk-S63MFSY6.js → chunk-F6TSYCKP.js} +4 -2
  14. package/dist/{chunk-S63MFSY6.js.map → chunk-F6TSYCKP.js.map} +1 -1
  15. package/dist/chunk-GZRXOUBE.js +176 -0
  16. package/dist/chunk-GZRXOUBE.js.map +1 -0
  17. package/dist/{chunk-B3QX32P5.js → chunk-P72NKAT5.js} +41 -24
  18. package/dist/chunk-P72NKAT5.js.map +1 -0
  19. package/dist/{chunk-VJ7MPS2K.js → chunk-S4D3Z723.js} +6 -6
  20. package/dist/{chunk-IMZGJ2X7.js → chunk-UW2DE6JX.js} +4 -4
  21. package/dist/{chunk-QDDUU625.js → chunk-WWNOVFDC.js} +4 -4
  22. package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
  23. package/dist/components.js +9 -9
  24. package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
  25. package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
  26. package/dist/hooks.d.ts +2 -2
  27. package/dist/hooks.js +7 -7
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +16 -14
  30. package/dist/index.js.map +1 -1
  31. package/dist/providers.d.ts +4 -3
  32. package/dist/providers.js +2 -2
  33. package/dist/rbac/index.d.ts +1 -1
  34. package/dist/rbac/index.js +8 -8
  35. package/dist/types.d.ts +2 -2
  36. package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
  37. package/dist/utils.d.ts +2 -15
  38. package/dist/utils.js +4 -145
  39. package/dist/utils.js.map +1 -1
  40. package/dist/validation.d.ts +1 -1
  41. package/docs/api/classes/ColumnFactory.md +1 -1
  42. package/docs/api/classes/ErrorBoundary.md +1 -1
  43. package/docs/api/classes/InvalidScopeError.md +1 -1
  44. package/docs/api/classes/MissingUserContextError.md +1 -1
  45. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  46. package/docs/api/classes/PermissionDeniedError.md +1 -1
  47. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  48. package/docs/api/classes/RBACAuditManager.md +1 -1
  49. package/docs/api/classes/RBACCache.md +1 -1
  50. package/docs/api/classes/RBACEngine.md +1 -1
  51. package/docs/api/classes/RBACError.md +1 -1
  52. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  53. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  54. package/docs/api/classes/StorageUtils.md +1 -1
  55. package/docs/api/enums/FileCategory.md +1 -1
  56. package/docs/api/interfaces/AggregateConfig.md +1 -1
  57. package/docs/api/interfaces/ButtonProps.md +1 -1
  58. package/docs/api/interfaces/CardProps.md +1 -1
  59. package/docs/api/interfaces/ColorPalette.md +1 -1
  60. package/docs/api/interfaces/ColorShade.md +1 -1
  61. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  62. package/docs/api/interfaces/DataRecord.md +1 -1
  63. package/docs/api/interfaces/DataTableAction.md +1 -1
  64. package/docs/api/interfaces/DataTableColumn.md +3 -3
  65. package/docs/api/interfaces/DataTableProps.md +1 -1
  66. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  67. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  68. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  69. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  70. package/docs/api/interfaces/FileMetadata.md +1 -1
  71. package/docs/api/interfaces/FileReference.md +1 -1
  72. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  73. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  74. package/docs/api/interfaces/FileUploadProps.md +1 -1
  75. package/docs/api/interfaces/FooterProps.md +1 -1
  76. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  77. package/docs/api/interfaces/InputProps.md +1 -1
  78. package/docs/api/interfaces/LabelProps.md +1 -1
  79. package/docs/api/interfaces/LoginFormProps.md +1 -1
  80. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  81. package/docs/api/interfaces/NavigationContextType.md +1 -1
  82. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  83. package/docs/api/interfaces/NavigationItem.md +1 -1
  84. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  85. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  86. package/docs/api/interfaces/Organisation.md +1 -1
  87. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  88. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  89. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  90. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  91. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  92. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  93. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  94. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  95. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  96. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  97. package/docs/api/interfaces/PaletteData.md +1 -1
  98. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  99. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +1 -1
  107. package/docs/api/interfaces/RBACLogger.md +1 -1
  108. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  109. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  110. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  111. package/docs/api/interfaces/RouteConfig.md +1 -1
  112. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  113. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  114. package/docs/api/interfaces/StorageConfig.md +1 -1
  115. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  116. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  117. package/docs/api/interfaces/StorageListOptions.md +1 -1
  118. package/docs/api/interfaces/StorageListResult.md +1 -1
  119. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  120. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  121. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  122. package/docs/api/interfaces/StyleImport.md +1 -1
  123. package/docs/api/interfaces/SwitchProps.md +1 -1
  124. package/docs/api/interfaces/ToastActionElement.md +1 -1
  125. package/docs/api/interfaces/ToastProps.md +1 -1
  126. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  128. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  132. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  133. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  134. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  135. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  136. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  137. package/docs/api/interfaces/UserEventAccess.md +1 -1
  138. package/docs/api/interfaces/UserMenuProps.md +1 -1
  139. package/docs/api/interfaces/UserProfile.md +1 -1
  140. package/docs/api/modules.md +37 -3
  141. package/docs/api-reference/hooks.md +53 -0
  142. package/docs/api-reference/providers.md +60 -0
  143. package/docs/core-concepts/authentication.md +2 -0
  144. package/docs/implementation-guides/authentication.md +1 -0
  145. package/docs/security/README.md +59 -0
  146. package/package.json +1 -1
  147. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
  148. package/src/components/PaceAppLayout/PaceAppLayout.tsx +48 -16
  149. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
  150. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
  151. package/src/index.ts +3 -0
  152. package/src/providers/services/AuthServiceProvider.tsx +4 -3
  153. package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
  154. package/src/rbac/engine.ts +2 -0
  155. package/src/services/AuthService.ts +79 -1
  156. package/src/services/__tests__/AuthService.test.ts +184 -0
  157. package/src/types/database.ts +21 -9
  158. package/src/types/rbac-functions.ts +2 -1
  159. package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
  160. package/src/utils/sessionTracking.ts +7 -81
  161. package/dist/chunk-B3QX32P5.js.map +0 -1
  162. package/dist/chunk-NFPV7MRN.js +0 -94
  163. package/dist/chunk-NFPV7MRN.js.map +0 -1
  164. package/src/providers/AuthProvider.simplified.tsx +0 -974
  165. package/dist/{DataTable-WFCHVWTY.js.map → DataTable-5HITILXS.js.map} +0 -0
  166. package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
  167. package/dist/{api-KG4A2X7P.js.map → api-5I3E47G2.js.map} +0 -0
  168. package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
  169. package/dist/{chunk-MOMYOQMC.js.map → chunk-3TKTL5AZ.js.map} +0 -0
  170. package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
  171. package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
  172. package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
  173. package/dist/{chunk-VJ7MPS2K.js.map → chunk-S4D3Z723.js.map} +0 -0
  174. package/dist/{chunk-IMZGJ2X7.js.map → chunk-UW2DE6JX.js.map} +0 -0
  175. package/dist/{chunk-QDDUU625.js.map → chunk-WWNOVFDC.js.map} +0 -0
  176. package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
  177. package/dist/{validation-D8VcbTzC.d.ts → validation-DnhrNMju.d.ts} +2 -2
@@ -1,974 +0,0 @@
1
- /**
2
- * @file Simplified Auth Provider
3
- * @package @jmruthers/pace-core
4
- * @module Providers
5
- * @since 2.0.0
6
- *
7
- * BREAKING CHANGES FROM v1:
8
- * - Single provider instead of nested contexts
9
- * - No service classes (direct React state)
10
- * - No observer pattern (React's built-in re-rendering)
11
- * - No UnifiedAuthProvider nesting
12
- *
13
- * This provides:
14
- * - Authentication (user, session)
15
- * - Organisation management
16
- * - Event management
17
- * - Inactivity tracking
18
- *
19
- * All in one provider with React state management.
20
- */
21
-
22
- import React, {
23
- createContext,
24
- useContext,
25
- useState,
26
- useEffect,
27
- useCallback,
28
- useMemo,
29
- useRef
30
- } from 'react';
31
- import {
32
- type SupabaseClient,
33
- type User,
34
- type Session,
35
- type AuthError
36
- } from '@supabase/supabase-js';
37
- import type { UUID } from '../rbac/types';
38
-
39
- // ============================================================================
40
- // TYPES
41
- // ============================================================================
42
-
43
- export interface Organisation {
44
- id: UUID;
45
- name: string;
46
- display_name?: string;
47
- subscription_tier?: string;
48
- settings?: Record<string, any>;
49
- is_active: boolean;
50
- created_at: string;
51
- updated_at?: string;
52
- }
53
-
54
- export interface Event {
55
- id: string;
56
- event_id: string;
57
- name: string;
58
- event_name?: string;
59
- organisation_id: UUID;
60
- event_date?: string; // Date from database for sorting
61
- start_date?: string; // Alias for event_date
62
- end_date?: string;
63
- status?: string;
64
- settings?: Record<string, any>;
65
- }
66
-
67
- export interface AuthContextType {
68
- // ========== Auth State ==========
69
- user: User | null;
70
- session: Session | null;
71
- isAuthenticated: boolean;
72
- authLoading: boolean;
73
- authError: AuthError | null;
74
- supabase: SupabaseClient;
75
-
76
- // ========== Auth Methods ==========
77
- signIn: (email: string, password?: string) => Promise<{ error: AuthError | null }>;
78
- signUp: (email: string, password: string) => Promise<{ error: AuthError | null }>;
79
- signOut: () => Promise<{ error: AuthError | null }>;
80
- resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
81
- updatePassword: (password: string) => Promise<{ error: AuthError | null }>;
82
- refreshSession: () => Promise<{ error: AuthError | null }>;
83
-
84
- // ========== Organisation State ==========
85
- selectedOrganisation: Organisation | null;
86
- organisations: Organisation[];
87
- organisationLoading: boolean;
88
- organisationError: Error | null;
89
- hasValidOrganisationContext: boolean;
90
- isContextReady: boolean;
91
-
92
- // ========== Organisation Methods ==========
93
- switchOrganisation: (orgId: UUID) => Promise<void>;
94
- refreshOrganisations: () => Promise<void>;
95
- getUserRole: (orgId?: UUID) => string | null;
96
- getPrimaryOrganisation: () => Organisation | null;
97
-
98
- // ========== Event State ==========
99
- events: Event[];
100
- selectedEvent: Event | null;
101
- eventLoading: boolean;
102
- eventError: Error | null;
103
-
104
- // ========== Event Methods ==========
105
- setSelectedEvent: (event: Event | null) => void;
106
- refreshEvents: () => Promise<void>;
107
-
108
- // ========== Inactivity State ==========
109
- showInactivityWarning: boolean;
110
- inactivityTimeRemaining: number;
111
- isIdle: boolean;
112
- isTracking: boolean;
113
-
114
- // ========== Inactivity Methods ==========
115
- resetActivity: () => void;
116
- startTracking: () => void;
117
- stopTracking: () => void;
118
-
119
- // ========== Session Management ==========
120
- sessionId: string | null;
121
- deviceFingerprint: string | null;
122
- hasConcurrentSessions: boolean;
123
- }
124
-
125
- export interface AuthProviderProps {
126
- children: React.ReactNode;
127
- supabaseClient: SupabaseClient;
128
- appName: string;
129
- persistState?: boolean;
130
- requireOrganisationContext?: boolean;
131
-
132
- // Inactivity configuration (MANDATORY for security)
133
- idleTimeoutMs: number;
134
- warnBeforeMs: number;
135
- onIdleLogout: (reason: 'inactivity') => void;
136
- renderInactivityWarning?: (args: {
137
- timeRemaining: number;
138
- onStaySignedIn: () => void;
139
- onSignOutNow: () => void;
140
- }) => React.ReactNode;
141
- }
142
-
143
- // ============================================================================
144
- // CONTEXT
145
- // ============================================================================
146
-
147
- const AuthContext = createContext<AuthContextType | null>(null);
148
-
149
- // ============================================================================
150
- // PROVIDER
151
- // ============================================================================
152
-
153
- export function AuthProvider({
154
- children,
155
- supabaseClient,
156
- appName,
157
- persistState = true,
158
- requireOrganisationContext = true,
159
- idleTimeoutMs = 30 * 60 * 1000, // 30 minutes - MANDATORY for security
160
- warnBeforeMs = 60 * 1000, // 1 minute
161
- onIdleLogout, // MANDATORY - must be provided
162
- renderInactivityWarning,
163
- }: AuthProviderProps) {
164
- // MANDATORY: Inactivity timeout cannot be disabled
165
- if (!idleTimeoutMs || idleTimeoutMs < 60000) {
166
- throw new Error(
167
- 'AuthProvider: idleTimeoutMs is MANDATORY and must be at least 60 seconds (60000ms) for security. ' +
168
- 'The dangerouslyDisableInactivity flag has been removed.'
169
- );
170
- }
171
-
172
- if (!onIdleLogout) {
173
- throw new Error(
174
- 'AuthProvider: onIdleLogout callback is MANDATORY and must be provided for security.'
175
- );
176
- }
177
-
178
- // ==========================================================================
179
- // AUTH STATE (replaces AuthService)
180
- // ==========================================================================
181
-
182
- const [user, setUser] = useState<User | null>(null);
183
- const [session, setSession] = useState<Session | null>(null);
184
- const [authLoading, setAuthLoading] = useState(true);
185
- const [authError, setAuthError] = useState<AuthError | null>(null);
186
-
187
- // ==========================================================================
188
- // ORGANISATION STATE (replaces OrganisationService)
189
- // ==========================================================================
190
-
191
- const [selectedOrganisation, setSelectedOrganisation] = useState<Organisation | null>(null);
192
- const [organisations, setOrganisations] = useState<Organisation[]>([]);
193
- const [organisationLoading, setOrganisationLoading] = useState(false);
194
- const [organisationError, setOrganisationError] = useState<Error | null>(null);
195
-
196
- // ==========================================================================
197
- // EVENT STATE (replaces EventService)
198
- // ==========================================================================
199
-
200
- const [events, setEvents] = useState<Event[]>([]);
201
- const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
202
- const [eventLoading, setEventLoading] = useState(false);
203
- const [eventError, setEventError] = useState<Error | null>(null);
204
-
205
- // ==========================================================================
206
- // INACTIVITY STATE (replaces InactivityService)
207
- // ==========================================================================
208
-
209
- const [showInactivityWarning, setShowInactivityWarning] = useState(false);
210
- const [inactivityTimeRemaining, setInactivityTimeRemaining] = useState(idleTimeoutMs);
211
- const [isIdle, setIsIdle] = useState(false);
212
- const [isTracking, setIsTracking] = useState(false);
213
-
214
- const lastActivityRef = useRef<number>(Date.now());
215
- const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
216
- const warningTimerRef = useRef<NodeJS.Timeout | null>(null);
217
- const deviceFingerprintRef = useRef<string | null>(null);
218
- const sessionIdRef = useRef<string | null>(null);
219
-
220
- // ==========================================================================
221
- // AUTH EFFECTS (replaces AuthService initialization)
222
- // ==========================================================================
223
-
224
- useEffect(() => {
225
- // Setup auth state listener
226
- const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
227
- (event, newSession) => {
228
- console.log('[AuthProvider] Auth state change:', event);
229
-
230
- if (event === 'SIGNED_OUT') {
231
- setSession(null);
232
- setUser(null);
233
- setAuthError(null);
234
- } else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
235
- setSession(newSession);
236
- setUser(newSession?.user ?? null);
237
- if (newSession) {
238
- setAuthError(null);
239
- }
240
- } else if (event === 'INITIAL_SESSION') {
241
- if (newSession) {
242
- setSession(newSession);
243
- setUser(newSession.user ?? null);
244
- setAuthError(null);
245
- }
246
- }
247
-
248
- setAuthLoading(false);
249
- }
250
- );
251
-
252
- // Initial session check
253
- supabaseClient.auth.getSession().then(({ data: { session: initialSession } }) => {
254
- setSession(initialSession);
255
- setUser(initialSession?.user ?? null);
256
- setAuthLoading(false);
257
- });
258
-
259
- return () => {
260
- subscription.unsubscribe();
261
- };
262
- }, [supabaseClient]);
263
-
264
- // ==========================================================================
265
- // ORGANISATION EFFECTS (replaces OrganisationService initialization)
266
- // ==========================================================================
267
-
268
- useEffect(() => {
269
- if (!user) {
270
- setOrganisations([]);
271
- setSelectedOrganisation(null);
272
- return;
273
- }
274
-
275
- // Load user's organisations
276
- const loadOrganisations = async () => {
277
- setOrganisationLoading(true);
278
- setOrganisationError(null);
279
-
280
- try {
281
- // Use RPC to get organisations with roles
282
- const { data, error } = await supabaseClient
283
- .rpc('rbac_user_organisation_roles_get', {
284
- p_user_id: user.id
285
- }) as { data: Array<{ organisation_id: UUID }> | null; error: any };
286
-
287
- if (error) throw error;
288
-
289
- if (data && data.length > 0) {
290
- // Fetch full organisation details
291
- const orgIds = data.map(r => r.organisation_id);
292
- const { data: orgs, error: orgsError } = await supabaseClient
293
- .from('organisations')
294
- .select('*')
295
- .in('id', orgIds)
296
- .eq('is_active', true) as { data: Organisation[] | null; error: any };
297
-
298
- if (orgsError) throw orgsError;
299
-
300
- setOrganisations(orgs || []);
301
-
302
- // Auto-select first organisation if none selected
303
- if (!selectedOrganisation && orgs && orgs.length > 0) {
304
- const savedOrgId = persistState ? localStorage.getItem(`pace_${appName}_org`) : null;
305
- const orgToSelect = savedOrgId
306
- ? orgs.find(o => o.id === savedOrgId) || orgs[0]
307
- : orgs[0];
308
- setSelectedOrganisation(orgToSelect);
309
- }
310
- }
311
- } catch (error) {
312
- console.error('[AuthProvider] Failed to load organisations:', error);
313
- setOrganisationError(error as Error);
314
- } finally {
315
- setOrganisationLoading(false);
316
- }
317
- };
318
-
319
- loadOrganisations();
320
- }, [user, supabaseClient, appName, persistState, selectedOrganisation]);
321
-
322
- // ==========================================================================
323
- // EVENT EFFECTS (replaces EventService initialization)
324
- // ==========================================================================
325
-
326
- useEffect(() => {
327
- if (!user || !selectedOrganisation) {
328
- setEvents([]);
329
- setSelectedEvent(null);
330
- return;
331
- }
332
-
333
- // Load user's events for the selected organisation
334
- const loadEvents = async () => {
335
- setEventLoading(true);
336
- setEventError(null);
337
-
338
- try {
339
- // Use RPC to get user's events
340
- const { data, error } = await supabaseClient
341
- .rpc('data_user_events_get', {
342
- p_user_id: user.id,
343
- p_app_name: appName
344
- }) as { data: Event[] | null; error: any };
345
-
346
- if (error) throw error;
347
-
348
- // Filter events for current organisation
349
- const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
350
- setEvents(orgEvents);
351
-
352
- // Auto-select next event in the future by date if none selected
353
- if (!selectedEvent && orgEvents.length > 0) {
354
- const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
355
- let eventToSelect: Event | null = null;
356
-
357
- if (savedEventId) {
358
- // Try to restore persisted event
359
- eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
360
- }
361
-
362
- // If no persisted event, select the next event in the future by date
363
- if (!eventToSelect) {
364
- const now = new Date();
365
- const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
366
- const futureEvents = orgEvents
367
- .filter((e): e is Event & { event_date: string } => {
368
- if (!e.event_date) return false;
369
- const eventDate = new Date(e.event_date);
370
- const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
371
- return startOfEventDate >= startOfToday;
372
- })
373
- .sort((a, b) => {
374
- const dateA = new Date(a.event_date);
375
- const dateB = new Date(b.event_date);
376
- return dateA.getTime() - dateB.getTime();
377
- });
378
-
379
- eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
380
- }
381
-
382
- // Fallback to most recent past event if no future events found
383
- if (!eventToSelect && orgEvents.length > 0) {
384
- const now = new Date();
385
- const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
386
- const pastEvents = orgEvents
387
- .filter((e): e is Event & { event_date: string } => {
388
- if (!e.event_date) return false;
389
- const eventDate = new Date(e.event_date);
390
- const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
391
- return startOfEventDate < startOfToday;
392
- })
393
- .sort((a, b) => {
394
- const dateA = new Date(a.event_date);
395
- const dateB = new Date(b.event_date);
396
- return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
397
- });
398
-
399
- eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
400
- }
401
-
402
- if (eventToSelect) {
403
- setSelectedEvent(eventToSelect);
404
- }
405
- }
406
- } catch (error) {
407
- console.error('[AuthProvider] Failed to load events:', error);
408
- setEventError(error as Error);
409
- } finally {
410
- setEventLoading(false);
411
- }
412
- };
413
-
414
- loadEvents();
415
- }, [user, selectedOrganisation, supabaseClient, appName, persistState, selectedEvent]);
416
-
417
- // ==========================================================================
418
- // INACTIVITY EFFECTS (replaces InactivityService)
419
- // ==========================================================================
420
-
421
- const resetActivity = useCallback(() => {
422
- lastActivityRef.current = Date.now();
423
- setInactivityTimeRemaining(idleTimeoutMs);
424
- setShowInactivityWarning(false);
425
- setIsIdle(false);
426
- }, [idleTimeoutMs]);
427
-
428
- const startTracking = useCallback(() => {
429
- if (isTracking) return;
430
-
431
- setIsTracking(true);
432
- resetActivity();
433
-
434
- // Activity event listeners
435
- const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
436
- const handleActivity = () => resetActivity();
437
-
438
- activityEvents.forEach(event => {
439
- window.addEventListener(event, handleActivity, { passive: true });
440
- });
441
-
442
- // Idle check interval
443
- idleTimerRef.current = setInterval(() => {
444
- const timeSinceActivity = Date.now() - lastActivityRef.current;
445
- const remaining = idleTimeoutMs - timeSinceActivity;
446
-
447
- setInactivityTimeRemaining(Math.max(0, remaining));
448
-
449
- // Show warning
450
- if (remaining <= warnBeforeMs && remaining > 0 && !showInactivityWarning) {
451
- setShowInactivityWarning(true);
452
- }
453
-
454
- // Auto logout
455
- if (remaining <= 0) {
456
- setIsIdle(true);
457
- onIdleLogout('inactivity');
458
- }
459
- }, 1000);
460
-
461
- // Cleanup
462
- return () => {
463
- activityEvents.forEach(event => {
464
- window.removeEventListener(event, handleActivity);
465
- });
466
- if (idleTimerRef.current) {
467
- clearInterval(idleTimerRef.current);
468
- }
469
- if (warningTimerRef.current) {
470
- clearTimeout(warningTimerRef.current);
471
- }
472
- };
473
- }, [isTracking, idleTimeoutMs, warnBeforeMs, showInactivityWarning, onIdleLogout, resetActivity]);
474
-
475
- const stopTracking = useCallback(() => {
476
- setIsTracking(false);
477
- if (idleTimerRef.current) {
478
- clearInterval(idleTimerRef.current);
479
- idleTimerRef.current = null;
480
- }
481
- if (warningTimerRef.current) {
482
- clearTimeout(warningTimerRef.current);
483
- warningTimerRef.current = null;
484
- }
485
- }, []);
486
-
487
- // Generate and track device fingerprint
488
- useEffect(() => {
489
- if (user) {
490
- // Generate device fingerprint (simple version - in production, use more sophisticated approach)
491
- if (!deviceFingerprintRef.current) {
492
- const fingerprint = btoa(JSON.stringify({
493
- userAgent: navigator.userAgent,
494
- language: navigator.language,
495
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
496
- screen: `${window.screen.width}x${window.screen.height}`,
497
- timestamp: Date.now()
498
- })).substring(0, 64);
499
- deviceFingerprintRef.current = fingerprint;
500
- }
501
-
502
- // Track session with device fingerprint
503
- const trackSession = async () => {
504
- if (!deviceFingerprintRef.current || !user) return;
505
-
506
- try {
507
- const { data, error } = await supabaseClient.rpc('rbac_session_track', {
508
- p_user_id: user.id,
509
- p_session_type: 'login',
510
- p_app_id: null, // Will be resolved by the RPC function
511
- p_ip_address: null, // Will be handled by backend
512
- p_user_agent: navigator.userAgent,
513
- p_device_fingerprint: deviceFingerprintRef.current
514
- });
515
-
516
- if (!error && data) {
517
- sessionIdRef.current = data;
518
-
519
- // Check for concurrent sessions
520
- const { data: concurrentData } = await supabaseClient.rpc('rbac_detect_concurrent_sessions', {
521
- p_user_id: user.id,
522
- p_device_fingerprint: deviceFingerprintRef.current,
523
- p_timeout_seconds: 300
524
- });
525
-
526
- if (concurrentData && concurrentData.has_concurrent) {
527
- console.warn(
528
- '[AuthProvider] Concurrent session detected:',
529
- concurrentData.concurrent_sessions_count,
530
- 'other session(s) active'
531
- );
532
- }
533
- }
534
- } catch (err) {
535
- console.error('[AuthProvider] Session tracking failed:', err);
536
- }
537
- };
538
-
539
- trackSession();
540
- }
541
- }, [user, supabaseClient]);
542
-
543
- // Auto-start tracking when user is authenticated
544
- useEffect(() => {
545
- if (user && !authLoading) {
546
- startTracking();
547
- } else {
548
- stopTracking();
549
- }
550
-
551
- return () => {
552
- stopTracking();
553
- };
554
- }, [user, authLoading, startTracking, stopTracking]);
555
-
556
- // ==========================================================================
557
- // AUTH METHODS
558
- // ==========================================================================
559
-
560
- const signIn = useCallback(async (email: string, password?: string) => {
561
- setAuthLoading(true);
562
- setAuthError(null);
563
-
564
- try {
565
- let result;
566
- if (password) {
567
- // Email + password sign in
568
- result = await supabaseClient.auth.signInWithPassword({ email, password });
569
- } else {
570
- // Magic link sign in
571
- result = await supabaseClient.auth.signInWithOtp({ email });
572
- }
573
-
574
- if (result.error) {
575
- setAuthError(result.error);
576
- return { error: result.error };
577
- }
578
-
579
- return { error: null };
580
- } catch (error) {
581
- const authError = error as AuthError;
582
- setAuthError(authError);
583
- return { error: authError };
584
- } finally {
585
- setAuthLoading(false);
586
- }
587
- }, [supabaseClient]);
588
-
589
- const signUp = useCallback(async (email: string, password: string) => {
590
- setAuthLoading(true);
591
- setAuthError(null);
592
-
593
- try {
594
- const { error } = await supabaseClient.auth.signUp({ email, password });
595
-
596
- if (error) {
597
- setAuthError(error);
598
- return { error };
599
- }
600
-
601
- return { error: null };
602
- } catch (error) {
603
- const authError = error as AuthError;
604
- setAuthError(authError);
605
- return { error: authError };
606
- } finally {
607
- setAuthLoading(false);
608
- }
609
- }, [supabaseClient]);
610
-
611
- const signOut = useCallback(async () => {
612
- setAuthLoading(true);
613
- setAuthError(null);
614
-
615
- try {
616
- const { error } = await supabaseClient.auth.signOut();
617
-
618
- if (error) {
619
- setAuthError(error);
620
- return { error };
621
- }
622
-
623
- // Clear local state
624
- setUser(null);
625
- setSession(null);
626
- setSelectedOrganisation(null);
627
- setOrganisations([]);
628
- setSelectedEvent(null);
629
- setEvents([]);
630
-
631
- // Clear persisted state
632
- if (persistState) {
633
- localStorage.removeItem(`pace_${appName}_org`);
634
- localStorage.removeItem(`pace_${appName}_event`);
635
- }
636
-
637
- return { error: null };
638
- } catch (error) {
639
- const authError = error as AuthError;
640
- setAuthError(authError);
641
- return { error: authError };
642
- } finally {
643
- setAuthLoading(false);
644
- }
645
- }, [supabaseClient, appName, persistState]);
646
-
647
- const resetPassword = useCallback(async (email: string) => {
648
- setAuthError(null);
649
-
650
- try {
651
- const { error } = await supabaseClient.auth.resetPasswordForEmail(email);
652
-
653
- if (error) {
654
- setAuthError(error);
655
- return { error };
656
- }
657
-
658
- return { error: null };
659
- } catch (error) {
660
- const authError = error as AuthError;
661
- setAuthError(authError);
662
- return { error: authError };
663
- }
664
- }, [supabaseClient]);
665
-
666
- const updatePassword = useCallback(async (password: string) => {
667
- setAuthError(null);
668
-
669
- try {
670
- const { error } = await supabaseClient.auth.updateUser({ password });
671
-
672
- if (error) {
673
- setAuthError(error);
674
- return { error };
675
- }
676
-
677
- return { error: null };
678
- } catch (error) {
679
- const authError = error as AuthError;
680
- setAuthError(authError);
681
- return { error: authError };
682
- }
683
- }, [supabaseClient]);
684
-
685
- const refreshSession = useCallback(async () => {
686
- setAuthError(null);
687
-
688
- try {
689
- const { data, error } = await supabaseClient.auth.refreshSession();
690
-
691
- if (error) {
692
- setAuthError(error);
693
- return { error };
694
- }
695
-
696
- if (data.session) {
697
- setSession(data.session);
698
- setUser(data.session.user);
699
- }
700
-
701
- return { error: null };
702
- } catch (error) {
703
- const authError = error as AuthError;
704
- setAuthError(authError);
705
- return { error: authError };
706
- }
707
- }, [supabaseClient]);
708
-
709
- // ==========================================================================
710
- // ORGANISATION METHODS
711
- // ==========================================================================
712
-
713
- const switchOrganisation = useCallback(async (orgId: UUID) => {
714
- const org = organisations.find(o => o.id === orgId);
715
- if (!org) {
716
- throw new Error(`Organisation ${orgId} not found`);
717
- }
718
-
719
- setSelectedOrganisation(org);
720
-
721
- if (persistState) {
722
- localStorage.setItem(`pace_${appName}_org`, orgId);
723
- }
724
-
725
- // Clear event selection when switching orgs
726
- setSelectedEvent(null);
727
- if (persistState) {
728
- localStorage.removeItem(`pace_${appName}_event`);
729
- }
730
- }, [organisations, appName, persistState]);
731
-
732
- const refreshOrganisations = useCallback(async () => {
733
- if (!user) return;
734
-
735
- setOrganisationLoading(true);
736
- setOrganisationError(null);
737
-
738
- try {
739
- const { data, error } = await supabaseClient
740
- .rpc('rbac_user_organisation_roles_get', {
741
- p_user_id: user.id
742
- }) as { data: Array<{ organisation_id: UUID }> | null; error: any };
743
-
744
- if (error) throw error;
745
-
746
- if (data && data.length > 0) {
747
- const orgIds = data.map(r => r.organisation_id);
748
- const { data: orgs, error: orgsError } = await supabaseClient
749
- .from('organisations')
750
- .select('*')
751
- .in('id', orgIds)
752
- .eq('is_active', true) as { data: Organisation[] | null; error: any };
753
-
754
- if (orgsError) throw orgsError;
755
-
756
- setOrganisations(orgs || []);
757
- }
758
- } catch (error) {
759
- console.error('[AuthProvider] Failed to refresh organisations:', error);
760
- setOrganisationError(error as Error);
761
- } finally {
762
- setOrganisationLoading(false);
763
- }
764
- }, [user, supabaseClient]);
765
-
766
- const getUserRole = useCallback((orgId?: UUID): string | null => {
767
- // This would require fetching role data - placeholder for now
768
- // In real implementation, we'd cache role data when loading organisations
769
- return 'member';
770
- }, []);
771
-
772
- const getPrimaryOrganisation = useCallback((): Organisation | null => {
773
- return organisations[0] || null;
774
- }, [organisations]);
775
-
776
- // ==========================================================================
777
- // EVENT METHODS
778
- // ==========================================================================
779
-
780
- const handleSetSelectedEvent = useCallback((event: Event | null) => {
781
- setSelectedEvent(event);
782
-
783
- if (persistState) {
784
- if (event) {
785
- localStorage.setItem(`pace_${appName}_event`, event.event_id);
786
- } else {
787
- localStorage.removeItem(`pace_${appName}_event`);
788
- }
789
- }
790
- }, [appName, persistState]);
791
-
792
- const refreshEvents = useCallback(async () => {
793
- if (!user || !selectedOrganisation) return;
794
-
795
- setEventLoading(true);
796
- setEventError(null);
797
-
798
- try {
799
- const { data, error } = await supabaseClient
800
- .rpc('data_user_events_get', {
801
- p_user_id: user.id,
802
- p_app_name: appName
803
- }) as { data: Event[] | null; error: any };
804
-
805
- if (error) throw error;
806
-
807
- const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
808
- setEvents(orgEvents);
809
-
810
- // Auto-select next event in the future by date if none selected
811
- if (!selectedEvent && orgEvents.length > 0) {
812
- const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
813
- let eventToSelect: Event | null = null;
814
-
815
- if (savedEventId) {
816
- // Try to restore persisted event
817
- eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
818
- }
819
-
820
- // If no persisted event, select the next event in the future by date
821
- if (!eventToSelect) {
822
- const now = new Date();
823
- const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
824
- const futureEvents = orgEvents
825
- .filter((e): e is Event & { event_date: string } => {
826
- if (!e.event_date) return false;
827
- const eventDate = new Date(e.event_date);
828
- const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
829
- return startOfEventDate >= startOfToday;
830
- })
831
- .sort((a, b) => {
832
- const dateA = new Date(a.event_date);
833
- const dateB = new Date(b.event_date);
834
- return dateA.getTime() - dateB.getTime();
835
- });
836
-
837
- eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
838
- }
839
-
840
- // Fallback to most recent past event if no future events found
841
- if (!eventToSelect && orgEvents.length > 0) {
842
- const now = new Date();
843
- const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
844
- const pastEvents = orgEvents
845
- .filter((e): e is Event & { event_date: string } => {
846
- if (!e.event_date) return false;
847
- const eventDate = new Date(e.event_date);
848
- const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
849
- return startOfEventDate < startOfToday;
850
- })
851
- .sort((a, b) => {
852
- const dateA = new Date(a.event_date);
853
- const dateB = new Date(b.event_date);
854
- return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
855
- });
856
-
857
- eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
858
- }
859
-
860
- if (eventToSelect) {
861
- setSelectedEvent(eventToSelect);
862
- }
863
- }
864
- } catch (error) {
865
- console.error('[AuthProvider] Failed to refresh events:', error);
866
- setEventError(error as Error);
867
- } finally {
868
- setEventLoading(false);
869
- }
870
- }, [user, selectedOrganisation, supabaseClient, appName, selectedEvent, persistState]);
871
-
872
- // ==========================================================================
873
- // COMPUTED VALUES
874
- // ==========================================================================
875
-
876
- const isAuthenticated = !!(user && session);
877
- const hasValidOrganisationContext = !!(selectedOrganisation && !organisationLoading);
878
- const isContextReady = isAuthenticated && (!requireOrganisationContext || hasValidOrganisationContext);
879
-
880
- // ==========================================================================
881
- // CONTEXT VALUE
882
- // ==========================================================================
883
-
884
- const contextValue = useMemo<AuthContextType>(() => ({
885
- // Auth
886
- user,
887
- session,
888
- isAuthenticated,
889
- authLoading,
890
- authError,
891
- supabase: supabaseClient,
892
- signIn,
893
- signUp,
894
- signOut,
895
- resetPassword,
896
- updatePassword,
897
- refreshSession,
898
-
899
- // Organisation
900
- selectedOrganisation,
901
- organisations,
902
- organisationLoading,
903
- organisationError,
904
- hasValidOrganisationContext,
905
- isContextReady: hasValidOrganisationContext,
906
- switchOrganisation,
907
- refreshOrganisations,
908
- getUserRole,
909
- getPrimaryOrganisation,
910
-
911
- // Event
912
- events,
913
- selectedEvent,
914
- eventLoading,
915
- eventError,
916
- setSelectedEvent: handleSetSelectedEvent,
917
- refreshEvents,
918
-
919
- // Inactivity
920
- showInactivityWarning,
921
- inactivityTimeRemaining,
922
- isIdle,
923
- isTracking,
924
- resetActivity,
925
- startTracking,
926
- stopTracking,
927
-
928
- // Session Management
929
- sessionId: sessionIdRef.current,
930
- deviceFingerprint: deviceFingerprintRef.current,
931
- hasConcurrentSessions: false, // TODO: implement state tracking
932
- }), [
933
- user, session, isAuthenticated, authLoading, authError,
934
- selectedOrganisation, organisations, organisationLoading, organisationError,
935
- hasValidOrganisationContext, isContextReady,
936
- events, selectedEvent, eventLoading, eventError,
937
- showInactivityWarning, inactivityTimeRemaining, isIdle, isTracking,
938
- signIn, signUp, signOut, resetPassword, updatePassword, refreshSession,
939
- switchOrganisation, refreshOrganisations, getUserRole, getPrimaryOrganisation,
940
- handleSetSelectedEvent, refreshEvents,
941
- resetActivity, startTracking, stopTracking,
942
- supabaseClient
943
- ]);
944
-
945
- // ==========================================================================
946
- // RENDER
947
- // ==========================================================================
948
-
949
- return (
950
- <AuthContext.Provider value={contextValue}>
951
- {children}
952
- {showInactivityWarning && renderInactivityWarning && renderInactivityWarning({
953
- timeRemaining: inactivityTimeRemaining,
954
- onStaySignedIn: resetActivity,
955
- onSignOutNow: signOut,
956
- })}
957
- </AuthContext.Provider>
958
- );
959
- }
960
-
961
- // ============================================================================
962
- // HOOK
963
- // ============================================================================
964
-
965
- export function useAuth(): AuthContextType {
966
- const context = useContext(AuthContext);
967
-
968
- if (!context) {
969
- throw new Error('useAuth must be used within AuthProvider');
970
- }
971
-
972
- return context;
973
- }
974
-