diff --git a/src/components/ProtectedRoute.js b/src/components/ProtectedRoute.js --- a/src/components/ProtectedRoute.js +++ b/src/components/ProtectedRoute.js @@ -9,50 +9,71 @@ const { isAuthenticated, loading, session, checkAuthStatus } = useAuth(); const location = useLocation(); const [redirecting, setRedirecting] = useState(false); + const [checkingStatus, setCheckingStatus] = useState(false); const checkCount = useRef(0); const maxChecks = 3; // Maximum number of checks to prevent infinite loops + // Perform an immediate auth check when the component mounts useEffect(() => { - // Prevent excessive auth checks - if (checkCount.current >= maxChecks) { - console.error("Maximum auth check attempts reached. Stopping to prevent infinite loop."); - return; - } + const checkAuth = async () => { + if (checkCount.current >= maxChecks) { + console.error("Maximum auth check attempts reached. Stopping to prevent infinite loop."); + return; + } + + // Only proceed if not already checking, not already redirecting, and not loading + if (!isAuthenticated && !checkingStatus && !redirecting && !loading) { + try { + console.log("ProtectedRoute: Checking authentication status"); + setCheckingStatus(true); + checkCount.current += 1; + await checkAuthStatus(); + } catch (error) { + console.error("ProtectedRoute: Auth check failed:", error); + } finally { + setCheckingStatus(false); + } + } + }; - // Only check if not already authenticated and not already redirecting - if (!isAuthenticated && !redirecting && !loading) { - checkCount.current += 1; - checkAuthStatus(); - } + // Call immediately on mount or when dependency values change + checkAuth(); - // Set up interval for periodic checks - but only if authenticated - // This prevents constantly checking while unauthenticated + // Set up interval for periodic checks only if authenticated let interval; - if (isAuthenticated) { - interval = setInterval(checkAuthStatus, 30000); // Check every 30 seconds + if (isAuthenticated && session) { + console.log("ProtectedRoute: Setting up periodic auth checks"); + interval = setInterval(() => { + checkAuthStatus().catch(err => { + console.error("Error in periodic auth check:", err); + }); + }, 30000); // Check every 30 seconds } return () => { - if (interval) clearInterval(interval); + if (interval) { + console.log("ProtectedRoute: Clearing periodic auth checks"); + clearInterval(interval); + } }; - }, [isAuthenticated, checkAuthStatus, redirecting, loading]); + }, [isAuthenticated, checkAuthStatus, redirecting, loading, checkingStatus, session]); // Show loading state while authentication is being checked - if (loading) { + if (loading || checkingStatus) { return ; } // If not authenticated, redirect to login with return URL if (!isAuthenticated && !redirecting) { + console.log("ProtectedRoute: Not authenticated, redirecting to login"); setRedirecting(true); // Prevent multiple redirects const returnUrl = encodeURIComponent(location.pathname); return ; } // Check if user is allowed - // Only check if we have detailed user info - // If we're using server-side sessions, we might not need this check if (session && session.handle && !isAccountAllowed(session)) { + console.log("ProtectedRoute: User not in allowlist, redirecting to supporter page"); return ; } @@ -60,6 +81,7 @@ checkCount.current = 0; // Render children if authenticated and allowed + console.log("ProtectedRoute: Authentication successful, rendering protected content"); return children; }; diff --git a/src/config/allowlist.js b/src/config/allowlist.js --- a/src/config/allowlist.js +++ b/src/config/allowlist.js @@ -6,19 +6,41 @@ // Helper function to check if an account is allowed export const isAccountAllowed = (session) => { - if (!session) return false; + console.log('Checking if account is allowed:', session); - // For Bluesky OAuth session - if (session.sub && session.handle) { - return ALLOWED_ACCOUNTS.includes(session.sub) || - ALLOWED_ACCOUNTS.includes(session.handle); + if (!session) { + console.log('No session provided, denying access'); + return false; } - // For server-side session - if (session.did && session.handle) { - return ALLOWED_ACCOUNTS.includes(session.did) || - ALLOWED_ACCOUNTS.includes(session.handle); + // Extract DID from various possible session formats + const did = session.did || session.sub || null; + + // Extract handle from various possible session formats + const handle = session.handle || null; + + console.log(`Checking permissions for DID: ${did}, handle: ${handle}`); + + // Check if either did or handle is in the allowlist + if (did && ALLOWED_ACCOUNTS.includes(did)) { + console.log('DID is in allowlist, granting access'); + return true; } + if (handle && ALLOWED_ACCOUNTS.includes(handle)) { + console.log('Handle is in allowlist, granting access'); + return true; + } + + // Also check if the handle (without domain) is in the allowlist + if (handle && handle.includes('.')) { + const handleWithoutDomain = handle.split('.')[0]; + if (ALLOWED_ACCOUNTS.includes(handleWithoutDomain)) { + console.log('Handle (without domain) is in allowlist, granting access'); + return true; + } + } + + console.log('Account not in allowlist, denying access'); return false; }; \ No newline at end of file diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -32,25 +32,37 @@ const [error, setError] = useState(null); const lastAuthCheck = useRef(0); const authCheckInProgress = useRef(false); + const didInitialCheck = useRef(false); // Initialize the OAuth client useEffect(() => { const initializeAuth = async () => { + if (didInitialCheck.current) return; + didInitialCheck.current = true; + try { // First check server-side authentication status + console.log('Checking server authentication status'); const serverAuthResponse = await fetch('/api/auth/status', { credentials: 'include' }); - const serverAuthData = await serverAuthResponse.json(); - - if (serverAuthData.isAuthenticated) { - setSession(serverAuthData.user); - setLoading(false); - return; + if (!serverAuthResponse.ok) { + console.error('Server auth check failed with status:', serverAuthResponse.status); + } else { + const serverAuthData = await serverAuthResponse.json(); + console.log('Server auth status:', serverAuthData); + + if (serverAuthData.isAuthenticated || serverAuthData.authenticated) { + console.log('Already authenticated on server, setting session'); + setSession(serverAuthData.user); + setLoading(false); + return; + } } // If not authenticated on the server, check client OAuth + console.log('Not authenticated on server, initializing OAuth client'); const oauthClient = new BrowserOAuthClient({ clientMetadata, handleResolver: 'https://bsky.social', @@ -65,45 +77,76 @@ if (result?.session) { console.log('Found existing OAuth session:', result.session); - // If client has session but server doesn't, we need to sync them + // Check if atproto_session exists in localStorage as a backup + const atprotoSession = localStorage.getItem('atproto_session'); + console.log('atproto_session in localStorage:', atprotoSession ? 'exists' : 'not found'); + + // Format session data for our internal use and sync with server + const sessionData = { + did: result.session.sub, + handle: result.session.handle + }; + + console.log('Syncing session with server:', sessionData); + + // Try to sync with server try { const syncResponse = await fetch('/api/sync-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - did: result.session.sub, - handle: result.session.handle - }), + body: JSON.stringify(sessionData), credentials: 'include' }); if (syncResponse.ok) { const syncData = await syncResponse.json(); - console.log('Session sync successful:', syncData); - // Use the server session data which may have more info + console.log('Initial session sync successful:', syncData); setSession(syncData.user); } else { - console.warn('Session sync failed, using client session'); - // Still use the client session if sync fails - setSession(result.session); + console.warn('Initial session sync failed:', await syncResponse.text()); + + // If sync fails, use client session in our internal format + console.log('Using client session as fallback'); + setSession({ + did: result.session.sub, + handle: result.session.handle, + displayName: result.session.handle + }); } } catch (syncError) { - console.error('Error syncing session:', syncError); - // If sync fails, still use the client session - setSession(result.session); + console.error('Error syncing initial session:', syncError); + + // If sync fails, still use client session in our internal format + setSession({ + did: result.session.sub, + handle: result.session.handle, + displayName: result.session.handle + }); } + } else { + console.log('No existing OAuth session found'); } // Listen for session deletion events oauthClient.addEventListener('deleted', (event) => { - if (event.data.did === session?.sub || event.data.did === session?.did) { + console.log('Session deletion event received:', event.data); + + // Get current session DID at the time of event + const currentSession = session; + const sessionDid = currentSession?.did || currentSession?.sub; + + if (event.data.did === sessionDid) { + console.log('Current session was deleted, logging out'); setSession(null); + // Also logout from server fetch('/api/logout', { method: 'POST', credentials: 'include' + }).catch(err => { + console.error('Error during server logout after deletion:', err); }); } }); @@ -224,13 +267,67 @@ const response = await fetch('/api/auth/status', { credentials: 'include' }); - const data = await response.json(); - if (data.isAuthenticated) { - setSession(data.user); + if (!response.ok) { + console.error('Auth status check failed with status:', response.status); + authCheckInProgress.current = false; + return !!session; // Return current state on error + } + + const data = await response.json(); + console.log('Auth status check response:', data); + + const isAuthenticated = data.isAuthenticated || data.authenticated; + + if (isAuthenticated && data.user) { + // If server session is different from current session, update it + const currentSessionJSON = session ? JSON.stringify(session) : ''; + const newSessionJSON = JSON.stringify(data.user); + + if (currentSessionJSON !== newSessionJSON) { + console.log('Updating session from server data'); + setSession(data.user); + } + authCheckInProgress.current = false; return true; } else { + // If server says not authenticated but we have a client session, + // try to synchronize sessions + if (session && client) { + try { + console.log('Server says not authenticated but we have a client session, trying to sync'); + + // Format session data properly + const sessionData = { + did: session.did || session.sub, + handle: session.handle + }; + + // Try to sync one more time + const syncResponse = await fetch('/api/sync-session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(sessionData), + credentials: 'include' + }); + + if (syncResponse.ok) { + console.log('Session sync successful during status check'); + const syncData = await syncResponse.json(); + setSession(syncData.user); + authCheckInProgress.current = false; + return true; + } + } catch (syncError) { + console.error('Error syncing during status check:', syncError); + } + } + + // If all attempts failed and the server says we're not authenticated + console.log('Server says not authenticated, clearing session'); setSession(null); authCheckInProgress.current = false; return false; @@ -238,9 +335,9 @@ } catch (err) { console.error('Error checking auth status:', err); authCheckInProgress.current = false; - return false; + return !!session; // Fall back to current session state } - }, [session]); + }, [session, client]); return (