My application will have 2 roles, Employee and Admin.
I'm trying to implement middleware so users get redirected if they are not authorized to see the content. Is handling not just general authentication but also user roles in React Router good practice?
My first thought was to add a custom role attribute to firebase.auth().currentUser, but adding attributes to currentUser is not allowed by firebase.
If so, how would I do it?
Through state or fetching it from my Firebase DB like this?:
var requireEmp = (nextState, replace, next) => {
var role;
var uid = firebase.auth().currentUser.uid;
firebase.database().ref('/users/' + uid + '/role').once('value').then((user) => {
role = user.val().role;
});
if (role !== 'employee') {
replace('/');
}
next();
};
...
<Router history={hashHistory}>
<Route path="/" >
<Route path="home" component={Main} onEnter={requireLogin}>
<Route path="work" component={Work} onEnter={requireEmp}/>
<Route path="profile" component={Profile} />
<IndexRoute component={Profile}/>
</Route>
</Route>
</Router>
I'm new to React and Redux and still a bit scared of working with state and important data such as the user role attribute.
What are some other areas I need to be really careful with concerning the implementation of user roles?
Thanks.
lets get that user roles working! Every project has its specificities, but here's how I would do it:
Before you first render your app, you've got to be sure that firebase user/currentUser/currentAuth has loaded. If you have roles, just make sure to fetch it it the user is logged in.
Here's an example:
On index.jsx:
import { initializeApp } from './myFirebase';
const routes = routesConfig(store);
let appHasRendered = false;
const renderAppOnce = () => {
if (appHasRendered) return;
render(
<Provider store={store}>
<Router history={syncedHistory} routes={routes} />
</Provider>,
document.getElementById('app')
);
appHasRendered = true;
};
initializeApp(renderAppOnce, store.dispatch);
and then on myFirebase.js:
export const initializeApp = (renderAppOnce, dispatch) => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
// We have a user, lets send him and his role to the store
firebaseRef.child('users/roles').once('value', (snap) => {
dispatch(authLoggedIn({
...user.toJSON(),
role: snap.val() || 'employee'
}));
renderAppOnce();
});
} else {
// There's no user, let's move on
dispatch(authLoggedOut());
renderAppOnce();
}
});
};
All right!!! We have all we need in our store. So now we just have to check that on our onEnter functions of our app:
const routesConfig = (store) => {
// Confirms user is not authenticated
const confirmNoAuth = (nextState, replace) => {
if (store.getState().user) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
};
// Confirms user is authenticated
const confirmAuth = (nextState, replace) => {
if (!store.getState().user) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
};
// Confirms user has a specific role
const confirmRole = role => ((nextState, replace) => {
if (store.getState().user.role !== role) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
});
return (<Route path="/">
<IndexRoute component={HomePage} />
<Route path="login" component={LoginPage} onEnter={confirmNoAuth} />
<Route path="dasboard" component={DashboardPage} onEnter={confirmAuth} />
<Route path="adminsonly" component={AdminDashboardPage} onEnter={confirmRole('admin')} />
</Route>);
};
Theres probably tons of problems on this code, but I believe you can understand the principles. Basically you should pre-fetch the role so you don't have to do it on every route change.
One other tip I can give you is that if you'll have tons of employees and just a handful of admins, just save the admins. This way you'll only have like 20 entries on your roles object instead of hundreds of thousands. That tiny || 'employees' can save you lots of space.
Keep in mind that you can just as easily add more roles if you need. Also, this example uses Redux, but you don't have to.
!!! IMPORTANT !!!
All of this will only keep people from accessing the pages, but smartypants can use the console or a rest client to try to stick their noses in parts of your database where they shouldn't! Be sure to understand and make good use of firebase rules to keep your database secure!
Let me know if it worked
Related
I'm writing an app that pulls my GitHub repos and displays certain information from them (descriptions, etc.). The app is set up so that if I am not 'initialized' (meaning I haven't sent up an account), then I am forced to the 'CreateAccount' page. The state of being 'initialized' is held in local storage. But, if anyone else wants to look at the app, they won't be 'initialized', so I need a second check to see if there is anything in the database (records for each repo). If so, then the viewer should be 'initialized' and sent to the Home page. All of this is done in the App component:
const App = () => {
const setupCtx = useContext(SetupContext);
const devCtx = useContext(DevDataContext);
// variable to control routing
let initialized = null;
if (localStorage.getItem("jtsy-signin") === "true") {
initialized = true;
} else {
API.findRepo()
.then((repo) => {
console.log('APP found 1 repo', repo)
if (repo) {
initialized = true
localStorage.setItem('jtsy-signin', 'true')
console.log('1 REPO initialized', initialized)
} else {
initialized = false;
}
})
}
useEffect(() => {
if (initialized) {
console.log('APP useEffect signin=true, redirect to Home page');
if (localStorage.getItem('jtsy-login') === 'true') {
setupCtx.updateLoggedIn();
}
// console.log('APP devUpdated', setupCtx.state.devUpdated)
if (setupCtx.state.devUpdated) {
API.getActiveDevData().then((activeDevData) => {
// console.log('APP activeDevData', activeDevData.data);
const developerData = {
developerLoginName: activeDevData.data.developerLoginName,
developerGithubID: activeDevData.data.developerGithubID,
repositories: activeDevData.data.repositories,
fname: activeDevData.data.fname,
lname: activeDevData.data.lname,
email: activeDevData.data.email,
linkedInLink: activeDevData.data.linkedInLink,
resumeLink: activeDevData.data.resumeLink,
active: true
}
console.log('APP after DB call', developerData)
devCtx.updateDev(developerData)
setupCtx.updateInitialized();
setupCtx.updateDevUpdated(false)
})
}
};
}, [setupCtx.state.devUpdated])
return (
<div className='App'>
<React.Fragment>
<Router>
<Switch>
{console.log('IN APP SWITCH initialized', initialized)}
{initialized ? (
<Route exact path="/" component={Home} />
) : (
<Route exact path="/" component={CreateAccountComp} />
)}
<Route exact path="/contact" component={Contact} />
<Route exact path="/about" component={About} />
<Route exact path="/developer" component={Developer} />
<Route exact path="/login" component={LoginModal} />
<Route exact path="/logout" component={LogoutModal} />
<Route exact path="/signin" component={CreateAccountComp} />
<Route exact path="/settings" component={Settings} />
<Route component={NoMatch} />
</Switch>
</Router>
</React.Fragment>
</div >
);
};
export default App;
The variable 'initialized' controls whether the Home or CreateAccount pages are rendered. As the code shows, first local storage is checked, and if there is a value there, 'initialized' is set to 'true' and the Home page renders. This works fine. But, if there is no value in local storage, I next call findRepo(), which executes a .findOne() in the Mongo database. Then, the value of 'initialized' is set accordingly. The delay in getting a response from the database is too long. If there is no value in local storage, initialized is 'null' when rendering occurs, so the app always goes to the "CreateAccount" page. I can tell that the database call is responding properly and initialized is getting set to 'true', but it's too late (CreateAccount is already rendered).
I tried putting the database call inside of useEffect, and in a separate useEffect, but neither worked. I need a better approach to make this work.
So I have went through your code a little and came up with a solution to add a loading flag to state and use that to render a loading screen if we are waiting on any sort of update.
You could make this loading screen anything you wanted to show when waiting for updates.
Below is the code changes made all in App.js
// add isLoading flag to state
const [state, setState] = useState(
{
loggedIn: null,
sync: false,
initialized: null,
isLoading: true
}
)
Anytime setState is called, update the isLoading flag to true.
// line 55 in App.js
setState({
...state,
initialized: true,
isLoading: false
})
// line 75
setState({
...state,
initialized: true,
isLoading: false
})
// line 103
setState({
...state,
initialized: false,
isLoading: false
})
Then the return update.
{state.isLoading && <div>Loading...</div>}
<Route exact path="/" component={state.initialized ? Home : CreateAccountComp} />
With these changes the loading screen will show when we are waiting on any sort of update to initialized. Then the home screen will once it is updated to true or the create account page will show if it is updated to false.
I'm currently working on a project with Firebase Integration and React.
I can register and login, but I do want some Guard in order to have access to the "connected" pages when I'm connected, or to be redirect to the default page for disconnected state if I'm not.
I coded the following for having the different Routes and set it in an App.tsx file :
const ConnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} component={component}/>
}
else{
route = <Route {...rest} render={() => <Redirect to ="/welcome" />} />
}
return route
}
const UnconnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} render={() => <Redirect to ="/home" />} />
}
else{
route = <Route {...rest} component={component}/>
}
return route
}
const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/" render={() => <Redirect to="/welcome" />} />
<UnconnectedRoute path="/welcome" component={UnconnectedHome} exact />
<ConnectedRoute path="/home" component={ConnectedHome} exact />
<UnconnectedRoute path="/login" component={Login} exact />
<UnconnectedRoute path="/register" component={Register} exact />
<ConnectedRoute path="/events/create" component={CreateEvent} exact />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
And for the firebase initialization I did the following :
export async function initializeApp(){
console.log("Checking if app is initialized...")
if (firebase.apps.length == 0){
console.log("App initializing...")
firebase.initializeApp(firebaseConfig);
await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
}
}
And the function to know if a user is connected (as a cookie) is this one :
export function isConnected(){
const res = firebase.auth().currentUser
console.log(res)
return res !== null
}
Still, when I reload the tab, it's always returning me FALSE !
So, I was wondering how could I init the firebase server before the launch of the App ? Is this the current problem ? I currently do not have any clue about it and it frustrates me so much...
If you have already encountered such a problem, that would really help me !
Thank you !
Firebase Authentication persists the user's authentication state in local storage. But when you load a new page (or reload the existing one), it has to check with the server whether the authentication state is still valid. And this check takes some time, during which there is no current user yet.
The typical way to get around such race conditions is to use an auth state listener to detect the current user:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in, redirect to "connected" pages
} else {
// No user is signed in, probably require them to sign in
}
});
I tried to answer in comment with the code, it didn't work.
I did this for now (it's not that good but at least it works) :
const connectedPaths =
[
"/home",
"/events/create"
]
const unconnectedPaths =
[
"/welcome",
"/login",
"register"
]
firebase.initializeApp(firebaseConfig);
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
if (!connectedPaths.includes(window.location.pathname)) window.location.href = "/home"
}
else {
if (!unconnectedPaths.includes(window.location.pathname)) window.location.href = "/welcome"
}
});
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>
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
I am a newbie to react and currently developing an application,
BACKGROUND:
It has admin, faculty, student, dashboards and a static landing page with buttons to /admin/login ,/faculty/login ,/student/login. which opens respective dashboards.
PROBLEM:
*when I log in with student's login i am able to access all the dashboards & vice-versa, i have a field named role inside my firebase nodes,
*while logging in i check user's role and it doesn't allow other users with other roles to login ,but once after i login i am able to access all dashboards(which should not happen) including the dashboard which i am supposed to open , i am using react and routes.js, my public and private routes.
i am trying to use a isstudent,isfaculty,isadmin flags to get role and restrict the access but not able to figure out how to traverse all the nodes.
any suggestion would be helpful ,thanks in advance.
ROUTES.js
const Routes = props => {
if (props.user) {
let isStudent=false;
let isFaculty=false;
let isAdmin=false;
const uid = props.user.uid;
const request = firebase
.database()
.ref(`student/${uid}`)
.once("value")
.then(snapshot => {
if (snapshot.val().role === "student") {
// isStudent=true
console.log(snapshot.val());
}
});
firebase
.database()
.ref(`faculty/${uid}`)
.once("value")
.then(snapshot => {
if (snapshot.val().role === "faculty") {
console.log(snapshot.val());
//isFaculty=true
}
});
firebase
.database()
.ref(`admin/${uid}`)
.once("value")
.then(snapshot => {
if (snapshot.val().role === "admin") {
console.log(snapshot.val());
//isAdmin=true
}
});
return (
<MainLayout>
<Switch>
<AdminPublicRoute
{...props}
exact
restricted={true}
path="/admin/login"
component={AdminLogin}
/>
{isAdmin&&<AdminPrivateRoute
{...props}
path="/admin/admindashboard"
exact
component={AdminDash}
/>}
<FacultyPublicRoute
{...props}
exact
restricted={true}
path="/faculty/login"
component={FacultyLogin}
/>
{isFaculty && <FacultyPrivateRoute
{...props}
path="/faculty/facultydashboard"
exact
component={FacultyDash}
/>}
<StudentPublicRoute
{...props}
exact
restricted={true}
path="/student/login"
component={StudentLogin}
/>
{isStudent&& <StudentPrivateRoute
{...props}
path="/student/studentdashboard"
exact
component={StudentDash}
/>}
</Switch>
</MainLayout>
Nicest - lodash map:
const roles = _.map(obj.admin, item => item.role)
Easiest - Object.keys
const roles = Object.keys(obj.admin).map(key => key.role)