Skip to content

Commit b5a4491

Browse files
jeremylongshoreclaude
andcommitted
fix(auth): trust session cookie in ProtectedRoute to fix E2E tests
ProtectedRoute was redirecting to /login immediately when onAuthStateChanged returned null during initial page load. This happened because Firebase client SDK needs time to restore auth state from IndexedDB after hard navigation. Changes: - Check for session cookie before redirecting - Add 3s timeout for auth check with cookie fallback - Trust middleware's session cookie validation - Only redirect if definitely no auth (no user AND no cookie) This fixes Dream Gym and other E2E tests that were failing due to auth state not persisting across page navigation. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 789b7bf commit b5a4491

1 file changed

Lines changed: 69 additions & 21 deletions

File tree

src/components/ProtectedRoute.tsx

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
/**
22
* ProtectedRoute Component
33
*
4-
* Client-side auth protection that works exactly like Perception's ProtectedRoute.
5-
* Uses Firebase onAuthStateChanged - no server-side session cookies needed.
4+
* Client-side auth protection that works with server-side session cookies.
5+
* The middleware already checks for session cookies before allowing access.
6+
* This component provides a loading state and handles email verification.
7+
*
8+
* Key insight: In Next.js with server session cookies, the middleware
9+
* protects routes at the edge. This component just needs to:
10+
* 1. Show loading while Firebase client SDK initializes
11+
* 2. Handle email verification redirect
12+
* 3. Trust that if we got here, the session cookie is valid
613
*/
714

815
'use client';
@@ -17,36 +24,68 @@ interface ProtectedRouteProps {
1724
requireEmailVerification?: boolean;
1825
}
1926

27+
// Check if session cookie exists (client-side check)
28+
function hasSessionCookie(): boolean {
29+
if (typeof document === 'undefined') return false;
30+
const cookies = document.cookie.split(';').map(c => c.trim());
31+
return cookies.some(c => c.startsWith('__session=') || c.startsWith('firebase-auth-token='));
32+
}
33+
2034
export default function ProtectedRoute({
2135
children,
2236
requireEmailVerification = true,
2337
}: ProtectedRouteProps) {
2438
const router = useRouter();
2539
const [loading, setLoading] = useState(true);
2640
const [user, setUser] = useState<FirebaseUser | null>(null);
41+
const [authCheckComplete, setAuthCheckComplete] = useState(false);
2742

2843
useEffect(() => {
44+
let timeoutId: NodeJS.Timeout;
45+
2946
const unsubscribe = onAuthStateChange((authUser) => {
3047
setUser(authUser);
48+
setAuthCheckComplete(true);
3149
setLoading(false);
3250
});
3351

34-
return () => unsubscribe();
35-
}, []);
52+
// Safety timeout: If onAuthStateChanged doesn't fire within 3s,
53+
// check for session cookie and proceed if present
54+
timeoutId = setTimeout(() => {
55+
if (!authCheckComplete) {
56+
console.log('[ProtectedRoute] Auth check timeout, checking session cookie');
57+
if (hasSessionCookie()) {
58+
console.log('[ProtectedRoute] Session cookie present, proceeding');
59+
// Trust the middleware - it already verified the session cookie
60+
setLoading(false);
61+
} else {
62+
console.log('[ProtectedRoute] No session cookie, redirecting to login');
63+
setLoading(false);
64+
router.push('/login');
65+
}
66+
}
67+
}, 3000);
68+
69+
return () => {
70+
unsubscribe();
71+
clearTimeout(timeoutId);
72+
};
73+
}, [authCheckComplete, router]);
3674

3775
useEffect(() => {
38-
if (!loading) {
39-
if (!user) {
40-
router.push('/login');
41-
} else if (requireEmailVerification && !user.emailVerified) {
42-
// Skip email verification in E2E test mode
43-
const isE2ETestMode = process.env.NEXT_PUBLIC_E2E_TEST_MODE === 'true';
44-
if (!isE2ETestMode) {
45-
router.push('/verify-email');
46-
}
76+
// Only redirect if we're sure there's no auth
77+
// If we have a session cookie but no Firebase user, trust the cookie
78+
if (!loading && authCheckComplete && !user && !hasSessionCookie()) {
79+
console.log('[ProtectedRoute] No user and no session cookie, redirecting');
80+
router.push('/login');
81+
} else if (!loading && authCheckComplete && user && requireEmailVerification && !user.emailVerified) {
82+
// Skip email verification in E2E test mode
83+
const isE2ETestMode = process.env.NEXT_PUBLIC_E2E_TEST_MODE === 'true';
84+
if (!isE2ETestMode) {
85+
router.push('/verify-email');
4786
}
4887
}
49-
}, [loading, user, requireEmailVerification, router]);
88+
}, [loading, authCheckComplete, user, requireEmailVerification, router]);
5089

5190
if (loading) {
5291
return (
@@ -56,15 +95,24 @@ export default function ProtectedRoute({
5695
);
5796
}
5897

59-
if (!user) {
60-
return null; // Will redirect
98+
// If we have a user, show content
99+
if (user) {
100+
// Skip email verification check in E2E test mode
101+
const isE2ETestMode = process.env.NEXT_PUBLIC_E2E_TEST_MODE === 'true';
102+
if (requireEmailVerification && !user.emailVerified && !isE2ETestMode) {
103+
return null; // Will redirect to verify-email
104+
}
105+
return <>{children}</>;
61106
}
62107

63-
// Skip email verification check in E2E test mode
64-
const isE2ETestMode = process.env.NEXT_PUBLIC_E2E_TEST_MODE === 'true';
65-
if (requireEmailVerification && !user.emailVerified && !isE2ETestMode) {
66-
return null; // Will redirect
108+
// If we have a session cookie but no Firebase user yet,
109+
// trust the server-side session and show content
110+
// The middleware already verified the session cookie is valid
111+
if (hasSessionCookie()) {
112+
console.log('[ProtectedRoute] Trusting session cookie, showing content');
113+
return <>{children}</>;
67114
}
68115

69-
return <>{children}</>;
116+
// No auth at all - will redirect
117+
return null;
70118
}

0 commit comments

Comments
 (0)