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
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:
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.