This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

remove auth from omnifeed

+282 -275
+7 -17
src/App.jsx
··· 19 19 import ZenPage from './components/ZenPage'; 20 20 import CompareScores from './components/CompareScores/CompareScores'; 21 21 import CollectionsFeed from './components/CollectionsFeed/CollectionsFeed'; 22 - import AdminRoute from './components/Admin/AdminRoute'; 23 - import ProtectedRoute from './components/ProtectedRoute'; 24 22 import Login from './components/Login/Login'; 25 23 import LoginCallback from './components/Login/LoginCallback'; 26 24 import { AuthProvider } from './contexts/AuthContext'; ··· 56 54 <Route path="/zen" element={<ZenPage />} /> 57 55 <Route path="/methodology" element={<ScoringMethodology />} /> 58 56 59 - {/* Protected Routes - Require Authentication */} 60 - <Route 61 - path="/omnifeed/:username" 62 - element={ 63 - <ProtectedRoute> 64 - <CollectionsFeed /> 65 - </ProtectedRoute> 66 - } 57 + {/* Omnifeed Routes - Now Public */} 58 + <Route 59 + path="/omnifeed/:username" 60 + element={<CollectionsFeed />} 67 61 /> 68 - <Route 69 - path="/omnifeed" 70 - element={ 71 - <ProtectedRoute> 72 - <CollectionsFeed /> 73 - </ProtectedRoute> 74 - } 62 + <Route 63 + path="/omnifeed" 64 + element={<CollectionsFeed />} 75 65 /> 76 66 77 67 {/* Handle both DIDs and regular usernames */}
+211 -246
src/components/CollectionsFeed/CollectionsFeed.js
··· 8 8 import MatterLoadingAnimation from '../MatterLoadingAnimation'; 9 9 import { Helmet } from 'react-helmet'; 10 10 import { useAuth } from '../../contexts/AuthContext'; 11 - import Loading from '../Loading/Loading'; 12 11 13 12 const CollectionsFeed = () => { 14 13 const { username } = useParams(); 15 14 const navigate = useNavigate(); 16 - const { isAuthenticated, loading: authLoading, session } = useAuth(); 15 + const { isAuthenticated } = useAuth(); 17 16 18 17 // Initialize state variables 19 18 const [handle, setHandle] = useState(username || ''); ··· 155 154 } 156 155 157 156 try { 158 - const response = await fetch(url, { 159 - credentials: 'include' 160 - }); 157 + const response = await fetch(url); 161 158 162 159 if (!response.ok) { 163 - if (response.status === 401) { 164 - // Try to refresh auth once 165 - const refreshResult = await isAuthenticated(); 166 - if (!refreshResult) { 167 - setError('Your session has expired. Please log in again.'); 168 - const returnUrl = encodeURIComponent(window.location.pathname); 169 - navigate(`/login?returnUrl=${returnUrl}`); 170 - return; 171 - } 172 - 173 - // If still authenticated, retry this request 174 - continue; 175 - } 176 - 177 - // Try to parse the error response 178 160 let errorMessage; 179 161 try { 180 162 const errorData = await response.json(); ··· 333 315 334 316 } catch (err) { 335 317 console.error('Error fetching collection records:', err); 336 - if (err.message && err.message.includes('authenticated')) { 337 - setError('Authentication error: ' + err.message); 338 - // Allow time to see the error before redirecting 339 - setTimeout(() => { 340 - const returnUrl = encodeURIComponent(window.location.pathname); 341 - navigate(`/login?returnUrl=${returnUrl}`); 342 - }, 3000); 343 - } else { 344 - setError(`Failed to load records: ${err.message}`); 345 - } 318 + setError(`Failed to load records: ${err.message}`); 346 319 setFetchingMore(false); 347 320 setChartLoading(false); 348 321 } 349 - }, [useRkeyTimestamp, isAuthenticated, navigate, collectionCursors]); 322 + }, [navigate]); 350 323 351 324 // Now define loadUserData after fetchCollectionRecords is defined 352 - const loadUserData = useCallback(async (userToLoad) => { 353 - console.log(`loadUserData called for: ${userToLoad}`); 325 + const loadUserData = useCallback(async (usernameOrDid) => { 326 + if (!usernameOrDid) { 327 + setError('Please enter a username or DID.'); 328 + return; 329 + } 330 + 331 + // Reset state for new search 354 332 setLoading(true); 333 + setShowContent(false); // Hide content while loading 355 334 setError(''); 356 - setSearchPerformed(true); 335 + setDid(''); 336 + setServiceEndpoint(''); 357 337 setCollections([]); 338 + setSelectedCollections([]); 358 339 setRecords([]); 359 340 setAllRecordsForChart([]); 360 - setSelectedCollections([]); 361 341 setCollectionCursors({}); 362 - setDid(''); 363 - setHandle(''); 364 - setDisplayName(''); 365 - setServiceEndpoint(''); 366 - setDisplayCount(25); // Reset display count on new search 367 - 342 + 368 343 try { 369 - const identifier = userToLoad || session?.handle; 370 - if (!identifier) { 371 - throw new Error("No user identifier available to load data."); 344 + // Continue with resolving the handle to DID 345 + let userDid = usernameOrDid; 346 + 347 + // If input doesn't look like a DID, try to resolve it as a handle 348 + if (!userDid.startsWith('did:')) { 349 + try { 350 + userDid = await resolveHandleToDid(usernameOrDid); 351 + } catch (resolveErr) { 352 + setError(`Could not resolve handle: ${resolveErr.message}`); 353 + setLoading(false); 354 + return; 355 + } 372 356 } 373 - 374 - // Resolve identifier first 375 - const resolveResponse = await fetch(`/api/resolve-identifier?identifier=${encodeURIComponent(identifier)}`); 376 - if (!resolveResponse.ok) { 377 - throw new Error(`Failed to resolve identifier: ${identifier}`); 357 + 358 + // Get service endpoint 359 + let endpoint; 360 + try { 361 + endpoint = await getServiceEndpointForDid(userDid); 362 + setServiceEndpoint(endpoint); 363 + } catch (endpointError) { 364 + console.error('Error getting service endpoint:', endpointError); 365 + setError(`Could not determine PDS endpoint for "${userDid}". The user's server may be offline.`); 366 + setInitialLoad(false); 367 + setLoading(false); 368 + return; 378 369 } 379 - const resolveData = await resolveResponse.json(); 380 - const resolvedDid = resolveData.did; 381 - const resolvedHandle = resolveData.handle; 382 - setDid(resolvedDid); 383 - setHandle(resolvedHandle); 384 - console.log('Resolved:', resolvedDid, resolvedHandle); 385 - 386 - // Fetch collections using resolved DID 387 - // Assume default endpoint or fetch dynamically if needed 388 - const defaultEndpoint = 'https://bsky.social'; // Or determine dynamically 389 - setServiceEndpoint(defaultEndpoint); 390 - 391 - // Use server-side endpoint for collections 392 - const collectionsResponse = await fetch(`/api/collections/${encodeURIComponent(resolvedDid)}?endpoint=${encodeURIComponent(defaultEndpoint)}`, { 393 - credentials: 'include' 394 - }); 395 - 396 - if (!collectionsResponse.ok) { 397 - if (collectionsResponse.status === 401) { 398 - // No need to call checkAuthStatus, just redirect 399 - throw new Error('Authentication required. Please log in again.'); 400 - } 401 - throw new Error(`Failed to fetch collections for ${resolvedDid}`); 370 + 371 + // Fetch profile information 372 + try { 373 + const publicApiEndpoint = "https://public.api.bsky.app"; 374 + const profileResponse = await fetch(`${publicApiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(userDid)}`); 375 + 376 + if (!profileResponse.ok) { 377 + throw new Error(`Error fetching profile: ${profileResponse.statusText}`); 378 + } 379 + 380 + const profileData = await profileResponse.json(); 381 + setHandle(profileData.handle); 382 + setDisplayName(profileData.displayName || profileData.handle); 383 + } catch (profileError) { 384 + console.error('Error fetching profile:', profileError); 385 + // Continue without profile data, not critical 402 386 } 403 - 404 - const collectionsData = await collectionsResponse.json(); 405 - const fetchedCollections = collectionsData.collections || []; 406 - setDisplayName(collectionsData.displayName || resolvedHandle); 407 - setCollections(fetchedCollections); 408 - setSelectedCollections(fetchedCollections); // Select all by default 409 - console.log('Fetched collections:', fetchedCollections); 410 - 411 - // Fetch records for all collections initially for the chart 412 - if (fetchedCollections.length > 0) { 413 - await fetchCollectionRecords(resolvedDid, defaultEndpoint, fetchedCollections); 387 + 388 + // Use our server-side API to fetch collections 389 + try { 390 + const collectionsResponse = await fetch(`/api/collections/${encodeURIComponent(userDid)}?endpoint=${encodeURIComponent(endpoint)}`); 391 + 392 + if (!collectionsResponse.ok) { 393 + throw new Error(`Error fetching collections: ${collectionsResponse.statusText}`); 394 + } 395 + 396 + const collectionsData = await collectionsResponse.json(); 397 + 398 + if (collectionsData.collections && collectionsData.collections.length > 0) { 399 + const sortedCollections = [...collectionsData.collections].sort(); 400 + setCollections(sortedCollections); 401 + // By default, select all collections 402 + setSelectedCollections(sortedCollections); 403 + 404 + // Fetch records for each collection 405 + await fetchCollectionRecords(userDid, endpoint, sortedCollections); 406 + } else { 407 + setError('No collections found for this user.'); 408 + } 409 + } catch (err) { 410 + console.error(`Error fetching collections:`, err); 411 + throw err; // Propagate error 414 412 } 415 - 413 + 414 + setSearchPerformed(true); 415 + setInitialLoad(false); 416 + setLoading(false); 417 + // Add a slight delay before showing content for smooth transition 418 + setTimeout(() => setShowContent(true), 100); 416 419 } catch (err) { 417 420 console.error('Error loading user data:', err); 418 - let userMessage = 'Failed to load user data. Please check the handle and try again.'; 419 - if (err.message?.includes('Authentication required')) { 420 - userMessage = 'Authentication error. Please log in again.'; 421 - // Redirect to login 422 - const returnUrl = encodeURIComponent(window.location.pathname); 423 - navigate(`/login?returnUrl=${returnUrl}`); 424 - return; // Stop further execution 425 - } 426 - setError(userMessage); 427 - } finally { 421 + setError(err.message || 'An error occurred while loading data.'); 422 + setInitialLoad(false); 428 423 setLoading(false); 429 - setInitialLoad(false); 430 - setShowContent(true); 424 + setShowContent(true); // Show content even on error so user can see error message 431 425 } 432 - }, [session?.handle, navigate]); 426 + }, [navigate, fetchCollectionRecords]); 433 427 434 428 // Now place useEffects after all the callbacks are defined 435 429 436 - // Effect to handle initial authentication and data loading 430 + // Verify authentication first 437 431 useEffect(() => { 438 - // Wait for AuthProvider to finish loading 439 - if (authLoading) { 440 - console.log("CollectionsFeed: Waiting for AuthProvider..."); 441 - return; 442 - } 443 - 444 - console.log("CollectionsFeed: AuthProvider loaded. isAuthenticated:", isAuthenticated); 445 - setInitialLoad(true); // Start initial load process 446 - setShowContent(false); 447 - 448 - if (!isAuthenticated) { 449 - console.log('CollectionsFeed: Not authenticated, redirecting to login'); 450 - const returnUrl = encodeURIComponent(window.location.pathname + window.location.search); 451 - navigate(`/login?returnUrl=${returnUrl}`); 452 - return; // Stop execution 453 - } 454 - 455 - // If authenticated, proceed to load data 456 - setError(''); // Clear previous errors 457 - if (username) { 458 - console.log('CollectionsFeed: Authenticated, username provided, loading data for:', username); 459 - setSearchTerm(username); // Update search term from URL 460 - loadUserData(username); 461 - } else if (session?.handle) { 462 - // If no username in URL, load data for the logged-in user 463 - console.log('CollectionsFeed: Authenticated, no username in URL, loading data for logged-in user:', session.handle); 464 - setSearchTerm(session.handle); 465 - loadUserData(session.handle); 466 - } else { 467 - // Authenticated but no username in URL and no handle in session (should not happen ideally) 468 - console.error('CollectionsFeed: Authenticated but no identifier found to load data.'); 469 - setError('Could not determine user to load data for.'); 432 + const verifyAuth = async () => { 433 + try { 434 + setLoading(true); 435 + setInitialLoad(true); 436 + setShowContent(false); 437 + 438 + // Reset states if username changes 439 + if (username) { 440 + setError(''); 441 + setSearchTerm(username); 442 + } 443 + 444 + // If a username is provided in the URL, load that user's data 445 + if (username && isAuthenticated) { 446 + console.log('Username provided in URL, loading data for:', username); 447 + loadUserData(username); 448 + } else { 449 + // Only set loading to false if we're not loading a specific user 450 + setLoading(false); 451 + setInitialLoad(false); 452 + setShowContent(true); 453 + } 454 + } catch (err) { 455 + console.error('Auth verification failed:', err); 456 + setError('Authentication failed. Please try logging in again.'); 470 457 setLoading(false); 471 458 setInitialLoad(false); 472 459 setShowContent(true); 473 - } 474 - 475 - // Safety timeout for initial load UI state 460 + 461 + // Add a delay before redirecting to show the error message 462 + setTimeout(() => { 463 + navigate('/login'); 464 + }, 2000); 465 + } 466 + }; 467 + 468 + verifyAuth(); 469 + 470 + // Safety timeout to ensure initialLoad is cleared after 10 seconds maximum 476 471 const loadingTimeout = setTimeout(() => { 477 - if (initialLoad) { 478 - console.warn("CollectionsFeed: Initial load timeout reached."); 479 - setInitialLoad(false); 480 - // Decide whether to show content or error based on current state 481 - if (!error) setShowContent(true); 482 - } 483 - }, 15000); // Increase timeout slightly 484 - 485 - // Cleanup function (no interval to clear anymore) 472 + setInitialLoad(false); 473 + }, 10000); 474 + 486 475 return () => { 487 476 clearTimeout(loadingTimeout); 488 477 }; 489 - // Depend on auth state and username param 490 - }, [isAuthenticated, authLoading, navigate, username, session?.handle, loadUserData]); 478 + }, [isAuthenticated, navigate, username, loadUserData]); 491 479 492 480 // Effect to watch for selected collections changes 493 481 useEffect(() => { ··· 566 554 567 555 // Handle refresh button click - only updates the feed, not the chart 568 556 const handleRefresh = async () => { 569 - // Check auth status directly 570 - if (!isAuthenticated) { 571 - console.warn("Refresh cancelled: User not authenticated."); 572 - navigate('/login'); // Redirect to login if session lost 573 - return; 574 - } 575 557 if (did && serviceEndpoint) { 558 + // Set a loading state but not for the chart 576 559 setLoading(true); 560 + 561 + // Reset display count to show only the first 25 records after refresh 577 562 setDisplayCount(25); 563 + 578 564 try { 565 + // Fetch just the most recent records for the feed display 566 + const refreshOnlyFeed = async () => { 579 567 let recentRecords = []; 568 + 569 + // Process each selected collection sequentially 580 570 for (const collection of selectedCollections) { 581 - try { 582 - const url = `/api/collections/${encodeURIComponent(did)}/records?endpoint=${encodeURIComponent(serviceEndpoint)}&collection=${encodeURIComponent(collection)}&limit=25`; 583 - const response = await fetch(url, { credentials: 'include' }); 584 - if (!response.ok) { 585 - if (response.status === 401) { 586 - // Directly navigate to login on 401 587 - throw new Error('Authentication required. Please log in again.'); 588 - } 589 - console.error(`Error refreshing ${collection}: ${response.statusText}`); 590 - continue; 591 - } 592 - const data = await response.json(); 593 - if (data.records && data.records.length > 0) { 594 - const processedRecords = data.records.map(record => { 595 - const contentTimestamp = extractTimestamp(record); 596 - const rkey = record.uri.split('/').pop(); 597 - const rkeyTimestamp = tidToTimestamp(rkey); 598 - return { 599 - ...record, 600 - collection, 601 - collectionType: record.value?.$type || collection, 602 - contentTimestamp, 603 - rkeyTimestamp, 604 - rkey, 605 - }; 606 - }); 607 - recentRecords = [...recentRecords, ...processedRecords]; 608 - } 609 - } catch (err) { 610 - console.error(`Error refreshing collection ${collection}:`, err); 611 - if (err.message?.includes('Authentication required')) throw err; // Re-throw auth error 612 - // Continue with other collections 571 + try { 572 + // Use server-side API endpoint for fetching records 573 + const url = `/api/collections/${encodeURIComponent(did)}/records?endpoint=${encodeURIComponent(serviceEndpoint)}&collection=${encodeURIComponent(collection)}&limit=25`; 574 + 575 + const response = await fetch(url); 576 + 577 + if (!response.ok) { 578 + console.error(`Error refreshing ${collection}: ${response.statusText}`); 579 + continue; // Skip this collection but continue with others 613 580 } 581 + 582 + const data = await response.json(); 583 + 584 + if (data.records && data.records.length > 0) { 585 + // Process the records with timestamps 586 + const processedRecords = data.records.map(record => { 587 + const contentTimestamp = extractTimestamp(record); 588 + const rkey = record.uri.split('/').pop(); 589 + const rkeyTimestamp = tidToTimestamp(rkey); 590 + 591 + return { 592 + ...record, 593 + collection, 594 + collectionType: record.value?.$type || collection, 595 + contentTimestamp, 596 + rkeyTimestamp, 597 + rkey, 598 + }; 599 + }); 600 + 601 + // Add to our records array 602 + recentRecords = [...recentRecords, ...processedRecords]; 603 + } 604 + } catch (err) { 605 + console.error(`Error refreshing collection ${collection}:`, err); 606 + // Continue with other collections 607 + } 614 608 } 609 + 610 + // Sort the refreshed records by timestamp (newest first) 615 611 const sortedRecords = recentRecords.filter(record => { 616 - if (useRkeyTimestamp) { 617 - return record.rkeyTimestamp !== null; 618 - } else { 619 - return record.contentTimestamp !== null; 620 - } 612 + if (useRkeyTimestamp) { 613 + return record.rkeyTimestamp !== null; 614 + } else { 615 + return record.contentTimestamp !== null; 616 + } 621 617 }).sort((a, b) => { 622 - const aTime = useRkeyTimestamp ? a.rkeyTimestamp : a.contentTimestamp; 623 - const bTime = useRkeyTimestamp ? b.rkeyTimestamp : b.contentTimestamp; 624 - return new Date(bTime) - new Date(aTime); 618 + const aTime = useRkeyTimestamp ? a.rkeyTimestamp : a.contentTimestamp; 619 + const bTime = useRkeyTimestamp ? b.rkeyTimestamp : b.contentTimestamp; 620 + return new Date(bTime) - new Date(aTime); 625 621 }); 622 + 623 + // Only update the feed display records, not the chart data 626 624 setRecords(sortedRecords.slice(0, 25)); 625 + }; 626 + 627 + await refreshOnlyFeed(); 627 628 } catch (err) { 628 - console.error("Error during feed refresh:", err); 629 - if (err.message?.includes('Authentication required')) { 630 - const returnUrl = encodeURIComponent(window.location.pathname); 631 - navigate(`/login?returnUrl=${returnUrl}`); 632 - } else { 633 - setError('Failed to refresh records. Please try again.'); 634 - } 629 + console.error("Error during feed refresh:", err); 630 + setError('Failed to refresh records. Please try again.'); 635 631 } finally { 636 - setLoading(false); 632 + // Ensure loading state is reset 633 + setLoading(false); 637 634 } 638 635 } 639 636 }; 640 637 641 638 // Handle load more button click 642 639 const handleLoadMore = async () => { 643 - // Verify authentication before proceeding 644 - if (!isAuthenticated) { 645 - console.warn("Load more cancelled: User not authenticated."); 646 - navigate('/login'); // Redirect to login if session lost 647 - return; 648 - } 649 - 650 640 setFetchingMore(true); 651 641 652 642 // First check if we already have more records locally that we can show ··· 668 658 // After fetching more, we can increase the display count to show them 669 659 setDisplayCount(prevCount => prevCount + 25); 670 660 } catch (error) { 671 - if (error.message?.includes('Authentication required')) { 672 - // Handle authentication errors 673 - const returnUrl = encodeURIComponent(window.location.pathname); 674 - navigate(`/login?returnUrl=${returnUrl}`); 675 - } else { 676 - setError('Failed to load more records. Please try again.'); 677 - } 661 + setError('Failed to load more records. Please try again.'); 678 662 } 679 663 } else { 680 664 setFetchingMore(false); ··· 753 737 )} 754 738 755 739 {/* Only show the main content when not in full-screen loading mode */} 756 - {(!username || !loading || error) && ( 740 + {(!username || !loading || error || showContent) && ( 757 741 <div className={`search-container ${showContent ? 'fade-in' : ''}`}> 758 742 <h1>OmniFeed</h1> 759 743 <p className="feed-description"> 760 744 View all repository collections for a Bluesky user, including custom collections from AT Protocol apps. 761 745 </p> 762 - 763 - {/* Authentication status banner */} 764 - {!isAuthenticated && ( 765 - <div className="auth-warning"> 766 - <p> 767 - <strong>Authentication Required:</strong> You need to be logged in to view the OmniFeed. 768 - Redirecting to login... 769 - </p> 770 - </div> 771 - )} 772 746 773 747 <form onSubmit={handleSubmit} className="search-form"> 774 748 <div className="search-box"> ··· 793 767 <div className="error-container"> 794 768 <div className="error-message"> 795 769 <p>{error}</p> 796 - {error.includes('authenticated') ? ( 797 - <button 798 - className="error-action-button" 799 - onClick={() => { 800 - const returnUrl = encodeURIComponent(window.location.pathname); 801 - navigate(`/login?returnUrl=${returnUrl}`); 802 - }} 803 - > 804 - Go to Login 805 - </button> 806 - ) : ( 807 - <button 808 - className="error-action-button" 809 - onClick={() => { 810 - setError(''); 811 - if (username) { 812 - loadUserData(username); 813 - } 814 - }} 815 - > 816 - Retry 817 - </button> 818 - )} 770 + <button 771 + className="error-action-button" 772 + onClick={() => { 773 + setError(''); 774 + if (username) { 775 + loadUserData(username); 776 + } 777 + }} 778 + > 779 + Retry 780 + </button> 819 781 </div> 820 782 </div> 821 783 )} ··· 992 954 </> 993 955 )} 994 956 </div> 957 + )} 958 + {!searchPerformed && !loading && !username && ( 959 + <p>Enter a username above to get started.</p> // Initial state message 995 960 )} 996 961 </div> 997 962 )}
+64 -12
src/components/ProtectedRoute.js
··· 1 - import React, { useEffect, useState } from 'react'; 1 + import React, { useEffect, useRef, useState } from 'react'; 2 2 import { Navigate, useLocation } from 'react-router-dom'; 3 3 import { useAuth } from '../contexts/AuthContext'; 4 4 import { isAccountAllowed } from '../config/allowlist'; ··· 6 6 7 7 // Component to protect routes that require authentication 8 8 const ProtectedRoute = ({ children }) => { 9 - const { isAuthenticated, loading, session } = useAuth(); 9 + const { isAuthenticated, loading, session, checkAuthStatus } = useAuth(); 10 10 const location = useLocation(); 11 + const [redirecting, setRedirecting] = useState(false); 12 + const [checkingStatus, setCheckingStatus] = useState(false); 13 + const checkCount = useRef(0); 14 + const maxChecks = 3; // Maximum number of checks to prevent infinite loops 15 + 16 + // Perform an immediate auth check when the component mounts 17 + useEffect(() => { 18 + const checkAuth = async () => { 19 + if (checkCount.current >= maxChecks) { 20 + console.error("Maximum auth check attempts reached. Stopping to prevent infinite loop."); 21 + return; 22 + } 23 + 24 + // Only proceed if not already checking, not already redirecting, and not loading 25 + if (!isAuthenticated && !checkingStatus && !redirecting && !loading) { 26 + try { 27 + console.log("ProtectedRoute: Checking authentication status"); 28 + setCheckingStatus(true); 29 + checkCount.current += 1; 30 + await checkAuthStatus(); 31 + } catch (error) { 32 + console.error("ProtectedRoute: Auth check failed:", error); 33 + } finally { 34 + setCheckingStatus(false); 35 + } 36 + } 37 + }; 38 + 39 + // Call immediately on mount or when dependency values change 40 + checkAuth(); 41 + 42 + // Set up interval for periodic checks only if authenticated 43 + let interval; 44 + if (isAuthenticated && session) { 45 + console.log("ProtectedRoute: Setting up periodic auth checks"); 46 + interval = setInterval(() => { 47 + checkAuthStatus().catch(err => { 48 + console.error("Error in periodic auth check:", err); 49 + }); 50 + }, 30000); // Check every 30 seconds 51 + } 52 + 53 + return () => { 54 + if (interval) { 55 + console.log("ProtectedRoute: Clearing periodic auth checks"); 56 + clearInterval(interval); 57 + } 58 + }; 59 + }, [isAuthenticated, checkAuthStatus, redirecting, loading, checkingStatus, session]); 11 60 12 - // Show loading state while authentication is being determined by AuthProvider 13 - if (loading) { 14 - console.log("ProtectedRoute: Waiting for AuthProvider loading..."); 15 - return <Loading message="Loading authentication status..." />; 61 + // Show loading state while authentication is being checked 62 + if (loading || checkingStatus) { 63 + return <Loading message="Checking authentication..." />; 16 64 } 17 65 18 - // If not authenticated after loading, redirect to login with return URL 19 - if (!isAuthenticated) { 20 - console.log("ProtectedRoute: Not authenticated after loading, redirecting to login"); 21 - const returnUrl = encodeURIComponent(location.pathname + location.search); 66 + // If not authenticated, redirect to login with return URL 67 + if (!isAuthenticated && !redirecting) { 68 + console.log("ProtectedRoute: Not authenticated, redirecting to login"); 69 + setRedirecting(true); // Prevent multiple redirects 70 + const returnUrl = encodeURIComponent(location.pathname); 22 71 return <Navigate to={`/login?returnUrl=${returnUrl}`} replace />; 23 72 } 24 73 25 - // Check if user is allowed (allowlist check remains) 74 + // Check if user is allowed 26 75 if (session && session.handle && !isAccountAllowed(session)) { 27 76 console.log("ProtectedRoute: User not in allowlist, redirecting to supporter page"); 28 77 return <Navigate to="/supporter" replace />; 29 78 } 30 79 80 + // Reset counter when rendering the protected content 81 + checkCount.current = 0; 82 + 31 83 // Render children if authenticated and allowed 32 - console.log(`ProtectedRoute: Authentication successful (${session?.handle}), rendering protected content for ${location.pathname}`); 84 + console.log("ProtectedRoute: Authentication successful, rendering protected content"); 33 85 return children; 34 86 }; 35 87