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+
2034export 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