简体   繁体   中英

struggling to implement RBAC in react, pages won't render after successful login

Im trying to implement RBAC in my react app, Im using nodejs for backend which works perfectly fine, and in react Im using vite and react-router-dom v6. I decided to store the token and role retrieved from my server, in Session Storage for simplicity. I have 2 roles in my app, "basic" and "admin", after I successfully logged in, I redirect based the role retrieved from my server. I have an HOC as "middleware", to check role permission in my route layout as well. The redirection works fine, however, I get an empty page for both roles when I test it, which both of them should render a simple h1 tag.. What am I missing, or doing wrong in my implementation?

My login page:

import {useState} from 'react'
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

const Login = () => {
  const navigate = useNavigate()
  const [input, setInput] = useState({
    email:"",
    password:""
  })

  const handleInputChange = (e) => {
    const {name, value} = e.target;
    setInput((prev) => ({
        ...prev, [name]:value
    }))
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    console.log(input);

    const url = "http://localhost:8080/auth/login";

    try {
        const response = await axios.post(`${url}`,
        input,
        {headers:{
            'content-type':'application/json'
        }}
        );

        console.log(response);

        if(response.status === 200) {
            console.log("ok");

          const token = response.data.token;
          const role = response.data.role
          console.log({"token":token, "role":role})


          sessionStorage.setItem('token', token);
          sessionStorage.setItem('role', role);

            if(role === "admin"){
              navigate('/admin');
            } else if( role === "basic") {
              navigate('/basic');
            } else {
              navigate('/login');
            }

        }
    } catch(error) {
        console.log({"error":error.response})
    }
  }


  return (
    <div>
      <h1>Log In</h1>
        <form onSubmit={handleSubmit}>
            <p>email:</p>
            <input type="text" name="email" value={input.email} onChange={handleInputChange}/>
            <p>password:</p>
            <input type="text" name="password" value={input.password} onChange={handleInputChange}/><br/>
            <button type='submit'>Login</button>
        </form>
    </div>
  )
}

export default Login

My router layout in app.js file:

import {  } from 'react'
import './App.css'
import { Route, Routes } from 'react-router'

import Access from './Auth/Access'

import Home from './pages/Home'
import Login from './pages/Login'
import Signup from './pages/Signup'
import Admin from './pages/Admin'
import Basic from './pages/Basic'
import { Error } from './pages/Error'



function App() {

  return (
    <div>
      <Routes>
        <Route path='/' element={<Home/>}/>
        <Route path='/login' element={<Login/>}/>
        <Route path='/signup' element={<Signup/>}/>
        <Route path='/basic' element={<Access role={'basic'}> <Basic/></Access>}/>
        <Route path='/admin' element={<Access role={'admin'}><Admin/></Access>}/>
        <Route path='*' element={<Error/>}/>
      </Routes>
    </div>
  )
}
export default App

My HOC that check the role in route, names as Access:

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

const Access = ({ role }) => {
  const userRole = sessionStorage.getItem("role");
  const token = sessionStorage.getItem('token');
  
  if (!token || (role && role !== userRole)) {
    // Redirect to login if token is not available or user role doesn't match
    return <Navigate to="/login" replace />;
  }

  // User has the required role or no role is specified, render the nested components
  return <Outlet />;
};

export default Access

I expected both paged to render with simple h1 tags, based on role redirection from login.

Admin page:

/* eslint-disable no-unused-vars */
import React from "react"

const Admin = () => {
  return (
    <div>wecome to admin page</div>
  )
}

export default Admin
// eslint-disable-next-line no-unused-vars
import React from 'react'

const Basic = () => {
  return (
    <div>welcome to basic page</div>
  )
}

export default Basic

after a lot of tweaking, I think Im finally was able to solve this problem.. I created an HOC to accept a role, then navigate the currently logged in conditionally.

here is my solution, but I will keep this post open in case someone will have a better solution then mine..

lets starts with my router layout in app.js file:

import {} from "react";
import "./App.css";
import { Route, Routes } from "react-router";

import Access from "./Auth/Access";

import Home from "./pages/Home";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Admin from "./pages/Admin";
import Basic from "./pages/Basic";
import BasicSubPage from "./pages/BasicSubPage";
import BasicSecondPage from "./pages/BasicSecondPage";
import PublicNav from "./pages/PublicNav";
import Error from "./pages/Error";

function App() {
  return (
    <div>

      <PublicNav/>
      <Routes>
        {/* public routes */}
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route path="/signup" element={<Signup />} />

        {/* basic protected routes */}
        <Route element={<Access role={"basic"}/>}>
          <Route path="/basic" element={<Basic/>}>
            <Route path="basicsub" element={<BasicSubPage/>}/>
            <Route path="/basic/basicsecond" element={<BasicSecondPage/>}/>
          </Route>
        </Route>

        {/* admin protected routes */}
        <Route element={<Access role={"admin"}/>}>
          <Route path="/admin" element={<Admin />} />
        </Route>

        <Route path="/error" element={<Error />} />
      </Routes>
    </div>
  );
}
export default App;

as you can see, my HOC is named as "Access" that accept a role as prop.

here is "Access" component: please note: this is a simple validation, as I wanted to keep things as simple as possible..

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

const Access = ({role}) => {
  const token = sessionStorage.getItem("token");
  const userRole = sessionStorage.getItem("role");

  if(!token || token && !role.includes(userRole)) {
    return <Navigate to="/error" replace/>
  }
  
  return <div>
     <Outlet/>
  </div>;
}

export default Access

my login component remains the same, so no change there, i added to sub pages for "basic" user, so when I can use "tab" layout to navigate between those 2 pages..

so here is my "basic" page, keep in note that the other pages has simple h1 tag rendered, so I didn't added them here..

basic page:

// eslint-disable-next-line no-unused-vars
import { useEffect, useState } from "react";
import { getInfo } from "../utils/basicUtils";
import { useNavigate , NavLink, Outlet} from "react-router-dom";

const Basic = () => {
  const navigate = useNavigate();
  const token = sessionStorage.getItem("token");

  const [personalData, setPersonalData] = useState({});

  useEffect(() => {
    if (!token) {
      sessionStorage.removeItem('token');
      sessionStorage.removeItem('role');
      navigate(0);
    }

    const getPersonalInfo = async () => {
      const url = 'http://localhost:8080/basic/info'
      const resp = await getInfo(`${url}`,token);
      
      if(resp.status === 403) {
        console.log("forbiden");
        sessionStorage.removeItem('token');
        sessionStorage.removeItem('role');
        navigate(0);
        navigate('/home');
      }
      setPersonalData(resp.data.personalInfo)
      console.log("success", resp.data.personalInfo);
    };

    getPersonalInfo()

    }, [token]);

    const logout = () => {
      sessionStorage.removeItem('token');
      sessionStorage.removeItem('role');
      navigate('/home')
    }


    return (
      // <NavLink className="Link"  to="/home">Home</NavLink>

      <div> 
      <h1>welcome {personalData.name}</h1>
      <button onClick={logout}>Logout</button>
        <nav>
          <NavLink to={"basicsub"}>basic-sub</NavLink>
          <NavLink to={"/basic/basicsecond"}>basic-sub-second</NavLink>
        </nav>
        <Outlet/>    
      </div>
    );
};
export default Basic;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM