skip to Main Content

I’m busy building a react app with firebase being used on the back-end. I’ve been building the app using react-router-dom and have recently created a protected route to prevent access to certain routes if you aren’t logged in. I was wondering though if its at all possible to trigger a redirect to a specific route if a user tries to access a route before there email has been verified. I’ve been trying but I don’t think my user context has access to the usual authentication fields by the time the protected route is first rendered. Here is my code below:

My Auth Context

import React, { createContext, useEffect, useState } from "react";
import { createUserWithEmailAndPassword, onAuthStateChanged, sendEmailVerification, signInWithEmailAndPassword, signOut, onIdTokenChanged } from 'firebase/auth';
import { auth } from '../../firebase-config';
import { createUserDocument } from "../../utils/userUtils";

const UserContext = createContext();

export const AuthContextProvider = ({ children }) => {
  const [user, setUser] = useState({});

  /** Create a new user */
  const createUser = async (email, password, role) => {
    try {

      await createUserWithEmailAndPassword(auth, email, password).then((userCredential) => {
        
        const currentUser = userCredential.user
        const userProps = { id: currentUser.uid, email: currentUser.email, role: role }
        createUserDocument(userProps)
        sendVerificationEmail(currentUser);

      });
    } catch (error) {
      console.error(error);
    }
  };

  /** Sends authenticated user a verification email */
  const sendVerificationEmail = (currentUser=user) => {
    const actionCodeSettings = {
      url: window.location.origin + '/home',
      handleCodeInApp: true,
    };

    sendEmailVerification(currentUser, actionCodeSettings).then(() => {
      console.log(`Verification email sent to user ${currentUser.email}`);
    })
  }

  /** Login a user into the tool */
  const login = async (email, password) => {
    const user = await signInWithEmailAndPassword(auth, email, password);
    return user
  };

  /** Logout a user from the tool */
  const logout = () => {
    return signOut(auth);
  };

  useEffect(() => {

    // Called when authorisation state changes
    const authState = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser)
    });

    // Called when user token changes
    const tokenState = onIdTokenChanged(auth, (currentUser) => {
      // console.log("User Token Changed", currentUser)
    })

    return () => {
      authState();
      tokenState();
    };
  }, []);

  const contextValues = {
    createUser,
    sendVerificationEmail,
    login,
    logout,
    user
  };

  return (
    <UserContext.Provider value={contextValues}>
      {children}
    </UserContext.Provider>
  );
};

export const UserAuth = UserContext;

Here is my protected route:

import React, { useContext } from 'react'
import { UserAuth } from '../Context/AuthContext'
import { Navigate } from 'react-router-dom'

const ProtectedRoute = ({children}) => {
    const { user  } = useContext(UserAuth);
    if(!user){
        return <Navigate to='/' />
    }
    else if(user && !user.emailVerified){
        
           // THIS DOESNT WORK!
           return <Navigate to='/email'/>
    }
    
    return children
}

export default ProtectedRoute

I’ll throw in my main App component too:

import './css/App.css';
import './css/style.css';
import React from 'react';
import { Routes, Route } from "react-router-dom";
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage'
import RegisterPage from './pages/RegisterPage';
import NoPage from './pages/NoPage';
import { AuthContextProvider } from './components/Context/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import VerifyEmailPage from './pages/VerifyEmailPage';

class App extends React.PureComponent {

  render() {  
    return ( 
      <div className='App'>
        <div className='Blur'>
          <AuthContextProvider>
          <Routes>
              <Route path="/" element={<LoginPage/>}/>
              <Route path="/register" element={<RegisterPage/>}/>
              <Route path="/email" element={<VerifyEmailPage/>}/>
              <Route path="/home" element={<ProtectedRoute><HomePage/></ProtectedRoute>}/>
              <Route path="/*" element={<NoPage/>} /> 
            </Routes>
          </AuthContextProvider>
        </div>
      </div>
  )}
    
}

export default App

When I try to access any page it re-directs to my /email route. I understand why too. Its because user is an empty object. Which I believe is because the auth context hasn’t had time to assign it the current user’s values yet. Not sure if there is a particular way to do this or if its possible to begin with? Any help would be greatly appreciated. Thanks!

2

Answers


  1. Chosen as BEST ANSWER

    I've updated my own component in order to achieve my desired functionality. I took the suggestions from Nazrul's answer and tweaked it to fit my use case. I now wait until my user object is definitely populated before checking if email is verified.

    If the user is object is empty I return null. The update seems to have done the trick. Although there is a brief flash of white when redirecting to the /email page probably because its likely returning null for a little while before user object is populated.

    Not sure if there is a way around that unfortunately, if anyone has any suggestions, feel free to let me know. If not, its something I can live with for now.

    My updated component:

    const ProtectedRoute = ({ children }) => {
        const { user } = useContext(UserAuth)
    
        // if user context is null, user isn't signed in
        if (!user) {
            return <Navigate to='/' />
        }
    
        // Check if user object is empty (i.e hasnt been set yet)
        const isUserEmpty = Object.keys(user).length === 0
    
        // If user object is populated
        if (!isUserEmpty) {
    
            // If user hasn't verified their email yet
            if (!user.emailVerified) {
                // User is authenticated but their email is not verified
                return <Navigate to="/email"/>
            }
    
            // If email is verified, return protected route
            return children
        }
    
        return null
    }
    

  2. If you are sure that user object is eventually being set and user has emailVerified property then you can add a conditional rendering check in the ProtectedRoute component.

    const ProtectedRoute = ({ children }) => {
      const { user } = useContext(UserAuth)
    
      if (!user) {
        // User object is not available yet, render a loading state or redirect to a loading page if desired.
        // here we are just returning null.
        return null
      }
    
      if (!user.emailVerified) {
        // User is authenticated but their email is not verified
        return <Navigate to="/email" />
      }
    
      // User is authenticated and email is verified, render the protected content
      return children;
    };
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search