mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 17:11:12 -05:00
Improve error handling and environment configuration across the application
Enhance input validation, update environment variable usage for Supabase and Turnstile, and refine image upload and auth logic for better robustness and developer experience. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cb061c75-702e-4b89-a8d1-77a96cdcdfbb Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/cb061c75-702e-4b89-a8d1-77a96cdcdfbb/ANdRXVZ
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
||||
import type { User, Session } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import type { Profile } from '@/types/database';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
@@ -23,6 +24,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [previousEmail, setPreviousEmail] = useState<string | null>(null);
|
||||
|
||||
// Refs for lifecycle and cleanup management
|
||||
const isMountedRef = useRef(true);
|
||||
const profileFetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const novuUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchProfile = async (userId: string) => {
|
||||
try {
|
||||
@@ -34,12 +40,33 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
console.error('Error fetching profile:', error);
|
||||
|
||||
// Show user-friendly error notification
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "Profile Loading Error",
|
||||
description: "Unable to load your profile. Please refresh the page or try again later.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setProfile(data as Profile);
|
||||
// Only update state if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
setProfile(data as Profile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error);
|
||||
|
||||
// Show user-friendly error notification
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "Profile Loading Error",
|
||||
description: "An unexpected error occurred while loading your profile.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,6 +79,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
// Get initial session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
@@ -64,6 +93,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const currentEmail = session?.user?.email;
|
||||
const newEmailPending = session?.user?.new_email;
|
||||
|
||||
@@ -81,8 +112,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
currentEmail !== previousEmail &&
|
||||
!newEmailPending
|
||||
) {
|
||||
// Clear any existing Novu update timeout
|
||||
if (novuUpdateTimeoutRef.current) {
|
||||
clearTimeout(novuUpdateTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Defer Novu update and notifications to avoid blocking auth
|
||||
setTimeout(async () => {
|
||||
novuUpdateTimeoutRef.current = setTimeout(async () => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
try {
|
||||
// Update Novu subscriber with confirmed email
|
||||
const { notificationService } = await import('@/lib/notificationService');
|
||||
@@ -119,6 +157,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating Novu after email confirmation:', error);
|
||||
} finally {
|
||||
novuUpdateTimeoutRef.current = null;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -129,18 +169,40 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
if (session?.user) {
|
||||
// Clear any existing profile fetch timeout
|
||||
if (profileFetchTimeoutRef.current) {
|
||||
clearTimeout(profileFetchTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Defer profile fetch to avoid deadlock
|
||||
setTimeout(() => {
|
||||
profileFetchTimeoutRef.current = setTimeout(() => {
|
||||
if (!isMountedRef.current) return;
|
||||
fetchProfile(session.user.id);
|
||||
profileFetchTimeoutRef.current = null;
|
||||
}, 0);
|
||||
} else {
|
||||
setProfile(null);
|
||||
if (isMountedRef.current) {
|
||||
setProfile(null);
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
subscription.unsubscribe();
|
||||
|
||||
// Clear any pending timeouts
|
||||
if (profileFetchTimeoutRef.current) {
|
||||
clearTimeout(profileFetchTimeoutRef.current);
|
||||
profileFetchTimeoutRef.current = null;
|
||||
}
|
||||
if (novuUpdateTimeoutRef.current) {
|
||||
clearTimeout(novuUpdateTimeoutRef.current);
|
||||
novuUpdateTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signOut = async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -36,9 +36,17 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
const [currentVersion, setCurrentVersion] = useState<EntityVersion | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fieldHistory, setFieldHistory] = useState<FieldChange[]>([]);
|
||||
|
||||
// Track if component is mounted to prevent state updates after unmount
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// Track the current channel to prevent duplicate subscriptions
|
||||
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
|
||||
|
||||
const fetchVersions = async () => {
|
||||
try {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const { data, error } = await supabase
|
||||
@@ -62,13 +70,20 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
changer_profile: profiles?.find(p => p.user_id === v.changed_by)
|
||||
})) as EntityVersion[];
|
||||
|
||||
setVersions(versionsWithProfiles || []);
|
||||
setCurrentVersion(versionsWithProfiles?.find(v => v.is_current) || null);
|
||||
// Only update state if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
setVersions(versionsWithProfiles || []);
|
||||
setCurrentVersion(versionsWithProfiles?.find(v => v.is_current) || null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching versions:', error);
|
||||
toast.error('Failed to load version history');
|
||||
if (isMountedRef.current) {
|
||||
toast.error('Failed to load version history');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,10 +97,14 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setFieldHistory(data as FieldChange[] || []);
|
||||
if (isMountedRef.current) {
|
||||
setFieldHistory(data as FieldChange[] || []);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching field history:', error);
|
||||
toast.error('Failed to load field history');
|
||||
if (isMountedRef.current) {
|
||||
toast.error('Failed to load field history');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,7 +120,9 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Error comparing versions:', error);
|
||||
toast.error('Failed to compare versions');
|
||||
if (isMountedRef.current) {
|
||||
toast.error('Failed to compare versions');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -121,12 +142,16 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Successfully rolled back to previous version');
|
||||
await fetchVersions();
|
||||
if (isMountedRef.current) {
|
||||
toast.success('Successfully rolled back to previous version');
|
||||
await fetchVersions();
|
||||
}
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Error rolling back version:', error);
|
||||
toast.error('Failed to rollback version');
|
||||
if (isMountedRef.current) {
|
||||
toast.error('Failed to rollback version');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -147,11 +172,15 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
await fetchVersions();
|
||||
if (isMountedRef.current) {
|
||||
await fetchVersions();
|
||||
}
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Error creating version:', error);
|
||||
toast.error('Failed to create version');
|
||||
if (isMountedRef.current) {
|
||||
toast.error('Failed to create version');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -164,6 +193,15 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
|
||||
// Set up realtime subscription for version changes
|
||||
useEffect(() => {
|
||||
if (!entityType || !entityId) return;
|
||||
|
||||
// Clean up existing channel if any
|
||||
if (channelRef.current) {
|
||||
supabase.removeChannel(channelRef.current);
|
||||
channelRef.current = null;
|
||||
}
|
||||
|
||||
// Create new channel
|
||||
const channel = supabase
|
||||
.channel('entity_versions_changes')
|
||||
.on(
|
||||
@@ -175,16 +213,35 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
filter: `entity_type=eq.${entityType},entity_id=eq.${entityId}`
|
||||
},
|
||||
() => {
|
||||
fetchVersions();
|
||||
if (isMountedRef.current) {
|
||||
fetchVersions();
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
channelRef.current = channel;
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
// Ensure cleanup happens in all scenarios
|
||||
if (channelRef.current) {
|
||||
supabase.removeChannel(channelRef.current).catch((error) => {
|
||||
console.error('Error removing channel:', error);
|
||||
});
|
||||
channelRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [entityType, entityId]);
|
||||
|
||||
// Set mounted ref on mount and cleanup on unmount
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
versions,
|
||||
currentVersion,
|
||||
|
||||
Reference in New Issue
Block a user