I protect /dashboard/*
routes with a RequireAuth
outlet component. RequireAuth
verifies a jwt token, and this works as expected. Navigating either with the url bar, or a react-router-dom component, from an unprotected route to a protected route verifies the token before granting access.
However, once I have landed on a 'dashboard/*' component I am able to navigate within those nested components using a without re-verifying the token (if the url is typed directly in then the token is verified.
How can I alter the behavior so that:
1.) user navigates from /login to /dashboard
2.) token is verified
3.) user navigates from /dashboard to /dashboard/new-path
4.) token is verified
App.jsx routes
<Routes>
{/* protected routes */}
<Route element={<RequireAuth />}>
<Route path="dashboard/*" element={<Dashboard />} />
</Route>
{/* public routes */}
<Route index element={<Public />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Routes>
Dashboard.jsx routes
<div>
<Navbar />
<Routes>
<Route path="/" element={<DashboardTimeline />} />
<Route path="/my-paths" element={<MyPaths />} />
<Route path="/new-path" element={<NewPathWizard />} />
</Routes>
</div>
RequireAuth.jsx
import { useState, useEffect } from 'react';
import { useLocation, Navigate, Outlet } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { selectCurrentToken, logOut } from './authSlice';
import { useVerifyTokenMutation } from './authApiSlice';
import FullScreenLoading from '../../components/general/FullScreenLoading';
function RequireAuth() {
const [authenticated, setAuthenticated] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const location = useLocation();
const [verifyToken] = useVerifyTokenMutation();
const token = useSelector(selectCurrentToken);
const dispatch = useDispatch();
useEffect(() => {
const authenticateToken = async () => {
const response = await verifyToken({ token }).unwrap();
const { verified } = response.data;
setIsLoading(false);
setAuthenticated(verified);
};
if (token) {
authenticateToken()
.catch((error) => {
const { verified } = error.data;
dispatch(logOut());
setIsLoading(false);
setAuthenticated(verified);
});
} else {
setIsLoading(false);
setAuthenticated(false);
}
}, []);
if (isLoading === true) {
return <FullScreenLoading />;
}
return (
authenticated && !isLoading
? <Outlet />
: <Navigate to="/" state={{ from: location }} replace />
);
}
export default RequireAuth;
I have tried wrapping the nested routes within Dashboard.jsx
in the RequireAuth
component as well, but it made no difference.
If you want the user's authentication/token status checked for each navigation then you can add the current location's pathname
as a dependency for the useEffect
hook. When the location.pathname
changes, the effect will be run.
Example;
function RequireAuth() {
const [authenticated, setAuthenticated] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const location = useLocation();
const { pathname } = location; // <-- access current pathname
const [verifyToken] = useVerifyTokenMutation();
const token = useSelector(selectCurrentToken);
const dispatch = useDispatch();
useEffect(() => {
const authenticateToken = async () => {
setIsLoading(false); // <-- start/restart loading condition
try {
const response = await verifyToken({ token }).unwrap();
const { verified } = response.data;
setAuthenticated(verified);
} catch(error) {
const { verified } = error.data;
dispatch(logOut());
setAuthenticated(verified);
} finally {
setIsLoading(false); // <-- clear loading when finished
}
};
if (token) {
authenticateToken();
} else {
setIsLoading(false);
setAuthenticated(false);
}
}, [pathname]); // <-- add as dependency
if (isLoading) {
return <FullScreenLoading />;
}
return (
authenticated && !isLoading
? <Outlet />
: <Navigate to="/" state={{ from: location }} replace />
);
}
export default RequireAuth;