I followed the marmelab documentation for applying role base restrictions: https://marmelab.com/react-admin/doc/3.4/Authorization.html.
I have successfully applied to my resources section:"Restricting Access To Resources or Views" and to my menu section:"Restricting Access to a Menu". However, on thing that I think is missing on the documentation and that I haven't yet found a solution was how to apply this type of restrictions to the customRoutes.
function App() {
return (
<Admin
theme={MyTheme}
layout={MyLayout}
history={history}
customRoutes={[
<PrivateRoute
exact
path="/system-status"
component={props => <SystemStatusScreen {...props} />}
/>,
<PrivateRoute
exact
path="/social-media"
component={props => <SocialMediaScreen {...props} />}
/>,
}
catchAll={NotFound}
authProvider={authProvider}
loginPage={LoginPage}
dataProvider={dataProvider}
>
...
React-admin allows to define a usePermissions function to retrieve permissions but those permissions are not loaded outside the tag thus I cannot precalculate the routes to pass to the Admin component as they are undefined.
Can someone help me on this issue?
Thank you in advance.
Hello there Daniel.
I found a solution for what you'd like to accomplish.
You should create a custom component like PrivateRoute and in order to prevent undefined comming from usePrivileges hook provided by React-Admin, you should check wheter or not the component is mounted.
import { FC } from 'react';
import { Route, Redirect, RouteProps } from 'react-router';
import { useAuthorization, useIsMounted } from 'src/lib';
import { Spinner } from 'src/components';
import { PrivateRouteProps } from './PrivateRoute.props';
const PrivateRoute: FC<PrivateRouteProps & RouteProps> = ({ privileges, ...props }) => {
const { permissions: role } = useAuthorization();
const isMounted = useIsMounted();
if (!isMounted()) return <Spinner screen />;
return privileges.includes(role) ? <Route {...props} /> : <Redirect to="/" />;
};
export default PrivateRoute;
The custom hook useIsMounted can be found here.
Extended: I'm checking if the role that the current user has is included in an array provided, to the component, of avaiable privileges.
Usage:
<PrivateRoute
exact
path="/add-user"
component={CreateUser}
key={1}
privileges={['super', 'admin']}
/>
Related
After login I am not redirecting to my users page I am using react metronic theme.
I think something is wrong with props.history.push()
Here what I have done so far.
Main component is below:
ReactDOM.render(
<MetronicI18nProvider>
<MetronicLayoutProvider>
<MetronicSubheaderProvider>
<MetronicSplashScreenProvider>
<ToastContainer/>
<App store={store} persistor={persistor} />
</MetronicSplashScreenProvider>
</MetronicSubheaderProvider>
</MetronicLayoutProvider>
</MetronicI18nProvider>,
document.getElementById("root")
);
App component:
export default function App({ store, persistor }) {
return (
/* Provide Redux store */
<Provider store={store}>
{/* Asynchronously persist redux stores and show `SplashScreen` while it's loading. */}
<PersistGate persistor={persistor} loading={<LayoutSplashScreen />}>
{/* Add high level `Suspense` in case if was not handled inside the React tree. */}
<React.Suspense fallback={<LayoutSplashScreen />}>
{/* Override `basename` (e.g: `homepage` in `package.json`) */}
<BrowserRouter>
{/*This library only returns the location that has been active before the recent location change in the current window lifetime.*/}
<MaterialThemeProvider>
{/* Provide `react-intl` context synchronized with Redux state. */}
<I18nProvider>
{/* Render routes with provided `Layout`. */}
<Routes />
</I18nProvider>
</MaterialThemeProvider>
</BrowserRouter>
</React.Suspense>
</PersistGate>
</Provider>
);
}
This is my routers component
<Switch>
{getToken()==null? (
<Route>
<AuthPage/>
</Route>
) : (
<Redirect from="/auth" to="/"/>
)}
<Route path="/error" component={ErrorsPage}/>
{getToken()==null ? (
<Redirect to="/auth/login"/>
) : (
<Layout>
<BasePage/>
</Layout>
)}
</Switch>
this is my base component
export default function BasePage() {
return (
<Suspense fallback={<LayoutSplashScreen/>}>
<Switch>
<Redirect exact from="/" to="/allUsers"/>
<ContentRoute path="/allUsers" component={AllUsers}/>
<Redirect to="error/error-v1"/>
</Switch>
</Suspense>
);
}
this is action which I fire after sign In button
props.onLogin(values).then((res) => {
toast.success('Login Successfull')
props.history.push('/allUsers')
}).catch((err) => {
console.log('error', err)
toast.error('Login failed')
})
and I have same AuthPage component.
api is fired success fully, token is saved successfully and toaster is shown 'Login successful'. But I am not redirecting to my desired page.
But if I refresh then I am redirected to home page.
Can anyone help me with this
** history.push is working in /allUsers route. But not working in Login component
Update: I think I found what I am doing wrong.
I don't need to perform any history.push() action when I will save token(which is I am doing in my onLogin action) it should automatic redirect to AllUsers Page.
I changed my approach to as below and now it is working.
const {isAuthorized} = useSelector(
({mainAuth}) => ({
isAuthorized: mainAuth.user != null,
}),
shallowEqual
);
replaced getToken with isAuthorized
But history.push() not working here is still mystery.
Please use history package version as v4.9.0 for react-router v5.2.0
Working example
https://codesandbox.io/s/quizzical-dan-z3725
Originally answered here: https://stackoverflow.com/a/64623542/1723410
I think in your case you are not getting history reference in props. You can try these two solutions. withRouter from react-router & useHistory from react-router-dom.
import { withRouter } from 'react-router';
componentName(){
const { match, location, history } = this.props
history.push('Your Page')
}
export default withRouter(componentName);
OR
import { useHistory } from "react-router-dom";
component(){
const history = useHistory();
history.push('Your Page')
}
Try this
import { useHistory } from "react-router-dom";
export someclass/ReactComponent{
.
.
.
const history = useHistory();
props.onLogin(values).then((res) => {
toast.success('Login Successfull')
history.push('/allUsers')
}).catch((err) => {
console.log('error', err)
toast.error('Login failed')
})
}
}
Or refer to this thread
I guess I have been able to reproduce your issue here but not sure yet about solution. But my while guess is that your root most Switch doesn't get triggered when you do history.push, so those expressions with getToken are not evaluated and thats why we are not redirected anywhere.
If I have time later to play around more, I might update this answer.
So essentially, I am trying have an authentication workflow. So basically, the Home route is protected, login is not, register is not, and then I have a verifyEmail page that opens if you arent verified.
const PrivateRoute = ({component: RouteComponent,...rest}) =>{
const{currentUser}=useContext(AuthContext)
function route (){
}
return(
<Route
{...rest}
render={routeProps =>
!!currentUser && currentUser != null
?
currentUser.emailVerified ? <RouteComponent {...routeProps}/>:(<Redirect to={"/verifyEmail"}/>)
:
(<Redirect to={"/login"}/>)
}
/>
)
}
export default PrivateRoute
And then in App.js I have
function App() {
return (
<div className={'App'}>
<AuthProvider>
<Router>
<div>
<PrivateRoute path="/" component={HomePage}/>
<Route path="/verifyEmail" component={Verify}/>
<Route path="/login" component={Login}/>
<Route path="/register" component={Register}/>
</div>
</Router>
</AuthProvider>
</div>
);
}
export default App;
Current User is basically the user credentials, I am using Firebase for authentication. The problem I am having is that after logging in it just shows a blank screen, when the email is not verified instead of showing the verifyEmail page.
To elaborate more on the problem, the actual problem is that if a user is not email verified, then it routes to nowhere, and gives me a blank screen so meaning <Redirect to="/verifyEmail" doesnt work. To debug this further I decided to replace that with a Hello</> and I saw a screen with Hello. So I dont think authentication is the problem, just that it doesnt Route to the appropriate page.
Please help.
You can create a HOC (a component) that wrap your Routes and you make your validation there.
Example:
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
export default function RequireAuth({ children }) {
const history = useHistory();
const auth = useSelector(state => state.auth)
useEffect(() => {
if(!auth){
history.push("/")
}
}, [auth])
return (
<>
{children}
</>
)
}
This is an example of a personal project where I use react-redux to take auth witch is only a boolean.
I use useHistory of react-router-dom to redirect to "/" in case the user isn't logged in.
And finally in my App.js:
<div>
<RequireAuth>
<Route path='/post' component={PostPage} />
</RequireAuth>
<Route path='/' exact component={LoginPage} />
</div>
I'm fairly new to React and Ionic. What I'm attempting to accomplished is to create a "protected route". I create a simple context called AuthContext:
import { createContext, Dispatch } from 'react';
/**
* Context interface for AuthAuthentication/Authorization
*
* #property isAuthenticated
* #property dispatch
*
* #interface
*/
interface AuthDefaultContext {
isAuthenticated: boolean;
dispatch: Dispatch<any>
}
/**
* Authentication/Authorization context for managing
* authenticating/ed and authorizing/ed users
*/
export const AuthContext = createContext<AuthDefaultContext>({
isAuthenticated: false,
dispatch: () => {}
});
and FunctionalComponent called ProtectedRoute:
interface ProtectedRouteProps {
ProtectedComponent: FunctionComponent,
routePath: string
}
export const ProtectedRoute: FunctionComponent<ProtectedRouteProps> = ({ ProtectedComponent, routePath }) => {
useEffect(() => {
console.log(`loading protected route '${routePath}' with component ${ProtectedComponent.name}`)
}, [ProtectedComponent, routePath] );
return (
<AuthContext.Consumer>
{
({ isAuthenticated }) => (
<Route path={ routePath } render={ () => isAuthenticated ? <ProtectedComponent/> : <Redirect to="/login" /> }/>
)
}
</AuthContext.Consumer>
);
}
In my main app component I wrap the IonReactRouter inside the AuthContext.Provider like so:
<IonApp>
<AuthContext.Provider value={{ isAuthenticated: authenticated, dispatch: dispatch }}>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/login" component={ Login } exact={ true } />
<ProtectedRoute routePath="/welcome" ProtectedComponent={ Welcome }/>
<ProtectedRoute routePath="/search" ProtectedComponent={ Dummy }/>
</IonRouterOutlet>
</IonReactRouter>
</AuthContext.Provider>
</IonApp>
The issue that I am having is that above works fine for the regular Route and first ProtectedRoute, but the /search/ does not work. The uri path changes in the browser url to /search but the dummy component doesn't render.
I noticed that if you swap <IonReactRouter> & <IonRouterOutlet> like so:
<IonRouterOutlet>
<IonReactRouter>
.....
</IonReactRouter>
</IonRouterOutlet>
all of the routes works but you lose the animation effect provided by the IonRouterOutlet. The navigation docs on Ionics website has the IonReactRouter wrapping IonRouterOutlet so I am assuming that's the correct implementation.
If I change the ProtectedRoute's to just the standard react Route all routes work fine with the animations. So the issue has to be with my Implementation of the ProtectedRoute component, I just can't figure it out. Any help would be most appreciated. Thank you!
UPDATE:
I still haven't found a solution for this issue, so I removed the IonRouterOutlet which seems to be the root cause. When doing so, everything works as expected. I posted this issue on the Ionic forum so when/if I get an answer, I'll post it here.
Thanks,
IonRouterOutlet expects its children to be either Routes or "Route like", which means the children need to have the same basic props that a Route does.
So, for your ProtectedRoute, it should work if you rename your incoming props routePath to path and ProtectedComponent to component.
I put together a working sample repo starting with your code here: https://github.com/elylucas/ionic-react-protected-route
Let me know if that helps.
Thanks
I've found that using the render prop on a Route in an IonRouterOutlet breaks it.
In your case you would have to replace the following:
<Route
path={routePath}
render={() => isAuthenticated ? <ProtectedComponent/> : <Redirect to="/login" />}
/>
With something like this:
{
isAuthenticated ?
<Route path={routePath} component={ProtectedComponent} /> :
<Redirect to="/login" />
}
My problem was that I had some pages nested in a reusable component "BasicPage" which caused problems:
interface BasicPage extends RouteComponentProps {
title: string;
renderContent: any;
history: any;
hasMenu: boolean;
backAction?: any;
}
/**
* helper Ionic Page which laysout the framework of the page so
* we dont need to repeat the boilerplate code. We also include
* the router by default to help with page navigation
*/
const BasicPage: React.FC<BasicPage> = ({
title,
renderContent,
history,
hasMenu,
backAction,
}) => {
return (
<>
<IonHeader>
<IonToolbar color="primary">
<IonButtons slot="start">
{hasMenu ? <IonMenuButton /> : null}
{backAction ? <IonBackButton defaultHref="/" text="" /> : null}
</IonButtons>
<IonTitle>{title}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>{renderContent(history)}</IonContent>
</>
);
};
export default withRouter(BasicPage);
The renderContent was not rendering my page. I haven't figured out what is the cause of that, but I'm now trying to work around that problem.
Problem: When I use history.push(), I can see that browser changes url, but it does not render my component listening on the path. It only renders if I refresh a page.
App.js file:
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import PropTypes from "prop-types";
//Components
import LoginForm from "../LoginForm/LoginForm";
import PrivateRoute from "../PrivateRoute/PrivateRoute";
import ServerList from "../ServerList/ServerList";
const App = ({ store }) => {
const isLoggedIn = localStorage.getItem("userToken");
return (
<Router>
<Provider store={store}>
<div className="App">
{isLoggedIn !== true && (
<Route exact path="/login" component={LoginForm} />
)}
<PrivateRoute
isLoggedIn={!!isLoggedIn}
path="/"
component={ServerList}
/>
</div>
</Provider>
</Router>
);
};
App.propTypes = {
store: PropTypes.object.isRequired
};
export default App;
Inside my LoginForm, I am making a request to an API, and after doing my procedures, I use .then() to redirect my user:
.then(() => {
props.history.push("/");
})
What happens: Browser changes url from /login to /, but component listening on / route is not rendered, unless I reload page.
Inside my / component, I use useEffect() hook to make another request to API, which fetches data and prints it inside return(). If I console.log inside useEffect() it happens twice, I assume initial one, and when I store data from an API inside component's state using useState() hook.
EDIT: adding PrivateRoute component as requested:
import React from "react";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ component: Component, isLoggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLoggedIn === true ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
};
export default PrivateRoute;
What I tried already:
1) Wrapping my default export with withRouter():
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
2) Creating custom history and passing it as prop to Router.
react-router-dom version is ^5.0.1. react-router is the same, 5.0.1
You have at two mistakes in your code.
You are not using <switch> component to wrap routes. So all routes are processed at every render and all components from each <route> are rendered.
You are using local store to exchange information between components. But change in local store is invisible to react, so it does not fire component re-rendering. To correct this you should use local state in App component (by converting it to class or using hooks).
So corrected code will look like
const App = ({ store }) => {
const [userToken, setUserToken] = useState(localStorage.getItem("userToken")); // You can read user token from local store. So on after token is received, user is not asked for login
return (
<Router>
<Provider store={store}>
<div className="App">
<Switch>
{!!userToken !== true && (
<Route exact path="/login"
render={props => <LoginForm {...props} setUserToken={setUserToken} />}
/>
)}
<PrivateRoute
isLoggedIn={!!userToken}
path="/"
component={ServerList}
/>
</Switch>
</div>
</Provider>
</Router>
);
};
And LoginForm should use setUserToken to change user token in App component. It also may store user token in local store so on page refresh user is not asked for login, but stored token is used.
Also be sure not to put anything between <Switch> and </Switch> except <Route>. Otherwise routing will not work.
Here is working sample
I'm trying to create a protected route that will redirect to /login when user is not authorized using Found Router for Relay Modern based off the example given by React Router:
const PrivateRoute = ({ component: Component, ...rest }) => {
return (<Route {...rest} render={props => {
if (!props) return
if (Component && props && props.viewer) {
return (<Component {...props} />)
} else {
throw new RedirectException('/login')
}
}}
/>)
}
I'm replacing the fakeAuth with real login logic, but the rest is the same. The Route just doesn't render.
Found Router seems to be light on examples around this specific problem. Any ideas?
I ended up splitting out my login and authenticated routes:
export default makeRouteConfig(
<Route>
<LoginRoute exact path='/login' Component={Login} query={LoginQuery} />
<PrivateRoute path='/' Component={App} query={AppQuery}>
</Route>
)
And extending Route for LoginRoute like this:
export class LoginRoute extends Route {
render({ Component, props }) {
if (!props) return undefined
if (Component && props && props.viewer) {
throw new RedirectException('/')
}
return (<Component {...props} />)
}
}
And the PrivateRoute looks much the same, but with different redirects in the case that there is no viewer.
It works pretty well.
Jimmy Jia, the creator of the project had some other suggestions that I didn't end up using but may be useful to any future readers. See my closed issue here.