How to implement Role based restrictions/permissions in react redux app? - reactjs

I have a React-Redux-KoaJs application with multiple components. I have few user roles as well. Now i want to display few buttons, tables and div to only specific roles and hide those from others. Please remember i dont want to hide the whole component, but just a part of the components. Can anyone help me? Thanks in advance.

You can check the role or permission in every component as #Eudald Arranz proposed. Or you can write a component that will checks permissions for you. For example:
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const ShowForPermissionComponent = (props) => {
const couldShow = props.userPermissions.includes(props.permission);
return couldShow ? props.children : null;
};
ShowForPermissionComponent.propTypes = {
permission: PropTypes.string.isRequired,
userPermissions: PropTypes.array.isRequired
};
const mapStateToProps = state => ({
userPermissions: state.user.permission //<--- here you will get permissions for your user from Redux store
});
export const ShowForPermission = connect(mapStateToProps)(ShowForPermissionComponent);
and then you can use this component like this:
import React from 'react';
import { ShowForPermission } from './ShowForPermission';
cons MyComponent = props => {
return (
<div>
<ShowForPermission permission="DELETE">
<button>Delete</button>
</ShowForPermission>
</div>
);
}

Be careful with that. If the actions of some roles are important you should always validate them at your backend. It's easy to change the values stored in redux at frontend allowing malicious use of the roles if there is no proper validation.
If you want to proceed on a possible approach is this:
Save the roles at your reducer
Bind the reducer to the component:
function mapStateToProps(state) {
const { user_roles } = state;
return { user_roles };
}
export default connect(mapStateToProps)(YourComponent);
Then at your component, you can check the user_roles and render the actions accordingly:
render() {
return (
<div>
{this.props.user_roles.role === "YOUR_ROLE_TO_CHECK" && <ActionsComponent />}
</div>
);
}
This will render the ActionsComponent only when the role is equal to the desired one.
Again, always validate the roles at your backend!

The best practice to solve this Problem is, simply prevent the app to generate unnecessary routes, rather checking current user role on each route it is great to generate only the routes that user have access.
So The Normal reouting is:
To control the whole view:
const App = () => (
<BrowserRouter history={history}>
<Switch>
<Route path="/Account" component={PrivateAccount} />
<Route path="/Home" component={Home} />
</Switch>
</BrowserRouter>
export default App;
);
Routing based on user role:
import { connect } from 'react-redux'
// other imports ...
const App = () => (
<BrowserRouter history={history}>
<Switch>
{
this.props.currentUser.role === 'admin' ?
<>
<Route path="/Account" exact component={PrivateAccount} />
<Route path="/Home" exact component={Home} />
</>
:
<Route path="/Home" exact component={Home} />
}
<Route component={fourOFourErroPage} />
</Switch>
</BrowserRouter>
const mapStateToProps = (state) => {
return {
currentUser: state.currentUser,
}
}
export default connect(mapStateToProps)(App);
So the user with an Admin role will have access to the Account page and for other users will have access to the Home page Only! and if any user try to access to another route, the 404 page error will appear.
I hope I've given a helpful solution.
For advanced details about this approach you can check this repo on github: Role-based-access-control with react
To hide just a presentational component:
{this.props.currentUser.role === 'admin' && <DeleteUser id={this.props.userId} /> }

So, I have figured out there is an alternate and easy approach to implement role based access (RBAC) on frontend.
In your redux store state, create a object called permissions (or you can name it whatever you like) like this:
const InitialState = {
permissions: {}
};
Then on your login action, setup the permissions that you want to provide like this:
InitialState['permissions'] ={
canViewProfile: (role!=='visitor'),
canDeleteUser: (role === 'coordinator' || role === 'admin')
// Add more permissions as you like
}
In the first permission you are saying that you can view profile if you are not a visitor.
In the second permission you are saying that you can delete a user only if you are an admin or a coordinator.
and these variables will hold either true or false on the basis of the role of the logged in user. So in your store state u will have a permission object with keys that represent permissions and their value will be decided on the basis of what your role is.
Then in your component use the store state to get the permission object. You can do this using connect like:
const mapStateToProps = (state) => {
permissions : state.permissions
}
and then connect these props to your Component like:
export default connect(mapStateToProps,null)(ComponentName);
Then you can use these props inside your component on any particular element which you want to show conditionally like this:
{(this.props.permissions.canDeleteUser) && <button onClick={this.deleteUser}>Delete User</button>}
The above code will make sure that the delete user button is rendered only if you have permissions to delete user i.e. in your store state permissions object, the value of canDeleteUser is true.
That's it, you have appplied a role based access. You can use this approach as it is easily scalable and mutable, since you will have all permsisions according to roles at one place.
Hope, this helps! If i missed out something please help me in the comments. :-)

I have implemented this in this rbac-react-redux-aspnetcore repository.
If someone wants to use Redux with Context API, then the below code snippet can be helpful.
export const SecuedLink = ({ resource, text, url }) => {
const userContext = useSelector(state => {
return state.userContext;
});
const isAllowed = checkPermission(resource, userContext);
const isDisabled = checkIsDisabled(resource, userContext);
return (isAllowed && <Link className={isDisabled ? "disable-control" : ""} to={() => url}>{text}</Link>)
}
const getElement = (resource, userContext) => {
return userContext.resources
&& userContext.resources.length > 0
&& userContext.resources.find(element => element.name === resource);
}
export const checkPermission = (resource, userContext) => {
const element = getElement(resource, userContext);
return userContext.isAuthenticated && element != null && element.isAllowed;
}
export const checkIsDisabled = (resource, userContext) => {
const element = getElement(resource, userContext);
return userContext.isAuthenticated && element != null && element.isDisabled;
}
To use the above snippet, we can use it like below
<SecuedLink resource='link-post-edit' url={`/post-edit/${post.id}`} text='Edit'></SecuedLink>
<SecuedLink resource='link-post-delete' url={`/post-delete/${post.id}`} text='Delete'></SecuedLink>
So, depending on the role, you can not only show/hide the element, but also can enable/disable them as well. The permission management is fully decoupled from the react-client and managed in database so that you don't have to deploy the code again and again just to support new roles and new permissions.

This post is very exactly what you need without any library:
react-permissions-and-roles

Related

Props lost after navigate and cloneElement

In an app I'm currently working on, the authentification has been done like this, to pass user's data into children components (in App.js, I would have rather used useContext to access user data in whatever component) [App.js]:
const RequireAuth = ({ children }) => {
// fetch user data from database, ...
return <>{React.cloneElement(children, { user: {...user, ...userExtraData} })}</>;
};
Then, an example of a Route is specified as [App.js]:
<Route
path="/addgame"
element={
<RequireAuth>
<FormAddGame />
</RequireAuth>
}
/>
However, my current problem is the following:
From a ComponentA, I want to navigate to /addgame (=FormAddGame component, see below) while setting an existingGame prop. Thus, I use [ComponentA.js]:
let navigate = useNavigate()
navigate('addgame', { existingGame: game })
The said FormAddGame component is [FormAddGame.js]:
function FormAddGame({ user }) {
const { existingGame } = useLocation()
console.log(existingGame)
...
}
export default FormAddGame;
However, while I correctly navigate to /addgame, the existingGame prop stays undefined once in FormAddGame while it should be not (as game is not undefined)
Try passing props like this:
navigate("addgame", { state: { existingGame: game } });
and destructure like this:
const { state: { existingGame } = {} } = useLocation();

Problem with wrapping whole React App (all pages) in certain functionality

I have React App consisting of few pages. LandingPage (to be clear, it is really the first page in app flow)is like this:
export const LocalLandingPage = () => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return (...here goes actual content...);
};
const LandingPage = withRouter(wrappedInLinkToSearchHOC(WithSnackBarHOC(LocalLandingPage)));
Here I call hook useFavorites which gathers some methods dealing with a specific subset of local Storage content(and dispatches actions to Redux store as well). The code above return call is checks support for local Storage and tells if some specific items are stored and in what quantity.
The story is that I have realized that if someone enters not the LandingPage but any other page of the app, the code will not be executed and support for localStorage not checked.
Besides, it is really not LandingPage business to deal with storage, so it should be removed anywhere.
My idea was to write HOC and wrap the application. So, here is this HOC (to keep things simple it is initially JS not TS) To make useFavorites work, it had to be a hook, too.
import useFavorites from '../hooks/useFavorites';
const useCheckSupportForLocalStorage = Component => {
const { Favorites } = useFavorites();
// React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
// }, []);
return props => <Component {...props} />;
};
export default useCheckSupportForLocalStorage;
Having it done, I have tried to use it on App like this
function App() {
return (
<Switch>
... here are routes...
</Switch>
);
}
export default useCheckSupportForLocalStorage(App);
It throws an error:
React Hook "useCheckSupportForLocalStorage" cannot be called at the top level.
That is truth, no doubt. So, the next idea was to create a temporary component from all routes.
function Routes() {
const Routes = useCheckSupportForLocalStorage(
<>
<Route exact path={Paths.landing} component={Awaiting(StarWars)} />
...here is the rest or outes...
</>,
);
return <Routes />;
}
export default Routes;
And use it in rewritten App like this
function App() {
return (
<Switch>
<Routes />
</Switch>
);
}
but it throws error
Routes' cannot be used as a JSX component.
Its return type '(props: any) => Element' is not a valid JSX element.
Type '(props: any) => Element' is missing the following properties from type 'ReactElement<any, any>': type, props, key
Forcing useCheckSupportForLocalStorage th have return type of ReactElement doesn't help, just leads to other error. I have checked few others options as well. What is wrong, how should it be written?
Basically I could stop using useFavorites in this hook, then it would be just a function - but it would be extreme headache.
You say
To make useFavorites work, it had to be a hook, too.
That is not true, since you have it in a component it will work just fine. And my understanding is that you want to wrap your whole app with it, so no need for the HOC. Just use it at the top of your app hierarchy.
something like
import useFavorites from '../hooks/useFavorites';
const CheckSupportForLocalStorage = ({ children }) => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return children;
};
export default CheckSupportForLocalStorage;
and
function App() {
return (
<CheckSupportForLocalStorage>
<Switch>
... here are routes...
</Switch>
</CheckSupportForLocalStorage>
);
}
export default App;

How to protected routes on client side in Nextjs and redirect if don't have admin role?

Now, I have user role in Context. I can check the role of context in useEffect on each page before fetch data and redirect to login if the user doesn't have the admin role:
useEffect(() => {
if (userInfo.role == 2) {
getPosts(pagerInfo.currentPage);
} else {
Router.push('/login');
}
}, []);
But I don't want to rewrite to check the user role on each page. How can I use it for every page?
Do it once in a top-level component, such as _app.js.
you have to write a higher order component, which takes a component as an argument, you apply some conditions and based on satisfied conditions you either allow or reject the access.
const withAuth = (Component) => (role) => {
// those props will be passed to HOC when you use this.
return (props) => {
//here you write your code to get the data
if (userInfo.role == 2) {
return <Component user={data} loading={loading} {...props} />;
} else {
Router.push('/login');
}
};
};
some of your pages are going to be public and some of them will be private. So will connect the private pages to this HOC.
// i pass two props to show how to use them
const OnlyAdmin = ({user, loading}) => {
return (
<BaseLayout loading={loading}>
<h1>I am Admin Page - Hello {user.name}</h1>
</BaseLayout>
)
}
export default withAuth(OnlyAdmin)('admin');

How connect Context with Redirect

I want to send information to second page if the user is logged in . I would like use Context to that.
Something about my code :
const Login = () => {
...
const [logged, setLogged] = React.useState(0);
...
const log = () => {
if (tempLogin.login === "Login" && tempLogin.password == "Haslo") {
setLogged(1);
}
...
return (
{logged == 1 && (
<Redirect to="/page" />
)}
I want to send logged to /page but i don't know how . None guide help me .Page is actually empty React file.
There are 2 ways handle that:
Passing state to route(as described in docs):
{logged == 1 && (
<Redirect to={{ path: "/page", state: { isLoggedIn: true } }} />
)}
And your component under /page route will access that flag as this.props.location.state.isLoggedIn
Utilize some global app state(Redux, Context API with <Provider> at root level or anything alike).
To me second option is better for keeping auth information:
Probably not only one target component will want to check if user is authorized
I'd expect you will need to store some authorization data to send with new requests(like JWT token) so just boolean flah accessible in single component would not be enough.
some operation on auth information like say logout() or refreshToken() will be probably needed in different components not in single one.
But finally it's up to you.
Thanks skyboyer
I solved this problem with Context method .I will try tell you how i do this becouse maybe someone will have the same problem
I created new file
import React from "react";
import { Redirect } from "react-router";
const LoginInfo = React.createContext();
export const LoginInfoProvider = props => {
const [infoLog, setInfoLog] = React.useState("");
const login = name => {
setInfoLog(name);
};
const logout = () => {
setInfoLog("old");
};
const { children } = props;
return (
<LoginInfo.Provider
value={{
login: login,
logout: logout,
infolog: infoLog
}}
>
{children}
</LoginInfo.Provider>
);
};
export const LoginInfoConsumer = LoginInfo.Consumer;
In App.js add LoginInfoProvider
<Router>
<LoginInfoProvider>
<Route exact path="/" component={Login} />
<Route path="/register" component={Register} />
<Route path="/page" component={Page} />
</LoginInfoProvider>
</Router>
In page with login (parts of code in my question) i added LoginInfoConsumer
<LoginInfoConsumer>

React Redux parent and child component sharing state URL keeps reloading old ID

so I have a redux store with has this basic structure:
{
user: {
id: 1,
currentCompanyId: 2,
}
companyDetails: {
id: 2,
name: 'Joes Company'
},
otherCompanies: [2,3,4],
}
I have a parent page which in the header has a dropdown that allows the user to link / switch to another company.
The parent component needs to know which company is selected so it can show the name in the heading.
The child component displays the details of the current company.
There will be various types of companies and the url's / sections will be different for each type. So in the child component I was trying to set the user's company and then load the details of that company.
The child component doesn't need to directly reference the user or current company.
So what I was doing was in the child component I would listen in willmount and willreceiveprops for a change to the url, then fire an action to update the user company. This will then cause the parent to re render as the user object has changed. Which in turn will create a new / remount the child component. So far this seemed logical.
The issue is that when I have selected company 2 and try to switch to company 3, it will set the company to 3, but then reset it back to 2 again.
I am not sure if this is to do with the URL having not updated or something. I have gone around in circles so much now that I am not sure what the workflow should be anymore.
edit
if I comment out this.loadContractorInfo(contractorId); from ContractorHome.js in componentWillMount() it will work correctly (i.e. the URL stays with the number in the link, vs reverting to the old one. I assume this is to do with redux actions are async, and although I am not doing any network calls it is some race condition between getting data for the contractor page and the contextheader wanting to display / update the current company
edit 2
so to confirm. I have loaded the page at the root of the site, all fine. I select a company / contractor from the dropdown. this loads fine. I go to change that selection to a different contractor from the dropdown. The first component to be hit will be the parent (contextheader), the location prop in nextprops will have updated to have the correct ID in the URL. the method execution at this point will NOT update any thing, no actions are fired. It then hits the child (contractorhome) willreceiveprops method, again the URL in location is good as is the match params. I have commented out all code in willrecieveprops so it does not do anything here either. It will then go back to the parent willreceive props and the location will have gone back to the previous ID in the URL.
app.js snippet:
render() {
return (
<div className="app">
<Router>
<div>
<div className="bodyContent">
<ContextHeader />
<Switch>
{/* Public Routes */}
<Route path="/" exact component={HomePage} />
<Route path="/contractor" component={ContractorRoute} />
<Route path="/building" component={BuildingRoute} />
</Switch>
</div>
<Footer />
</div>
</Router>
</div>
);
}
contextheader:
componentWillReceiveProps(nextProps) {
const { setCompany } = this.props;
const currentInfo = this.props.sharedInfo && this.props.sharedInfo.currentCompany;
const newInfo = nextProps.sharedInfo && nextProps.sharedInfo.currentCompany;
if (newInfo && newInfo.id && (!currentInfo || currentInfo.id !== newInfo.id)) {
setCompany(newInfo.id, newInfo.type);
}
}
render() {
const { user, companies, notifications } = this.props;
/* render things here */
}
);
}
}
const mapStateToProps = state => ({
user: state.user,
sharedInfo: state.sharedInfo,
companies: state.companies,
notifications: state.notifications,
});
const mapDispatchToProps = dispatch => ({
setCompany: (companyId, type) => dispatch(setCurrentCompany(companyId, type)),
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ContextHeader));
contractorRoute:
const ContractorRoute = ({ match }) => (
<Switch>
<Route path={`${match.path}/:contractorId`} component={ContractorHome} />
</Switch>
);
contractorHome
componentWillMount() {
const contractorId = parseInt(this.props.match.params.contractorId, 10);
this.setSharedCompany(contractorId);
this.loadContractorInfo(contractorId);
}
componentWillReceiveProps(nextProps) {
const newContractorId = parseInt(nextProps.match.params.contractorId, 10);
if (this.props.match.params.contractorId !== nextProps.match.params.contractorId) {
this.setSharedCompany(newContractorId);
}
}
setSharedCompany(contractorId) {
const { sharedInfo, setCompany } = this.props;
if (typeof contractorId === 'number') {
if (!sharedInfo || !sharedInfo.currentCompany || !sharedInfo.currentCompany.id || sharedInfo.currentCompany.id !== contractorId) {
setCompany(contractorId);
}
}
}
loadContractorInfo(contractorId) {
const { sharedInfo, getContractorInfo, busy } = this.props;
if (!busy && sharedInfo && sharedInfo.currentCompany && sharedInfo.currentCompany.id === contractorId) {
getContractorInfo(contractorId);
}
}
render() { /*render lots of things here*/};
const mapStateToProps = (state) => {
const selector = state.contractor.details;
return {
sharedInfo: state.sharedInfo,
details: selector.info,
error: selector.request != null ? selector.request.error : null,
busy: selector.request && selector.request.busy,
};
};
const mapDispatchToProps = dispatch => ({
getContractorInfo: contractorId => dispatch(getContractor(contractorId)),
setCompany: contractorId => dispatch(setSharedinfoCurrentCompany(contractorId, 'contractor')),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractorHome);

Resources