React Router v5.1.2 Public & Protected Authenticated & Role Based routes - reactjs

Goal is to have /login as the only public route, once logged in user has routes based on user role.
Authentication is done with Keycloak I get users from keycloak.idTokenParsed.preferred_username: admin, manager, engineer, operator.
If operator tries to go to role restricted route gets redirected to /notauthorized page. (This part not done)
If not logged in user gets redirected to /login page. (This part is done/works)
Is there a better way to do this? Not repeating routes & adding additional users in Routes.jsx kind of a mess.
How do I implement role restricted redirect to /notauthorized?
App.js (does not have all the imports and missing bottom part with mapStateToProps, mapDispatchToProps & export default App )
import React, { useEffect } from "react";
import { Route, Redirect, Switch } from "react-router-dom"
let routeWithRole = [];
let user = '';
const AppContainer = ({ keycloak }) => {
if(keycloak && keycloak.token) {
user = keycloak.idTokenParsed.preferred_username
if( user === 'admin') {
routeWithRole = admin;
} else if( user === 'engineer') {
routeWithRole = engineer
} else if(user === 'manager') {
routeWithRole = manager
} else {
routeWithRole = operator
}
}
return (
<div>
{(keycloak && keycloak.token) ?
<React.Fragment>
<Switch>
{routeWithRole.map((prop, key) => {
console.log('App.js Prop & Key ', prop, key)
return (
<Route
path={prop.path}
key={key}
exact={true}
component={prop.component}
/>
);
})}
<Redirect from={'/'} to={'/dashboard'} key={'Dashboard'} />
</Switch>
</React.Fragment>
:
<React.Fragment>
<Switch>
{publicRoutes.map((prop, key) => {
return (
<Route
path={prop.path}
key={key}
exact={true}
component={(props) =>
<prop.component
keycloak={keycloak}
key={key} {...props} />
}
/>
);
})}
<Redirect from={'/'} to={'/login'} key={'login'} />
</Switch>
</React.Fragment>
}
</div>
)
}
Routes.jsx (missing all the impotrs)
export const publicRoutes = [
{ path: "/login", type: "public", name: "landing page", component: LandingPageContainer },
]
export const admin = [
{ path: "/createUser", name: "Create User", component: CreateUser},
{ path: "/editUser", name: "Edit User", component: EditUser},
{ path: "/createdashboard", name: "Create Dashboard", component: CreateDashboard },
{ path: "/editashboard", name: "Edit Dashboard", component: EditDashboard },
{ path: "/createcalendar", name: "Create Calendar", component: CreateCalendar },
{ path: "/editcalendar", name: "list of factories", component: EditCalendar },
{ path: "/dashboard", name: "Dashboard", component: Dashboard }
]
export const engineer = [
{ path: "/createdashboard", name: "Create Dashboard", component: CreateDashboard },
{ path: "/editashboard", name: "Edit Dashboard", component: EditDashboard },
{ path: "/dashboard", name: "Dashboard", component: Dashboard },
{ path: "/notauthorized", name: "Not Authorized", component: Notauthorized }
]
export const manager = [
{ path: "/createcalendar", name: "Create Calendar", component: CreateCalendar },
{ path: "/editcalendar", name: "Edit Calendar", component: EditCalendar },
{ path: "/dashboard", name: "Dashboard", component: Dashboard },
{ path: "/notauthorized", name: "Not Authorized", component: Notauthorized }
]
export const operator = [
{ path: "/dashboard", name: "Dashboard", component: Dashboard },
{ path: "/notauthorized", name: "Not Authorized", component: Notauthorized }
]

I will consider the option when we have known "keycloak" before react initialization (not async loading data for "keycloak"). You will be able to improve if you understand the idea
The main idea is to show all routes but almost all of them will be protected routes. See the example:
render (
<Switch>
<Route exact path="/login"> // public route
<LandingPageContainer />
</Route>
<AuthRoute exact path="/dashboard"> // for any authorized user
<Dashboard />
</AuthRoute>
<AdminRoute path="/create-user"> // only for admin route
<CreateUser />
</AdminRoute>
<AdminOrEngineerRoute path="/create-dashboard"> // only for admin or engineer route
<CreateDashboard />
</AdminOrEngineerRoute>
<Redirect to="/dashboard" /> // if not matched any route go to dashboard and if user not authorized dashboard will redirect to login
</Switch>
);
Then you can create list of components like this:
const AVAILABLED_ROLES = ['admin', 'engineer'];
const AdminOrEngineerRoute = ({ children, ...rest }) {
const role = keycloak && keycloak.token ? keycloak.idTokenParsed.preferred_username : '';
return (
<Route
{...rest}
render={({ location }) =>
AVAILABLED_ROLES.includes(role) && ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
As a result AdminOrEngineerRoute will allow to pass to this route only admin or engineer in other case you will get /login page
Always yours "IT's Bruise"

Related

How can I automatically logout a user in React JS when their token has expired, as indicated by the "exp" field in the decoded token?

My issue with my current code is,it is not automatically redirecting to signIn page when token expires,but after token expire if I am refreshing the page manually ,then it redirects to signIn page.plzzzz check my code and give any solution.....
My code i:
import React, { useEffect, useState } from 'react';
import { Navigate, useRoutes, useNavigate } from 'react-router-dom';
import JwtDecode from "jwt-decode";
// layouts
import DashboardLayout from './layouts/dashboard';
import SimpleLayout from './layouts/simple';
// pages
import SignIn from './pages/Auth/SignIn';
import SignUp from './pages/Auth/SignUp';
import Otp from './pages/Auth/Otp';
import Dashboard from './pages/Dashboard/AppAnalytics';
import AlumniListing from './pages/Alumni/AlumniListing';
import Page404 from './pages/Page404';
import Profile from './pages/Profile/index';
import EditProfile from './pages/EditProfile/index';
import Payment from './pages/Auth/Payment';
import PaymentStatus from './pages/Auth/PaymentStatus';
// ----------------------------------------------------------------------
export default function Router() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const auth = localStorage.getItem("token");
const navigate = useNavigate();
const checkLogin = () => {
if (auth) {
try {
const decodedToken = JwtDecode(auth);
const expiration = new Date(decodedToken?.exp * 1000);
console.log('decodedToken:', decodedToken);
console.log('expiration:', expiration.getTime);
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime >= decodedToken.exp) {
console.log("JWT has expired or will expire soon");
setIsLoggedIn(false);
localStorage.removeItem("token");
} else {
console.log("JWT is valid for more than 5 minutes");
setIsLoggedIn(true);
}
} catch (error) {
console.error("Error decoding JWT:", error);
setIsLoggedIn(false);
localStorage.removeItem("token");
}
} else {
setIsLoggedIn(false);
}
};
useEffect(() => {
checkLogin();
if (!isLoggedIn) {
navigate("/auth/sign-in");
} else {
navigate("/dashboard/analytics");
}
}, [auth,]);
const routes = useRoutes([
// {
// path: '/',
// element: <checkLogin />
// },
{
path: '/dashboard',
element: isLoggedIn ? <DashboardLayout /> : <Navigate to='/auth/sign-in' />,
children: [
{ element: <Navigate to="/dashboard/analytics" />, index: true },
{ path: 'analytics', element: <Dashboard /> },
{ path: 'alumni', element: <AlumniListing /> },
{ path: 'profile', element: <Profile /> },
{ path: 'edit-profile', element: <EditProfile /> },
],
},
{
path: 'auth/sign-in',
element: !isLoggedIn ? <SignIn /> : <Navigate to='/dashboard/analytics' />,
},
{
path: 'payment',
element: <Payment />,
},
{
path: 'payment/gateway-response',
element: <PaymentStatus />,
},
{
path: 'verify-otp',
element: !isLoggedIn ? <Otp /> : <Navigate to='/dashboard/analytics' />,
},
{
path: 'auth/sign-up',
element: !isLoggedIn ? <SignUp /> : <Navigate to='/dashboard/analytics' />,
},
{
element: <SimpleLayout />,
children: [
{ path: '404', element: <Page404 /> },
{ path: '*', element: <Navigate to="/404" /> },
],
},
{
path: '*',
element: <Navigate to="/404" replace />,
},
]);
return routes;
}
when I am consoling decodedToken,I am getting :
{payload: {…}, iat: 1675398393, exp: 1675398453}
exp
:
1675398453
iat
:
1675398393
payload
:
email
:
"uma.swain#cybrain.co.in"
id
:
"58332666-806d-4d4f-81b5-efb331acd4e2"
index
:
"6730c5c3-c558-44d8-b771-2661f2c28a2a"
tenant
:
"sales.alumni.cybraintech.in"
timestamp
:
"2023-02-03T04:26:33.296Z"
[[Prototype]]
:
Object
[[Prototype]]
:
Object
Thanks in advance.....
I have developed a library called AuthBee for the same use case which can be used for any javascript project.
You can use it to get your job done.
You can find more on the library here
example

Set onclick event on React list (Nav Bar)

I am trying to implement onclick into navbar items so when i click one of the items in nav bar, it will load some component.
below is the list for the navbar:
class Navbar extends React.Component {
render () {
const menuItems = [
{
title: 'Home',
url: '/',
},
{
title: 'Assets',
url: '/Assets',
},
{
title: 'Service Report',
url: '/Servicereport',
},
{
title: 'Change Request',
url: '/Changerequest',
},
{
title: 'Logout',
url: '/logout',
}
];
return (
<nav>
<ul className="menus">
{menuItems.map((menu, index) => {
return (
<MenuItems
items={menu}
key={index}
/>
);
})}
</ul>
</nav>
);
};
}
export default Navbar;
How can i implement?
If need more information, please let me know. thank you.
I am trying to implement onclick into navbar items
You can try the onClick event in React and call a function to redirect to the specified location using useNavigate hook of React Router. You first need to install react router DOM.
npm i react-router-dom
Try the below code for redirecting
import { useNavigate } from "react-router-dom";
class Navbar extends React.Component {
render () {
const menuItems = [
{
title: 'Home',
url: '/',
},
{
title: 'Assets',
url: '/Assets',
},
{
title: 'Service Report',
url: '/Servicereport',
},
{
title: 'Change Request',
url: '/Changerequest',
},
{
title: 'Logout',
url: '/logout',
}
];
const navigate = useNavigate();
return (
<nav>
<ul className="menus">
{menuItems.map((menu, index) => {
return (
<MenuItems
onClick = {() => navigate(`${menu.url}`)}
items={menu}
key={index}
/>
);
})}
</ul>
</nav>
);
};
}
export default Navbar;
You just have to add an onclick attributes to your MenuItems.
class Navbar extends React.Component {
const handleOnClick = (e) => {
if(e.currentTarget == "Something") {
// Do something
} else {
// Do something else
}
}
render () {
const menuItems = [
{
title: 'Home',
url: '/',
},
{
title: 'Assets',
url: '/Assets',
},
{
title: 'Service Report',
url: '/Servicereport',
},
{
title: 'Change Request',
url: '/Changerequest',
},
{
title: 'Logout',
url: '/logout',
}
];
return (
<nav>
<ul className="menus">
{menuItems.map((menu, index) => {
return (
<MenuItems
items={menu}
key={index}
onClick={handleOnClick}
/>
);
})}
</ul>
</nav>
);
};
}
export default Navbar;
<MenuItems onClick={() => {…}} {…this.props} /> attach onClick handler as a prop. Is MenuItems defined? I would change the name to MenuItem.

React first gives null values and after a while values are initialized

I am trying to create a component to make the validation if the user is authenticated or not.
It is not I am redirecting to log in, however, when I am checking if is it authenticated, first the session is null and after 2000ms says that is authenticated.
App.js
function App() {
const session = useSelector((state) => state.user.user);
useEffect(() => {
if (token) {
axios
.get(`http://localhost:4500/api/user/current`, {
headers: {
token: token,
},
})
.then((res) => {
dispatch(login(res.data));
});
} else {
dispatch(logout());
console.log("no token");
}
}, []);
console.log(session?.role + " XX S"); //first goes null and then after it is initialized
return (
<Route
element={
<RequireAuth allowedRoles={[3]} session_role={session?.role} /> //session?.role first is null and it is a problem with the logic of validation
}
>
<Route path="/protected" element={<Protected />} />
</Route>
)
RequireAuth.js
const RequireAuth = ({ allowedRoles, session_role }) => {
const session = useSelector((state) => state.user.user);
const location = useLocation();
console.log(session_role + " ROLE ");
return session_role?.find((role) => allowedRoles?.includes(role)) ? (
<Outlet />
) : session ? (
<Navigate to="/unauthorised" state={{ from: location }} replace />
) : (
<Navigate to="/login" state={{ from: location }} replace />
);
};
export default RequireAuth;
Reducer
export const userSlice = createSlice({
name: "user",
initialState: {
user: null,
isLoggedIn: false,
},
reducers: {
login: (state, action) => {
state.user = action.payload;
state.isLoggedIn = true;
},
logout: (state) => {
state.user = null;
state.isLoggedIn = false;
},
},
});
function App() {
const dispatch = useDispatch();
let token = 1;
useEffect(() => {
async function initializeToken() {
if (token) {
setTimeout(() => {
dispatch(login({
user: {
id: 'id',
name: 'name',
surname: 'surname',
email: 'email',
role: 'role',
},
}))
}, 2000);
} else {
dispatch(logout());
console.log("no token");
}
}
initializeToken();
}, [dispatch]);
const session = useSelector((state) => state.user);
const sessionObject = session ? JSON.parse( JSON.stringify(session)) : null ;
console.log(sessionObject, 'sessionObject');
const isAuth = sessionObject ? sessionObject.isLoggedIn : null ;
console.log(isAuth + "isAuth");
const role = sessionObject ? sessionObject.user && session.user.user ? session.user.user.role : null : null
console.log(role, 'role')
return (
<div className="App">
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="login" element={<Login />} />
<Route exact path="unauthorised" element={<Unauthorised />} />
<Route
element={
<RequireAuth allowedRoles={[3]} session_role={session?.role} />
}
>
<Route path="/protected" element={<Protected />} />
</Route>
</Routes>
</Router>
</div>
);
}
export default App;
Have used setTimeout instead of api , it can simulate same stuff. The reason behind null is parsing the data, not the initial state i thought it would be. The above code should do the trick.

How to add new parameters to url without refreshing page in react?

I work in react and I have a shop page that have some filters. When I apply a filter, I want to also add this filter into the url but without reloading page.
I mean like this :
currentUrl : something.com
newUrl : something.com/brand-nike
I want to change url without refresh.
I already tried these codes but both reload page :
window.history.pushState({ path: url }, "", url);
window.history.replaceState(null, "", url);
And also I am using react-router-dom for routing.
This is my App.js :
return (
<ToastProvider>
<ShopFilterContext.Provider value={shopFilterHook}>
<CartContext.Provider value={cartHook}>
<React.Fragment>
<Router basename={BASENAME}>{renderRoutes(routes)}</Router>
</React.Fragment>
</CartContext.Provider>
</ShopFilterContext.Provider>
</ToastProvider>
);
This is renderRoutes function :
export const renderRoutes = (routes = []) => {
return (
<Suspense fallback={<Loader />}>
<Switch>
{routes.map((route, i) => {
const Guard = route.guard || Fragment;
const Layout = route.layout || Fragment;
const Component = route.component;
return (
<Route
key={i}
path={route.path}
exact={route.exact}
render={(props) => {
props["layoutObj"] = route.layoutObj;
return (
<Guard>
<Layout>{route.routes ? renderRoutes(route.routes) : <Component {...props} />}</Layout>
</Guard>
);
}}
/>
);
})}
</Switch>
</Suspense>
);
};
And this is routes variable
const routes = [
{
path: "/account/*",
layout: NifsyLayout,
routes: [
{
exact: true,
path: "/account/*",
component: lazy(() => import("./pages/MyAccount")),
},
{
path: "/account/*",
exact: true,
component: () => <Redirect to={ACCOUNT_URL} />,
},
],
},
{
path: "/search/*",
layout: NifsyLayout,
routes: [
{
exact: true,
path: "/search/*",
component: lazy(() => import("./pages/Shop")),
},
{
path: "/search/*",
exact: true,
component: () => <Redirect to={ACCOUNT_URL} />,
},
],
},
{
path: "*",
layout: NifsyLayout,
routes: [
{
exact: true,
path: "/not-found",
component: lazy(() => import("./pages/NotFound")),
},
{
path: "*",
exact: true,
component: () => <Redirect to={BASE_URL} />,
},
],
},
];
And this is for navigation : (history is hook from useHistory)
let url = "/search";
if (filter.category) url = url + "/" + filter.category;
if (filter.brands) {
for (let i = 0; i < filter.brands.length; i++) {
url = url + filter.brands[i];
}
}
if (filter.collections) {
for (let i = 0; i < filter.collections.length; i++) {
url = url + filter.collections[i];
}
}
url = url + "?q=" + filter.keyword;
url = url + "&start=" + filter.start;
url = url + "&size=" + filter.size;
url = url + "&sortby=" + filter.sort;
url = url + "&filter=" + (typeof filter.filters == "string" ? filter.filters : JSON.stringify(filter.filters));
// multi refresh
//window.history.pushState({ path: url }, "", url);
//window.history.replaceState(null, "Nifsy", url);
history.push(url);
It might be late to answer but I searched a lot and tried various alternatives. Finally what worked for me :
window.history.replaceState("", "",/new_path);
Assuming you're using functional components and the react router, from react-router-dom you can import the hook useHistory.
const history = useHistory();
history.push("/your/path");
// or
history.replace("/your/path");
To not reloading you have to design your Route and define your path here . this will help you to get the page without loading .

react router dom, replaces the browser url but does not go to the page

Thank you very much in advance
I'm trying to access a child page of a Drawer component, however when I try to use hisotry.push ('route name') it just replaces it in the url but does not navigate to the link,I'm using a navigation service through history I don't know if the interference and the component is the drawer's child but it has the same address.
file sagas
src/service/routes.js
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
src/modules/sagas.js
function* detailsUsers({ payload: { user } }) {
yield put(defineInformationDetails(user));
history.push(`Drawer/EditAdm`);
}
src/router/index.js
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Login from '../pages/Login';
// import Panel from '../pages/Panel';
import Panel from './Drawer';
import * as isUserLogged from '../store/modules/auth/actions';
import ResetPassword from '../pages/ResetPassword';
import ChangePassword from '../pages/ChangePassword';
export default function Routes() {
return (
<Switch>
<Route path="/" exact component={Login} />
<Route path="/Login" component={Login} />
<Route path="/ResetPassword" component={ResetPassword} />
<Route path="/ChangePassword" component={ChangePassword} />
<Route path="/Drawer" component={Panel} />
</Switch>
);
}
src/routes/drawer.js
const [drawer, setDrawer] = useState({
name: 'Menu',
subname: '',
options: [
{
id: '1',
name: 'Home',
open: true,
link: `/`,
icon: () => <IconHome />,
route: {
path: `${path}/`,
exact: true,
main: () => <Home />,
},
suboptions: [],
},
{
id: '2',
name: 'Cadastro de Usuário',
open: false,
link: false,
icon: () => <IconUserList />,
route: null,
suboptions: [
{
id: '1',
name: 'Buscar/Editar Usuário',
open: false,
link: `/SearchUser`,
icon: () => <IconSearchFa />,
route: {
path: `${path}/SearchUser`,
exact: true,
main: () => <EditUser />,
},
},
{
id: '2',
name: 'Administrador',
open: false,
link: `/AdmRegistration`,
icon: () => <IconRegisters />,
route: {
path: `${path}/AdmRegistration`,
exact: true,
main: () => <AdmRegistrator />,
},
},
{
id: '5',
name: 'Fornecedor',
open: false,
link: `/SupplierRegistrator`,
icon: () => <IconRegisters />,
route: {
path: `${path}/SupplierRegistrator`,
exact: true,
main: () => <SuppliedRegistrator />,
},
},
{
id: '6',
name: 'Controle de acesso nos eventos',
open: false,
link: `/EventAcess`,
icon: () => <IconEvent />,
route: {
path: `${path}/EventAcess`,
exact: true,
main: () => <EventAcess />,
},
},
{
id: '7',
name: 'Editar Administrador',
open: false,
link: `/EditAdm`,
icon: () => {},
route: {
path: `${path}/EditAdm`,
exact: true,
main: () => <EditAdm />,
},
},
],
},
],
});
return (
<Router>
<AreaPanel>
<Header
name={
localStorage.getItem('NeoTicket#name') !== null
? localStorage
.getItem('NeoTicket#name')
.substring(0, 1)
.toUpperCase()
.concat(localStorage.getItem('NeoTicket#name').substring(1))
: 'Nome Usuário'
}
functionOnClickDrawer={() => setOpenDrawer(!openDrawer)}
openDrawer={openDrawer}
functionOnClickUser={() => setOpenUser(!openUser)}
openUser={openUser}
/>
<AreaDrawerPanel>
<AreaDrawer open={openDrawer}>
<Drawer
name={drawer.name}
subname={drawer.subname}
openDrawer={openDrawer}
options={drawerSearch.options}
functionOnClickOpenSuboption={id =>
functionOpenSuboption(id, drawerSearch)
}
functionSearchText={text => setSearchText(text)}
functionOnClickName={name => functionGetNamePage(name)}
path={path}
/>
</AreaDrawer>
<AreaContent open={openDrawer}>
<HeaderContainer>
{/* {drawer.options.map((option, index) => {
const { link } = option;
if (link) {
return (
<HeaderContent
key={index.toString()}
name={namePage === '' ? 'TESTE' : namePage}
/>
);
}
})} */}
<Switch>
{drawer.options.map((option, index) => {
const { link, route, suboptions } = option;
if (link) {
return (
<Route
key={index.toString()}
path={route.path}
exact={route.exact}
children={<route.main />}
/>
);
}
return suboptions.map((suboptions, indexParam) => {
return (
<Route
key={indexParam.toString()}
path={suboptions.route.path}
exact={suboptions.route.exact}
children={<suboptions.route.main />}
/>
);
});
})}
</Switch>
</HeaderContainer>
</AreaContent>
</AreaDrawerPanel>
</AreaPanel>
</Router>
I don't understand how to solve.
in file sagas replace.
history.push(`Drawer/EditAdm`)
to
const { pathname } = history.location;
window.location.href = `${pathname}/EditAdm`;
possible resolution:
using the component with withRouter
\src\pages\UserRegister\index.js
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { Container } from './styles';
import * as HomeActions from '../../store/modules/home/actions';
import Button from '../../components/ButtonIcon';
function UserRegistration({ history }) {
const dispatch = useDispatch();
function onChangePageChildren() {
dispatch(HomeActions.changePageChildren(history));
}
return (
<Container>
<a>Tela1</a>
<Button
title="chamar sagas para ir para tela de listagem"
functionOnClick={() => onChangePageChildren()}
/>
</Container>
);
}
export default withRouter(UserRegistration);
however with this he will leave the navigation properties ready to be used so you can take the history property and pass it as a parameter through the action and through it through the sagas.
sagas file
import { all, call, put, takeLatest } from 'redux-saga/effects';
import { createBrowserHistory } from 'history';
import {
loading,
successAction,
failureAction,
breakAction,
} from '../common/actions';
function* requestChangePage({ payload: { history } }) {
yield put(loading(''));
history.push('/Drawer/userList');possível resolução:
abrace o componente com withRouter
\src\pages\UserRegister\index.js
it is worth mentioning that in this case when using the drawer always on the page with a header and requiring the paging to happen in an area, these being defined as children of this component Drawer, for now this works but I believe that the best ways to do it.
Remembering that this way it will not affect the states of the page reducers.

Resources