I am writing a React.js application (v15.3) using react-router (v2.8.1) and ES6 syntax. I cannot get the router code to intercept all transitions between pages to check if the user needs to login first.
My top level render method is very simple (the app is trivial as well):
render()
{
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel}/>
<Route path="all" component={NotesPanel}/>
</Route>
</Router>
);
}
All the samples on the web use ES5 code or older versions of react-router (older than version 2), and my various attempts with mixins (deprecated) and willTransitionTo (never gets called) have failed.
How can I set up a global 'interceptor function' to force users to authenticate before landing on the page they request?
Every route has an onEnter hook which is called before the route transition happens. Handle the onEnter hook with a custom requireAuth function.
<Route path="/search" component={Search} onEnter={requireAuth} />
A sample requireAuth is shown below. If the user is authenticated, transition via next(). Else replace the pathname with /login and transition via next(). The login is also passed the current pathname so that after login completes, the user is redirected to the path originally requested for.
function requireAuth(nextState, replace, next) {
if (!authenticated) {
replace({
pathname: "/login",
state: {nextPathname: nextState.location.pathname}
});
}
next();
}
In v4 you just create a route component that checks if uses is authenticated and return the next components and of course next component can be other routes.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Redirect } from 'react-router-dom';
import AuthMiddleware from 'modules/middlewares/AuthMiddleware';
class PrivateRoute extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool,
isLoggedIn: PropTypes.func.isRequired,
isError: PropTypes.bool.isRequired
};
static defaultProps = {
isAuthenticated: false
};
constructor(props) {
super(props);
if (!props.isAuthenticated) {
setTimeout(() => {
props.isLoggedIn();
}, 5);
}
}
componentWillMount() {
if (this.props.isAuthenticated) {
console.log('authenticated');
} else {
console.log('not authenticated');
}
}
componentWillUnmount() {}
render() {
const { isAuthenticated, component, isError, ...rest } = this.props;
if (isAuthenticated !== null) {
return (
<Route
{...rest}
render={props => (
isAuthenticated ? (
React.createElement(component, props)
) : (
<Redirect
to={{
pathname: isError ? '/login' : '/welcome',
state: { from: props.location }
}}
/>
)
)}
/>
);
} return null;
}
}
const mapStateToProps = (state) => {
return {
isAuthenticated: state.auth.isAuthenticated,
isError: state.auth.isError
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
isLoggedIn: () => AuthMiddleware.isLoggedIn()
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute);
This version of the onEnter callback finally worked for react-router (v2.8):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login')
}
The link which explains react-router redirection differences between V1 vs v2 is here. Relevant section quoted below:
Likewise, redirecting from an onEnter hook now also uses a location descriptor.
// v1.0.x
(nextState, replaceState) => replaceState(null, '/foo')
(nextState, replaceState) => replaceState(null, '/foo', { the: 'query' })
// v2.0.0
(nextState, replace) => replace('/foo')
(nextState, replace) => replace({ pathname: '/foo', query: { the: 'query' } })
Full Code Listing Below (react-router version 2.8.1):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login');
}
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel} onEnter={this.requireAuth}/>
<Route path="all" component={NotesPanel} onEnter={this.requireAuth}/>
</Route>
</Router>
);
}
If you're using react-router 4 and above, use Render props and redirect to solve this. Refer: onEnter not called in React-Router
This is not a safe solution
You can try n use useEffect hook on every page requiring login as:
useEffect(() => {
const token = localStorage.getItem('token');
if(!token) {
history.push('/login');
}
}
This uses useHistory hook from 'react-router-dom'
you just need to initialize it before calling it as:
const history = useHistory();
As already stated above it is not a safe sloution, but a simple one
You can do it by simply creating a RestrictedRoute or Private component and pass your redux user authenticated state to this and in RestrictedRoute component redirect if state is false, in case of true redirect to component.
code:
const RestrictedRoute = ({ component: Component, authUser, auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.is_authenticated && auth.is_authorized && authUser ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/signin',
state: { from: props.location },
}}
/>
)
}
/>
);
<Switch>
<RestrictedRoute path={`${match.url}app`}
authUser={authUser}
auth={{ is_authenticated, is_authorized }}
component={MainApp}
/>
<Route path='/signin' component={SignIn} />
<Switch>
Related
I would like to refresh the current page (home) when the user tries to go back via browser, after logged in.
What's the best way to solve this? Any suggestions?
I was trying to do something like this inside index.tsx:
if (id) {
const rollback = history.goBack();
if (rollback) {
history.push('/');
}
}
Obs: In this case, '/' is my home page, and i can't apply the logic above because "An expression of type 'void' cannot be tested for truthiness".
Sorry for anything i'm still new at react and trying to learn.
Don't know if i could do something inside my router, here it is anyway:
import React, { Suspense, lazy } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import history from '../utils/history';
import LoadingPage from '../components/organisms/LoadingPage';
const DownloadExams = lazy(() => import('../pages/private/DownloadExams'));
const Home = lazy(() => import('../pages/private/Home'));
const ProfileSelector = lazy(() => import('../pages/private/ProfileSelector'));
const AppRoutes = () => {
return (
<Router history={history}>
<Suspense fallback={<LoadingPage />}>
<Switch>
<Route exact path={'/'} component={Home} />
<Route exact path={'/baixar-exames'} component={DownloadExams} />
<Route exact path={'/profile'} component={ProfileSelector} />
</Switch>
</Suspense>
</Router>
);
};
export default AppRoutes;
Any suggestions?
Thanks!
User logIn time you can store a token or flag and store it in localStorage. After that, you can check if the user login so redirects to the page. You can also create some HOC for the same.
Example :
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
const isLoggedIn = localStorage.getItem("token");
return (
<Route
{...rest}
render={(props) => {
if (isLoggedIn) {
return <Component {...props} />;
} else {
return (
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
);
}
}}
/>
);
};
Example Usage :
<ProtectedRoute path="/home" exact component={Home} />
This will redirect the user to /home after login.
I need to make a router system that distinguishes between authenticated and non-authenticated users.
What I want to achieve with this is that authed users can see their dashboard and personal details, but unauthed users can only see the landing page, the login form and the register form.
I currently have context management so I know whether a user is logged in or not. I just need to figure out all the redirects and routing related things.
My code is making all sorts of "strange" things. I don't really know where I have messed it up.
AppContent.js
This is where all the routing happens.
AppLayout is just a layout wrapper with a header and a footer that must be visible only when logged in.
const AppContent = () => (
<Router>
<Switch>
<PublicRoute component={() => (<h1>Landing page</h1>)} path='/' exact />
<PublicRoute component={LoginPage} path='/login' exact />
<PublicRoute component={() => (<h1>Register page</h1>)} path='/register' exact />
<PrivateRoute component={Home} path='/dashboard' exact />
<PrivateRoute component={Demo} path='/demo' exact />
<PrivateRoute component={Profile} path='/profile' exact />
<PublicRoute component={() => (<h1>not found</h1>)} path='*' />
<PrivateRoute component={() => (<h1>not found</h1>)} path='*' />
</Switch>
</Router>
);
PublicRoute.js
const PublicRoute = ({ component: Component, ...rest }) => {
const { user, isLoading } = useContext(AuthContext);
if (isLoading) {
return (
<h1>Loading...</h1>
);
}
return (
<Route {...rest} render={(props) => (user ? <Redirect to='/dashboard' /> : <Component {...props} />)} />
);
};
PrivateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user, isLoading } = useContext(AuthContext);
if (isLoading) {
return (
<h1>Loading...</h1>
);
}
return (
<Route
{...rest}
render={(props) => (user ? (
<AppLayout>
<Component {...props} />
</AppLayout>
) : <Redirect to='/login' />)}
/>
);
};
AuthStore.js
This is where I manage my auth context.
const AuthStore = ({ children }) => {
const [token, setToken] = useState();
const [user, setUser] = useState();
const [isLoading, setIsLoading] = useState(false);
const setUserToken = async (userToken) => {
localStorage.setItem('token', userToken);
setToken(userToken);
try {
const decodedToken = jwtDecode(userToken);
const currentUserData = await AuthService.getUserData(decodedToken.username);
setUser(currentUserData);
} catch (error) {
console.log(error.name);
}
};
useEffect(() => {
setIsLoading(true);
const localToken = localStorage.getItem('token');
if (localToken) {
setUserToken(localToken);
}
setIsLoading(false);
}, []);
return (
<AuthContext.Provider value={{
user, token, setUserToken, setUser, isLoading,
}}
>
{children}
</AuthContext.Provider>
);
};
Thank you so much.
EDIT
What I am really looking for:
When Unauthorized:
When searching for /dashboard, redirect to /login ✅
When searching for non existing route, redirect to /login ✅
When searching for another unauthorized route like /register, redirect to it fine ✅
When Authorized:
When searching for /login, redirect to /dashboard ✅
When searching for non existing route, redirect to a 404 page, preferably one of the two last routes, the ones that look like <PrivateRoute component={() => (<h1>not found</h1>)} path='*' ✅
When searching for another authorized route like /profile, redirect to it fine ❌ ➡️ It is currently reloading the routes, showing the login page and after half a second it shows /dashboard, but not /profile
According to React Router documentation, direct children of <Switch> component can only be <Route> or <Redirect> components from react-router-dom package.
All children of a <Switch> should be <Route> or <Redirect> elements. Only the first child to match the current location will be rendered.
Your <AppLayout /> component is the problem. Remove it from <Switch>and move it inside your auth pages.
edit
your auth provider is going trough async flow anytime you type in a new address in the browser and press enter, meaning every time you will have a state in your app where user is not logged in, then the components that are consuming this state are reacting to your change. also in the effect you should await setUserToken because loading is set to false before that async function resolves.
the last thing that is not working on your list is because you coded it like that ;), your public route redirects all calls it receives to the /dashboard if there is user, regardless if it should be a 404 route or existing route. i would suggest different approach. create a config where you declare if route is protected or not, then have only one AuthRoute component that will take all the props of the route, and based on this new flag and state of user in the context, do the work
const routes = [{ name: 'home', path: '/', exact: true, isProtected: false, component: Home }, { name: 'protected', path: '/dashboard', exact: true, isProtected: true, component: Dashboard }];
const AuthRoute = ({ name, path, exact, isProtected, component: Component }) => {
const { user } = useContext(userContext);
if (isProtected && !user) { // redirect to login }
if (!isProtected || (isProtected && user)) { // render route with props }
}
///...
<Switch>
{routes.map(route => <AuthRoute {...route} />
</Switch>
I was facing the same problems just 3 to 4 days back when a very kind person who is just amazing, helped me out on the reactiflux server of discord.
You might be trying to go to one protected route and that would work but the other protected route will not work right??
All you have to do is create a Higher Order Component (HOC) in a separate file which you can maybe call protectedRoute.js and inside this file:
import React, { useState, useEffect } from "react";
import { Redirect } from "react-router-dom";
export function protectedRoute(Component) {
return function ComponentCheck(props) {
const [check, setCheck] = useState(false);
const [currentUser, setCurrentUser] = useState(false);
useEffect(() => {
const user = false;
setCurrentUser(user);
setCheck(true);
}, []);
const RedirectToLogin = <Redirect to="/" />;
return check && (currentUser ? <Component {...props} /> : RedirectToLogin);
};
}
In the above file the '/' route in my case is the login page but instead of const RedirectToLogin = <Redirect to='/' /> you can say const RedirectToLogin = <Redirect to='/login' />
and then you can restrict a route like for example I want the /users route to be accessible only if user === true. So for that i can in the users.js file say:
import { protectedRoute } from "./protectedRoute";
const Users = () => {
return <h2>Users</h2>;
};
export default protectedRoute(Users);
and then I also want the /dashboard route to be protected so:
import { protectedRoute } from "./protectedRoute";
const Dashboard = () => {
return <h2>Dashboard</h2>;
};
export default protectedRoute(Dashboard);
but I dont want the /home route to be protected so:
const Home = () => {
return <h2>Home</h2>;
};
export default Home;
I'm trying to implement some security into my app and ended up creating this code:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
auth comes from my state management that looks exactly like this when viewed from the Redux Devtools installed on my Chrome browser; here it is:
isAuthenticated and loading are usually true when a user is loggedIn; that works just fine. The problem I'm having is that my PrivateRoute does not redirect to the auth/login page when no one is loggedIn. Does anyone has any idea on how to fix this?. This is an example of one of my routes that need the PrivateRoute component:
<PrivateRoute exact path='/edit-basics' component={EditBasics} />
The route above is a page to edit the current loggedIn user info only available to him/her. I'm still accessing to it without being loggedIn.
So, you're probably getting errors from having the && operator and two expressions inside of the ternary operation that you're passing to the render method of Route.
You'll have to find a different way to validate if it's still loading.
In JSX if you pair true && expression it evaluates to expression – so basically you're returning !loading as a component to render.
Read more: https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
// TWO OPERATIONS WITH && WILL CAUSE ERROR
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
Also,
React Router's authors recommend constructing this kind of private route with child components instead of passing the component to render as a prop.
Read more: https://reacttraining.com/react-router/web/example/auth-workflow
function PrivateRoute({ auth, children, ...rest }) {
return (
<Route
{...rest}
render={() =>
!auth.isAuthenticated ? (
<Redirect
to={{
pathname: "/auth/login",
state: { from: location }
}}
/>
) : (
children
)
}
/>
);
}
And to call that route:
<PrivateRoute path="/protected" auth={auth} >
<ProtectedPage customProp="some-prop" />
</PrivateRoute>
It looks like you are not passing down the auth prop to the PrivateRoute. Try adding it.
<PrivateRoute exact path='/edit-basics' component={EditBasics} auth={this.props.auth}/>
maybe something like this
const PrivateRoute = ({ component,
auth: { isAuthenticated, loading },
...options }) => {
let history = useHistory();
useLayoutEffect(() => {
if ( !isAuthenticated && !loading) history.push('/auth/login')
}, [])
return <Route {...options} component={component} />;
};
I'm trying to learn react, and am setting up routes in my application which require you to log in. I'm trying to adapt the example given here
The code I've written should either redirect the user or display the protected route. But when I log in I'm still being redirected.
I believe the issue is in my PrivateRoute class below. I pass it a authenticated property which is set in the parent class, but it doesn't appear to update.
In app.js we declare the authenticator, where we perform async login with our backend.
I pass the checkLoggedIn function to the login component, where we set the parent's authenticated state property to true. I'm console.log()ing the state just to check it's occurring, which it is.
When I then click the Link to /protected route I'm still being redirected.
app.js
// imports ...
let authenticator = new Authenticator();
class ProtectedComponent extends Component {
render() {
return (
<h1>Protected!</h1>
);
}
}
class App extends Component {
constructor(props){
super(props);
this.state = {
authenticator: authenticator,
authenticated: authenticator.isLoggedIn(),
}
}
checkLoggedIn() {
this.setState({authenticated: true});
console.log(this.state);
}
render() {
let routes, links = null;
links = <div className="links">
<Link to="/login">Login</Link>
<Link to="/protected">Protected</Link>
</div>;
routes = <div className="routes">
<Route
path="/login"
render={() =>
<Login
authenticator={this.state.authenticator}
loginCallback={this.checkLoggedIn} />
}
/>
<PrivateRoute
path="/protected"
component={ProtectedComponent}
authenticated={this.state.authenticated}
/>
</div>;
return (
<Router className="App">
{links}
{routes}
</Router>
);
}
}
export default App;
PrivateRoute.js
// imports ....
const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
<Route {...rest} render={props =>
authenticated === true
? (<Component {...props} />)
: (<Redirect to={{
pathname: "/login",
state: { from: props.location }
}} />
)
}/>
);
export default PrivateRoute;
Login.js
// imports ...
class Login extends Component {
constructor(props) {
super(props);
this.authenticator = props.authenticator;
this.loginCallback = props.loginCallback;
this.state = {
identifier: "",
password: "",
}
}
updateState = (e, keyName = null) => {
this.setState({[keyName]: e.target.value})
}
attemptLogin = (e) => {
this.authenticator.loginPromise(this.state.identifier, this.state.password)
.then(resp => {
if(resp.data.success === true) {
this.authenticator.setToken(resp.data.api_token);
this.loginCallback();
} else {
this.authenticator.removeToken()
}
})
.catch(err => {
console.error(err);
});
}
render(){
<button onClick={this.attemptLogin}> Log In </button>
}
}
export default Login;
I'm setting the authenticated state to true in the callback method, but when I go to the protected route (and run it's render method) it appears to be evaluating to false.
If I'm misunderstanding the react props system, let me know. If you'd like to see any more of the code let me know and I'll amend the question.
You have to create a PrivateRoute HOC component first:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
localStorage.getItem('bpm-user')
? <Component {...props} />
: <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)} />
)
and wrap your routes that most be protected:
<Switch>
<Route path="/login" component={Login} />
<PrivateRoute path="/new/index" component={NewIndex} />
<PrivateRoute path="/jobs/index" component={JobsIndex} />
<PrivateRoute path="/unions/index" component={UnionsIndex} />
<PrivateRoute exact path="/" component={ListIndex} />
<PrivateRoute exact path="/charges" component={MunicipalCharges} />
</Switch>
and use Link
<Link to="/jobs/index">Jobs</Link>
my login reducer
import axios from 'axios';
import * as actionTypes from './AuthActionTypes';
export const login = (user) => {
return dispatch => {
// for example => dispatch({type:actionTypes.REQUEST_LOGIN_USER});
axios({
method: 'post',
url: '/api/auth/login',
data: { 'username': user.username, 'password': user.password },
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
.then(res => {
localStorage.setItem('bpm-user', JSON.stringify(res.data));
dispatch({
type: actionTypes.LOGIN_USER,
payload: res.data
})
})
.catch(error => {
// TODO... for example => dispatch({type:actionTypes.FAILD_LOGIN_USER, payload:error});
})
}
}
export const logout = () => {
localStorage.removeItem('bpm-user');
}
like the example codes that i copied from my own project
I am using React-Router V4 and I want to hand over an 'authenticated' prop to decide wether to send the user to a login page or to the requested page:
PrivateRoute.js
import React from "react";
import {
Route,
Redirect,
} from "react-router-dom";
const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props =>
authenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
export default PrivateRoute;
My App.js looks like this:
class App extends Component {
is_authenticated() {
const token = localStorage.getItem('access_token');
//if token is not expired send on the way
if (token && jwt_decode(token).exp > Date.now() / 1000) {
return true;
} else {
// if token is expired try to refresh
const refresh_token = localStorage.getItem('refresh_token');
if(refresh_token) {
axios.post(config.refresh_url,{},{
headers: {"Authorization": `Bearer ${refresh_token}`},
"crossdomain": true,
mode: 'no-cors',}).then(
response => {
const access_token = response.data.access_token;
localStorage.setItem('access_token', access_token)
return access_token ? true : false
}
)
}
}
return false
}
render() {
const client = this.client;
return (
<Router>
<div>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/" component={Home} authenticated={this.is_authenticated()} />
</div>
</Router>
);
}
}
export default App;
Since the Axios Call is async the component renders before the call is finished.
How can I make the render wait for the token to be refreshed?
I'd keep isAuthenticated in the App components state. Then, you can use setState in the isAuthenticated call to cause a re-render on the result. This will also be important when the tokens expire.