Prevent routing in React when user manually changes url in browser tab - reactjs

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.

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.

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

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.

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

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

Related

Is it possible to trigger function after logging in by withAuthenticator() of AWS Amplify?

I would like to trigger function when user login by withAuthenticator() of Amplify.
(I need to send state data to other component by contextAPI of React.js)
I've found explanation of SignIn() function, but I didn't find something about function when users login
▼Main.tsx (Main page, everyone can watch)
import React from 'react';
import { BrowserRouter as Router, Route } from "react-router-dom";
import Login from './Login';
import Mypage from './mypage/Index';
import Menu from './Menu';
import Hoge from './Hoge';
class Main extends React.Component {
render(){
return (
<div className="Main">
<Router>
<Menu />
//**Mypage ← only logged in can watch
<Route exact path="/mypage" component={Mypage} />
<Route path="/main" component={Hoge} />
</Router>
</div>
);
}
}
export default Main;
▼mypage.tsx (looged in users can watch)
import React from 'react';
import { RouteComponentProps, Link } from 'react-router-dom';
import { withAuthenticator } from 'aws-amplify-react';
import Auth from '#aws-amplify/auth';
import AuthContext from '../context/auth-context';
interface MatchParams {
id: number;
}
interface State {
user: '',
}
class Mypage extends React.Component<RouteComponentProps<MatchParams>, State> {
constructor(props: RouteComponentProps) {
super(props);
this.state = { user: '' };
}
async componentDidMount() {
let user = await Auth.currentAuthenticatedUser()
this.setState({user: user.username});
}
//**(it's just what I want now)
//loggedIn() {
// console.log("logged in!");
//}
render() {
return (
<div className="mypage">
<h1>mypage</h1>
<div>
<ul>
<li><Link to='hoge'>hoge</Link></li>
</ul>
</div>
</div>
);
}
}
export default withAuthenticator(Mypage);
Answer to J.Hesters
・your ideas 1,2
actually I thought about these ideas(creating signIn form myself), but I wanted to know how it works without them (i should have write about it at first time)
・your idea 3
I inserted console.log("login in!!") in componentDidUpate() but it didnt work after login
Maybe I gotta use the way 1 or 2, but if you get why it doesnt work ur idea3, plz write it here Anyway thx for answering sir ^^
I may be late to answer but you should be using Amplify Logger.
You can then launch whatever code when you need to based on when the event happens. I would do something like this.
import { Hub, Logger } from 'aws-amplify';
const logger = new Logger('Logger', 'INFO');
const listener = (data) => {
switch (data.payload.event) {
case 'signIn':
logger.info('user signed in');
break;
case 'signUp':
logger.info('user signed up');
break;
case 'signOut':
logger.info('user signed out');
break;
case 'signIn_failure':
logger.info('user sign in failed');
break;
case 'configured':
logger.info('the Auth module is configured');
break;
default:
logger.error('Something went wrong, look at data object', data);
}
}
Hub.listen('auth', listener);
You can just write whatever code you want to execute after the await keyword.
async componentDidMount() {
let user = await Auth.currentAuthenticatedUser();
this.setState({user: user.username});
console.log('logged in.');
}
Edit:
As far as I know, you can't explicitly overwrite withAuthenticator's methods. So you have three options as far as I'm concerned:
Supply a custom <SignIn /> component to with authenticator in which you handle the login process manually and invoke whatever function you like as soon as the login method finishes.
Write the whole UI login UI yourself and use Amplify's Auth methods explicitly whenever you need to. Here is an example repository doing it.
Use componentDidUpdate() to trigger code after the component rerenders when the user logs in. Be careful to not create infinite loops with setState. componentDidUpdate only gets called, when the component rerenders. Components within withAuthenticator don't neccesarily rerender.

How to pass state to React JS High Order Component

I am using OIDC redux connector for user state. I have a few components that require authentication. I would like to use something like export default connect(mapStateToProps, mapDispatchToProps)(withAuth(Component)); and request data from state inside my authentication service.
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { push } from 'connected-react-router'
export const withAuth = (Component) => {
return props => {
return <Component {...props} />
}
}
Is it possible to get state in the render function? So I can check the user beinig logged in and redirect to the sign-in page if there is no user signed in?
BTW: How would I redirect? I have tried using redirect-router-dom and <Redirect /> But then it complains about set state being changed too often ... But that might be my mistake. I get this error when I render a Redirect: Error: Maximum update depth exceeded.
If I understand correctly you want to "decorate" your component with additional logic to handle an authentication redirect?
I suggest using a "decorator" pattern here e.g.:
export const withAuth = (Component) => {
const mapStateToProps = (state, ownProps) => ({
authenticated: state.authenticated // Or what you need to do to determine this
});
return connect(mapStateToProps)(class extends React.Component {
render() {
const { authenticated, ...componentProps } = props;
if (authenticated) {
return <Component {...componentProps }>;
}
return <Redirect to="/login" />;
}
});
}
Then when you need this you can do things like:
export default withAuth(connect(yourOwnMapStateToProps)(YourComponent))
Just figured it out, I changed the store so instead of returning a function, it returns the object. So I can load in all js files. It might not be the best solution. If there is a better way to get the store in code, I would love to hear about how to do that. The configurestore function is what I found in quite a lot of examples.
import { store } from '../configureStore';
Using store.getState() I can get the current state.
The redirect issue I am having is similar to: How to use react-transition-group with react-router-dom

How to get access to state with higher order component

I want to create a higher order component that checks if a user has been logged in. If they have, I show the component if not, I want to redirect them to the login page.
can someone explain what I'm doing wrong here?
Here's the HOC:
import React from 'react';
import { connect } from 'react-redux';
const withAuthentication = (Component) => {
class WithAuthentication extends React.Component {
componentDidMount() {
console.log(this.props.sessionId);
}
render() {
return this.props.sessionId ? <Component {...this.props} /> : null;
}
}
const mapStateToProps = (state) => ({
sessionId: state.auth.userInfo.authUserInfo.sessionId
})
return connect(mapStateToProps, null)(WithAuthentication);
}
export default withAuthentication;
then I call it like this:
....
import withAuthentication from './containers/withAuthentication';
const Hello = () => <h1> Hello</h1>;
....
<Route path="/" component={ withAuthentication(Hello) }/>
I stripped more code that I think is unrelated to this...
TIA for you help!
Update: The code that is causing the propblem seems to be this:
const mapStateToProps = (state) => ({
sessionId: state.auth.userInfo.authUserInfo.sessionId
})
The error: TypeError: Cannot read property 'sessionId' of undefined
So basically, what you are doing in nesting one component into another i.e. a Functional component returning another component after some logical level verification.
What you are doing seems good. You should be able to access the props(not State), in a functional component like this
let aComponent(props)=>{
return <p>{props.name}'s logged in status is {props.loggedIn}</p>
}
if you are using the component like this
<aComponent title="Hello" loggedIn={this.props.isLoggedIn}/> //isLoggedIn from redux store
Also, If the Logical/Authentication verification fails in withAuthentication, you should call the router API to navigate to your desired page.
i.e.
you should call this, if you are using react-router
this.props.router.push('/login')

Dispatch Action right after state variable is set

I have an initial redux state like this:
{
loggedInUserId: null,
comments: []
}
Here's how my React App looks like:
class App extends Component {
componentWillMount() {
this.props.getLoggedInUserId();
}
render() {
return (
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/comments" component={Comments} />
</Switch>
);
}
}
In my App, I dispatch an action getLoggedInUserId() which asynchronously fills the loggedInUserId in the state.
The HomePage is a dumb component showing some text. I start the app (route is now '/'), see the HomePage component, then I navigate to the Comments page, which has:
componentWillMount() {
this.props.fetchComments(this.props.loggedInUserId); // Dispatch action to do API call to fetch user's comments
}
render() {
// Show this.props.comments nicely formatted
}
Everything works, I see the list of comments in the Comments component.
But if I refresh the page on the route /comments, then by the time the Comments runs componentWillMount, the loggedInUserId has not been loaded yet, so it will call fetchComments(null).
Right now, to fix this, I'm doing in my Comments component:
componentWillMount() {
if (!this.props.loggedInUserId) return;
this.props.fetchComments(this.props.loggedInUserId);
}
componentWillReceiveProps(nextProps) {
if (!this.props.loggedInUserId && nextProps.loggedInUserId) {
nextProps.fetchComments(nextProps.loggedInUserId);
}
}
which works well. But I'm doing this in 10+ components, and it seems like a lot of work which can be factorized, but I didn't find an elegant way to do it.
So I'm asking you how do you generally deal with this kind of situation? Any idea is welcome:
HOC
side-effects
other libraries
I'm using wrapper around Route, which checks if users are logged in and if not, redirect them to login page. Wrapped routes are rendered only after userId of authenticated user is fetched.
import * as React from 'react'
import { Route, Redirect } from 'react-router-dom'
import URLSearchParams from 'url-search-params'
class AuthRoute extends React.Component {
componentDidMount() {
if (!this.props.isLoading) {
this.props.getLoggedInUserId()
}
}
render() {
if (this.props.isLoading) {
// first request is fired to fetch authenticated user
return null // or spinner
} else if (this.props.isAuthenticated) {
// user is authenticated
return <Route {...this.props} />
} else {
// invalid user or authentication expired
// redirect to login page and remember original location
const search = new URLSearchParams({
next: this.props.location.pathname,
})
const next =
this.props.location.pathname !== '/' ? `?${search.toString()}` : ''
return <Redirect to={`/login${next}`} />
}
}
}
You need to update your reducer which handle getLoggedInUserId action to store also isLoading state.
You probably want the initial state to be rendered by the server into 'index.html' (or what have you) and hydrated on the client.
This initial state would include loggedInUserId and data for the /comments page.
Check out https://redux.js.org/docs/recipes/ServerRendering.html
I think using HOC will be clean here. As all the common logic will be at the same place. Use composition here
Let say you have components A, B, C, D
Now you want to write some common function on the componentWillReceiveProps lifecycle of all the components.
Write a HOC like:
class HOC extends React.Component {
componentWillReceiveProps(nextProps) {
//Your commomn logic
}
render() {
const childrenWithProps = React.Children.map(this.props.children,
child => React.cloneElement(child, {
...this.props,
})
return (
<div>
{childrenWithProps}
</div>
)
}
}
Write your components like this:
class A extends React.Component {
componentWillReceiveProps(nextProps) {
//your uncommone logic
}
render(){
return (
<HOC {...this.props}>
<div>
//Your page jsx
</div>
</HOC>
)
}
}
same way write for component B, C, and D. This pattern is useful when there is lot common among components. So better have a look at your usecase
OP writing. After reading nice ideas here, I decided to go with a custom HOC:
import React, { Component } from 'react';
const requireProp = (As, propsSelector, propsToDispatch) =>
class Wrapper extends Component {
componentWillMount() {
if (!propsSelector(this.props) && typeof propsToDispatch === 'function') {
propsToDispatch(this.props);
}
}
render() {
const { ...props } = this.props;
return !!propsSelector(this.props) && <As {...props} />;
}
};
export default requireProp;
To see how I use it, see this gist.

Route authorization HOC cause to remount children 3-times

I'm using an HOC component to restrict access to the route for non-logged users. The problem that this HOC remount children components while mounting or re-rendering when access this route directly from url(on the app first load). For example I have a 3 times did mount in the PaperWorkProgress component.
Route definition:
<Route path="/paperwork/progress" component={RequireAuth(PaperWorkProgress)}/>
Here the HOC code:
import React, {Component} from 'react';
import {connect} from 'react-redux';
export default function(ComposedComponent) {
class Authentication extends Component {
// check if token exists in storage
componentWillMount() {
const token = localStorage.getItem('token');
if (!token) {
const {pathname, search} = this.props.location;
this.props.history.push({
pathname: '/signin',
search: `?redirect_to=${pathname}${search}`,
});
}
}
// additional check
componentWillUpdate(nextProps) {
if (!nextProps.loggedIn) {
const {pathname, search} = this.props.location;
this.props.history.push({
pathname: '/signin',
search: `?redirect_to=${pathname}${search}`,
});
}
}
render() {
return <ComposedComponent {...this.props} />;
}
}
function mapStateToProps(state) {
return {loggedIn: state.session.loggedIn};
}
return connect(mapStateToProps)(Authentication);
}
Any ideas?
This question may be from a while ago already, but I just encountered the same problem.
In the end I found out that my HOC function was actually called on every route change.
What helped for me was to create the authorized component only once on initialization:
const AuthorisedDashboard = requireLogin(Dashboard);
and then later just use it
<Route path="/dashboard" component={AuthorisedDashboard} />
Or, you know, I guess you could just export the component with the HOC function already applied if it is only ever used in authorised mode...
I'm not sure this will make a difference about the re-rendering problem, but your code feels wrong.
First, you seems to have 2 source of truth, your redux store and the localStorage, which complicates things. If you want to "hydrate" your store from previous navigation information, you should use the createStore "preloadedState" argument, not checking everytime in your component. Cf Redux doc and this video from the creator of Redux himself Video for persisting and rehydrating State. Once your state comes only from your store it starts to be more simple.
Second,
When you push to the history object inside the component, It feels like you are mutating the component own props (as history is a prop). That feels weird to me and could be the root of your problem.
Why not use the Redirect component inside your render method like this instead ? cf React router docs. The component will looks like this (obviously you would need to change your Login component too, like in the docs)
import React, { Component } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
export default function(ComposedComponent) {
class Authentication extends Component {
render() {
return !this.props.loggedIn ? (
<Redirect
to={{
pathname: "/login",
state: { from: this.props.location }
}}
{...this.props}
/>
) : (
<ComposedComponent {...this.props} />
);
}
}
function mapStateToProps(state, ownProps) {
return { loggedIn: state.session.loggedIn, ...ownProps };
}
return connect(mapStateToProps)(Authentication);
}

Resources