Make redux-auth-wrapper wait until session checked

444 Views Asked by At

so here is my auth.js code:

import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper';
import { connectedRouterRedirect } from 'redux-auth-wrapper/history4/redirect';
import { createBrowserHistory } from 'history';

// import Spinner from '../components/layout/Spinner';

const locationHelper = locationHelperBuilder({});
createBrowserHistory();

export const UserIsAdmin = connectedRouterRedirect({
  wrapperDisplayName: 'UserIsAdmin',
//   AuthenticatingComponent: Spinner,
  redirectPath: (state, ownProps) => 
    locationHelper.getRedirectQueryParam(ownProps) || '/',
  allowRedirectBack: true,
  authenticatedSelector: state => state.user.isAuthenticated && state.user.isAdmin
});

export const UserIsAuthenticated = connectedRouterRedirect({
  wrapperDisplayName: 'UserIsAuthenticated',
//   AuthenticatingComponent: Spinner,
  redirectPath: (state, ownProps) =>
    locationHelper.getRedirectQueryParam(ownProps) || '/',
  allowRedirectBack: true,
  authenticatedSelector: state => state.user.isAuthenticated
});

export const UserIsNotAuthenticated = connectedRouterRedirect({
  wrapperDisplayName: 'UserIsNotAuthenticated',
//   AuthenticatingComponent: Spinner,
  redirectPath: (state, ownProps) =>
    locationHelper.getRedirectQueryParam(ownProps) || '/',
  allowRedirectBack: true,
  authenticatedSelector: state => !state.user.isAuthenticated
});

and here is where i need to make redux-auth-wrapper to wait until I update the state with the user data to send him wherever he was before refreshing the page:

const MainRoutes = ( { cookies } ) => {
    // state
    const { isAuthenticated } = useSelector( state => state.user );

    // dispatch
    const dispatch = useDispatch();

    const login = () => dispatch( loginAction() );
    const logout = () => dispatch( logoutAction() );

    // check if session is active ( cookie ) 
    useEffect(() => {
        if( !isAuthenticated ) {

            const checkSession = async () => {
                const cookie = cookies.get('token');
                if( cookie && cookie.trim() !== '' ) {
                    axiosClient.defaults.headers.Authorization = `Bearer ${ cookie }`;
                    login();
                } else logout();
            };

            checkSession()
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [ cookies, isAuthenticated ]);

    return (  
        <Switch>
            <Route exact path="/" component={ Courses } />

            <Route path="/admin" component={  UserIsAdmin( Admin )  } />
            <Route path="/profile" component={  UserIsAuthenticated( Profile )  } />

            <Route exact path="/login" component={ UserIsNotAuthenticated( Login ) } />
            <Route exact path="/signin" component={ UserIsNotAuthenticated( Signin ) } />
            <Route exact path="/send-email" component={ UserIsNotAuthenticated( Email ) } />
            <Route exact path="/recover" component={ UserIsNotAuthenticated( Recover ) } />

            <Route exact path="/policy" component={ Policy } />
            <Route exact path="/usage" component={ Usage } />
            <Route exact path="/faqs" component={ FAQS } />
        </Switch>
    );
}

export default withRouter(withCookies(MainRoutes));

Here bassically I check if exists a session cookie, soy I auto log the user in. The problem is, that when I go to some route (for example: /admin, which is protected, and therefore is being supervised by redux-auth.wrapper), and I refresh the page, it always sends me back to '/', because the check of the isAuthenticated and isAdmin is done before my MainRoutes component can log the user in, which of course fails the check in the authenticated selector of the auth.js, and sends me to '/'. My first idea to solve this was to store those 2 flags in the localStorage, so I will be driven to the previous path even if my user didn't finished logging in. But I was wondering if there is any way to specificaly say to redux-auth-wrapper, to wait until my useEffect function has finished.

Thank you.

2

There are 2 best solutions below

4
On BEST ANSWER

This should hold the first render and give the component a chance to verify the login status, give it a try.

But I was wondering if there is any way to specificaly say to redux-auth-wrapper, to wait until my useEffect function has finished.

Note: the solution is not specific to redux-auth-wrapper.

const MainRoutes = ( { cookies } ) => {

    const { isAuthenticated } = useSelector( state => state.user );

    /* use a state to hold the render */
    const [isFirstRender, setFirstRender] = useState(true)

    const dispatch = useDispatch();

    const login = () => dispatch( loginAction() );
    const logout = () => dispatch( logoutAction() );

    /* after the first render the user should be logged in (if previously was) */
    useEffect(() => {
        setFirstRender(false)
    }, [])

    useEffect(() => {
        if( !isAuthenticated ) {

            const checkSession = async () => {
                const cookie = cookies.get('token');
                if( cookie && cookie.trim() !== '' ) {
                    axiosClient.defaults.headers.Authorization = `Bearer ${ cookie }`;
                    login();
                } else logout();
            };

            checkSession()
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [ cookies, isAuthenticated ]);

    /* If the effect checking the auth runs fast you can leave 
    the return as this, otherwise you might want to show a loading 
    indicator */
    return (
        <>  
            {!isFirstRender &&
                <Switch>
                    <Route exact path="/" component={ Courses } />
                    <Route path="/admin" component={  UserIsAdmin( Admin )  } />
                    <Route path="/profile" component={  UserIsAuthenticated( Profile )  } />
                    <Route exact path="/login" component={ UserIsNotAuthenticated( Login ) } />
                    <Route exact path="/signin" component={ UserIsNotAuthenticated( Signin ) } />
                    <Route exact path="/send-email" component={ UserIsNotAuthenticated( Email ) } />
                    <Route exact path="/recover" component={ UserIsNotAuthenticated( Recover ) } />
                    <Route exact path="/policy" component={ Policy } />
                    <Route exact path="/usage" component={ Usage } />
                    <Route exact path="/faqs" component={ FAQS } />
                </Switch>
            }
        </>
    );
}

export default withRouter(withCookies(MainRoutes));
0
On

You can use the property authenticatingSelector to hold off on the redirect until you're ready. It tells your connectedRouterRedirect object that you're doing authentication stuff and that it needs to wait for it.

const UserIsAdmin = connectedRouterRedirect({
   wrapperDisplayName: 'UserIsAdmin',
   redirectPath: /* ... */,
   allowRedirectBack: true,
   authenticatedSelector: state => state.user.isAuthenticated && state.user.isAdmin,
   authenticatingSelector: state => state.user.isLoading
});

And then in your useEffect:

useEffect(() => {
   // You can either use an async here or just bake the dispatch call into your "doSomething" method.
   (async () => {
      await doSomething();
      dispatch(setLoadingStatus({ isLoading: false }));
   })();
}, [/* your dependencies here */]);

It would of course require you to add a property to your user redux state, ie. user.isLoading or whatever you want to name it, that has the default value of true.