Refactor: Simplify auth and profile handling

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 17:22:56 +00:00
parent be26c08640
commit 149c0704fe
9 changed files with 78 additions and 122 deletions

View File

@@ -5,16 +5,13 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { User, Settings, LogOut } from 'lucide-react'; import { User, Settings, LogOut } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { AuthModal } from './AuthModal'; import { AuthModal } from './AuthModal';
export function AuthButtons() { export function AuthButtons() {
const { const { user, loading: authLoading, signOut } = useAuth();
user, const { data: profile, isLoading: profileLoading } = useProfile(user?.id);
profile,
loading,
signOut
} = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
toast toast
@@ -43,7 +40,7 @@ export function AuthButtons() {
}; };
// Show loading skeleton only during initial auth check // Show loading skeleton only during initial auth check
if (loading) { if (authLoading) {
return ( return (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div className="h-8 w-16 bg-muted animate-pulse rounded" /> <div className="h-8 w-16 bg-muted animate-pulse rounded" />
@@ -83,17 +80,6 @@ export function AuthButtons() {
</> </>
); );
} }
// Debug logging for avatar rendering
console.log('[AuthButtons] Component render:', {
hasUser: !!user,
hasProfile: !!profile,
hasAvatarUrl: !!profile?.avatar_url,
avatarUrl: profile?.avatar_url,
displayName: profile?.display_name,
username: profile?.username,
loading
});
return <DropdownMenu> return <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full"> <Button variant="ghost" className="relative h-8 w-8 rounded-full">

View File

@@ -13,6 +13,7 @@ import { Separator } from '@/components/ui/separator';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { User, Upload, Trash2, Mail, AlertCircle, X } from 'lucide-react'; import { User, Upload, Trash2, Mail, AlertCircle, X } from 'lucide-react';
import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { PhotoUpload } from '@/components/upload/PhotoUpload';
@@ -36,7 +37,8 @@ const profileSchema = z.object({
type ProfileFormData = z.infer<typeof profileSchema>; type ProfileFormData = z.infer<typeof profileSchema>;
export function AccountProfileTab() { export function AccountProfileTab() {
const { user, profile, refreshProfile, pendingEmail, clearPendingEmail } = useAuth(); const { user, pendingEmail, clearPendingEmail } = useAuth();
const { data: profile, refreshProfile } = useProfile(user?.id);
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [avatarLoading, setAvatarLoading] = useState(false); const [avatarLoading, setAvatarLoading] = useState(false);

View File

@@ -6,13 +6,12 @@ import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { Download, BarChart3, Upload, FileText, History } from 'lucide-react'; import { Download, BarChart3, Upload, FileText, History } from 'lucide-react';
export function DataExportTab() { export function DataExportTab() {
const { const { user } = useAuth();
user, const { data: profile } = useProfile(user?.id);
profile
} = useAuth();
const { const {
toast toast
} = useToast(); } = useToast();

View File

@@ -11,6 +11,7 @@ import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { useUnitPreferences } from '@/hooks/useUnitPreferences';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { MapPin, Calendar, Globe, Accessibility, Ruler } from 'lucide-react'; import { MapPin, Calendar, Globe, Accessibility, Ruler } from 'lucide-react';
@@ -28,14 +29,9 @@ interface AccessibilityOptions {
reduced_motion: boolean; reduced_motion: boolean;
} }
export function LocationTab() { export function LocationTab() {
const { const { user } = useAuth();
user, const { data: profile, refreshProfile } = useProfile(user?.id);
profile, const { toast } = useToast();
refreshProfile
} = useAuth();
const {
toast
} = useToast();
const { preferences: unitPreferences, updatePreferences: updateUnitPreferences } = useUnitPreferences(); const { preferences: unitPreferences, updatePreferences: updateUnitPreferences } = useUnitPreferences();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [parks, setParks] = useState<any[]>([]); const [parks, setParks] = useState<any[]>([]);

View File

@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { Eye, UserX, Shield, Search } from 'lucide-react'; import { Eye, UserX, Shield, Search } from 'lucide-react';
import { BlockedUsers } from '@/components/privacy/BlockedUsers'; import { BlockedUsers } from '@/components/privacy/BlockedUsers';
@@ -26,14 +27,9 @@ interface ProfilePrivacy {
show_pronouns: boolean; show_pronouns: boolean;
} }
export function PrivacyTab() { export function PrivacyTab() {
const { const { user } = useAuth();
user, const { data: profile, refreshProfile } = useProfile(user?.id);
profile, const { toast } = useToast();
refreshProfile
} = useAuth();
const {
toast
} = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [preferences, setPreferences] = useState<PrivacySettings | null>(null); const [preferences, setPreferences] = useState<PrivacySettings | null>(null);
const form = useForm<ProfilePrivacy & PrivacySettings>({ const form = useForm<ProfilePrivacy & PrivacySettings>({

View File

@@ -8,12 +8,10 @@ import { authLog, authWarn, authError } from '@/lib/authLogger';
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
session: Session | null; session: Session | null;
profile: Profile | null;
loading: boolean; loading: boolean;
pendingEmail: string | null; pendingEmail: string | null;
sessionError: string | null; sessionError: string | null;
signOut: () => Promise<void>; signOut: () => Promise<void>;
refreshProfile: () => Promise<void>;
verifySession: () => Promise<boolean>; verifySession: () => Promise<boolean>;
clearPendingEmail: () => void; clearPendingEmail: () => void;
} }
@@ -23,61 +21,16 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
function AuthProviderComponent({ children }: { children: React.ReactNode }) { function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [pendingEmail, setPendingEmail] = useState<string | null>(null); const [pendingEmail, setPendingEmail] = useState<string | null>(null);
const [sessionError, setSessionError] = useState<string | null>(null); const [sessionError, setSessionError] = useState<string | null>(null);
// Refs for lifecycle and cleanup management // 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 novuUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const fetchedUserIdRef = useRef<string | null>(null);
const previousEmailRef = useRef<string | null>(null); const previousEmailRef = useRef<string | null>(null);
const fetchProfile = async (userId: string) => {
console.log('[Auth] 📡 fetchProfile called for userId:', userId);
try {
const { data, error } = await supabase
.from('profiles')
.select(`*, location:locations(*)`)
.eq('user_id', userId)
.maybeSingle();
console.log('[Auth] 📦 Profile fetch result:', {
hasData: !!data,
hasError: !!error,
avatar_url: data?.avatar_url
});
if (error && error.code !== 'PGRST116') {
authError('[Auth] ❌ Error fetching profile:', error);
return;
}
if (isMountedRef.current && data) {
console.log('[Auth] ✅ Setting profile state with data:', {
userId: data.user_id,
username: data.username,
avatar_url: data.avatar_url,
});
setProfile(data as Profile);
} else {
console.log('[Auth] ⚠️ NOT setting profile - mounted:', isMountedRef.current, 'hasData:', !!data);
}
} catch (error) {
authError('[Auth] ❌ Exception in fetchProfile:', error);
}
};
const refreshProfile = async () => {
if (user) {
await fetchProfile(user.id);
}
};
// Verify session is still valid - simplified // Verify session is still valid - simplified
const verifySession = async (updateLoadingState = false) => { const verifySession = async () => {
try { try {
const { data: { session }, error } = await supabase.auth.getSession(); const { data: { session }, error } = await supabase.auth.getSession();
@@ -95,7 +48,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
authLog('[Auth] Session verified:', session.user.email); authLog('[Auth] Session verified:', session.user.email);
// Update state if session was found but not set // Update state if session was found but not set
if (!user && isMountedRef.current) { if (!user) {
setSession(session); setSession(session);
setUser(session.user); setUser(session.user);
} }
@@ -142,17 +95,13 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
authLog('[Auth] INITIAL_SESSION - no user'); authLog('[Auth] INITIAL_SESSION - no user');
setSession(null); setSession(null);
setUser(null); setUser(null);
setProfile(null);
setLoading(false); setLoading(false);
return; return;
} }
} else if (event === 'SIGNED_OUT') { } else if (event === 'SIGNED_OUT') {
authLog('[Auth] SIGNED_OUT - clearing state'); authLog('[Auth] SIGNED_OUT - clearing state');
console.log('[Auth] 🔴 SIGNED_OUT - clearing profile and fetch tracking');
fetchedUserIdRef.current = null;
setSession(null); setSession(null);
setUser(null); setUser(null);
setProfile(null);
setLoading(false); setLoading(false);
return; return;
} else { } else {
@@ -222,24 +171,6 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
if (currentEmail) { if (currentEmail) {
previousEmailRef.current = currentEmail; previousEmailRef.current = currentEmail;
} }
// Handle profile fetching for authenticated users (async, doesn't block loading)
if (session?.user) {
// Only fetch if we haven't fetched for this user yet
if (fetchedUserIdRef.current !== session.user.id) {
console.log('[Auth] 🔄 Fetching profile for user:', session.user.id);
fetchedUserIdRef.current = session.user.id;
if (profileFetchTimeoutRef.current) {
clearTimeout(profileFetchTimeoutRef.current);
profileFetchTimeoutRef.current = null;
}
fetchProfile(session.user.id);
} else {
console.log('[Auth] ✅ Profile already fetched for user:', session.user.id);
}
}
}); });
// THEN get initial session (this may trigger INITIAL_SESSION event) // THEN get initial session (this may trigger INITIAL_SESSION event)
@@ -258,15 +189,9 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
return () => { return () => {
authLog('[Auth] Cleaning up auth provider'); authLog('[Auth] Cleaning up auth provider');
isMountedRef.current = false;
fetchedUserIdRef.current = null;
subscription.unsubscribe(); subscription.unsubscribe();
// Clear any pending timeouts // Clear any pending timeouts
if (profileFetchTimeoutRef.current) {
clearTimeout(profileFetchTimeoutRef.current);
profileFetchTimeoutRef.current = null;
}
if (novuUpdateTimeoutRef.current) { if (novuUpdateTimeoutRef.current) {
clearTimeout(novuUpdateTimeoutRef.current); clearTimeout(novuUpdateTimeoutRef.current);
novuUpdateTimeoutRef.current = null; novuUpdateTimeoutRef.current = null;
@@ -289,12 +214,10 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const value = { const value = {
user, user,
session, session,
profile,
loading, loading,
pendingEmail, pendingEmail,
sessionError, sessionError,
signOut, signOut,
refreshProfile,
verifySession, verifySession,
clearPendingEmail, clearPendingEmail,
}; };

50
src/hooks/useProfile.tsx Normal file
View File

@@ -0,0 +1,50 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { Profile } from '@/types/database';
export function useProfile(userId: string | undefined) {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: ['profile', userId],
queryFn: async () => {
if (!userId) return null;
console.log('[useProfile] Fetching profile for userId:', userId);
const { data, error } = await supabase
.from('profiles')
.select('*, location:locations(*)')
.eq('user_id', userId)
.maybeSingle();
if (error) {
console.error('[useProfile] Error:', error);
throw error;
}
console.log('[useProfile] Profile loaded:', {
username: data?.username,
avatar_url: data?.avatar_url
});
return data as Profile;
},
enabled: !!userId,
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
retry: 2,
});
const refreshProfile = () => {
if (userId) {
console.log('[useProfile] Invalidating profile cache for userId:', userId);
queryClient.invalidateQueries({ queryKey: ['profile', userId] });
}
};
return {
...query,
refreshProfile,
};
}

View File

@@ -13,6 +13,7 @@ import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { useUsernameValidation } from '@/hooks/useUsernameValidation'; import { useUsernameValidation } from '@/hooks/useUsernameValidation';
import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX, FileText, Image } from 'lucide-react'; import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX, FileText, Image } from 'lucide-react';
import { Profile as ProfileType } from '@/types/database'; import { Profile as ProfileType } from '@/types/database';
@@ -36,9 +37,8 @@ export default function Profile() {
const { const {
toast toast
} = useToast(); } = useToast();
const { const { user: authUser } = useAuth();
refreshProfile const { refreshProfile } = useProfile(authUser?.id);
} = useAuth();
const [profile, setProfile] = useState<ProfileType | null>(null); const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);

View File

@@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Settings, User, Shield, Eye, Bell, MapPin, Download, MonitorSmartphone } from 'lucide-react'; import { Settings, User, Shield, Eye, Bell, MapPin, Download, MonitorSmartphone } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header'; import { Header } from '@/components/layout/Header';
import { AccountProfileTab } from '@/components/settings/AccountProfileTab'; import { AccountProfileTab } from '@/components/settings/AccountProfileTab';
@@ -14,8 +15,11 @@ import { LocationTab } from '@/components/settings/LocationTab';
import { DataExportTab } from '@/components/settings/DataExportTab'; import { DataExportTab } from '@/components/settings/DataExportTab';
export default function UserSettings() { export default function UserSettings() {
const { user, loading } = useAuth(); const { user, loading: authLoading } = useAuth();
const { data: profile, isLoading: profileLoading } = useProfile(user?.id);
const [activeTab, setActiveTab] = useState('profile'); const [activeTab, setActiveTab] = useState('profile');
const loading = authLoading || profileLoading;
if (loading) { if (loading) {
return ( return (