1

I am trying to implement protected routes for orders router but even the user is authenticated it always redirect to login. I have used Navigate component from react-router-dom and I have passed the isAuth state for app.js to privateRoute.js component and I received that isAuth as prop in privateRoute.js but if I don't use the navigate component then I get the isAuth true when user authenticates but it I used navigate component then it redirects to login routes before isAuth is set to true. Need help!!

    //app.js
    function App(props) {
      const navigate = useNavigate();
      const [authState, setAuthState] = useState({
        isAuth: false,
        token: null,
        userId: null,
      });
      const [showNav, setShowNav] = useState(true);
      useEffect(() => {
        if (window.location.href.includes("admin")) {
          setShowNav(false);
        }
        const token = localStorage.getItem("token");
        const expiryDate = localStorage.getItem("expiryDate");
        if (!token || !expiryDate) {
          return;
        }
        const userId = localStorage.getItem("userId");
        const remainingMilliseconds =
          new Date(expiryDate).getTime() - new Date().getTime();
        setAuthState((prevState) => {
          return {
            ...prevState,
            isAuth: true,
            token: token,
            userId: userId,
          };
        });
        setAutoLogout(remainingMilliseconds);
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [authState.isAuth]);
    
      const handleUserCredentialsSubmission = (userData, setErrors) => {
        const formData = new FormData();
        formData.append("name", userData.name);
        formData.append("email", userData.email);
        formData.append("password", userData.password);
    
        fetch("http://localhost:3080/signup", { method: "POST", body: formData })
          .then((res) => {
            if (res.status !== 200 && res.status !== 201) {
              if (res.status === 409) {
                throw new Error("Email address already exists!");
              } else {
                throw new Error("Creating a user failed!");
              }
            }
            return res.json();
          })
          .then((resData) => {
            navigate("/login");
          })
          .catch((err) => {
            setErrors((prevState) => {
              return {
                ...prevState,
                signupError: err.message,
              };
            });
            throw new Error(err);
          });
      };
    
      const logoutHandler = () => {
        setAuthState((prevState) => {
          return {
            ...prevState,
            isAuth: false,
            token: null,
          };
        });
        localStorage.removeItem("token");
        localStorage.removeItem("userId");
        localStorage.removeItem("expiryDate");
        navigate("/login");
      };
    
      const setAutoLogout = (remainingTime) => {
        setTimeout(() => {
          logoutHandler();
        }, remainingTime);
      };
    
      const handleUserlogin = (userData, setErrors, setUserCredentials) => {
        const formData = new FormData();
        formData.append("email", userData.email);
        formData.append("password", userData.password);
    
        fetch("http://localhost:3080/login", { method: "POST", body: formData })
          .then((res) => {
            if (res.status !== 200 && res.status !== 201) {
              throw new Error("Invalid Email Address & Password");
            }
            return res.json();
          })
          .then((resData) => {
            setAuthState((prevState) => {
              return {
                ...prevState,
                isAuth: true,
                token: resData.token,
                userId: resData.userId,
              };
            });
            localStorage.setItem("token", resData.token);
            localStorage.setItem("userId", resData.userId);
            const remainingMilliseconds = 60 * 60 * 1000;
            const expiryDate = new Date(
              new Date().getTime() + remainingMilliseconds
            );
            localStorage.setItem("expiryDate", expiryDate.toISOString());
            navigate("/");
          })
          .catch((err) => {
            setAuthState((prevState) => {
              return {
                ...prevState,
                isAuth: false,
                token: null,
                userId: null,
              };
            });
            setUserCredentials((prevState) => {
              return {
                ...prevState,
                email: "",
                password: "",
              };
            });
            setErrors((prevState) => {
              return {
                ...prevState,
                loginError: err.message,
              };
            });
            throw new Error(err);
          });
      };
    
      const handleAddToCart = (productId) => {
        fetch("http://localhost:3080/cart", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + authState.token,
          },
          body: JSON.stringify({
            prodId: productId,
          }),
        })
          .then((res) => {
            if (res.status !== 200 && res.status !== 201) {
              throw new Error("User does not Exists");
            }
            return res.json();
          })
          .then((resData) => {
            navigate("/cart");
          })
          .catch((err) => {
            throw new Error(err);
          });
      };
      console.log(authState.isAuth);
    
      return (
        <div className="App">
          {showNav && (
            <Nav
              token={authState.token}
              isAuth={authState.isAuth}
              onLogout={logoutHandler}
            />
          )}
          {/* <Nav /> */}
          <Routes>
            <Route
              path="/product"
              element={
                <Product isAuth={authState.isAuth} AddToCart={handleAddToCart} />
              }
            />
            <Route
              path="/product/:productId"
              element={
                <ProductDetails
                  {...props}
                  isAuth={authState.isAuth}
                  AddToCart={handleAddToCart}
                />
              }
            />
            <Route path="/cart" element={<Cart token={authState.token} />} />
            <Route
              path="/orders"
              element={
                <PrivateRoute {...authState}>
                  <Orders token={authState.token} />
                </PrivateRoute>
              }
            />
            <Route
              path="/checkout"
              element={<Checkout token={authState.token} />}
            />
            <Route path="/login" element={<Login onSingIn={handleUserlogin} />} />
            <Route
              path="/signup"
              element={<Signup onSignUp={handleUserCredentialsSubmission} />}
            />
            <Route path="/admin/*" element={<Admin />}>
              <Route
                path="product"
                element={<AddProduct token={authState.token} />}
              />
              <Route path="products" element={<AdminProducts Admin={!showNav} />} />
            </Route>
            <Route path="/" element={<Home />} />
            {/* <Route path="*" element={<Navigate to="/" />} /> */}
          </Routes>
        </div>
      );
    }
    
    export default App;

    //privateRoute.js
    const PrivateRoute = (props) => {
      console.log(props.isAuth);
      if (!props.isAuth) {
        return <Navigate to="/login" replace />;
      }
      return props.children;
    };

    export default PrivateRoute;
   
Drew Reese
  • 165,259
  • 14
  • 153
  • 181

1 Answers1

1

The authState.isAuth state's initial value looks to match the "unauthenticated" state, so when the app is initially loading/mounting and the user is on a protected route the PrivateRoute bounces them off it to the login route to authenticate.

Use an initial authState.isAuth value that isn't one of the confirmed/validated/verified authentication state values. In other words, anything other than true or false. Update the PrivateRoute component to check for this initial condition and conditionally render null or some loading indicator until the authentication status is confirm.

App

const [authState, setAuthState] = useState({
  isAuth: undefined,
  token: null,
  userId: null,
});

privateRoute.js

const PrivateRoute = ({ isAuth }) => {
  if (isAuth === undefined) {
    return null; // or loading indicator/spinner/etc
  }

  return isAuth
    ? children
    : <Navigate to="/login" replace />;
};

Note that you will need to explicitly set the authState.isAuth state for the unauthenticated logic paths, i.e. authentication fails, token expired, etc.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • I have set isAuth to undefined but still its not working. It shows me the orders route but it does not redirect to login for unauthenticated user – Rohan Rana Magar Oct 10 '22 at 07:32
  • @RohanRanaMagar I thought the issue was the code *always* redirecting to `"/login"`. Can you clarify what the issue *actually* is? Do you need a more concrete example of [route protection](/a/66289280/8690857)? – Drew Reese Oct 10 '22 at 07:35
  • you are right. I always redirecting to "/login" router even the user is authenticated. I have set isAuth State to false initially and once the user is logged in it is set to true .Here is how I create private router const PrivateRoute = (props) => { console.log(props.isAuth); if (props.isAuth === undefined) { return ; } return props.isAuth ? : ; }; – Rohan Rana Magar Oct 10 '22 at 10:55
  • here is the github link https://github.com/Rohan-mgr/MERN-Online-Store – Rohan Rana Magar Oct 10 '22 at 13:33
  • can you please have look the above github link and help me to solve the protected routes problem – Rohan Rana Magar Oct 12 '22 at 07:24
  • @RohanRanaMagar I did take a look at your repo when you first posted it. Just took a look again now to see if I'd missed anything. The code there is still using an initial false `authState.isAuth` value and now it seems the `PrivateRoute` can never redirect to `"/login"` since if `isAuth === false` is true then the `Loading` component is rendered. – Drew Reese Oct 15 '22 at 04:26
  • the problem is authState.isAuth will never true coz isAuth is set to true after a api call so before setting the isAuth state to true the privateRouter component redirect to login and it is never set to true. Need the solution for this – Rohan Rana Magar Oct 15 '22 at 06:21
  • @RohanRanaMagar That's the entire point of using an initial value that ***isn't*** true or false and waiting to render either the `children` or the `Navigate` component. If the initial `isAuth` state matches either of the "authenticated" or "unauthenticated" state then the `PrivateRoute` component is going to not wait until the auth status is confirmed. Don't start from an "unauthenticated" `false` state, start from an "I don't know the status" `undefined` state. Does this make sense? – Drew Reese Oct 15 '22 at 07:01
  • Yes it totally make sense and I have start with undefined but still it does not work? – Rohan Rana Magar Oct 18 '22 at 09:11
  • It still redirecting me to login page – Rohan Rana Magar Oct 18 '22 at 09:23
  • ` const PrivateRoute = (props) => { if (props.isAuth === undefined) { ; } console.log(props.isAuth); return props?.isAuth ? : ; }; ` – Rohan Rana Magar Oct 18 '22 at 09:26
  • @RohanRanaMagar Then it seems `props.isAuth` is always falsey for some reason. FWIW I am unable to reproduce the issue you describe. Here's a running [codesandbox](https://codesandbox.io/s/react-router-dom-v6-always-redirect-to-login-routes-even-user-is-authenticated-be31e8?file=/src/App.js) demo. If you navigate directly to `"/orders"` there's a 50% chance of already being authenticated, otherwise you can "log in" via the `"/login"` route. Can you create a *running* codesandbox demo that reproduces the issue you describe that we could inspect live? Feel free to fork from my sandbox. – Drew Reese Oct 19 '22 at 05:59