react router dom v6 private route with axios [duplicate] - javascript

This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed last month.
I have App.js only contains route:
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Login />} path="/login"></Route>
<Route element={<NotFound />} path="*"></Route>
<Route element={<PrivateRoutes />}>
<Route element={<Home />} path="/"></Route>
</Route>
</Routes>
</BrowserRouter>
);
}
And my PrivateRoute.js is
function PrivateRoutes() {
const key = sessionStorage.getItem("key")
// I would like to use Axios here to verify if the token is legal
return (
key === "123" ? <Outlet /> : <Navigate to="/login"></Navigate>
)
}
I would like to use Axios to verify if the token is legal, but Axios is asynchronous, so I can not get response and compare with local token.
What should I do ?

You can make an async/await function of your PrivateRoutes :
function PrivateRoutes() {
const [component, setComponent] = React.useState(null);
const key = sessionStorage.getItem("key");
async function getData() {
await axios.get('/verify-token', {
params: {
token: key
}
}).then(response => {
if (response.data.valid) {
setComponent(<Outlet />);
} else {
setComponent(<Navigate to="/login"></Navigate>);
}
}).catch(error => {
console.error(error);
});
}
React.useEffect(() => {
getData();
}, []);
return component;
}

Related

protected routes in react js app always returns undefined

here is my protectedroute component
am using react-router-dom v6 and accessing the token from localStorage
and either ways user is always returning undefined
import { Outlet, Navigate} from "react-router-dom";
import axios from "axios";
const ProtectedRoute = () => {
const userAuth = () => {
axios.get("http://localhost:5000/isUserAuth", {
headers: {
"x-access-token": localStorage.getItem("token")
}}).then((response) => {
console.log(response.data)
if(response.data.auth) {
console.log(true)
return true;
} else {
console.log(false)
return false;
}
})
}
let auth = userAuth()
console.log("auth",auth)
return (
auth? <Outlet/> : <Navigate to="/"/>
)
}
export default ProtectedRoute
my app.js
function App() {
return (
<BrowserRouter>
<ToastContainer position='top-center'/>
<Routes>
<Route element={<ProtectedRoutes/>}>
<Route exact path='/home'
element={< Home />}/>
<Route exact path='/add'
element={< AddCust />} />
<Route exact path='/update/:id'
element={< AddCust />} />
<Route exact path='/view/:id'
element={< View />} />
<Route exact path='/table'
element={< Table />} />
<Route exact path='/edit-order/:id'
element={< Table />} />
<Route exact path='/orders'
element={< Orders />} />
</Route>
<Route exact path='/' element={< Login />} />
</Routes>
</BrowserRouter>
);
}
export default App;
this is what is consoled logged
enter image description here
which is weired whether a token exists or not auth is always undefined
Nothing is actually returned from the userAuth function, so auth is undefined. While you could return the axios Promise object, this will make userAuth an asynchronous function and not usable as a ternary condition to conditionally render the Outlet component or redirect.
A solution then is to convert auth to a React state, updated in the GET request flow, and conditionally render null or a loading indicator until the auth status resolves.
Example:
const ProtectedRoute = () => {
const { pathname } = useLocation();
const [auth, setAuth] = React.useState(); // initially undefined
React.useEffect(() => {
const checkAuth = async () => {
try {
const response = await axios.get(
"http://localhost:5000/isUserAuth",
{
headers: {
"x-access-token": localStorage.getItem("token")
},
}
);
setAuth(!!response.data.auth);
} catch(error) {
// handle error, log, etc...
setAuth(false);
}
};
checkAuth();
}, [pathname]); // trigger auth check on route change
if (auth === undefined) {
return null; // loading indicator/spinner/etc
}
return auth
? <Outlet/>
: <Navigate to="/" replace state={{ from: pathname }} />;
};

Configuring private routes with react-router-dom v6

When the visitor goes to / (home), I want him to be redirected to /connexion" if he is not connected. I created Private routes for that, which works fine. Now, I want to implement the logic that will redirect the user according to if he is connected or not.
I have these routes in App.jsx:
import ProtectedRoutes from './middlewares/ProtectedRoutes';
return (
<>
<Routes>
<Route path="/connexion" element={<Login />} />
<Route path="/auto-connexion" element={<AutoConnect />} />
<Route element={<AppLayout />} >
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<Home />} />
<Route path="/logical-entity-selection" element={<LogicalEntitySelection />} />
<Route path="/produits" element={<Products />} />
<Route path="/produits/:id" element={<Product />} />
<Route path="/actualites" element={<Articles />} />
<Route path="/actualites/id" element={<Article />} />
<Route path="/mes-parametres" element={<MyAccount />} />
<Route path="/mes-outils-et-services" element={<MyToolsAndServices />} />
<Route path='*' element={<Login />} />
</Route>
</Route>
</Routes>
</>
);
An this ProtectedRoutes.tsx :
import { useEffect, useState } from "react"
import { Navigate, Outlet } from "react-router-dom"
import jwt_decode from "jwt-decode"
import instance from "../api/axios"
export default function ProtectedRoutes() {
const [isLoggedIn, setIsLoggedIn] = useState(Boolean)
const token = window.localStorage.getItem("token") || ''
const decodedToken: any = jwt_decode(token)
const uuid = decodedToken.uuid
const isAuth = async () => {
await instance.get(`/users/${uuid}`, {
headers: {
'Authorization': `Bearer ${token}`
}
}).then((res) => {
console.log(res)
if (res.status === 200) setIsLoggedIn(true)
return setIsLoggedIn(false)
})
}
useEffect(() => {
isAuth()
}, [isLoggedIn])
return isLoggedIn ? <Outlet /> : <Navigate to={'/connexion'} />
}
The problem is that with this code, React render the Login Component because it returns always false, even if I have a status 200 after my request and set the new state to true.
How can I make my request FIRST, then set the new state for isLoggedIn, then decide to render Login component or Home component ?
I hope I made it clear. Don't hesitate to question me if not. Any help on this ?
You would need a loading state in addition to what you have to make it work correctly, I called it isChecking. Also the below block of code that you have should be changed, because you are setting isLoggedIn to true and right after to false.
if (res.status === 200) setIsLoggedIn(true)
return setIsLoggedIn(false)
Solution:
import { useEffect, useState } from "react"
import { Navigate, Outlet } from "react-router-dom"
import jwt_decode from "jwt-decode"
import instance from "../api/axios"
export default function ProtectedRoutes() {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isChecking, setIsChecking] = useState(true)
const token = window.localStorage.getItem("token") || ''
const decodedToken: any = jwt_decode(token)
const uuid = decodedToken.uuid
const isAuth = async () => {
await instance.get(`/users/${uuid}`, {
headers: {
'Authorization': `Bearer ${token}`
}
}).then((res) => {
console.log(res)
if (res.status === 200) setIsLoggedIn(true)
setIsChecking(false);
return;
})
}
useEffect(() => {
isAuth()
}, [isLoggedIn])
if(isChecking) return <p>Checking....</p>
return isLoggedIn ? <Outlet /> : <Navigate to={'/connexion'} />
}

Redirect to dashboard after successful payment

This is my app.js where all the routes
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route element={<PrivateRoute />}>
<Route exact path="/dashboard" element={<Dashboard />} />
<Route exact path="/payment" element={<Payment />} />]
</Route>
<Route exact path="/login" element={<Login />} />
</Routes>
</Router>
This is my PrivateRoute component
function PrivateRoute({ fetchMe, ...props }) {
const [type, setType] = useState("xxxxx");
const isAuthenticated = localStorage.getItem("authToken");
const navigate = useNavigate();
const [lodar, setLodar] = useState(false);
useEffect(() => {
setLodar(false);
if (isAuthenticated) {
(async () => {
const {
value: { user },
} = await fetchMe();
console.log({ data: user.step1 });
if (user.step === 1) {
navigate("/payment");
}
setLodar(false);
})();
}
}, []);
return (
<Spin indicator={antIcon} spinning={lodar}>
{isAuthenticated ? (
<>
<Header type={type} setType={setType} />
<Outlet context={[type, setType]} />
</>
) : (
<Navigate to="/login" />
)}
</Spin>
);
}
export default PrivateRoute;
So what I want to do here is to always redirect the user to the "/payment" after signup. and if the user again comes after login then it will again redirect it to the payment page so for that I am keeping a flag in my database user.step and checking by api call on the PrivateRoute component.
The issue is it loads the "/dashboard" page before the fetchUser api call which should not happen and show some lodar before. How can I do that?
Is there any better approach doing this since I always have to make an api call?
Kindly help!!!
Assuming <Spin indicator={antIcon} spinning={lodar}> is conditionally rendering either a loading spinner/indicator or the wrapped children then I think the issue is just the initial lodar state value. It doesn't appear the lodar state is ever toggled true.
I suggest starting with an initially true state so the component doesn't immediately render the Outlet or redirect when the component mounts, prior to any auth checks happening via the useEffect hook.
Example:
function PrivateRoute({ fetchMe, ...props }) {
const [type, setType] = useState("xxxxx");
const isAuthenticated = localStorage.getItem("authToken");
const navigate = useNavigate();
const [lodar, setLodar] = useState(true); // <-- initially true
useEffect(() => {
setLodar(true); // <-- toggle true when starting async logic
if (isAuthenticated) {
(async () => {
const {
value: { user },
} = await fetchMe();
console.log({ data: user.step1 });
if (user.step === 1) {
navigate("/payment");
}
setLodar(false); // <-- clear loading when complete
})();
}
}, []);
return (
<Spin indicator={antIcon} spinning={lodar}>
{isAuthenticated ? (
<>
<Header type={type} setType={setType} />
<Outlet context={[type, setType]} />
</>
) : (
<Navigate to="/login" />
)}
</Spin>
);
}

Await API calls before render private route

I have protected routes that passing auth enabled true or false to allow users access auth page. Every page load, it calls the auth token API and check for valid. If token invalid, redirect to login page.
ProtectedRoute.js
const ProtectedRoute = ({isEnabled, ...props}) => {
return (isEnabled) ? <Route {...props} /> : <Redirect to="/login"/>;
};
export default ProtectedRoute;
Routes.js
import {withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [isAuth, setIsAuth] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
})()
},[isAuth])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} isEnabled={isAuth} />
<Route path="/" component={Login} />
</Switch>
)
}
App.js
const history = createBrowserHistory();
function App() {
return (
<Router history={history}>
<Routes/>
</Router>
)
}
export default App;
Dashboard.js
export const Dashboard = () => {
return (
<div class="ui form-user center raised padded segment">
<a href="/section1">
<div id="section1" class="ui floated right basic red button">
Directory #1
</div>
</a>
</div>
)
}
Problem
When auth users browse authenticated page (Dashboard), redirects to login page. The reason is that verify token API returns after ProtectedRoute rendered as isAuth equals to false.
You can use another state variable to wait for api execution before initializing routes.
const [isAuth, setIsAuth] = useState(false)
const [checked, setChecked] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
setChecked(true)
})()
},[isAuth])
Then in Routes, you can do something like:
<Switch>
{
!checked?(<React.Fragment/>):!isAuth?(<Route path="/" component={Login} />):(<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />)
}
</Switch>
I usually separate session routes and no session routes in separate hooks and it works fine.
Instead of isAuth being a boolean, you use a string or number to increase the number of states. For example rename isAuth to loginState which can be "pending", "authenticated" or "unauthenticated". Then use "pending" as the initial state and add an additional scenario. You could for example return null to render nothing, render a spinning circle, etc.
Here is an example rendering null (nothing) while the login token is being authenticated:
ProtectedRoute.js
const loginRoutes = {
pending: ( ) => null,
authenticated: (...props) => <Route {...props} />,
unauthenticated: ( ) => <Redirect to="/login"/>,
};
const ProtectedRoute = ({loginState = "pending", ...props}) => {
const LoginRoute = loginRoutes[loginState];
return <LoginRoute {...props} />;
};
export default ProtectedRoute;
Route.js
import { withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [loginState, setLoginState] = useState("pending")
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200) {
setLoginState(res.data.valid ? "authenticate" : "unauthenticated")
} else setLoginState("unauthenticated");
} else setLoginState("unauthenticated")
})()
}, [loginState])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />
<Route path="/" component={Login} />
</Switch>
)
}
As a disclaimer, I have no experience with React Router so I've kept the example as close to the original as possible.
You could use the initial state as null to differentiate the different states of your component.
null -> API token no called yet.
true -> token verified
false -> token verification failed.
export default function ProtectedRoute(props) {
const [isAuth, setIsAuth] = React.useState(false)
const history = useHistory();
React.useEffect(() => {
async function validateToken() {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(true)
return;
}
}
history.push('/login')
}
validateToken();
}, [isAuth]);
if (isAuth === null) return null;
return props.children;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={HomePage} />
<Route exact path="/login" component={Login} />
<ProtectedRoute>
<Route path="/dashboard" component={Dashboard} />
</ProtectedRoute>
</Switch>
</Router>
)
}

Validating React route with axios async function

So I am creating a web app with server (node+express) and client (cra) sides.
I have an issue with validating user according to the jwt token I set as a cookie. The validation api endpoint on server side works as it should (tested it with postman) but the problem is the async check function that returns the promise therefore the route doesn't really know is it validated since the response is pending.
Here is the api endpoint on server side:
/api/token.js
router.get('/',
jwt({secret:'token-secret' }),
function (req,res) {
console.log(req);
if(!req.user) return res.sendStatus(401);
res.sendStatus(200);
}
)
and here is the app.js on the client side: src/app.js that handles routing ( /dashboard should be available only for validated users)
function App() {
function checkToken() {
let token = Cookies.get('Access Token')
axios.get('http://localhost:9000/api/token', {
headers: {
'Authorization': `bearer ${token}`
}
}).then(res => {
return res.status;
}).catch(err => console.log(err));
}
const handleToken = async () => {
const result = await checkToken();
return result;
}
return (
<BrowserRouter>
<Route exact={true} path='/' render={() => (
<div className="App">
<Home />
</div>
)}/>
<Route exact={true} path='/dashboard' render={() => (
<div className="App">
{console.log('checktoken log', handleToken())}
{checkToken() ? <Dashboard /> : <Login />}
</div>
)}/>
<Route exact={true} path='/login' render={() => (
<div className="App">
<Login />
</div>
)}/>
</BrowserRouter>
);
}
At this point I am aware that perhaps I shouldn't be doing a validation in this way since there is probably no way I can get the return before the render, perhaps it should be done in a lifecycle hook componentWillMount but I haven't been able to introduce it to this file (or everything should be done in an entirely different file).
Thanks
p.s. I've omitted all imports and export defaults since that is not relevant here
Well, I've made it with a couple of substantial changes. First, in order to use history.push I had to refactor BrowserRouter part so now it looks like this
app.js
render() {
return (
<Router history={history}>
<Route exact path='/' component={Home} />
<Route exact path='/dashboard' component={Dashboard} />
<Route exact path='/login' component={Login} />
</Router>
);
}
Then I've decided not to use api/token.js. Instead of this api endpoint I've created a Higher Order Component that will check for cookies that have been set during login. Part that gave me most trouble is asynchronous nature of fetching cookies. That was solved with setTimeout inside getCookie function, I've called this function in componentDidMount lifecycle.
src/components/withAuth.js
state = {
data: false,
open: false,
auth: false
}
componentDidMount() {
this.getCookie();
}
getCookie(){
this.setState({
open: true,
})
setTimeout(() => {
const cookie = Cookies.get('Access Token')
if(cookie) {
this.setState({
data: true,
open: false,
auth: true
})
} else if (cookie === undefined) {
this.setState({
auth: true,
open: false
})
}
}, 700)
}
In the end, in order to protect the route I've wrapped the component with HOC
src/Views/Dashboard.js
import requireAuthentication from '../components/withAuth';
class Dashboard extends Component {
render() {
return (
<div>
<DashboardContent />
</div>
);
}
}
export default requireAuthentication(Dashboard);

Categories