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)
Related
I'm trying to make multiple pages about some game character using React Router
For multiple pages, I want to load data from firebase and insert the data into pages.
the all character page formats are same and just need to change detailed data
For example,
we have
character 'a' : Age : 12, Gender : Male
character 'b' : Age: 13, Gender : Female
and each character page would need to show the character's data with loaded data
I need to show the data of only one character at one page.
Here is my code about routing
function App() {
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
const onCharChange = (text: string) => {
setCurChar(text);
};
return (
<>
<title>Tekken_info 0.1.0</title>
<GlobalStyle />
<Wrapper>
<PageContent>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/Data" exact={true}>
<Page data={data} />
</Route>
</Switch>
</PageContent>
</Wrapper>
</>
);
}
export default App;
But how I did temporarily was not quite satisfying...
I load character data with useState and load it in to '/Data' page
I don't think this is good way and want to make one route for characters.
For example
when we access to '/a' load data about a only 'a'....
If anything you don't understand about my description and question
let me know
Instead of passing the data as props to Page component, You could move the useEffect into the page itself. so that only curChar is a route path variable and can be accessed inside the page something like:
/Data/curChar
Change the path to
path="/Data/:curChar"
Inside the page
const Page = () => {
let {
curChar
} = useParams();
//move the useEffect here.
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
return <div > Now showing details of {
curChar
} < /div>;
}
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
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