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.
Related
I am trying to implement a protected route to only allow logged in users to access my app. The code I have written seems to work, I am redirected to my login page when I try to access the homepage without being logged in, however once I am logged in I can access the page but I does not render and I get the following error: Click here for error
I have tried multiple methods and wrapping the element in my protected route seems like the V6 way of doing things, however it doesn't seem to work for me:
My protected route
interface PrivateRouteProps extends RouteProps {
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({...rest}) => {
const auth = useAuth();
if (auth?.user === null) {
return <Navigate to='/'/>;
}
return <Route {...rest}/>;
};
export default PrivateRoute;
My app (route usage)
function App() {
useEffect(() => {
API
.get('api', '/reservation', {})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error.response);
});
}, [])
return (
<Router>
<Routes>
<Route path='/' element={<LoginPage />}/>
<Route path='/consultAndReserve' element={<PrivateRoute><Navbar/><ConsultReserve/></PrivateRoute>} />
<Route path='/consultAndReserve/:date' element={<><Navbar/><ConsultReserveWithDate/></>}/>
<Route path='/myReservations' element={<><Navbar/><Reservations/></>}/>
<Route path='/tracing' element={<><Navbar/><Tracing/></>}/>
</Routes>
</Router>
);
}
What am I doing wrong?
It's fairly trivial to create a "PrivateRoute" component, even in TypeScript.
In your case you can't directly render a Route component as the error points out. This was a breaking change between RRDv5 and RRDv6. You can render the children prop since you are directly wrapping the components you want to protect.
Example:
const PrivateWrapper = ({ children }: { children: JSX.Element }) => {
const auth = useAuth();
return auth?.user ? children : <Navigate to="/" replace />;
};
Usage:
<Routes>
...
<Route
path="/consoleAndReserve"
element={(
<PrivateWrapper>
<Navbar />
<ConsultReserve />
</PrivateWrapper>
)}
/>
...
</Routes>
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']}
/>
I am working on a project where i need to integrate Auth functionality, but i counter with a redirection issue. after Logout i am not landing over /auth, it still resides on /dashboard component, but if I refresh the the page it redirects to the /auth component.
Testing Scenario:
once loge In, then same time Logout, it will work fine, will take you to the /auth
once Log In, type in the url auth, it will redirect to the Dashboard same time, which is absolutely fine. but after this, if you try to logout, it will logout, but the url will not redirect to auth.
Logout Functionality
const logout = () => {
dispatch({type: actionTypes.LOGOUT})
history.push('/auth')
setUserProfile(null)
}
Logout Reducer
import { AUTH, LOGOUT, LOGIN_CODE } from '../constants/ActionTypes'
const authReducer = (state={authData: null}, action) => {
switch (action.type) {
case LOGOUT:
localStorage.removeItem("userProfile")
return {...state, authData: null}
default:
return state
}
}
export default authReducer
Routes
<switch>
<Route path="/" component={()=> <Redirect to="/dashboard" />} exact />
<PrivateRoute path="/" component={Dashboard} auth={user} exact />
<PrivateRoute path="/dashboard" component={Dashboard} auth={user} exact />
<PublicRoute path="/auth" component={Login} auth={user} restricted={true} exact />
<swtich>
PrivateRoute Code
import React from 'react';
import {Route,Redirect} from 'react-router-dom';
const PrivateRoute = ({component : Component, auth, ...rest})=>{
return(
<Route {...rest} render={props =>{
if(!auth)
return <Redirect to="/auth"/>
return <Component {...props}/>
}}/>
)
}
export default PrivateRoute;
PublicRoute Code
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PublicRoute = ({auth, component: Component, restricted, ...rest}) => {
return (
// restricted = false meaning public route
// restricted = true meaning restricted route
<Route {...rest} render={props => (
auth && restricted ?
<Redirect to="/" />
: <Component {...props} />
)} />
);
};
export default PublicRoute;
import Router from 'next/router'
Then after logout send:
Router.reload();
You must Call function localStorage when you logout:
localStorage.clear();
it will redirect you into your auth page.
I searched every possible solution to fix this uncertain behavior of react-router-dom, but did not find any logical solution for this. but now at the end I came up with the hot fix for this type of issue. just need to add the tiny one line snippet in logout functionality. which I mentioned below for best reference.
reload route after logOut
window.location.reload()
this function will not hold the component and url too. once logout functionality run, will redirect to the auth route right away.
I am currently in the process of creating a react webpage, using starlette as my web server framework that hooks up my database and provides the API. To improve code separation and unneeded loading of files I divided my page into two separately built react pages. One for the login page before verification, and one for the main page once verification has been completed and the user has a valid token. The problem with this is that both react web pages send GET requests as an example to: /static/js/2.91da4595.chunk.js.
I am wondering if it is possible for me to change where react will send requests to when looking for static files. So for example my login page will look to /otherstatic/js/2.91da4595.chunk.js instead.
There might be a more elegant way to reach the point I want to, so feel free to suegest a different method. Let me know if any further explanation or code is needed, and I can add it to this post.
You may need to do code-splitting. Read here for further information.
Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need, and reduced the amount of code needed during the initial load.
I assume you used react-router-dom, so here's a simple implementation:
import React, { Suspense } from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
const HomePage = React.lazy(() => import('./HomePage'));
const LoginPage = React.lazy(() => import('./LoginPage'));
function MyApp() {
const [auth, setAuth] = React.useState({
isLoading: true,
isAuthenticated: false,
data: null,
})
React.useEffect(() => {
const checkAuth = () => {
// call setAuth here
}
checkAuth()
}, [])
const MyRoute = ({ component: Component, authorized: false, ...rest }) => (
<Route
{...rest}
render={props => {
if (auth.isLoading) return null
if (authorized) { // Home page access
return auth.isAuthenticated
? <Component {...prop} />
: <Redirect to="/login" />
} else { // Login page access
return !auth.isAuthenticated
? <Component {...prop} />
: <Redirect to="/" />
}
}}
/>
)
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<MyRoute path="/login" component={LoginPage} authorized={false} />
<MyRoute path="/" component={HomePage} authorized={true} />
</Switch>
</Suspense>
</BrowserRouter>
);
}
folks. I'm learning how to integrate React with Express using React Router, and I've run into a problem with authenticating users. I'm trying to use a higher order component to conditionally render a protected route based on a user's authorization status.
const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
if (!AUTHORIZED) {
return <Redirect to="/login" />;
}
return <Component {...props} />;
}}
/>
);
};
The problem I'm having is in the if (!AUTHORIZED) statement. I'm using Passport to handle authentication on the Express server side, and I have an endpoint set up for retrieving user information and authorization status, but I can't figure out how to get access to that data before the page renders. If I was using a class component instead of a functional component, (learning hooks also), I think I could get the data with the componentWillMount lifecycle method, but I read that's bad practice. Any ideas on how I could move forward from here would be much appreciated!
***edit***
A couple of things I tried to get this working...
I tried adding an authorization module to fetch the data for me.
class Auth {
constructor() {
this.authenticated = false;
}
async isAuthenticated() {
console.log("hitting auth route");
await fetch("/api/auth")
.then(res => res.json())
.then(json => {
if (json.error) {
this.authenticated = false;
}
this.authenticated = true;
});
return this.authenticated;
}
}
export default new Auth();
I import the module and plug auth.authenticated() in place of the placeholder AUTHORIZED. This function gets skipped, because it's asynchronous, and the redirect will always occur.
So I need to add await to auth.authenticated(). But now I need to have async further up the chain, so I foolishly add async in front of props, as such:
render={async props => {
So now It's trying to render a promise object instead of a component, and we get the error Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
Promises all the way down.
Answering this in case anyone runs into a similar issue...
The first solution I had was based on Firealem Erko's comment. On login, I saved a variable with the user's ID to local storage and referenced that in my component. This was a good first solution, but later was improved by something rotimi-best mentioned in his comment. It turns out you can indeed pass props to these components, which I did not realize in my inexperience. So that is now the way I'm doing it. The final solution is as follows:
const ProtectedRoute = ({
component: Component,
logged,
setLogged,
...rest
}) => {
return (
<Route
{...rest}
render={props => {
if (!logged) {
return (
<Redirect
to={{
pathname: "/login",
state: { flashInfo: "Please log in to continue." }
}}
/>
);
} else {
return <Component {...props} logged={logged} setLogged={setLogged} />;
}
}}
/>
);
};
And here's the parent component where I'm passing in the props:
function App() {
let [logged, setLogged] = useState(false);
useEffect(() => {
if (window.localStorage.getItem("qrs")) {
setLogged(true);
} else {
setLogged(false);
}
}, []);
return (
<div className="App">
<BrowserRouter>
<Nav logged={logged} setLogged={setLogged} />
<Switch>
<ProtectedRoute
exact
path="/dashboard"
component={Dashboard}
logged={logged}
setLogged={setLogged}
/>
<Route
path="/register"
exact
render={props => <Register {...props} logged={logged} />}
/>
<Route
path="/login"
exact
render={props => (
<Login {...props} logged={logged} setLogged={setLogged} />
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
Thanks to all the commenters for their suggestions!