React authorizing user access

Viewed 78

beginner here. I have the following code which gives access to various pages depending on the presence of a user and him being an admin.

const [currentUser, setCurrentUser] = useState(undefined);
const [isAdmin, setIsAdmin] = useState(false);
   useEffect(() => {
     const user = AuthService.getCurrentUser();
     if (user) {
        setCurrentUser(user);
        if (user.isAdmin)
           setIsAdmin(true);
     }
  }, []);
  <Route exact path={"/"} element={<Home props={currentUser} />} />

  <Route exact path="/login" element={!currentUser ? <Login /> : <Navigate to='/' />} />
  
  <Route exact path="/watch/:id" element={isAdmin ? <Watch /> : <Navigate to='/login' />} />

  <Route exact path="/movies" element={<Movies />} />

  <Route exact path="/admin"  >
    <Route index element={isAdmin ? <AdminPage /> : <Navigate to='/' />} />    
    <Route path="moviespanel">
      <Route index element={<Moviespanel />} />              
  </Route>

Now, the conditions in 4 first routes work just fine. the one in index element does not work when isAdmin variable changes from false to true. It does not work in any different way i write it. isAdmin === true ? ... : ... etc etc

Bear in mind that the same condition is in Route /watch and there it works perfectly fine. Does it matter that it is in an index element ?

I tried to use isAdmin && which works for both conditions but i do not know how to get it to Navigate to "/" when isAdmin is false.

... Edit,

I actually found out that the condition works if I follow the paths through buttons/links, they do not work when I type in the url directly in the browser.

Is there something in the ternary operator "?" that i do not know ?

2 Answers

Would something like this works perhaps? I haven't really used react-router-dom specifically, but still.

  <Route exact path="/admin">
    { isAdmin ? (
      <Fragment>
        <Route index element={<AdminPage />} />
        <Route path="/movies" element={<AdminMovies />} />

        {/* more <Route path= ... here? */}
      </Fragment>
    ) : (
      <Navigate to='/' />
    )}   
  </Route>

Issue

Typically you'd abstract the authentication and/or roles status into a wrapper component to be used on specific routes. This issue is almost always that the initial state values for users and roles match the unauthenticated or disallowed role values. In other words, the initial state matches what you protect/guard and keep users from accessing.

Solutions

The solution is to either use a loading state or indeterminate initial state values to force the UI to wait until an authentication/role status is confirmed.

Using a loading state

const [currentUser, setCurrentUser] = useState(undefined);
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  setIsLoading(true);
  const user = AuthService.getCurrentUser();
  if (user) {
    setCurrentUser(user);
    setIsAdmin(user.isAdmin);
  }
  setIsLoading(false);
}, []);

...

if (isLoading) {
  return null; // or loading indicator/spinner/etc...
}

return (
  ...
  <Route path={"/"} element={<Home props={currentUser} />} />
  <Route path="/login" element={!currentUser ? <Login /> : <Navigate to='/' />} />
  <Route path="/watch/:id" element={isAdmin ? <Watch /> : <Navigate to='/login' />} />
  <Route path="/movies" element={<Movies />} />
  <Route path="/admin">
    <Route index element={isAdmin ? <AdminPage /> : <Navigate to='/' />} />    
    <Route path="moviespanel">
      <Route index element={<Moviespanel />} />              
    </Route>
  </Route>
  ...
);

Using indeterminate initial state values

const [currentUser, setCurrentUser] = useState(); // <-- undefined
const [isAdmin, setIsAdmin] = useState();         // <-- undefined

useEffect(() => {
  const user = AuthService.getCurrentUser();
  if (user) {
    setCurrentUser(user);     // <-- defined
    setIsAdmin(user.isAdmin); // <-- defined
  }
}, []);

...

return (
  ...
  <Route path={"/"} element={<Home props={currentUser} />} />
  <Route
    path="/login"
    element={currentUser === undefined
      ? null
      : currentUser
        ? <Navigate to='/' />
        : <Login />
    }
  />
  <Route
    path="/watch/:id"
    element={isAdmin === undefined
      ? null
      : isAdmin
        ? <Watch />
        : <Navigate to='/login' />
    }
  />
  <Route path="/movies" element={<Movies />} />
  <Route path="/admin">
    <Route
      index
      element={isAdmin === undefined
        ? null
        : isAdmin
          ? <AdminPage />
          : <Navigate to='/' />
      }
    />    
    <Route path="moviespanel">
      <Route index element={<Moviespanel />} />              
    </Route>
  </Route>
  ...
);

Wrapper components

For either solution, using a wrapper component allows for cleaner, more DRY code. Ideally you'd also move the state into a React context or similar and access directly in the wrapper components instead of passing in as props. This is just to provide an idea for code structure/organization.

Example:

import { Navigate, Outlet } from 'react-router-dom';

const PrivateRoute = ({ isLoading, user }) => {
  if (isLoading) return null;
  return user ? <Outlet /> : <Navigate to='/login' replace />;
};

const AdminRoute = ({ isLoading, isAdmin }) => {
  if (isLoading) return null;
  return isAdmin ? <Outlet /> : <Navigate to='/' replace />;
};

const AnonymousRoute = ({ isLoading, user }) => {
  if (isLoading) return null;
  return user ? <Navigate to='/' replace /> : <Outlet />;
};

...

const [currentUser, setCurrentUser] = useState(undefined);
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  setIsLoading(true);
  const user = AuthService.getCurrentUser();
  if (user) {
    setCurrentUser(user);
    setIsAdmin(user.isAdmin);
  }
  setIsLoading(false);
}, []);

...

return (
  ... public routes ...
  <Route path="/" element={<Home props={currentUser} />} />
  <Route path="/movies" element={<Movies />} />

  <Route element={<AnonymousRoute isLoading={isLoading} user={currentUser} />}>
    <Route path="/login" element={<Login />} />
  </Route>

  <Route element={<PrivateRoute isLoading={isLoading} user={currentUser} />}>
    ... authenticated routes ...

    <Route element={<AdminRoute isLoading={isLoading} isAdmin={isAdmin}/>}>
      ... admin routes ...
      <Route path="/watch/:id" element={<Watch />} />
      <Route path="/admin">
        <Route index element={<AdminPage />} />    
        <Route path="moviespanel" element={<Moviespanel />} />
      </Route>
    </Route>
  </Route>
  ...
);
Related