I'm adding some authorization checks to different pages in an app, but running into some errors passing down a piece of state.
I have
const RequireAuth = Component => {
return class App extends React.Component {
state = {
isAuthenticated: false,
isLoading: true
};
_updateLogin = () => {
this.setState({ isAuthenticated: true, isLoading: false });
};
render() {
const { isAuthenticated, isLoading } = this.state;
if (isLoading) {
return <div>Placeholder for animation</div>;
}
if (!isAuthenticated) {
return <Redirect to="/auth/login-page" />;
}
return <Component {...this.props} />;
}
};
};
And I am trying to pass down the _updateLogin function to the AdminLayout component, where I have a login page.
ReactDOM.render(
<Router history={hist}>
<Switch>
<Route
path="/admin"
component={RequireAuth(AdminLayout)}
login={() => {this._updateLogin}}
/>
<Route path="/auth" component={AuthLayout} />
<Redirect from="/" to="/admin/dashboard" />
</Switch>
</Router>,
document.getElementById("root")
);
If I run the above code, I get
Line 43:23: Expected an assignment or function call and instead saw an expression no-unused-expressions
If I change the login to
login={this._updateLogin}
it returns
"Cannot read property '_updateLogin' of undefined.
Any tips?
Related
I have a react/redux app, using 'react-router-dom' for routing. I am using the following in my App.js (tags are react-bootstrap, in order to provide private routes. Expected behaviour is to be redirected to /login if, and only if, the user is not logged in (based on the presence of a cookie). Actual behaviour is that the app immediately redirects to /login, even when the user is logged in.
class MainContainer extends Component {
constructor(props) {
super(props);
this.props.loadAuthCookie();
}
PrivateRoute = ({ component: ChildComponent, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (!this.props.auth.loggedIn && !this.props.auth.authPending) {
return <Redirect to="/login" />;
} else {
return <ChildComponent {...props} />;
}
}}
/>
);
};
render() {
const { PrivateRoute } = this;
return (
<Router>
<Container fluid id="root">
<Header />
<Switch>
<Row className="pageContainer">
<Col>
<PrivateRoute exact path="/" component={HomeScreen} />
<Route
exact
path="/clusters/"
component={clusterOverviewScreen}
/>
<Route exact path="/login" component={LoginPage} />
</Col>
</Row>
</Switch>
</Container>
</Router>
);
}
}
const mapStateToProps = (state) => {
return {
auth: state.auth,
};
};
const mapDispatchToProps = (dispatch) => {
return {
loadAuthCookie: () => {
return dispatch(loadAuthCookie());
},
};
};
const RootContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MainContainer);
export default class App extends Component {
render() {
return (
<Provider store={store}>
<RootContainer />
</Provider>
);
}
}
The function this.props.loadAuthCookie dispatches a redux action which pulls the cookie that contains the auth token (if present), and puts it into the state.
Initial state is
{
"authPending": false
"loggedIn": false
"authToken": null
}
Which becomes:
{
"authPending": true
"loggedIn": false
"authToken": null
}
and finally:
{
"authPending": false
"loggedIn": true
"authToken": TOKEN
}
I am relatively new to react. My guess is that by the time the PrivateRoute function runs, the redux action has not yet set the state, and therefore, the PrivateRoute component redirects to the login page. This guess is based on logging props.auth.loggedIn in the private route - this returns false, but by the time the login page to which I am redirected has finished loading, the component props state that this.props.loggedIn is true.I am not sure how to fix this, though.
Edit: loadAuthCookie action:
export const loadAuthCookie = () => (dispatch) => {
dispatch(AuthReducer.setAuthPending());
try {
const cookie = Cookies.get("token");
if (cookie) {
dispatch(AuthReducer.setLoginSuccess(cookie));
}
} catch (err) {
console.log(err);
dispatch(AuthReducer.setLogout());
}
};
The PrivateRoute looks like it's implemented incorrectly.
It should be designed to 'wait' until either authPending becomes false or a loggedIn becomes true.
But in reality it requires both of them to be true for it to NOT redirect to login. So implement to 'wait' if authPending: true until loggedIn:true.
Here is table of the outcomes.
Also don't use conditions like !loggedIn && !authPending where readability is terrible especially when you've been working hours on it. I've over looked this because I can't be asked to comprehend what I'm seeing.
<Route
{...rest}
render={(props) => {
if (this.props.auth.loggedIn) {
return <ChildComponent {...props} />;
} else if (this.props.auth.authPending) {
return <LoadingScreen/> // Or could simply div with text. Not different component
// Then state updates after x seconds and re-renders
} else {
return <Redirect to="/login" />;
}
}}
/>
Edit: I actually got the logic wrong, either authPending & loggedIn can be true to redirect to the child element because of the ! inverse operator.
function test(first, second) {
if (!first && !second) {
console.log('login');
} else {
console.log('child');
}
}
test(true, true);
test(false,false);
test(true, false);
test(false, true);
output:
child
login
child
child
See still can't get my head around!;
I think Tony is right; you probably need to call this.props.loadAuthCookie(); later in the lifecycle of this component.
The constructor is run when (and only when) the component is instantiated; as opposed to when it's mounted or rendered. The constructor is usually called once, and before mounting or rendering.
For this reason, calling this.props.loadAuthCookie() might work inside of componentDidMount as well, but I'd go with Tony's suggestion and try it in the render() function.
I'm using React Router v5.2.0 and implementing public / private routes with async authentication.
Essentially the workflow is:
PublicRoute for login,
PrivateRoute for dashboard
and a PublicRoute for /landing (so if a user is logged in, but they aren't subscribed to a service they will be redirected to a landing page).
All of my routes work except for '/landing'
The app will redirect to /landing succesfully, but the Landing component doesn't show / mount. (The console.log will not trigger either)
I am using redux and I've tried adding withRouter but receive the same error.
If I remove the public or private route component and use a regular Route, the component will show.
App.js
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/" component={Dashboard} />
<PublicRoute exact restricted={true} path="/landing" component={Landing} />
<PublicRoute exact restricted={false} path="/login" component={Login} />
</Switch>
</BrowserRouter>
PublicRoute:
class PublicRoute extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
isAuthenticated: false,
isSubscribed: false
};
}
async componentDidMount() {
console.log("loading");
const isLoginCheck = await isLogin();
const isSubscribedCheck = await isSubscribed();
this.setState({
loading: false,
isAuthenticated: isLoginCheck || false,
isSubscribed: isSubscribedCheck || false
})
}
render() {
const { component: Component, restricted, ...rest } = this.props;
return (
<Route
{...rest}
render={props => (
//If a user is authenticated, subscribed and the route is restricted, redirect to dashboard
this.state.isAuthenticated && this.state.isSubscribed && restricted ? (
<Redirect to="/" />
) : this.state.loading ? (
//If loading, show a loading component (placeholder currently)
<div>LOADING</div>
) : this.state.isAuthenticated && (!this.state.isSubscribed) ? (
//If the user is authenticated, but not subscribed. redirect to landing
<Redirect to="/landing" />
) :
(
//Else redirect to the component.
<Component {...props} />
))
}
/>
);
}
}
export default PublicRoute;
Landing:
class Landing extends React.Component {
componentDidMount() {
console.log('component mounted');
}
render() {
console.log('Component was called');
return (
<h1>Landing page - not a valid subscriber. </h1>
)
}
}
export default Landing;
Thanks!
I want to make a PrivateRoute in React with an async LoginCheck. I have tried this:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loggedIn: false
}
}
componentDidMount() {
//ASYNC FUNCTION
setTimeout(() => {
this.setState({loggedIn: true});
}, 1000)
}
render() {
console.log("RENDERED", this.state.loggedIn);
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
this.state.loggedIn === true
? <Component {...props} />
: <Redirect to="/login" />
)}/>
)
return(
<Router>
<Switch>
<Route path="/" exact component={Home} />
<PrivateRoute path="/private" component={PrivatePage} />
</Switch>
</Router>
)
}
}
But that is not working.
In the console it shows first "RENDERED false" and after a second "RENDERED true". So it is rendered with the right parameters but in the const PrivateRoute he is redirecting to the login page so loggedIn is false.
Why? What can I do?
You can create a function checkAuth() to seperate the validation as follow
import React from "react";
import {
BrowserRouter as Router,
Switch,
Redirect,
Route,
} from "react-router-dom";
import Home from "./Home";
import PrivatePage from "./Private";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loggedIn: true
}
}
componentDidMount() {
//ASYNC FUNCTION
setTimeout(() => {
this.setState({ loggedIn: false });
}, 1000)
}
render() {
console.log("RENDERED", this.state.loggedIn);
const checkAuth = () => {
try {
// To be replaced with your condition
if (this.state.loggedIn === true) {
return true;
}
} catch (e) {
return false;
}
return false;
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkAuth() ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/' }} />
)
)} />
)
return (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<PrivateRoute path="/private" component={PrivatePage} />
</Switch>
</Router>
)
}
}
Visit the url "http://localhost/private" as loggedIn: true this route is authorized but after 0.2s the state of loggedIn will be update it false and the protected route will be redirected to the home page.
I'm trying to protect my routes in ReactJS.
On each protected routes I want to check if the user saved in localStorage is good.
Below you can see my routes file (app.js) :
class App extends Component {
render() {
return (
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/signup" component={SignUp} />
<Route path="/contact" component={Contact} />
<ProtectedRoute exac path="/user" component={Profile} />
<ProtectedRoute path="/user/person" component={SignUpPerson} />
<Route component={NotFound} />
</Switch>
<Footer />
</div>
);
}
}
My protectedRoute file :
const ProtectedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
AuthService.isRightUser() ? (
<Component {...props} />
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)} />
);
export default ProtectedRoute;
And my function isRightUser. This function send a status(401) when the data aren't valid for the user logged :
async isRightUser() {
var result = true;
//get token user saved in localStorage
const userAuth = this.get();
if (userAuth) {
await axios.get('/api/users/user', {
headers: { Authorization: userAuth }
}).catch(err => {
if (!err.response.data.auth) {
//Clear localStorage
//this.clear();
}
result = false;
});
}
return result;
}
This code is not working and I don't know really why.
Maybe I need to call my function AuthService.isRightUser() with a await before the call and put my function async ?
How can I update my code to check my user before accessing a protected page ?
I had the same issue and resolved it by making my protected route a stateful class.
Inside switch I used
<PrivateRoute
path="/path"
component={Discover}
exact={true}
/>
And my PrivateRoute class is as following
class PrivateRoute extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
isLoading: true,
isLoggedIn: false
};
// Your axios call here
// For success, update state like
this.setState(() => ({ isLoading: false, isLoggedIn: true }));
// For fail, update state like
this.setState(() => ({ isLoading: false, isLoggedIn: false }));
}
render() {
return this.state.isLoading ? null :
this.state.isLoggedIn ?
<Route path={this.props.path} component={this.props.component} exact={this.props.exact}/> :
<Redirect to={{ pathname: '/login', state: { from: this.props.location } }} />
}
}
export default PrivateRoute;
When you annotate a function with async like you did in AuthService.isRightUser() it returns a Promise and you are not treating the response of the method accordingly.
As you suggested, you could call the method AuthService.isRightUser() with await and annotate the function you are passing to the render property with async.
Or you could treat the response of AuthService.isRightUser() with a .then() and .catch() instead of the ternary operator
I'm trying to write Authentication checking for my DashBoard. But the function itself is not getting called. Can anyone give me some solution for this? I'm developing in ReactJs.
This is the Route part :
<Router>
<div>
<Route exact path={"/"} component={Home} />
<Route path={"/SignUp"} component={SignUp} />
<Route path={"/SignIn"} component={SignIn} />
<Route path={"/Dashboard"} component={Dashboard} onEnter={this.requireAuth} />
</div>
</Router>
This is the function :
requireAuth (nextState, replace) {
console.log("?????????????",this.state.getToken);
if(!this.state.getToken) {
replace({pathname: '/'});
}
}
In react-router v4, you can make use of render prop to Route along with the lifecycle methods to replace the onEnter functionality existing in react-router v3.
See this answer for more details:
onEnter prop in react-router v4
However since all you want to do is authentication in the onEnter prop, you could easily create a HOC that does that
const RequireAuth = (Component) => {
return class App extends Component {
componentWillMount() {
const getToken = localStorage.getItem('token');
if(!getToken) {
this.props.history.replace({pathname: '/'});
}
}
render() {
return <Component {...this.props} />
}
}
}
export { RequireAuth }
and use it like
<Route path={"/Dashboard"} component={RequireAuth(Dashboard)}/>
Edit: In case you need to make a network call to find if the use if authenticated of not, you would write the HOC like
const RequireAuth = (Component) => {
return class App extends Component {
state = {
isAuthenticated: false,
isLoading: true
}
componentDidMount() {
AuthCall().then(() => {
this.setState({isAuthenticated: true, isLoading: false});
}).catch(() => {
this.setState({isLoading: false});
})
}
render() {
const { isAuthenticated, isLoading } = this.state;
if(isLoading) {
return <div>Loading...</div>
}
if(!isAuthenticated) {
return <Redirect to="/login" />
}
return <Component {...this.props} />
}
}
}
export { RequireAuth }
Update:
In addition to the HOC, you can also go for the PrivateRoute component like
const PrivateRoute = ({component: Component, isAuthenticated, isLoading, ...rest }) => {
if(isLoading) {
return <div>Loading...</div>
}
if(!isAuthenticated) {
return <Redirect to="/login" />
}
return <Component {...this.props} />
}
}
}
export { PrivateRoute };
and you can use it like
class App extends Component {
state = {
isAuthenticated: false,
isLoading: true
}
componentDidMount() {
AuthCall().then(() => {
this.setState({isAuthenticated: true, isLoading: false});
}).catch(() => {
this.setState({isLoading: false});
})
}
render() {
<Router>
<div>
<Route exact path={"/"} component={Home} />
<Route path={"/SignUp"} component={SignUp} />
<Route path={"/SignIn"} component={SignIn} />
<PrivateRoute path={"/Dashboard"} component={Dashboard} isAuthenticated={this.state.isAuthenticated} isLoading={this.isLoading}/>
</div>
</Router>
}
}