How to protect routes, not to be accessed directly through URL? - javascript

//this route is define in app.js
<Route path="/edit" component={Edit}/>
//this is navlink
<NavLink to={{pathname:"/edit",state:{index:index}}}>Edit</NavLink>
// class
class Edit extends Component {
constructor(props){
super(props)
this.state = {
index:props.location.state.index,
title:'',
};
}
componentDidMount(){
axios.get("http://localhost:5000/gettitle",{headers: {token: Cookies.get('adtoken')}})
.then(response => {
this.setState({
title:response.data.jobs[this.state.index].title,
})
})
.catch(error => {
console.log(error)
})
}
render() {
}
}
export default Edit;
When I click on this Navlink it moves to /edit with props, but when I directly write /edit through URL it gives errors because it is accessing /edit component without props
How can I protect /edit so that it cant be accessed directly through URL?
Thanks

You can use PrivateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
props?.location.state?.index ? (
<Component {...props} />
) : (
<Redirect to="/404" />
)
}
/>
);
Here an example

If you are using react-router-v3 or less you can use Route onEnter callback
<Route onEnter={checkIfValidUrl}></Route>
and in callback function you will get get replace function. which you can use to redirect to desire page.
const checkIfValidUrl = (nextState, replace, cb) => {
// Your condition goes here.
}
But if you are using react-router-v4 or higher can create a High order component (HOC) to check if that route is valid route or not.
If your application supports server side rendering you can write code on server to handle this scenario.

It does seem like you are using React Router, thus my answer will be for that library.
In order to protect /edit you could trigger <Redirect /> if props are missing.
EditPage component would look something like this:
import React from 'react'
import { Redirect } from 'react-router-dom'
const EditPage = ({ index }) => {
if(index){
return <Redirect to="/" />
} else {
return <div>my protected page</div>
}
}
export default EditPage

Related

Redirecting to another page(route) from a React class component

I need some help to solve the following issue with using React.
In some web app I have a landing page, where I want to redirect the user to the login page in case she or he is not logged in.
I want to use the following landing page (taken from some tutorial I found on the net) in order to use it as a model for mine.
The problem is that this is a function component while my landing page is a class component. According to what I understand I guess I need to consider the code inside useEffect and (somewhat) transfer it to componentDidMount() in my class component. But I don't know how to do that. history.replace will not work in a class component (no Hooks in Classes). Any advice from a more React experienced user will be very welcome.
import React, { useEffect, useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useHistory } from "react-router";
import "./Dashboard.css";
import { auth, db, logout } from "./firebase";
....
function Dashboard() {
const [user, loading, error] = useAuthState(auth);
const [name, setName] = useState("");
const history = useHistory();
....
useEffect(() => { // Important part for my question !
if (loading) return;
if (!user) return history.replace("/");
....
}, [user, loading]);
return (
<div>
{/*...*/}
<button className="dashboard__btn" onClick={logout}>
Logout
</button>
</div>
);
}
export default Dashboard;
Here is what I tried on my Class Component:
class MyCompo extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("--componentDidMount(MyCompo)--");
const { history } = this.props
history.push("/login");
}
.....
}
But I get the following error:
TypeError: history is undefined
componentDidMount
=============== Added information ===============
Below is the relevant part of the code I have been working on:
This part is what works:
<Route exact path="/" component={TopMenu}>
{true && <Redirect to="/login" />}
</Route>
What I tried in the Links Component did not work.
The code:
....
ReactDOM.render(
<Router>
<Switch>
<Route exact path="/" component={TopMenu}>
{true && <Redirect to="/login" />}
</Route>
<Route exact path="/login" component={Login} />
<Route exact path="/section1" component={Section1Page}/>
<Route exact path="/section2" component={Section2Page}/>
<Route exact path="/section3" component={Section3Page}/>
</Switch>
</Router>,
document.getElementById('root')
);
....
const TopMenu = () => {
return (
<div className='page_container'>
<Title/>
<Links path='/'/>
<button className="dashboard__btn" onClick={logout}>
Logout
</button>
</div>
)
};
class Links extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("--componentDidMount(Links)--");
// This is some code I tried with no success.
const { history } = this.props
//history.push("/login");
}
componentDidUpdate(prevProps, prevState) {
console.log("--componentDidUpdate(Links)--");
}
render() {
return (
<div className='links_container'>
{(this.props.path != '/mng') &&
<React.StrictMode>
<Link to='/mng'>{mnMgrStr()}</Link><br/>
</React.StrictMode>}
{(this.props.path != '/other') &&
<React.StrictMode>
<Link to='/other'>{otherInpStr()}</Link><br/>
</React.StrictMode>}
.......
</div>
)
}
}
Following the example on the React Router docs you can use withRouter if your component isn't already receiving the route props, otherwise you can access history from the props.
class MyComponent extends React.Component {
...
componentDidMount() {
const { history } = this.props
// do whatever with history here
}
...
}
In react-router-dom version 5 there are a couple ways a class component can access the history object.
Rendered directly by a Route component via the component, or render or children function props so route props (i.e. history, location, and match) are passed.
component: <Route path="....." component={MyCompo} />
render: <Route path="....." render={routeProps => <MyCompo {...routeProps} />} />
Access the history object from the passed route props:
class MyCompo extends React.Component {
componentDidMount() {
const { history } = this.props;
history.push("/login");
}
...
}
Decorated by the withRouter Higher Order Component so the route props are injected.
import { withRouter } from 'react-router-dom';
class MyCompo extends React.Component {
componentDidMount() {
const { history } = this.props;
history.push("/login");
}
...
}
export default withRouter(MyCompo);
Well I hope by answering this question I can save lot of time of others. Don't need to panic it's not a major issue. I will explain step by step reason and solution.
First of all why this happening is
In react-router-dom **V6 (version 6) latest ** there is no history export or redirect.
There is navigate construct.
So to achieve in functional component there is useNavigate() hook.
Now coming to answer...
To redirect in class component using react-router-dom V6 we have to use component.
So now one has to follow the following steps:
Import navigate
import { Navigate } from "react-router-dom";
Use Navigate to redirect
So above I discussed syntax to do so now coming to your exact problem
You have to redirect user to login if he is not logged in
You can follow these steps:
create state to store status of user like logged in or not (inside constructor of class)
this.state = {
userLogged: false,
};
in your render method you have to add some condition like if user is not logged in take user to login page. see below..
render() {
const { userLogged } = this.state;
if (goToPay) {
return (
<Navigate to="/cart" state={selectedTiffin} props={selectedTiffin} />
);
}
}
That's it.
It can be confusing so I am giving full example so you can save your lot of time..
import React from "react";
import { Navigate } from "react-router-dom";
class Solve extends React.Component {
constructor(props) {
super(props);
this.state = {
userLogged: false,
};
}
// here you can write code to set the status of user like logged in or not
render() {
const { userLogged } = this.state;
if (userLogged ) {
return (
<Navigate to="/cart" />
);
}
return (
<>
Here you can return your original component that should be render when user is log in
</>
);
}
}
I hope this will help and work. Thank You

Rewriting custom route to use render prop instead of component prop in React Router

I have a React app that uses React Router. I have a bug in my code that is due to something in my custom ProtectedRoute component:
const ProtectedRoute = ({ component, ...args }) => (
<Route
component={withAuthenticationRequired(component, {
onRedirecting: () => <Loading />,
})}
{...args}
/>
);
export default ProtectedRoute;
You can read my other post that goes into my problem in detail here: Auth0 ProtectedRoute component preventing component from changing with state
But now I am just trying to rewrite this function to use the render prop instead of the component prop (which I believe is causing the issue). This is what I tried but it didn't work:
const ProtectedRoute = ({component: ComponentToRender, path, rest}) => {
return <Route
{...rest}
path={path}
render={(props) => {
withAuthenticationRequired(() => (<ComponentToRender {...props}/>), {
onRedirecting: () => <Loading/>
})
}
}
/>
}
Does anyone know how to rewrite this to use the render prop?
Thanks!
I think the arrow function you passed to render isn't returning anything because you're using body brackets and not using return. Read more about arrow functions on MDN
You could do it like this:
render={(props) => (
withAuthenticationRequired(() => (<ComponentToRender {...props}/>), {
onRedirecting: () => <Loading/>
})
)}

What is the best way to make private route in reactjs

I made a private route in my app basically all the route in my app is private. So to make Private route I added a state isAuthenticated which is false by default but when user login in it becomes true. So on this isAuthenticated state I implemented a condition in private route component. But the problem with this is when user is logged in and refresh the page. I get redirect to / home page. I don't want that.
I am use token authentication here.
Here is my PrivateRoute.js
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
};
}
export default connect(mapStateToProps)(PrivateRoute);
If all your routes when authenticated are private you can also just skip the autentication by route and use following
import PrivateApp from './PrivateApp'
import PublicApp from './PublicApp'
function App() {
// set isAuthenticated dynamically
const isAuthenticated = true
return isAuthenticated ? <PrivateApp /> : <PublicApp />
}
That way you do not need to think for each route if it is authenticated or not and can just define it once and depending on that it will render your private or public app. Within those apps you can then use the router normally and do not need to think who can access which route.
If you validate the token against a server (which you should) then it's an asynchronous operation and isAuthenticated will be falsy initially, causing the redirect on refresh.
You one way around this is an isAuthenticating state, e.g.
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, isAuthenticating, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if( isAuthenticating ){ return <Spinner /> }
return isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
isAuthenticating: state.authentication.inProgress
};
}
export default connect(mapStateToProps)(PrivateRoute);
isAuthenticating should be true by default then set to false when the server response is received and the user.isAuthenticated state is known.

PrivateRoute on React is not redirecting when loggedIn user is false

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} />;
};

React router v4 use declarative Redirect without rendering the current component

I am using a similar code like this to redirect in my app after users logged in. The code looks like the following:
import React, { Component } from 'react'
import { Redirect } from 'react-router'
export default class LoginForm extends Component {
constructor () {
super();
this.state = {
fireRedirect: false
}
}
submitForm = (e) => {
e.preventDefault()
//if login success
this.setState({ fireRedirect: true })
}
render () {
const { from } = this.props.location.state || '/'
const { fireRedirect } = this.state
return (
<div>
<form onSubmit={this.submitForm}>
<button type="submit">Submit</button>
</form>
{fireRedirect && (
<Redirect to={from || '/home'}/>
)}
</div>
)
}
}
Works fine when a successful login has been triggered. But there is the case, that logged in users enter the login page and should be automatically redirected to the "home" page (or whatever other page).
How can I use the Redirect component without rendering the current component and without (as far as I understand discouraged) imperative pushing to the history (e.g. in componentWillMount)?
Solution 1
You could use withRouter HOC to access history via props.
Import withRouter.
import {
withRouter
} from 'react-router-dom';
Then wrap with HOC.
// Example code
export default withRouter(connect(...))(Component)
Now you can access this.props.history. For example use it with componentDidMount().
componentDidMount() {
const { history } = this.props;
if (this.props.authenticated) {
history.push('/private-route');
}
}
Solution 2 Much better
Here is example on reacttraining.
Which would perfectly work for you.
But you just need to create LoginRoute to handle problem you described.
const LoginRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Redirect to={{
pathname: '/private-route',
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
and inside <Router /> just replace
<Route path="/login" component={Login}/>
with
<LoginRoute path="/login" component={Login}/>
Now everytime somebody will try to access /login route as authenticated user, he will be redirected to /private-route. It's even better solution because it doesn't mount your LoginComponent if condition isn't met.
Here is another solution which doesn't touch React stuff at all. E.g. if you need to navigate inside redux-saga.
Have file history.js:
import {createBrowserHistory} from 'history';
export default createBrowserHistory();
Somewhere where you define routes, don't use browser router but just general <Router/>:
import history from 'utils/history';
...
<Router history={history}>
<Route path="/" component={App}/>
</Router>
That's it. Now you can use same history import and push new route.
In any part of your app:
import history from 'utils/history';
history.push('/foo');
In saga:
import {call} from 'redux-saga/effects';
import history from 'utils/history';
...
history.push('/foo');
yield call(history.push, '/foo');

Categories

Resources