Fix: Implement complete fix

This commit is contained in:
gpt-engineer-app[bot]
2025-10-11 16:12:45 +00:00
parent 120f68c926
commit 0330fbd1f3
5 changed files with 380 additions and 35 deletions

View File

@@ -0,0 +1,105 @@
import { useEffect, useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { authStorage } from '@/lib/authStorage';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
export function AuthDiagnostics() {
const [diagnostics, setDiagnostics] = useState<any>(null);
const [isOpen, setIsOpen] = useState(false);
const runDiagnostics = async () => {
const storageStatus = authStorage.getStorageStatus();
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
const results = {
timestamp: new Date().toISOString(),
storage: storageStatus,
session: {
exists: !!session,
user: session?.user?.email || null,
expiresAt: session?.expires_at || null,
error: sessionError?.message || null,
},
network: {
online: navigator.onLine,
},
environment: {
url: window.location.href,
isIframe: window.self !== window.top,
cookiesEnabled: navigator.cookieEnabled,
}
};
setDiagnostics(results);
console.log('[Auth Diagnostics]', results);
};
useEffect(() => {
// Run diagnostics on mount if there's a session issue
const checkSession = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
await runDiagnostics();
setIsOpen(true);
}
};
// Only run if not already authenticated
const timer = setTimeout(checkSession, 3000);
return () => clearTimeout(timer);
}, []);
if (!isOpen || !diagnostics) return null;
return (
<Card className="fixed bottom-4 right-4 w-96 z-50 shadow-lg">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Authentication Diagnostics</span>
<Button variant="ghost" size="sm" onClick={() => setIsOpen(false)}></Button>
</CardTitle>
<CardDescription>Debug information for session issues</CardDescription>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div>
<strong>Storage Type:</strong>{' '}
<Badge variant={diagnostics.storage.persistent ? 'default' : 'destructive'}>
{diagnostics.storage.type}
</Badge>
</div>
<div>
<strong>Session Status:</strong>{' '}
<Badge variant={diagnostics.session.exists ? 'default' : 'destructive'}>
{diagnostics.session.exists ? 'Active' : 'None'}
</Badge>
</div>
{diagnostics.session.user && (
<div className="text-xs text-muted-foreground">
User: {diagnostics.session.user}
</div>
)}
{diagnostics.session.error && (
<div className="text-xs text-destructive">
Error: {diagnostics.session.error}
</div>
)}
<div>
<strong>Cookies Enabled:</strong>{' '}
<Badge variant={diagnostics.environment.cookiesEnabled ? 'default' : 'destructive'}>
{diagnostics.environment.cookiesEnabled ? 'Yes' : 'No'}
</Badge>
</div>
{diagnostics.environment.isIframe && (
<div className="text-xs text-yellow-600 dark:text-yellow-400">
Running in iframe - storage may be restricted
</div>
)}
<Button onClick={runDiagnostics} variant="outline" size="sm" className="w-full mt-2">
Refresh Diagnostics
</Button>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,26 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { authStorage } from "@/lib/authStorage";
import { useEffect, useState } from "react";
export function StorageWarning() {
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
const status = authStorage.getStorageStatus();
setShowWarning(!status.persistent);
}, []);
if (!showWarning) return null;
return (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Storage Restricted</AlertTitle>
<AlertDescription>
Your browser is blocking session storage. You'll need to sign in again if you reload the page.
To fix this, please enable cookies and local storage for this site.
</AlertDescription>
</Alert>
);
}

View File

@@ -10,8 +10,10 @@ interface AuthContextType {
profile: Profile | null;
loading: boolean;
pendingEmail: string | null;
sessionError: string | null;
signOut: () => Promise<void>;
refreshProfile: () => Promise<void>;
verifySession: () => Promise<boolean>;
clearPendingEmail: () => void;
}
@@ -23,14 +25,17 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
const [sessionError, setSessionError] = useState<string | null>(null);
// Refs for lifecycle and cleanup management
const isMountedRef = useRef(true);
const profileFetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const profileRetryCountRef = useRef(0);
const sessionVerifiedRef = useRef(false);
const novuUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const previousEmailRef = useRef<string | null>(null);
const fetchProfile = async (userId: string) => {
const fetchProfile = async (userId: string, retryCount = 0) => {
try {
const { data, error } = await supabase
.from('profiles')
@@ -39,10 +44,22 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
.maybeSingle();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching profile:', error);
console.error('[Auth] Error fetching profile:', error);
// Show user-friendly error notification
// Retry up to 3 times with exponential backoff
if (retryCount < 3 && isMountedRef.current) {
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
console.log(`[Auth] Retrying profile fetch in ${delay}ms (attempt ${retryCount + 1}/3)`);
setTimeout(() => {
if (isMountedRef.current) {
fetchProfile(userId, retryCount + 1);
}
}, delay);
return;
}
// Show user-friendly error notification only after all retries
if (isMountedRef.current && retryCount >= 3) {
toast({
title: "Profile Loading Error",
description: "Unable to load your profile. Please refresh the page or try again later.",
@@ -55,17 +72,19 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Only update state if component is still mounted
if (isMountedRef.current) {
setProfile(data as Profile);
profileRetryCountRef.current = 0; // Reset retry count on success
}
} catch (error) {
console.error('Error fetching profile:', error);
console.error('[Auth] Error fetching profile:', error);
// Show user-friendly error notification
// Retry logic for network errors
if (retryCount < 3 && isMountedRef.current) {
const delay = Math.pow(2, retryCount) * 1000;
setTimeout(() => {
if (isMountedRef.current) {
toast({
title: "Profile Loading Error",
description: "An unexpected error occurred while loading your profile.",
variant: "destructive",
});
fetchProfile(userId, retryCount + 1);
}
}, delay);
}
}
};
@@ -76,20 +95,41 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
}
};
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
if (!isMountedRef.current) return;
// Verify session is still valid
const verifySession = async () => {
try {
const { data: { session }, error } = await supabase.auth.getSession();
if (error) {
console.error('[Auth] Session verification failed:', error);
setSessionError(error.message);
return false;
}
if (!session) {
console.log('[Auth] No active session found');
return false;
}
console.log('[Auth] Session verified:', session.user.email);
sessionVerifiedRef.current = true;
// Update state if session was found but not set
if (!user && isMountedRef.current) {
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
setUser(session.user);
fetchProfile(session.user.id);
}
setLoading(false);
});
// Listen for auth changes
return true;
} catch (error) {
console.error('[Auth] Session verification error:', error);
return false;
}
};
useEffect(() => {
// CRITICAL: Set up listener FIRST to catch all events
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
@@ -100,12 +140,22 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const currentEmail = session?.user?.email;
const newEmailPending = session?.user?.new_email;
// Clear any error
setSessionError(null);
// Explicitly handle SIGNED_IN event for iframe compatibility
if (event === 'SIGNED_IN' && session) {
console.log('[Auth] SIGNED_IN detected, setting session and user');
setSession(session);
setUser(session.user);
sessionVerifiedRef.current = true;
setLoading(false);
} else if (event === 'SIGNED_OUT') {
console.log('[Auth] SIGNED_OUT detected, clearing session');
setSession(null);
setUser(null);
setProfile(null);
sessionVerifiedRef.current = false;
} else {
setSession(session);
setUser(session?.user ?? null);
@@ -200,9 +250,47 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
setLoading(false);
});
// THEN get initial session
supabase.auth.getSession().then(({ data: { session }, error }) => {
if (!isMountedRef.current) return;
if (error) {
console.error('[Auth] Initial session fetch error:', error);
setSessionError(error.message);
}
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
fetchProfile(session.user.id);
sessionVerifiedRef.current = true;
}
setLoading(false);
});
// Session verification fallback after 2 seconds
const verificationTimeout = setTimeout(() => {
if (!sessionVerifiedRef.current && isMountedRef.current) {
console.log('[Auth] Session not verified, attempting manual verification');
verifySession();
}
}, 2000);
// Handle page visibility changes
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && isMountedRef.current) {
console.log('[Auth] Tab became visible, verifying session');
verifySession();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
isMountedRef.current = false;
subscription.unsubscribe();
clearTimeout(verificationTimeout);
document.removeEventListener('visibilitychange', handleVisibilityChange);
// Clear any pending timeouts
if (profileFetchTimeoutRef.current) {
@@ -234,8 +322,10 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
profile,
loading,
pendingEmail,
sessionError,
signOut,
refreshProfile,
verifySession,
clearPendingEmail,
};

View File

@@ -6,6 +6,7 @@ class AuthStorage {
private storage: Storage | null = null;
private memoryStorage: Map<string, string> = new Map();
private storageType: 'localStorage' | 'sessionStorage' | 'memory' = 'memory';
private sessionRecoveryAttempted = false;
constructor() {
// Try localStorage first
@@ -28,32 +29,113 @@ class AuthStorage {
this.storageType = 'memory';
console.error('[AuthStorage] Both localStorage and sessionStorage blocked, using in-memory storage ⛔');
console.error('[AuthStorage] Sessions will NOT persist across page reloads!');
// Attempt to recover session from URL
this.attemptSessionRecoveryFromURL();
}
}
// Listen for storage events to sync across tabs (when possible)
if (this.storage) {
window.addEventListener('storage', this.handleStorageChange.bind(this));
}
}
private attemptSessionRecoveryFromURL() {
if (this.sessionRecoveryAttempted) return;
this.sessionRecoveryAttempted = true;
try {
const urlParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = urlParams.get('access_token');
const refreshToken = urlParams.get('refresh_token');
if (accessToken && refreshToken) {
console.log('[AuthStorage] Recovering session from URL parameters');
// Store in memory
this.memoryStorage.set('sb-auth-token', JSON.stringify({
access_token: accessToken,
refresh_token: refreshToken,
expires_at: Date.now() + 3600000, // 1 hour
}));
// Clean URL
window.history.replaceState({}, document.title, window.location.pathname);
}
} catch (error) {
console.error('[AuthStorage] Failed to recover session from URL:', error);
}
}
private handleStorageChange(event: StorageEvent) {
// Sync auth state across tabs
if (event.key?.startsWith('sb-') && event.newValue) {
console.log('[AuthStorage] Syncing auth state across tabs');
}
}
getItem(key: string): string | null {
try {
if (this.storage) {
return this.storage.getItem(key);
const value = this.storage.getItem(key);
if (value) {
// Verify it's not expired
if (key.includes('auth-token')) {
try {
const parsed = JSON.parse(value);
if (parsed.expires_at && parsed.expires_at < Date.now()) {
console.warn('[AuthStorage] Token expired, removing');
this.removeItem(key);
return null;
}
} catch {}
}
}
return value;
}
return this.memoryStorage.get(key) || null;
} catch (error) {
console.error('[AuthStorage] Error reading from storage:', error);
return this.memoryStorage.get(key) || null;
}
}
setItem(key: string, value: string): void {
try {
if (this.storage) {
this.storage.setItem(key, value);
} else {
}
// Always keep in memory as backup
this.memoryStorage.set(key, value);
} catch (error) {
console.error('[AuthStorage] Error writing to storage:', error);
// Fallback to memory only
this.memoryStorage.set(key, value);
}
}
removeItem(key: string): void {
try {
if (this.storage) {
this.storage.removeItem(key);
} else {
}
this.memoryStorage.delete(key);
} catch (error) {
console.error('[AuthStorage] Error removing from storage:', error);
this.memoryStorage.delete(key);
}
}
// Get storage status for diagnostics
getStorageStatus(): { type: string; persistent: boolean; warning: string | null } {
return {
type: this.storageType,
persistent: this.storageType !== 'memory',
warning: this.storageType === 'memory'
? 'Sessions will not persist across page reloads. Please enable cookies/storage for this site.'
: null
};
}
}
export const authStorage = new AuthStorage();

View File

@@ -14,6 +14,7 @@ import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha';
import { notificationService } from '@/lib/notificationService';
import { StorageWarning } from '@/components/auth/StorageWarning';
export default function Auth() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
@@ -70,6 +71,11 @@ export default function Auth() {
setSignInCaptchaToken(null);
try {
console.log('[Auth] Attempting sign in...', {
email: formData.email,
timestamp: new Date().toISOString(),
});
const {
data,
error
@@ -80,19 +86,54 @@ export default function Auth() {
captchaToken: tokenToUse
}
});
if (error) throw error;
console.log('[Auth] Sign in successful', {
user: data.user?.email,
session: !!data.session,
sessionExpiry: data.session?.expires_at
});
// Verify session was stored
setTimeout(async () => {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
console.error('[Auth] Session not found after login!');
toast({
variant: "destructive",
title: "Session Error",
description: "Login succeeded but session was not stored. Please check your browser settings and enable cookies/storage."
});
} else {
console.log('[Auth] Session verified after login');
toast({
title: "Welcome back!",
description: "You've been signed in successfully."
});
}
}, 500);
} catch (error: any) {
// Reset CAPTCHA widget to force fresh token generation
setSignInCaptchaKey(prev => prev + 1);
console.error('[Auth] Sign in error:', error);
// Enhanced error messages
let errorMessage = error.message;
if (error.message.includes('Invalid login credentials')) {
errorMessage = 'Invalid email or password. Please try again.';
} else if (error.message.includes('Email not confirmed')) {
errorMessage = 'Please confirm your email address before signing in.';
} else if (error.message.includes('Too many requests')) {
errorMessage = 'Too many login attempts. Please wait a few minutes and try again.';
}
toast({
variant: "destructive",
title: "Sign in failed",
description: error.message
description: errorMessage
});
} finally {
setLoading(false);
@@ -251,6 +292,7 @@ export default function Auth() {
<main className="container mx-auto px-4 py-16">
<div className="max-w-md mx-auto">
<StorageWarning />
<div className="text-center mb-8">
<div className="flex items-center justify-center gap-2 mb-4">