Search code examples
javascriptreactjsreact-routerreact-router-dom

React Protected routes issue, route in not navigating from login to dashbaord


I am building a management application with a role-based user login, I assigned all roles in my JWT token and checked them on Routes to see if the user is admin, super admin, or simple user. Now the issue I'm facing is that this check function is called before even tokens are set. Now, I know this may not be the best approach but I'm not very confident in using context API so I tried to use a simple approach here using JWT data to check for user role.

The first time I try to login, it successfully logins and sets cookies but it does not redirect to dashboard, I have to manually put dashboard in my URL to access dashboard page but if I logout once and then login again without closing browser it directs (I think maybe due to cache, I'm not sure), I have tested my logout functionality, it clears the cache and on the backend it also blacklist that token, and the routes are also protected, that I have test, if I'm not login or I don't have valid JWT token it does not let user go to any route, the only issue I'm facing is that it does not redirect to dashboard first time. From what I have deduced after debugging my code is that it calls the jwt data before even they are set, now obviously if I put dashboard in URL manually it will let me to dashboard because tokens are properly set and now they can be used so all other routes work perfect after that but first when I try to redirect after I login, it redirects before even token are set. I have tried to delay it but it somehow not working. Can someone please look into my code and give me some advice on how I can make it work. Thank you in advance.

Here are my routes

import React from "react";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import DashboardLayout from "./components/layout/dashlayout";
import AdminDashboard from "./pages/admin/dashboard";
import PageNotFoundError from "./pages/404error";
import LoginPage from "./pages/auth/login";
import ListCompaniesPage from "./pages/admin/listcompanies";
import ListUsersPage from "./pages/admin/listusers";
import { getUser } from "./lib/jwtdecode";


function App() {

  const user = getUser();
  let is_super = false;
  if (user?.super) {
    is_super = true;
  }
  console.log(is_super);

  return (
    <BrowserRouter>
      <Routes>
        <Route path="*" element={<PageNotFoundError />} />
        <Route path="/" element={<Navigate to="/login" replace />} />
        <Route path="/login" element={<LoginPage />} />

        <Route element={is_super ? <DashboardLayout replace /> : <Navigate to="/login" replace />}>
          <Route path="/dashboard" element={<AdminDashboard />} />
          <Route path="/list-companies" element={<ListCompaniesPage />} />
          <Route path="/list-users" element={<ListUsersPage />} />
          /* other routes ** /
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;


Let me share my login code as well

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import logo from "../../assets/logo.png";
import axiosInstance from "../../lib/apiconfig";
import Cookies from "js-cookie";


function LoginPage() {
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);
  const [formData, setFormData] = useState({ email: "", password: "" });
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    await axiosInstance
      .post("login/", formData)
      .then((response) => {
        Cookies.set("access", response.data.access);
        Cookies.set("refresh", response.data.refresh);
        if (response.status === 200) {
          navigate("/dashboard");
        } else {
          alert("Error logging in:", response);
        }
      })
      .catch((error) => {
        alert("Error logging in, please try again");
      })
      .finally(() => {
        setLoading(false);
      });
  };
  return (
    <div className="flex flex-col items-center justify-center h-screen">
     <form method="post" onSubmit={handleSubmit}>
     /* Form design **/  
     </form>   
    </div>
  );
}

export default LoginPage;

here is another way I tried to make it work

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import logo from "../../assets/logo.png";
import axiosInstance from "../../lib/apiconfig";
import Cookies from "js-cookie";


function LoginPage() {
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);
  const [formData, setFormData] = useState({ email: "", password: "" });
  const [loginSuccessful, setLoginSuccessful] = useState(false);

  useEffect(() => {
    if (loginSuccessful) {
      const user = getUser();
      const redirectTo = user.admin ? "/dashboard" : "/overview";
      navigate(redirectTo);
    }
  }, [loginSuccessful]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);

    await axiosInstance
      .post("login/", formData)
      .then((response) => {
        Cookies.set("access", response.data.access);
        Cookies.set("refresh", response.data.refresh);
        if (response.status === 200 && Cookies.get("access")) {
          setLoginSuccessful(true);
        }
      })
      .catch((error) => {
        console.error(error);
      })
      .finally(() => {
        setLoading(false);
      });
  };
  return (
    <div className="flex flex-col items-center justify-center h-screen">
     <form method="post" onSubmit={handleSubmit}>
     /* Form design **/  
     </form>   
    </div>
  );
}

export default LoginPage;


Solution

  • Issue

    The problem here is that your authentication flow only sets a cookie, there is nothing that triggers the App component to rerender and recompute the user and is_admin values. You've effectively a stale closure over these computed values from the time App mounted. The access works when you manually reload the page by editing the URL because this remounts App and it can read the current cookie value.

    Solution

    Ideally you'd create some sort of global state (React Context API, Redux, etc) that would hold the authentication status and any part of the app can read it at any time.

    A sort of trivial solution would be to run getUser each time a protected route is accessed instead of only once when a parent component is rendered. You can create a ProtectedRoutes component that reads and computes the current access.

    Example:

    import { Navigate, Outlet } from "react-router-dom";
    import { getUser } from "./lib/jwtdecode";
    
    export const SuperProtectedRoutes = () => {
      const user = getUser();
      const is_super = !!user?.super;
    
      return is_super ? <Outlet /> : <Navigate to="/login" replace />;
    };
    
    import React from "react";
    import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
    import DashboardLayout from "./components/layout/dashlayout";
    import AdminDashboard from "./pages/admin/dashboard";
    import PageNotFoundError from "./pages/404error";
    import LoginPage from "./pages/auth/login";
    import ListCompaniesPage from "./pages/admin/listcompanies";
    import ListUsersPage from "./pages/admin/listusers";
    import { SuperProtectedRoutes } from "../path/to/SuperProtectedRoutes";
    
    function App() {
      return (
        <BrowserRouter>
          <Routes>
            <Route path="*" element={<PageNotFoundError />} />
            <Route path="/" element={<Navigate to="/login" replace />} />
            <Route path="/login" element={<LoginPage />} />
    
            <Route element={<SuperProtectedRoutes />}>
              <Route element={<DashboardLayout replace />}>
                <Route path="/dashboard" element={<AdminDashboard />} />
                <Route path="/list-companies" element={<ListCompaniesPage />} />
                <Route path="/list-users" element={<ListUsersPage />} />
                {/* other routes */}
              </Route>
            </Route>
          </Routes>
        </BrowserRouter>
      );
    }