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