Reputation: 568
MERN APP (MongoDB, Express, ReactJS, NodeJS) - on back-end i control the logged user with token and give access to user based on role.
Question is: How to control role of user on front-end (ReactJS) if the user is admin or client?
Ex: If user is logged I send a token from backend to frontend and I could control Routes something like this if the user is logged in:
if(token){ //if user is logged
<Route path="/dashboard" exact>
<Products/> //Products Page
</Route>
}else{
<Route path="/dashboard" exact>
<Page1 /> //AnyPage
</Route>
}
I want to give access to Users Page only for admin users:
if(token && role === 'admin'){ //if user is logged and role is admin
<Route path="/dashboard" exact>
<Users /> //Users Page
</Route>
}...
I could control this thing on backend, I just want to take any other idea about this issue on frontend. Thank you :)
Upvotes: 3
Views: 10246
Reputation: 5991
What I would do in such situation:
When user signs in, API should return their info, including permissions. Store that info in state.
Create a function, checkUserPermission(user = { roles: [] }, permission)
. This function takes user
object, permission
string (i.e. "route.admin"
, "component.Authenticate"
) and should return true or false depending on user's access level. That will be one place which decides if current user is allowed to do this or that. It can consist of simple property check (if (user.role === 'admin')
) or something more complex (look at example).
Create SecuredRoute
component which is a wrapper around Route
but takes user
and permission
parameters and redirects away if user can not see this route.
Use checkUserPermission
function to conditionally render links or components.
As already mentioned in other answer, API must do it's own permission checks on every request because hiding things on UI side won't stop malicious and knowledgeable user
//src/utils/checkUserPermission.js
export default function checkUserPermission(user = { roles: [] }, permission) {
const allowAccessForRoles = {
"route.admin": ["admin"],
"route.authenticated": ["user", "admin"],
"route.home": ["*"], //means "Any role"
"component.Authenticate": ["*", "!user", "!admin"], //Any role except user and admin
"component.BecomeAdmin": ["user"],
"component.LogOut": ["user", "admin"]
};
//If we don't have such permission in list, access denied for everyone
if (!Array.isArray(allowAccessForRoles[permission])) {
return false;
}
//Check if any of user's roles explicitly denies access
for (const role of user.roles) {
if (allowAccessForRoles[permission].includes("!" + role)) {
return false;
}
}
//If list of allowed roles contains '*', access allowed for everyone
if (allowAccessForRoles[permission].includes("*")) {
return true;
}
//Check if any of user's roles allowes access
for (const role of user.roles) {
if (allowAccessForRoles[permission].includes(role)) {
return true;
}
}
return false;
}
//src/SecuredRoute
import React from "react";
import { Route, Redirect } from "react-router-dom";
import checkUserPermission from "./utils/checkUserPermission";
const SecuredRoute = ({
user,
permission,
redirectTo = "/",
children,
...rest
}) => {
const allowed = checkUserPermission(user, permission);
if (allowed) {
return <Route {...rest} render={() => children} />;
}
return (
<Route
{...rest}
render={({ location }) => (
<Redirect
to={{
pathname: redirectTo,
state: { from: location }
}}
/>
)}
/>
);
};
export default SecuredRoute;
//src/App.js
import React, { useState } from "react";
import "./styles.css";
import { BrowserRouter as Router, Switch, Link } from "react-router-dom";
import SecuredRoute from "./SecuredRoute";
import checkUserPermission from "./utils/checkUserPermission";
const AdminPage = () => <div>Admin page is open for admins</div>;
const AuthenticatedPage = () => (
<div>Authenticated page is open for users and admins</div>
);
const HomePage = () => <div>Home page is open for everyone</div>;
export default function App() {
const [user, setUser] = useState();
return (
<Router>
<div className="App">
<div className="Nav">
<div>
<Link to="/">Go to Home Page</Link>
</div>
{checkUserPermission(user, "route.authenticated") ? (
<div>
<Link to="/authenticated">Go to Authenticated Page</Link>
</div>
) : null}
{checkUserPermission(user, "route.admin") ? (
<div>
<Link to="/admin">Go to Admin Page</Link>
</div>
) : null}
</div>
<div className="Controls">
{checkUserPermission(user, "component.Authenticate") ? (
<button type="button" onClick={() => setUser({ roles: ["user"] })}>
Become authenticated
</button>
) : null}
{checkUserPermission(user, "component.BecomeAdmin") ? (
<button type="button" onClick={() => setUser({ roles: ["admin"] })}>
Become admin
</button>
) : null}
{checkUserPermission(user, "component.LogOut") ? (
<button type="button" onClick={() => setUser()}>
Log out
</button>
) : null}
</div>
<div className="Main">
<Switch>
<SecuredRoute user={user} permission="route.home" exact path="/">
<HomePage />
</SecuredRoute>
<SecuredRoute
user={user}
permission="route.authenticated"
path="/authenticated"
>
<AuthenticatedPage />
</SecuredRoute>
<SecuredRoute user={user} permission="route.admin" path="/admin">
<AdminPage />
</SecuredRoute>
</Switch>
</div>
</div>
</Router>
);
}
Upvotes: 3
Reputation: 11581
You're generally on the right track. That kind of pattern (detecting logged in and/or role status and conditionally rendering components) are generally called "protected routes".
You could do something like this:
const ProtectedRoute = ({showRoute, ...props}) => {
if (showRoute) {
return <Route {...props} />
} else {
// return null to simply not render the route if not logged in
return null;
// or you could return <Redirect to='/foo' /> to send them elsewhere
}
}
const App = () => {
const isLoggedIn = true;
return (
<Router>
<ProtectedRoute showRoute={isLoggedIn} path="/dashboard" component={Users} />
</Router>
);
}
Or, maybe there are cases where you want to render one version of a component or the other depending on if they're logged in. You could use a <Switch>
for that to make sure you only render one component (and not both if they are logged in)
const App = () => {
const isLoggedIn = true;
return (
<Router>
<Switch>
<ProtectedRoute showRoute={isLoggedIn} path="/dashboard" component={LoggedInDashboard} />
<Route path="/dashboard" component={LoggedOutDashboard} />
</Switch>
</Router>
);
}
Now, something that is very important: you still need to authenticate every API call or user action on the backend. It makes for a good user experience to conditionally render components based on some state, but it is very very insecure. Remember that anyone with React devtools installed can read and write to React app state at will, so they could simply toggle isLoggedIn
to true
or make themselves an "admin" role. Every API request should be authenticated with a secure token. Perhaps you know all of that already but it really bears repeating.
Edit: you can get even more streamlined by using React context to have routes that "just know" whether the user is logged in or not (or whatever) and render appropriately:
const userLogin = React.createContext(false);
const LoggedInRoute = (props) => {
const showRoute = useContext(userLogin);
if (showRoute) {
return <Route {...props} />
} else {
return null;
}
}
const App = () => {
const [userLoggedIn, setUserLoggedIn] = React.useState(false);
return (
<userLogin.Provider value={userLoggedIn}>
<Router>
<LoggedInRoute path="/dashboard" component={Users} />
</Router>
</userLogin.Provider>
);
}
Upvotes: 2