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;