@jmruthers/pace-core 0.5.43 → 0.5.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-BGK2YF7A.js → DataTable-PWLLTSP7.js} +5 -5
- package/dist/{UnifiedAuthProvider-DGQsy-vY.d.ts → UnifiedAuthProvider-CQNiemcB.d.ts} +2 -2
- package/dist/{chunk-BLZBTCBT.js → chunk-2T6QEWMI.js} +2 -2
- package/dist/{chunk-DNAASEYY.js → chunk-3FAB54BI.js} +4 -4
- package/dist/{chunk-37LRETMD.js → chunk-3PNBACK3.js} +2 -2
- package/dist/{chunk-ZSLTSF55.js → chunk-6AQ7X3EE.js} +5 -5
- package/dist/{chunk-X4Y4KT5T.js → chunk-D4X7PPGX.js} +3 -3
- package/dist/{chunk-SAB5UT2E.js → chunk-FZ7EBWOT.js} +3 -3
- package/dist/{chunk-B3MGS7VR.js → chunk-GIISFLMP.js} +3 -3
- package/dist/{chunk-RL267HOF.js → chunk-NYF3CUNC.js} +2 -2
- package/dist/{chunk-P27KH7XF.js → chunk-OQ6DTLZ6.js} +156 -196
- package/dist/chunk-OQ6DTLZ6.js.map +1 -0
- package/dist/{chunk-APHGXR2D.js → chunk-VCHXOYD5.js} +3 -3
- package/dist/components.d.ts +1 -1
- package/dist/components.js +7 -7
- package/dist/hooks.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -10
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -3
- package/dist/rbac/index.js +5 -5
- package/dist/utils.js +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/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/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.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/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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/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 +3 -3
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +756 -0
- package/src/components/DataTable/__tests__/DataTable.test.tsx +880 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +702 -0
- package/src/components/PrintButton/__tests__/PrintButton.test.tsx +271 -0
- package/src/providers/AuthProvider.tsx +131 -230
- package/src/providers/OrganisationProvider.tsx +72 -10
- package/src/providers/__tests__/AuthProvider.test.tsx +619 -0
- package/src/providers/__tests__/EventProvider.test.tsx +190 -0
- package/src/providers/__tests__/InactivityProvider.test.tsx +645 -0
- package/src/providers/__tests__/OrganisationProvider.test.tsx +343 -0
- package/src/providers/__tests__/README.md +167 -0
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +581 -0
- package/src/rbac/__tests__/rbac-core.test.tsx +277 -0
- package/dist/chunk-P27KH7XF.js.map +0 -1
- /package/dist/{DataTable-BGK2YF7A.js.map → DataTable-PWLLTSP7.js.map} +0 -0
- /package/dist/{chunk-BLZBTCBT.js.map → chunk-2T6QEWMI.js.map} +0 -0
- /package/dist/{chunk-DNAASEYY.js.map → chunk-3FAB54BI.js.map} +0 -0
- /package/dist/{chunk-37LRETMD.js.map → chunk-3PNBACK3.js.map} +0 -0
- /package/dist/{chunk-ZSLTSF55.js.map → chunk-6AQ7X3EE.js.map} +0 -0
- /package/dist/{chunk-X4Y4KT5T.js.map → chunk-D4X7PPGX.js.map} +0 -0
- /package/dist/{chunk-SAB5UT2E.js.map → chunk-FZ7EBWOT.js.map} +0 -0
- /package/dist/{chunk-B3MGS7VR.js.map → chunk-GIISFLMP.js.map} +0 -0
- /package/dist/{chunk-RL267HOF.js.map → chunk-NYF3CUNC.js.map} +0 -0
- /package/dist/{chunk-APHGXR2D.js.map → chunk-VCHXOYD5.js.map} +0 -0
|
@@ -34,14 +34,6 @@ export interface AuthContextType {
|
|
|
34
34
|
|
|
35
35
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
36
36
|
|
|
37
|
-
export const useAuth = () => {
|
|
38
|
-
const context = useContext(AuthContext);
|
|
39
|
-
if (!context) {
|
|
40
|
-
throw new Error('useAuth must be used within an AuthProvider');
|
|
41
|
-
}
|
|
42
|
-
return context;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
37
|
export interface AuthProviderProps {
|
|
46
38
|
children: React.ReactNode;
|
|
47
39
|
supabaseClient?: SupabaseClient;
|
|
@@ -83,312 +75,213 @@ export function AuthProvider({ children, supabaseClient }: AuthProviderProps) {
|
|
|
83
75
|
};
|
|
84
76
|
}, []);
|
|
85
77
|
|
|
86
|
-
//
|
|
78
|
+
// FIXED: Proper session restoration with better error handling
|
|
87
79
|
useEffect(() => {
|
|
88
|
-
if (!supabaseClient)
|
|
80
|
+
if (!supabaseClient) {
|
|
81
|
+
setAuthLoading(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
89
84
|
|
|
90
|
-
|
|
85
|
+
let isMounted = true;
|
|
86
|
+
|
|
87
|
+
const initializeAuth = async () => {
|
|
91
88
|
try {
|
|
92
|
-
DebugLogger.log('AuthProvider',
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
DebugLogger.log('AuthProvider', 'Initializing authentication...');
|
|
90
|
+
|
|
91
|
+
// First, try to get the current session
|
|
92
|
+
const { data: { session: currentSession }, error: sessionError } = await supabaseClient.auth.getSession();
|
|
95
93
|
|
|
96
|
-
if (
|
|
97
|
-
console.warn('
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
if (sessionError) {
|
|
95
|
+
console.warn('[AuthProvider] Error getting current session:', sessionError);
|
|
96
|
+
// Don't treat session errors as fatal - user might not be logged in
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentSession && isMounted) {
|
|
100
|
+
DebugLogger.log('AuthProvider', 'Session restored from storage:', currentSession.user?.email);
|
|
101
|
+
setSession(currentSession);
|
|
102
|
+
setUser(currentSession.user);
|
|
103
|
+
setAuthError(null);
|
|
104
|
+
} else if (isMounted) {
|
|
105
|
+
DebugLogger.log('AuthProvider', 'No session found in storage');
|
|
106
|
+
// Try to get user anyway (in case session is expired but user exists)
|
|
107
|
+
const { data: { user: currentUser }, error: userError } = await supabaseClient.auth.getUser();
|
|
108
|
+
|
|
109
|
+
if (userError) {
|
|
110
|
+
DebugLogger.log('AuthProvider', 'No user found:', userError.message);
|
|
111
|
+
} else if (currentUser && isMounted) {
|
|
112
|
+
DebugLogger.log('AuthProvider', 'User found without session:', currentUser.email);
|
|
113
|
+
setUser(currentUser);
|
|
114
|
+
// Don't set session if it's null - this prevents issues
|
|
103
115
|
}
|
|
104
|
-
setAuthLoading(false);
|
|
105
|
-
return;
|
|
106
116
|
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
setUser(initialUser);
|
|
111
|
-
} else {
|
|
112
|
-
DebugLogger.log('AuthProvider', 'No initial user found');
|
|
117
|
+
|
|
118
|
+
if (isMounted) {
|
|
119
|
+
setAuthLoading(false);
|
|
113
120
|
}
|
|
114
|
-
setAuthLoading(false);
|
|
115
121
|
} catch (error) {
|
|
116
|
-
console.error('Error
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
setTimeout(() => loadInitialUser(1), 1000);
|
|
121
|
-
return;
|
|
122
|
+
console.error('[AuthProvider] Error during auth initialization:', error);
|
|
123
|
+
if (isMounted) {
|
|
124
|
+
setAuthLoading(false);
|
|
125
|
+
// Don't set auth error for initialization failures - user might not be logged in
|
|
122
126
|
}
|
|
123
|
-
setAuthLoading(false);
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
129
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Reasonable timeout to allow for auth state changes while preventing infinite hanging
|
|
146
|
-
const timeoutId = setTimeout(() => {
|
|
147
|
-
if (authLoading) {
|
|
148
|
-
console.warn('AuthProvider: Auth loading timeout reached - this may indicate a network issue or slow auth response');
|
|
149
|
-
setAuthLoading(false);
|
|
150
|
-
setAuthError(new AuthError('Authentication timeout - please try refreshing the page'));
|
|
151
|
-
}
|
|
152
|
-
}, 10000); // Increased to 10 seconds to allow for proper auth flow
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
DebugLogger.log('AuthProvider', 'Setting up auth state change listener...');
|
|
156
|
-
const authStateChange = supabaseClient.auth.onAuthStateChange(
|
|
157
|
-
(event, session) => {
|
|
158
|
-
try {
|
|
159
|
-
DebugLogger.log('AuthProvider', 'Auth state changed:', event, session?.user?.email);
|
|
160
|
-
|
|
161
|
-
// Clear timeout immediately when we get an auth state change
|
|
162
|
-
clearTimeout(timeoutId);
|
|
130
|
+
// Set up auth state change listener
|
|
131
|
+
const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
|
|
132
|
+
(event, session) => {
|
|
133
|
+
if (!isMounted) return;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
DebugLogger.log('AuthProvider', 'Auth state changed:', event, session?.user?.email);
|
|
137
|
+
|
|
138
|
+
// Handle different auth events
|
|
139
|
+
if (event === 'SIGNED_OUT') {
|
|
140
|
+
DebugLogger.log('AuthProvider', 'User signed out, clearing all state');
|
|
141
|
+
setSession(null);
|
|
142
|
+
setUser(null);
|
|
143
|
+
setAuthError(null);
|
|
144
|
+
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
145
|
+
DebugLogger.log('AuthProvider', 'User signed in or token refreshed');
|
|
146
|
+
setSession(session);
|
|
147
|
+
setUser(session?.user ?? null);
|
|
163
148
|
|
|
164
|
-
//
|
|
165
|
-
if (
|
|
166
|
-
DebugLogger.log('AuthProvider', 'User signed out, clearing all state');
|
|
167
|
-
// Clear all state immediately and aggressively
|
|
168
|
-
setSession(null);
|
|
169
|
-
setUser(null);
|
|
170
|
-
setAuthLoading(false);
|
|
149
|
+
// Only clear auth error if we have a valid session
|
|
150
|
+
if (session) {
|
|
171
151
|
setAuthError(null);
|
|
172
|
-
}
|
|
173
|
-
|
|
152
|
+
}
|
|
153
|
+
} else if (event === 'INITIAL_SESSION') {
|
|
154
|
+
DebugLogger.log('AuthProvider', 'Initial session event');
|
|
155
|
+
if (session) {
|
|
174
156
|
setSession(session);
|
|
175
|
-
setUser(session
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Only clear auth error if we have a valid session
|
|
179
|
-
if (session) {
|
|
180
|
-
setAuthError(null);
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
// Handle other events (INITIAL_SESSION, etc.)
|
|
184
|
-
if (session) {
|
|
185
|
-
setSession(session);
|
|
186
|
-
setUser(session.user ?? null);
|
|
187
|
-
setAuthLoading(false);
|
|
188
|
-
if (session) {
|
|
189
|
-
setAuthError(null);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
157
|
+
setUser(session.user ?? null);
|
|
158
|
+
setAuthError(null);
|
|
192
159
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Always set loading to false after any auth state change
|
|
163
|
+
setAuthLoading(false);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn('[AuthProvider] Error in auth state change handler:', error);
|
|
166
|
+
if (isMounted) {
|
|
196
167
|
setAuthLoading(false);
|
|
197
168
|
}
|
|
198
169
|
}
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const subscription = authStateChange?.data?.subscription || authStateChange;
|
|
202
|
-
|
|
203
|
-
return () => {
|
|
204
|
-
clearTimeout(timeoutId);
|
|
205
|
-
if (subscription && typeof subscription.unsubscribe === 'function') {
|
|
206
|
-
DebugLogger.log('AuthProvider', 'Cleaning up auth state listener');
|
|
207
|
-
subscription.unsubscribe();
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.error('AuthProvider: Error setting up auth state change listener:', error);
|
|
212
|
-
|
|
213
|
-
// Don't set authError for connection issues or auth session missing errors
|
|
214
|
-
if (error instanceof Error &&
|
|
215
|
-
!error.message.includes('No API key found') &&
|
|
216
|
-
!error.message.includes('AuthSessionMissingError') &&
|
|
217
|
-
!error.message.includes('Auth session missing')) {
|
|
218
|
-
setAuthError(error as AuthError);
|
|
219
170
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Initialize auth
|
|
174
|
+
initializeAuth();
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
isMounted = false;
|
|
178
|
+
subscription?.unsubscribe();
|
|
179
|
+
};
|
|
224
180
|
}, [supabaseClient]);
|
|
225
181
|
|
|
226
182
|
// Auth methods
|
|
227
183
|
const signIn = useCallback(async (email: string, password?: string) => {
|
|
228
184
|
if (!supabaseClient) {
|
|
229
|
-
|
|
230
|
-
setAuthError(error);
|
|
231
|
-
return { error };
|
|
185
|
+
return { error: new AuthError('Supabase client not available') };
|
|
232
186
|
}
|
|
233
187
|
|
|
234
|
-
setAuthLoading(true);
|
|
235
|
-
setAuthError(null);
|
|
236
|
-
|
|
237
188
|
try {
|
|
238
|
-
const { error } = await supabaseClient.auth.signInWithPassword({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
189
|
+
const { error } = await supabaseClient.auth.signInWithPassword({
|
|
190
|
+
email,
|
|
191
|
+
password: password || '',
|
|
192
|
+
});
|
|
242
193
|
return { error };
|
|
243
|
-
} catch (
|
|
244
|
-
|
|
245
|
-
setAuthError(authError);
|
|
246
|
-
return { error: authError };
|
|
247
|
-
} finally {
|
|
248
|
-
setAuthLoading(false);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return { error: error as AuthError };
|
|
249
196
|
}
|
|
250
197
|
}, [supabaseClient]);
|
|
251
198
|
|
|
252
199
|
const signUp = useCallback(async (email: string, password: string) => {
|
|
253
200
|
if (!supabaseClient) {
|
|
254
|
-
|
|
255
|
-
setAuthError(error);
|
|
256
|
-
return { error };
|
|
201
|
+
return { error: new AuthError('Supabase client not available') };
|
|
257
202
|
}
|
|
258
203
|
|
|
259
|
-
setAuthError(null);
|
|
260
204
|
try {
|
|
261
|
-
const { error } = await supabaseClient.auth.signUp({
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
205
|
+
const { error } = await supabaseClient.auth.signUp({
|
|
206
|
+
email,
|
|
207
|
+
password,
|
|
208
|
+
});
|
|
265
209
|
return { error };
|
|
266
|
-
} catch (
|
|
267
|
-
|
|
268
|
-
setAuthError(authError);
|
|
269
|
-
return { error: authError };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { error: error as AuthError };
|
|
270
212
|
}
|
|
271
213
|
}, [supabaseClient]);
|
|
272
214
|
|
|
273
215
|
const signOut = useCallback(async () => {
|
|
274
|
-
DebugLogger.log('AuthProvider', 'signOut called');
|
|
275
|
-
|
|
276
|
-
// Immediately set loading to false to prevent timeout warnings
|
|
277
|
-
setAuthLoading(false);
|
|
278
|
-
|
|
279
216
|
if (!supabaseClient) {
|
|
280
|
-
|
|
281
|
-
setUser(null);
|
|
282
|
-
setSession(null);
|
|
283
|
-
setAuthError(null);
|
|
284
|
-
return { error: null };
|
|
217
|
+
return { error: new AuthError('Supabase client not available') };
|
|
285
218
|
}
|
|
286
|
-
|
|
287
|
-
// Clear state immediately before calling Supabase signOut
|
|
288
|
-
DebugLogger.log('AuthProvider', 'Pre-clearing state before signOut call');
|
|
289
|
-
setUser(null);
|
|
290
|
-
setSession(null);
|
|
291
|
-
setAuthError(null);
|
|
292
|
-
|
|
219
|
+
|
|
293
220
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
299
|
-
setTimeout(() => reject(new Error('SignOut timeout')), 3000)
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
const { error } = await Promise.race([signOutPromise, timeoutPromise]) as any;
|
|
303
|
-
|
|
304
|
-
if (error && !error.message?.includes('SignOut timeout')) {
|
|
305
|
-
console.error('[AuthProvider] signOut error:', error);
|
|
306
|
-
// Don't set auth error for logout - we've already cleared state
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
DebugLogger.log('AuthProvider', 'signOut process completed');
|
|
310
|
-
return { error: null }; // Always return success for logout
|
|
311
|
-
} catch (err) {
|
|
312
|
-
console.warn('[AuthProvider] signOut exception (ignored):', err);
|
|
313
|
-
// Ignore errors during logout - state is already cleared
|
|
314
|
-
return { error: null };
|
|
221
|
+
const { error } = await supabaseClient.auth.signOut();
|
|
222
|
+
return { error };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return { error: error as AuthError };
|
|
315
225
|
}
|
|
316
226
|
}, [supabaseClient]);
|
|
317
227
|
|
|
318
228
|
const resetPassword = useCallback(async (email: string) => {
|
|
319
229
|
if (!supabaseClient) {
|
|
320
|
-
|
|
321
|
-
setAuthError(error);
|
|
322
|
-
return { error };
|
|
230
|
+
return { error: new AuthError('Supabase client not available') };
|
|
323
231
|
}
|
|
324
232
|
|
|
325
|
-
setAuthError(null);
|
|
326
233
|
try {
|
|
327
234
|
const { error } = await supabaseClient.auth.resetPasswordForEmail(email);
|
|
328
|
-
if (error) {
|
|
329
|
-
setAuthError(error);
|
|
330
|
-
}
|
|
331
235
|
return { error };
|
|
332
|
-
} catch (
|
|
333
|
-
|
|
334
|
-
setAuthError(authError);
|
|
335
|
-
return { error: authError };
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return { error: error as AuthError };
|
|
336
238
|
}
|
|
337
239
|
}, [supabaseClient]);
|
|
338
240
|
|
|
339
241
|
const updatePassword = useCallback(async (password: string) => {
|
|
340
|
-
if (!supabaseClient)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
242
|
+
if (!supabaseClient) {
|
|
243
|
+
return { error: new AuthError('Supabase client not available') };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const { error } = await supabaseClient.auth.updateUser({
|
|
248
|
+
password,
|
|
249
|
+
});
|
|
250
|
+
return { error };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return { error: error as AuthError };
|
|
344
253
|
}
|
|
345
|
-
return { error };
|
|
346
254
|
}, [supabaseClient]);
|
|
347
255
|
|
|
348
256
|
const refreshSession = useCallback(async () => {
|
|
349
257
|
if (!supabaseClient) {
|
|
350
|
-
|
|
351
|
-
setAuthError(error);
|
|
352
|
-
return { error };
|
|
258
|
+
return { error: new AuthError('Supabase client not available') };
|
|
353
259
|
}
|
|
354
260
|
|
|
355
|
-
setAuthError(null);
|
|
356
261
|
try {
|
|
357
262
|
const { error } = await supabaseClient.auth.refreshSession();
|
|
358
|
-
if (error) {
|
|
359
|
-
setAuthError(error);
|
|
360
|
-
}
|
|
361
263
|
return { error };
|
|
362
|
-
} catch (
|
|
363
|
-
|
|
364
|
-
setAuthError(authError);
|
|
365
|
-
return { error: authError };
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return { error: error as AuthError };
|
|
366
266
|
}
|
|
367
267
|
}, [supabaseClient]);
|
|
368
268
|
|
|
369
|
-
// Memoized derived values
|
|
370
|
-
const isAuthenticated = !!user;
|
|
371
|
-
|
|
372
269
|
// Memoized context value
|
|
373
|
-
const contextValue = useMemo
|
|
270
|
+
const contextValue = useMemo(() => ({
|
|
374
271
|
user,
|
|
375
272
|
session,
|
|
376
|
-
isAuthenticated,
|
|
273
|
+
isAuthenticated: !!user && !!session,
|
|
377
274
|
authLoading,
|
|
378
275
|
authError,
|
|
379
276
|
error: authError, // Alias for backward compatibility
|
|
380
|
-
supabase: supabaseClient || null,
|
|
381
|
-
|
|
277
|
+
supabase: supabaseClient || null, // Ensure null instead of undefined
|
|
382
278
|
signIn,
|
|
383
279
|
signUp,
|
|
384
280
|
signOut,
|
|
385
281
|
resetPassword,
|
|
386
282
|
updatePassword,
|
|
387
283
|
refreshSession,
|
|
388
|
-
}), [
|
|
389
|
-
user, session, isAuthenticated, authLoading, authError, supabaseClient,
|
|
390
|
-
signIn, signUp, signOut, resetPassword, updatePassword, refreshSession,
|
|
391
|
-
]);
|
|
284
|
+
}), [user, session, authLoading, authError, supabaseClient, signIn, signUp, signOut, resetPassword, updatePassword, refreshSession]);
|
|
392
285
|
|
|
393
286
|
return (
|
|
394
287
|
<AuthContext.Provider value={contextValue}>
|
|
@@ -396,3 +289,11 @@ export function AuthProvider({ children, supabaseClient }: AuthProviderProps) {
|
|
|
396
289
|
</AuthContext.Provider>
|
|
397
290
|
);
|
|
398
291
|
}
|
|
292
|
+
|
|
293
|
+
export const useAuth = (): AuthContextType => {
|
|
294
|
+
const context = useContext(AuthContext);
|
|
295
|
+
if (!context) {
|
|
296
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
297
|
+
}
|
|
298
|
+
return context;
|
|
299
|
+
};
|
|
@@ -166,8 +166,15 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
|
|
|
166
166
|
setError(null);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
|
|
170
|
+
// FIXED: Additional check to prevent loading during auth state changes
|
|
171
|
+
if (isLoading) {
|
|
172
|
+
DebugLogger.log("OrganisationProvider", "Already loading, skipping duplicate load");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setIsLoading(true);
|
|
177
|
+
setError(null);
|
|
171
178
|
|
|
172
179
|
try {
|
|
173
180
|
DebugLogger.log("OrganisationProvider", "Loading organisations for user:", user.id);
|
|
@@ -214,15 +221,42 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
|
|
|
214
221
|
throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
// FIXED: Debug log to identify any problematic membership data
|
|
225
|
+
memberships.forEach((membership: any, index: number) => {
|
|
226
|
+
if (!membership.organisation_id || membership.organisation_id.trim() === '') {
|
|
227
|
+
console.warn(`[OrganisationProvider] Membership ${index} has invalid organisation_id:`, membership);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
217
231
|
// Get organisation details for the memberships
|
|
218
232
|
const organisationIds = memberships
|
|
219
233
|
.map((m: any) => m.organisation_id)
|
|
220
|
-
.filter((id: string) =>
|
|
234
|
+
.filter((id: string) => {
|
|
235
|
+
// FIXED: Better validation to prevent empty string UUID errors
|
|
236
|
+
if (!id || typeof id !== 'string') {
|
|
237
|
+
console.warn("[OrganisationProvider] Invalid organisation ID (not string):", id);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const trimmedId = id.trim();
|
|
241
|
+
if (trimmedId === '') {
|
|
242
|
+
console.warn("[OrganisationProvider] Empty organisation ID found");
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
// Validate UUID format
|
|
246
|
+
const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
|
|
247
|
+
if (!isValidUuid) {
|
|
248
|
+
console.warn("[OrganisationProvider] Invalid UUID format:", trimmedId);
|
|
249
|
+
}
|
|
250
|
+
return isValidUuid;
|
|
251
|
+
});
|
|
221
252
|
|
|
222
253
|
if (organisationIds.length === 0) {
|
|
254
|
+
console.warn("[OrganisationProvider] No valid organisation IDs found in memberships:", memberships);
|
|
223
255
|
throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
|
|
224
256
|
}
|
|
225
257
|
|
|
258
|
+
DebugLogger.log("OrganisationProvider", "Valid organisation IDs:", organisationIds);
|
|
259
|
+
|
|
226
260
|
const { data: organisations, error: orgError } = await supabase
|
|
227
261
|
.from('organisations')
|
|
228
262
|
.select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at')
|
|
@@ -264,14 +298,25 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
|
|
|
264
298
|
const persistedOrgString = localStorage.getItem(STORAGE_KEYS.SELECTED_ORGANISATION);
|
|
265
299
|
if (persistedOrgString) {
|
|
266
300
|
const persistedOrg = JSON.parse(persistedOrgString) as Organisation;
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
301
|
+
// FIXED: Validate persisted org ID before using it
|
|
302
|
+
if (persistedOrg.id && typeof persistedOrg.id === 'string' && persistedOrg.id.trim() !== '') {
|
|
303
|
+
const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
|
|
304
|
+
if (validPersistedOrg) {
|
|
305
|
+
initialOrg = validPersistedOrg;
|
|
306
|
+
DebugLogger.log("OrganisationProvider", "Restored persisted organisation:", initialOrg.display_name);
|
|
307
|
+
} else {
|
|
308
|
+
console.warn("[OrganisationProvider] Persisted organisation not found in active orgs, clearing cache");
|
|
309
|
+
localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.warn("[OrganisationProvider] Invalid persisted organisation ID, clearing cache");
|
|
313
|
+
localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
|
|
271
314
|
}
|
|
272
315
|
}
|
|
273
316
|
} catch (storageError) {
|
|
274
317
|
console.warn("[OrganisationProvider] Failed to restore persisted organisation:", storageError);
|
|
318
|
+
// Clear potentially corrupted cache
|
|
319
|
+
localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
|
|
275
320
|
}
|
|
276
321
|
|
|
277
322
|
// 2. Fall back to org_admin role organisation (highest privilege)
|
|
@@ -320,10 +365,27 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
|
|
|
320
365
|
}
|
|
321
366
|
}, [user, session, supabase]);
|
|
322
367
|
|
|
323
|
-
// Load organisations when
|
|
368
|
+
// FIXED: Load organisations only when authentication is complete and stable
|
|
324
369
|
useEffect(() => {
|
|
325
|
-
|
|
326
|
-
|
|
370
|
+
// Only load organizations if we have a valid user and session
|
|
371
|
+
// and we're not in the middle of authentication loading
|
|
372
|
+
if (user && session && supabase && !isLoading) {
|
|
373
|
+
DebugLogger.log("OrganisationProvider", "Authentication stable, loading organizations...");
|
|
374
|
+
loadUserOrganisations();
|
|
375
|
+
} else if (!user && !session) {
|
|
376
|
+
// Clear state if no authentication
|
|
377
|
+
DebugLogger.log("OrganisationProvider", "No authentication, clearing organization state");
|
|
378
|
+
setSelectedOrganisation(null);
|
|
379
|
+
setOrganisations([]);
|
|
380
|
+
setUserMemberships([]);
|
|
381
|
+
setRoleMapState(new Map());
|
|
382
|
+
setIsLoading(false);
|
|
383
|
+
setError(null);
|
|
384
|
+
// FIXED: Clear localStorage when no authentication to prevent stale data
|
|
385
|
+
localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
|
|
386
|
+
localStorage.removeItem(STORAGE_KEYS.ORGANISATION_CONTEXT);
|
|
387
|
+
}
|
|
388
|
+
}, [user, session, supabase, isLoading, loadUserOrganisations]);
|
|
327
389
|
|
|
328
390
|
// Handle logout and redirect to login
|
|
329
391
|
const handleLogoutAndRedirect = useCallback(async () => {
|