Role based react-router
Asked Answered
D

3

11

What is the best way to conditionally render routes in react-router based on user role. I have a case where not all roles have permission to view certain routes. And also I need to deal with subroutes. So if one of main routes is something like /posts I want only admin and student to access that route and its subroutes (posts/today for example). I am using react-router version 5.3.2.

Driftwood answered 17/1, 2022 at 15:8 Comment(1)
Define Roles and Permissions at the backend. After login (authentication), control the routes based on permissions (authorization). Create a component like Check which would check if current user is having enough permissions to see this page or not. Google for keywords "authorization" in general and in react app. Maybe you will find some code example. Before you code, it is important to understand it. So, if you focus on understanding, you can write code in your own way.Subprincipal
P
12

I has a similar issue with react-router-dom v6, but you can tweak it to your benefits

First create a source of truth wheither a user can or can't access some route, for roles based auth I'd think of a hook like

export function useUserRoles() {

    // some logic or api call to get the roles
    // for demonstration purposes it's just hard coded
    const userRoles: Array<typeof UserRoles[number]> = ['admin', 'root'];

    // return the current user roles
    return userRoles;
}

Then we make a component that can decide using that hook weither to render the route or redirect to login(or some page)

export function RolesAuthRoute({ children, roles }: { children: ReactNode, roles: Array<typeof UserRoles[number]> }) {

    const userRoles = useUserRoles();

    const canAccess = userRoles.some(userRole => roles.includes(userRole));


    if (canAccess)
        return (
            <Fragment>
                {children}
            </Fragment>
        );

    return (<Navigate to="/dashboard/login" />);
}

then the route defined wraps that component to act as a guard that decide if you can or can't access that route

            <Route
                path="users"
                element={
                    <Suspense fallback={<ProjectLayoutLoader />}>

                        <RolesAuthRoute roles={['admin']}>

                            <ProjectUsersPage />

                        </RolesAuthRoute>

                    </Suspense>
                } />

Photovoltaic answered 18/1, 2022 at 5:30 Comment(0)
Q
0

i have two role "parent" and "manager" and i write this code and work it for me:

function App(props) {

  return (

          <div className="App ">
            <Routes>
                    <Route exact path="/" element={<Home />} />
             
           <Route exact path="/" element={<ProtectedRoute />}>
              <Route exact path="/admin" element={<StudentLists />} />
              <Route exact path="/student_pk_api/:id" element={<IdFilter />} />
              <Route exact path="/absentes" element={<AbsentStu />} />
              <Route exact path="/students_by_field_and_level_:grade_api/:studyfield_code" element={<FieldGradeFilter />} />
              <Route exact path="/absent_yesterday_api" element={<YesterdayAbs />} />
              <Route exact path="/absent_today_api" element={<TodayAbs />} /> 
           </Route>
           <Route exact path="/" element={<ProtectedUserRoute />}>
           <Route exact path="/users" element={<SchoolDit />} />
           </Route>
             </Routes>
          </div>
    

  );
}

for cheking roles i write two function:

for manager role:

const  ProtectedRoute = () => {
    const [manager, setmaneger] = useState(localStorage.getItem("role")==="manager"?true:null);

    return manager ? <Outlet /> : <Navigate to="/" />;
}

  export default ProtectedRoute

for parent role:

const  ProtectedUserRoute = () => {
   
    const [parent, setParent] = useState(localStorage.getItem("role")==="parent"?true:null);

    return parent ? <Outlet /> : <Navigate to="/" />;
}
export default ProtectedUserRoute
Quatre answered 29/4, 2022 at 13:8 Comment(0)
A
0

Of course, there are many ways to implement this. I'll share my working example with React Router DOM v6.

The steps :

  1. Using a hook to get authentication data (you can use context or Redux).
  2. create the a mapping objects for the protected paths with the roles data
  3. Defining routes with unique IDs related to each path's roles.
  4. Combining everything in the ProtectedRoute and retrieving path data using useOutlet.

So lets get started.

1. useAuth hook

const token = await getToken() 

const useAuth = () => {
  const [user, setUser] = useState<user | null>(null);

  useEffect(() => {
    if (token) {
      const isTokenValid = checkTokenValidity(token);
      const userData = isTokenValid ? decodeToken(token) : null;

      if (isTokenValid) {
        setUser(userData);
      } else {
        setUser(null);

        removeCookie("token");
      }
    } else {
      setUser(null);
    }
  }, []);

  return { user };
};

** I assume the inner methods are stright forawrd and no need to expand

Now let's define the router and create the roles mapping object.

2. Define path role mapping object

export const paths = {
  boo: { label: "Boo", path: "boo", roles: null },
  foo: { label: "Foo", path: "foo", roles: ["admin"] },
  zoo: { label: "API", path: "api", roles: ["admin", "manager"] },
};
  • I like this approach because I can also use this mapping for the navigation bar, but you can decide which properties to include.

3. Define the router and utilze the path maping object

const createRouteId = (key: keyof typeof paths) => {
  return JSON.stringify({ [key]: paths[key].roles });
};

export const router = createBrowserRouter([
  {
    path: "/",
    element: <ProtectedRoute />,
    children: [
      { path: paths.boo.path, element: <Boo/>, id: createRouteId("boo") },
      { path: paths.foo.path, element: <Foo />, id: createRouteId("foo") },
      { path: paths.zoo.path, element: <Zoo/>, id: createRouteId("zoo") }
    ],
    errorElement: <ErrorPage />,
  }]

The id prop should be uniqe and include the roles data per path using the method createRouteId

  1. Combine all in the ProtectdRoute component and utilze the useOutlet
const isRoleValid = (userRoles: string[] | undefined, outletRole: string[] | null) => {
  if (!userRoles) return false;
  if (!outletRole) return true;
  return userRoles.some((role) => outletRole.includes(role));
};

function ProtectdRoute() {
  const { user } = useAuth();
  const location = useLocation();
  const [locationRoles, setLocationRoles] = useState(null);
  const getRouteIdFromOutlet = useOutlet();

   useEffect(() => {
    const routeLocationData = getRouteIdFromOutlet?.props.children.props.match.route;
    const locationKey = routeLocationData?.path;
    const locationRolesObj = routeLocationData?.id && JSON.parse(routeLocationData?.id);
    if (locationRolesObj && locationKey) {
      setLocationRoles(locationRolesObj[locationKey]);
    }
  }, [user, location.pathname]);

  if (!isRoleValid(user?.roles, locationRoles))
    return <ErrorPage errorProp={{ status: 403, message: "role unauthorized" }} />;

  return user ? <Outlet /> : <Login />;
}

the useOutlt is pretty nested but it does contain the id information which include our roles per spesific path.

Ambie answered 12/3 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.