I'm building a todo web app using firebase, react and redux. So I have protected rotes such as "/todos/add", "/dashboard" and guests Routes as "/","/signup", "/login" which can't be accessed if you're are authenticated.
My problem is when I'm authenticated in the "/todos/add" route and I reload the page the app reloads and my auth actions are dispatched putting my user in the redux store, but I'm redirect to "/login" an then to "/dashboard". but I want to be on the same page before the reload. my code:
PrivateRoute.js
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
import PropTypes from "prop-types";
const PrivateRoute = ({
isAuthenticated,
isLoading,
component: Component,
...rest
}) => (
<Route
{...rest}
render={props =>
isAuthenticated ? (
<Component {...props} {...rest} />
) : (
<Redirect to="/login" />
)
}
/>
);
const mapStateToProps = state => ({
isAuthenticated: !!state.user.uid,
isLoading: state.user.isLoading
});
PrivateRoute.propTypes = {
component: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
export default connect(mapStateToProps)(PrivateRoute);
GuestRoute.js
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
import PropTypes from "prop-types";
const GuestRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
!isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to="/dashboard"
/>
)
}
/>
);
const mapStateToProps = state => ({
isAuthenticated: !!state.user.uid,
isLoading: !!state.user.isLoading
});
GuestRoute.propTypes = {
component: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
export default connect(mapStateToProps)(GuestRoute);
App.js
import React from "react";
import { Route, Switch } from "react-router-dom";
// COMPONENTS
import HomePage from "./components/pages/homepage/HomePage";
import SignupPage from "./components/pages/signuppage/SignupPage";
import LoginPage from "./components/pages/loginpage/LoginPage";
import DashboardPage from "./components/pages/dashboardpage/DashboardPage";
// HOC
import Layout from "./components/hoc/layout/Layout";
// ROUTES
import PrivateRoute from "./routes/PrivateRoute";
import GuestRoute from "./routes/GuestRoute";
import AddTodosPage from "./components/pages/todos/add/AddTodosPage";
const App = () => (
<Layout>
<Switch>
<Route path="/" exact component={HomePage} />
<GuestRoute path="/signup" exact component={SignupPage} />
<GuestRoute path="/login" exact component={LoginPage} />
<PrivateRoute path="/todos/add" exact component={AddTodosPage} />
<PrivateRoute path="/dashboard" exact component={DashboardPage} />
</Switch>
</Layout>
);
export default App;
FIX Set isLoading to true by default, which would be the value at refresh. (Set isLoading to false after firebase authentication.) And condition in <PrivateRoute> should be (isLoading || isAuthenticated ). But Important : Here isLoading and isAuthenticated should be set together after firebase authentication response. Or isAuthenticated should be set accordingly before isLoading is set to false.
Explanation
As soon as you refresh in <PrivateRoute> of /todos/add, it'll look for isAuthenticated value. Here seems it's false, and therefore will be redirected to /login route. When in <GuestRoute> of /login, it'll again look at !isAuthenticated value. By this time isAuthenticated value seems to be true making it redirect back to /dashboard.
So from this observation it can be seen that isAuthenticated value is not set to true as soon as refreshed. But rather this firebase authentication request is asynchronous action, where there will be some notable in response. But before the response, redirecting happens taking default isAuthenticated value or undefined as value. In-between this re-directions, firebase authentication call will be done and set isAuthenticated to true for the final redirection to /dashboard. But here make use of isLoading value to delay redirections. Set isLoading to true by default, which would be the value at refresh. (Set isLoading to false after firebase authentication.) And condition in <PrivateRoute> should be (isLoading || isAuthenticated ). IMPORTANT : Here isLoading and isAuthenticated should be set together after firebase authentication response. Or isAuthenticated should be set accordingly before isLoading is set to false. (Else if isLoading set to false and if isAuthenticated still remains false or undefined it'll be redirected!)
Instead of rendering your route, try to render something like 'a loading state'.
Otherwise you will end up displaying your private route to an unauthenticated user if the auth call takes some time to resolve.
const PrivateRoute = ({
component: Component,
permission: Permission,
waitingForAuth: Waiting,
...rest
}) => (
<Route
{...rest}
render={props => {
if (Waiting) return <LinearProgress />;
return Permission ? <Component {...props} /> : <Redirect to="/" />;
}}
/>
);
I was facing a similar issue with my stack (firebase + vuex + vuejs). My solution was to add a userLoading State to my store, which is a Promise<void>;.
My design is to pass the auth instance to my store state constructor and resolve the userLoading promise when the auth state changes.
My route uses await for the promise, then checks if there is a user.
Here is my state constructor:
public constructor(auth: firebase.auth.Auth) {
this.user = auth.currentUser;
this.userLoading = new Promise(resolve => {
auth.onAuthStateChanged(async (user: firebase.User | null) => {
if (user) {
await user!.getIdToken(true);
const decodedToken = await user!.getIdTokenResult();
this.stripeRole = decodedToken.claims.stripeRole;
}
this.user = user;
resolve();
});
});
}
Related
Am trying to implement the ProtectedRoute. But when i pass authentication, meaning am authenticated rightly from the auth API, get my token, i get pushed to the main page without a problem.
However, when i type in the browser the URL manually to the protected route, i get redirected to the Login page.
Am storing the isAuthenticated flag in a redux store using redux-toolkit. Check out my code below
ProtectedRoute.jsx
import React, { useState, useEffect } from 'react';
import { Route, Redirect } from "react-router-dom";
import { useSelector } from 'react-redux'
const ProtectedRoute = ( { component: Component, ...rest }) => {
const isLoggedIn = useSelector(state => state.auth.isAuthenticated);
const [ isAuthed, setAuthed ] = useState(false);
useEffect(() => {
setAuthed(isLoggedIn);
}, [isLoggedIn]);
return (
<Route
{...rest}
render={
props => (
isAuthed === true
? <Component {...props} />
: <Redirect to={{ pathname: '/', state: { from: props.location} }}/>
)
}
/>
)
}
export default ProtectedRoute
The composition of the App.js are as follows
<Router>
<Switch>
<Route exact path="/" component={SignIn} />
<ProtectedRoute exact path="/main" component={ Dashboard } />
<Route path="*">
<div>404 Not found</div>
</Route>
</Switch>
</Router>
I tried passing this login as a prop from the App.js to the ProtectedRoute but still i cant get the result i want.
What i realized though is that there's double rendering. On the first time, reading the isLoggedIn variable produces a false result and therefore forces a redirect.
Typing URL manually or copying the URL to another tab destroys the OLD state. This happens when we try to get our state from redux Store using useSelector.
I solved it by using backend code and providing expiry time for each user logged in and than linked that user with store's State.
To check if user's time is expired or not i used npm package Decode and this small code.
const token = user?.token; //this user is srored in localstorage
// JWT..
if (token) {
const decodedToken = decode(token);
if (decodedToken.exp * 1000 < new Date().getTime()) logout(i); //here is what you
// need at frontend
}
setUser(JSON.parse(localStorage.getItem("profile")));
I am building a backend admin app, and it will not stay on the correct route/page when refreshing the browser.
When the user logs in, it redirects to /dashboard, updates the state with user info and access token, sets a refresh token on the server in a httponly cookie.
When the access token expires and a route gets a 401 it calls check refresh token and gets a new access token and updates state. All of this works great except on refresh.
I believe it may have something to do with the HashRouter maybe? Not sure but I'm pulling out my hair because I'm sure it's something small I'm missing.
When visiting the page I've created a PrivateRoute for all traffic other than /login.
App.js
import React, {Component} from 'react';
import {HashRouter, Route, Switch} from 'react-router-dom';
import PrivateRoute from "./components/PrivateRoute";
import './scss/style.scss';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
// Pages
const Login = React.lazy(() => import('./views/pages/login/Login'));
// Containers
const TheLayout = React.lazy(() => import('./containers/TheLayout'));
class App extends Component {
render() {
return (
<HashRouter>
<React.Suspense fallback={loading}>
<Switch>
<Route exact path="/login" name="Login" render={props => <Login {...props}/>}/>
<PrivateRoute path="/" name="Home" render={props => <TheLayout {...props}/>}/>
</Switch>
</React.Suspense>
</HashRouter>
);
}
}
export default App;
In PrivateRoute is where I put the initial check whether there is the token and isLoggedIn state, if not it dispatches my refresh token function to update the state, which also functions properly, but still redirects to /login.
PrivateRoute.js
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {useDispatch, useSelector} from "react-redux";
import axios from "axios";
import {eraseToken, getRefreshedToken} from "../store/auth";
import store from "../store";
axios.defaults.withCredentials = true;
axios.interceptors.request.use(req => {
console.log(req)
const token = store.getState().auth.user.token;
if (token) {
req.headers.Authorization = 'Bearer ' + token;
} else {
//req.headers.Authorization = null;
}
return req;
});
axios.interceptors.response.use(res => {
return res;
}, async (error) => {
if (401 === error.response.status) {
if ('/refresh' !== error.response.config.url) {
await store.dispatch(getRefreshedToken());
} else {
store.dispatch(eraseToken());
}
} else {
return Promise.reject(error);
}
}
);
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
console.log(token, isLoggedIn);
if (!isLoggedIn || !token) {
dispatch(getRefreshedToken());
}
// check again after state update
token = useSelector(state => state.auth.user.token);
isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return (
<Route {...rest} render={props => (
(token && isLoggedIn)
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}}/>
)}/>
)
}
export default PrivateRoute;
This is where I'm confused. Once I log in I get redirected fine to /dashboard because token etc exists in state, however if I refresh the page while at /dashboard it calls dispatch(getRefreshedToken()); which works fine, and updates the state with the correct values, which should then just render the component (I assumed) instead of redirecting back to /login.
If I manually change the url and replace /login with /dashboard it views the dashboard fine still as the state exists properly.
The PrivateRoute renders a component called TheLayout, which renders a component called TheContent which maps routes to views to display.
routes.js
const routes = [
{ path: '/', exact: true, name: 'Home' },
{ path: '/dashboard', name: 'Dashboard', component: Dashboard },
{ path: '/users', name: 'Users', component: Users }
]
export default routes;
TheContent.js
import React, {Suspense} from 'react';
import {Redirect, Route, Switch} from 'react-router-dom';
import {CContainer, CFade} from '#coreui/react';
// routes config
import routes from '../routes';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
const TheContent = () => {
return (
<main className="c-main">
<CContainer fluid>
<Suspense fallback={loading}>
<Switch>
{routes.map((route, idx) => {
return route.component && (
<Route
key={idx}
path={route.path}
exact={route.exact}
name={route.name}
render={props => (
<CFade>
<route.component {...props} />
</CFade>
)}/>
)
})}
<Redirect from="/" to="/dashboard"/>
</Switch>
</Suspense>
</CContainer>
</main>
)
}
export default React.memo(TheContent);
Not sure if that might have something to do with it?
UPDATE
I realized that the code was not waiting on dispatch to finish updating the state, so I changed the PrivateRoute to instead use useEffect with dependencies, and got it working how I needed.
However, trying to understand useEffect better, it's firing my refresh and calling the server multiple times once my cookie expires. Is there any way to stop this from happening? Here is my updated Private Route.
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
const history = useHistory();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
let isRefreshingToken = useSelector(state => state.auth.isRefreshingToken);
useEffect(() => {
if (!isLoggedIn && !token && !isRefreshingToken) {
(async () => {
await dispatch(getRefreshedToken())
.catch(err => {
history.push('/login');
});
})();
}
}, [token, isLoggedIn, isRefreshingToken, dispatch, history])
return (
<Route {...rest} render={props => (
<Component {...props} />
)}/>
)
}
I have the initial login configured where a user is able to login and get redirected to home, however, I'm running into an issue updating the state using { useEffect }. The state [isAuthenticated] appears to be updating correctly after viewing the console (see screenshot below), however, when I try to navigate to '/home' the protected route restricts me for some reason.
Console.log
False represents the initial state of [ isAuthenticated ] and True represents the state after { useEffect } runs.
App.js
import "./App.css";
import Login from "./Components/Login";
import Home from "./Components/Home";
import ProtectedRoute from "./Components/ProtectedRoute";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
async function setLoginStatus() {
try {
await axios.post("/checkIfSessionExists").then((response) => {
if (response.status === 200) {
setIsAuthenticated(true);
console.log(isAuthenticated)
} else {
setIsAuthenticated(false);
}
});
} catch (error) {
console.log(error)
}
}
setLoginStatus();
}, [isAuthenticated]);
return (
<div className="App">
<Router>
<Switch>
<Login
exact
path="/"
component={Login}
setIsAuthenticated={setIsAuthenticated}
isAuthenticated={isAuthenticated}
/>
<ProtectedRoute
exact
path="/home"
component={Home}
isAuthenticated={isAuthenticated}
/>
</Switch>
</Router>
</div>
);
}
export default App;
ProtectedRoute.js
import { Route, Redirect } from "react-router-dom";
import axios from "axios";
import { Component, useState } from "react";
const ProtectedRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/" />
)
}
/>
);
export default ProtectedRoute;
Here are the steps of what happens which leads your code to not work the way you expected:
initially isAuthenticated is false
so the user will be routed to the Login component no matter what url they input
after the component was rendered, useEffect runs. you send a request and then set isAuthenticated to true
now the component rerenders, but the user stays in the Login component
The problem is that you need to reroute any users that are authenticated in to a different route (in your case, Home) after they were in the Login component to authenticate
The following example works and will do the following:
if the user is authenticated:
the user can navigate to the /home route
if they go to a different route, it will redirect them to /home
if the user is NOT authenticated:
the user can navigate to the / route
if they go to a different route, it will redirect them to /
I am using <>...</> which is equivalent to <React.Fragment>, which allows you to include adjacent elements in it, which is otherwise not allowed
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
...
return (
<div className="App">
<Router>
<Switch>
{isAuthenticated
?
// the user is autenticated
<>
<Route path="/home" component={Home} />
<Redirect to="/home" />
</>
:
// the user is not autenticated
<>
<Route path="/" component={Login} />
<Redirect to="/" />
</>
}
</Switch>
</Router>
</div>
);
}
Currently I have the following react router
<BrowserRouter>
<Switch>
{/* It should display the register page if the isAuthenticated state is true */}
<Route exact path="/register" render={(props) => ((this.state.isAuthenticated) ? <RegisterPage /> : <NewLandingPage {...props} /> )} />
<Route exact path="/login" render={(props) => <LoginPage />} />
</Switch>
</BrowserRouter>
In my App.js
And then in the constructor of my App.js I have the following states, and authservice call
this.state = {
isAuthenticated: false
}
isAuthenticated().then((result) => {
if (result == true) {
this.state.isAuthenticated = true;
console.log("its true authenticated");
console.log(this.state.isAuthenticated);
console.log("----")
} else {
this.state.isAuthenticated = false;
console.log("its false not autheticated");
console.log(this.state.isAuthenticated);
console.log("----")
}
});
By default, my isAuthenticated state is false. So i wouldnt be able to display "/register" if I hadnt an auth service.
But I have implemented an isAuthenticated() to make the app.js check if theres a valid user. It actually displays the desired data. For example, if im autheticated, the result would be true, and I would receive the following in my console.
console.log("its true authenticated");
console.log(this.state.isAuthenticated); //true
But seems that the browserrouter is not taking into effect the isAuthenticated() method, and is relying solely in the default value, which is false, so It will always hide my register page will never be displayed besides me having an authservice there
this.state = {
isAuthenticated: false
}
Any idea of why is my browser not detecting the states change?
EDIT:
isAuthenticated() function by request
import axios from 'axios'
import Cookies from 'js-cookie';
export default function isAuthenticated(){
var accesstoken = Cookies.get('accesstoken');
return axios({ method: 'post', url: 'http://localhost:3003/verify', headers: { Authorization: `Bearer ${accesstoken}` } })
.then(function (response) {
if(response.data.status === "valid"){
console.log("service valid")
return true;
}else{
console.log("service invalid")
return false;
}
});
}
When you change the state with redux(for example with an action you bring the component from actions in redux, and it sets new state in the reducer), it will effect after the component mount, there is a way to render the component(If the props have changed) and get the new state status, with ComponentDidUpdate method:
So the actions effect the global state(in reducer), and you bring this state to your component. ComponentDidUpdate will take a look and when the state you bring with the props has changed, it will update the component and allow you to execute setState
ComponentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.isAuthenticated !== prevProps.isAuthenticated) {
this.setState({
isAuthenticated:true
});
// I suppose you call the state from the reducer by this.props.isAuthenticated
}
}
Also for checking the authentication state, I am using an auth.js util like this, it gets the authenticated state from reducer, makes a redirect to login page instead of the component, if authenticated is false:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props =>
authenticated === true ? (
<Redirect to="/user" />
) : (
<Component {...props} />
)
}
/>
);
const mapStateToProps = state => ({
authenticated: state.user.authenticated
});
AuthRoute.propTypes = {
user: PropTypes.object
};
export default connect(mapStateToProps)(AuthRoute);
so in my app.js, I can check the auth state and redirect if necessary:
import AuthRoute from "./util/AuthRoute.js";
<Switch>
<Route exact path="/user" component={home} />
<AuthRoute exact path="/login" component={userLogin} />
<AuthRoute exact path="/signup" component={userSignup} />
<Route exact path="/ticketList" component={ticketList} />
</Switch>
Possibly the issue is that the authentication code is in your constructor which gets called once. You might want to try it a slightly different way. Create a const (outside your class) to hold the switchable component content:
export const RegisterOrLandingPage = () => {
if (isAuthenticated()) {
return <RegisterPage />;
}
return <NewLandingPage />;
}
Your isAuthenticated() code should ideally be in a function too, rather than sat in the constructor or used as a state, and abstracted out to its own component (i.e., callable from anywhere, usable by anyone).
Then your route can be slightly different:
<Route exact path="/register" component={RegisterOrLandingPage} />
When some user try to access any page of my application, a React Router middleware is called to check if the user is logged in. The problem is that to access the Redux store I need to use store.getState() and if I use store.listen(...) I receive an error ('component' doesn't exist).
const PrivateRoute = ({ component: Component, ...rest }) => { // Receive component and path
return (
<Route { ...rest } render={(props) => {
return store.getState().login.token.status === 200
? <Component { ...props } />
: <Redirect to="/login" />
}}
/>
)
}
// store.subscribe(PrivateRoute) // Error when some page is requested
export default PrivateRoute;
I know that to listen changes on this function I need to pass a component, but I think it's not possible because it's a middleware. Do I really need to listen for changes in this case, if yes, how can I do this (how to mock this type of thing) ?
Obs : Example <PrivateRoute path="/private" component={() => <h1>Private</h1>}/>
Try this.
const PrivateRoute = ({ component: Component, hasLogin, ...rest }) => (
<Route
{...rest}
render={props => (
hasLogin ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/login'
}}
/>
)
)}
/>
)
export default connect(state => ({
hasLogin: state.login.hasLogin
}))(PrivateRoute)
In the switch call like this
<PrivateRoute exact path='/' component={some page} />
Using Redux
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
const PrivateRoute = ({ component: Component, auth: auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
After login have you dispatch an action to store the the login values in store.
if yes check that.
and i think you can store the login token in localStorage read about WebStorage and easily get it back using store.get('keyname').