29

I am stuck in a issue that happens when user manually changes the route in browser tab and presses enter. This forces my react router to navigate to the state entered by user. I want to prevent this and allow routing only through the flow I have implemented by button clicks in my website.

Some of my screens need data that will be available only if the user navigates the site using the flow expected. If user directly tries to navigate to a particular route by manually changing the route in url then he may skip the desired flow and hence the app will break.

Other scenario, in case I want to restrict some users from accessing some routes but the user knows the path and manually enters that in browser url then he will be presented with that screen but should not be.

halfer
  • 19,824
  • 17
  • 99
  • 186
Peter
  • 10,492
  • 21
  • 82
  • 132
  • Do you also want to stop them from leaving your site by typing *"www.stackoverflow.com"* in the URL field? What about people coming **from** stackoverflow or other sites, should they allowed to choose what page to go to? Please consider not messing with how the normal browser navigation mechanisms work. It inevitably leds to a poor user experience. – ivarni Aug 22 '17 at 06:03
  • @ivarni : Question explained in detail. – Peter Aug 22 '17 at 06:23
  • I would rather suggest that the pages that need any data from previous steps in a flow redirects the user back to the start of the flow if that data is missing. That will also works for when users refresh their browser with F5/Ctrl+R or bookmarks a page and comes back another time. This should solve your problem without having to fight against how browsers work. – ivarni Aug 22 '17 at 06:30
  • @ivarni thought about it but that would be annoying to the end user. – Peter Aug 22 '17 at 07:58
  • @VishalGulati Are you using Redux? – zhuber Sep 09 '19 at 09:46
  • @zhuber Yes I'm – Peter Sep 10 '19 at 03:06
  • I would also like a similar implementation, if user changes the URL manually and the target route has any condition (say route is meant for admin only) then the user is sent back to previous page or URL location he/she was on. – Sushmit Sagar Mar 27 '20 at 08:15

5 Answers5

9

What I do is use a prop from previous page, if that prop is undefined(meaning user did not follow due process :) hehe ) I simply send the user back to the landing page or wherever.

Chigo Kawa
  • 117
  • 1
  • 4
4

You can create a route guard using HOC. For example, you don't want unauthorized user to pass route /profile, then you can do the following:

// requireAuthorized.js (HOC)
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {Redirect} from 'react-router-dom'

const connector = connect(
  state => ({
    isAuthorized: state.profile !== null // say, you keep user profile in redux
  })
)

export default (WrappedComponent) => {
  return (
    connector(
      class extends Component {
        static propTypes = {
          isAuthorized: PropTypes.bool.isRequired
        }

        render () {
          const {isAuthorized, ...clearedProps} = this.props
          if (isAuthorized) {
            return <WrappedComponent {...clearedProps} />
          } else {
            return <Redirect to={{pathname: '/login'}} />
          }
        }
      }
    )
  )
}


// ProfilePage.jsx
import React from 'react'
...
import requireAdmin from '../hocs/requireAdmin' // adjust path

class ProfilePage extends React.Component {
  ...
  render () {
    return (
      <div>
        ...
      </div>
    )
  }
}

export default requireAdmin(ProfilePage)

Pay attention to the export statement in my ProfilePage.js

GProst
  • 9,229
  • 3
  • 25
  • 47
  • Looks good, thanks! That wont be sufficient for my first problem regarding flow. Any idea how to handle that? – Peter Aug 22 '17 at 07:57
  • Could you please provide some example of that flow? `Link` component in `react-router` has nested property called `state`, if it helps. For example, `My button`. And then in component or HOC you can access this state like this: `this.props.location.state.byButtonClick`. If it is not true, then redirect user. – GProst Aug 22 '17 at 08:15
  • that will require including this check in all components impacted. Is there any other solution to make React router listen to route changes done via events in app and not manual change? – Peter Aug 22 '17 at 09:58
  • Sorry, don't know such solution. – GProst Aug 22 '17 at 10:20
2

I'd suggest using this library for cleanest solution (or at least make personal similar implementation of it).

Then you'd create authentication check HOC:

export const withAuth = connectedReduxRedirect({
    redirectPath: '/login',
    authenticatedSelector: state => state.user.isAuthenticated, // or whatever you use
    authenticatingSelector: state => state.user.loading,
    wrapperDisplayName: 'UserIsAuthenticated'
});

And you could easily create flow HOC:

export const withFlow = (step) = connectedReduxRedirect({
    redirectPath: '/initial-flow-step',
    authenticatedSelector: state => state.flow[step] === true,
    wrapperDisplayName: 'FlowComponent'
});

Then initialize your component

const AuthenticatedComponent = withAuth(Dashboard)
const SecondStepComponent = withFlow("first-step-finished")(SecondStep)
const ThirdStepComponent = withFlow("second-step-finished")(ThirdStep)

You can easily create authenticated flow step by composing HOC:

const AuthSecondStepComponent = withAuth(withFlow("first-step-finished")(SecondStep))

Only thing that is important is that you update your redux state correctly as going through your step flow. When user finishes first step you'd set

state.flow["first-step-finished"] = true // or however you manage your state

so that when user navigates manually to specific page, he wouldn't have that redux state because its an in-memory state and would be redirected to redirectPath route.

zhuber
  • 5,364
  • 3
  • 30
  • 63
1

Something like this is suitable. You make HOC Route with a wrap to function that deals with authentication/context props. Note: this deals with direct access to the route, not to the menu items and such. That must be treated in a simmilar way on the menu / menuItem components.

import requireAuth from "../components/login/requireAuth";

class Routes extends React.Component<RoutesProps, {}> {
    render() {
        return (
            <div>
                <Switch>
                    <Route exact={true} path="/" component={requireAuth(Persons, ["UC52_003"])} />
                    <Route path="/jobs" component={requireAuth(Jobs, ["UC52_006"])} />
                </Switch>
            </div>
        )
    }
}


export default function (ComposedComponent, privileges) {

    interface AuthenticateProps {
        isAuthenticated: boolean
        userPrivileges: string[]
    }
    class Authenticate extends React.Component<AuthenticateProps, {}> {
        constructor(props: AuthenticateProps) {
            super(props)
        }

        render() {
            return (
                isAuthorized(this.props.isAuthenticated, privileges, this.props.userPrivileges) &&
                <ComposedComponent {...this.props} /> || <div>User is not authorised to access this page.</div>
            );
        }
    }

    function mapStateToProps(state) {
        return {
            isAuthenticated: state.userContext ? state.userContext.isAuthenticated : false,
            userPrivileges: state.userContext ? state.userContext.user ? state.userContext.user.rights : [] : []
        };
    }

    return connect(mapStateToProps, null)(Authenticate);
}
jan
  • 11
  • 1
0

you can put the condition in useEffect of the given page/screen and push it back if it doesnt have the required values.. example below

enter image description here

rsb
  • 345
  • 1
  • 5