Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failing to redirect to protected route #85

Closed
williamgranli opened this issue Oct 13, 2016 · 12 comments
Closed

Failing to redirect to protected route #85

williamgranli opened this issue Oct 13, 2016 · 12 comments
Labels

Comments

@williamgranli
Copy link

williamgranli commented Oct 13, 2016

I'm getting this error for some reason... In all the examples authData is a string.

warning.js:36 Warning: Failed prop type: Invalid prop authData of type string supplied to UserIsAuthenticated(Connect(Foo)), expected object.
in UserIsAuthenticated(Connect(Foo)) (created by Connect(UserIsAuthenticated(Connect(Foo))))
in Connect(UserIsAuthenticated(Connect(Foo))) (created by RouterContext)
in div (created by App)
in App (created by RouterContext)
in RouterContext (created by Router)
in ScrollBehaviorContainer (created by Router)
in Router
in IntlProvider (created by LanguageProvider)
in LanguageProvider (created by Connect(LanguageProvider))
in Connect(LanguageProvider)
in Provider

@mjrussell
Copy link
Owner

@williamgranli what do you mean in the examples authData is a string?

In the README and examples, authData (the result of applying authSelector to the state) is a javascript object. Currently you could use a string as the result of authSelector and change the predicate function but you will get that warning from the propType checker.

@williamgranli
Copy link
Author

I see... thanks for the help @mjrussell . I managed to solve the login part (so far so good at least)

Right now I'm getting into an infinite redirect loop between the route I'm at and /login whenever I logout however.

@mjrussell
Copy link
Owner

mjrussell commented Oct 13, 2016

If you can post snippets of your versions of react-router-redux (if using), react router, and redux-auth-wrapper as well as route setup, HOCs and store config I might be able to help see something.

Originally I got an email about this issue that said you were using react-boilerplate (seems you've either deleted or edited the original text). Did you look at #40 ?

@williamgranli
Copy link
Author

williamgranli commented Oct 13, 2016

@mjrussell: I edited the original issue.

A lot of code incoming. I'm guessing you're quite familiar with the type of setup since most of it is copied from #40 .

/app/routes.js:
Sagas are commented out for now since I'm getting an error while injecting sagas. Could give you more info about that later on.

// These are the pages you can go to.
// They are all wrapped in the App component, which should contain the navbar etc
// See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information
// about the code splitting business
import { getAsyncInjectors } from 'utils/asyncInjectors';
import { routerActions } from 'react-router-redux';
import { UserAuthWrapper } from 'redux-auth-wrapper';

const errorLoading = (err) => {
  console.error('Dynamic page loading failed', err); // eslint-disable-line no-console
};

const loadModule = (cb, hoc = null) => (componentModule) => {
  if (hoc) cb(null, hoc(componentModule.default));
  else cb(null, componentModule.default);
};


// Redirects to /login by default
const UserIsAuthenticated = UserAuthWrapper({ // eslint-disable-line new-cap
  // how to get the user state
  authSelector: state => state.get('user'), // eslint-disable-line no-unused-vars
  // predicate: username => false, // eslint-disable-line no-unused-vars
  redirectAction: routerActions.replace, // the redux action to dispatch for redirect
  wrapperDisplayName: 'UserIsAuthenticated', // a nice name for this auth check
});


export default function createRoutes(store) {
  // Create reusable async injectors using getAsyncInjectors factory
  const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars

  return [
    {
      path: '/',
      name: 'home',
      getComponent(nextState, cb) {
        const importModules = Promise.all([
          System.import('containers/HomePage'),
        ]);

        const renderRoute = loadModule(cb, UserIsAuthenticated);

        importModules.then(([component]) => {
          renderRoute(component);
        });

        importModules.catch(errorLoading);
      },
    }, {
      path: '/login',
      name: 'loginPage',
      getComponent(nextState, cb) {
        const importModules = Promise.all([
          System.import('containers/LoginPage/reducer'),
          // System.import('containers/LoginPage/sagas'),
          System.import('containers/LoginPage'),
        ]);

        const renderRoute = loadModule(cb, null);

        importModules.then(([reducer, /* sagas,*/ component]) => {
          injectReducer('loginPage', reducer.default);
          // injectSagas(sagas.default);
          renderRoute(component);
        });

        importModules.catch(errorLoading);
      },
    }, {
      path: '/test',
      name: 'logoutForm',
      getComponent(nextState, cb) {
        const importModules = Promise.all([
          System.import('containers/LogoutForm/reducer'),
          // System.import('containers/LogoutForm/sagas'),
          System.import('containers/LogoutForm'),
        ]);

        const renderRoute = loadModule(cb, UserIsAuthenticated);

        importModules.then(([reducer, /* sagas, */component]) => {
          injectReducer('logoutForm', reducer.default);
          // injectSagas(sagas.default);
          renderRoute(component);
        });

        importModules.catch(errorLoading);
      },
    }, {
      path: '*',
      name: 'notfound',
      getComponent(nextState, cb) {
        System.import('containers/NotFoundPage')
          .then(loadModule(cb))
          .catch(errorLoading);
      },
    },
  ];
}

/app/containers/HomePage/index.js

/*
 * HomePage
 *
 * This is the first thing users see of our App, at the '/' route
 *
 * NOTE: while this component should technically be a stateless functional
 * component (SFC), hot reloading does not currently support SFCs. If hot
 * reloading is not a neccessity for you then you can refactor it and remove
 * the linting exception.
 */

import React, { Component, PropTypes } from 'react';

// import { FormattedMessage } from 'react-intl';
// import messages from './messages';

export default class HomePage extends Component { // eslint-disable-line react/prefer-stateless-function
  static propTypes = {
    authData: PropTypes.object.isRequired,
  };
  render() {
    return (
      <div>
        <button onClick={this.onClick}>Logout</button>
        <h1>
          {JSON.stringify(this.props)}
          User object = {JSON.stringify(this.props.authData)}
        </h1>
      </div>
    );
  }
}

/app/containers/components/Loginpage/index.js

/*
 *
 * LoginPage
 *
 */

import React, { Component, PropTypes } from 'react';
import { routerActions } from 'react-router-redux';
import { connect } from 'react-redux';

import { login } from '../User/actions';


export class LoginPage extends Component { // eslint-disable-line react/prefer-stateless-function
  static propTypes = {
    isAuthenticated: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.bool,
    ]),
    login: PropTypes.func.isRequired,
    replace: PropTypes.func.isRequired,
    redirect: PropTypes.string.isRequired,
  };

  componentWillMount() {
    const { isAuthenticated, replace, redirect } = this.props;
    if (isAuthenticated) replace(redirect);
  }

  componentWillReceiveProps(nextProps) {
    const { isAuthenticated, replace, redirect } = nextProps;
    const { isAuthenticated: wasAuthenticated } = this.props;
    if (!wasAuthenticated && isAuthenticated) replace(redirect);
  }

  onClick = (e) => {
    e.preventDefault();
    this.props.login({
      name: this.refs.name.value,
      isAdmin: this.refs.admin.checked,
    });
  };

  render() {
    return (
      <div>
        <h2>Enter your name</h2>
        <input type="text" ref="name" />
        <br />
        <input type="checkbox" ref="admin" />
        <br />
        <button onClick={this.onClick}>Login</button>
      </div>
    );
  }
}


function mapStateToProps(state, ownProps) {
  const isAuthenticated = state.get('user') || false;
  const redirect = ownProps.location.query.redirect || '/';
  return {
    isAuthenticated,
    redirect,
  };
}

export default connect(mapStateToProps, { login, replace: routerActions.replace })(LoginPage);

/app/containers/LogoutForm/index.js

/*
 *
 * LogoutForm
 *
 */

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import selectLogoutForm from './selectors';
import styles from './styles.css';
import { logout } from '../User/actions';

export class LogoutForm extends Component { // eslint-disable-line react/prefer-stateless-function
  static propTypes = {
    logout: PropTypes.func.isRequired,
    authData: PropTypes.object.isRequired,
  }
  onClick = (e) => {
    e.preventDefault();
    this.props.logout();
  };
  render() {
    return (
      <div className={styles.logoutForm}>
        {JSON.stringify(this.props)}

        <button onClick={this.onClick}>Logout</button>
      </div>
    );
  }
}

const mapStateToProps = selectLogoutForm();

// function mapDispatchToProps(dispatch) {
//   return {
//     dispatch,
//   };
// }

export default connect(mapStateToProps, { logout })(LogoutForm);

Whatever is in the User component is copied from your example (actions, selector and reducer).

Let me know if there's anything else you need.

Thanks!!

@williamgranli
Copy link
Author

williamgranli commented Oct 13, 2016

@mjrussell this solved the redirect loop for me:

In LoginPage/index:

componentWillMount() {
    const { replace, redirect } = this.props;
    let { isAuthenticated } = this.props;
    if (Object.keys(isAuthenticated).length === 0 && isAuthenticated.constructor === Object) {
      isAuthenticated = false;
    }
    if (isAuthenticated) replace(redirect);
  }

  componentWillReceiveProps(nextProps) {
    const { isAuthenticated, replace, redirect } = nextProps;
    let { isAuthenticated: wasAuthenticated } = this.props;
    if (Object.keys(wasAuthenticated).length === 0 && wasAuthenticated.constructor === Object) {
      wasAuthenticated = false;
    }
    if (!wasAuthenticated && isAuthenticated) replace(redirect);
  }

So, for some reason the logout sets the user object to {} which didn't trigger the evaluations in above methods... Not sure if I did something wrong which made that happen.

@mjrussell
Copy link
Owner

Glad its working for you. You can use an auth wrapper to protect Login also. In your case, you are essentially rewriting the auth wrapper for login.

See the loading example for an example of how to do this. Regarding the user object to {} that depends on how your actions and reducer are set up which aren't included in the code you posted.

@williamgranli
Copy link
Author

This is how my actions and reducer look:

/*
 *
 * User actions
 *
 */

import {
  USER_LOGGED_IN,
  USER_LOGGED_OUT,
} from './constants';

export function login(data) {
  localStorage.setItem('token', JSON.stringify(data));
  return {
    type: USER_LOGGED_IN,
    payload: data,
  };
}

export function logout() {
  localStorage.removeItem('token');
  return {
    type: USER_LOGGED_OUT,
  };
}

/*
 *
 * User reducer
 *
 */

import { fromJS } from 'immutable';
import {
  USER_LOGGED_IN,
  USER_LOGGED_OUT,
} from './constants';

const token = JSON.parse(localStorage.getItem('token')) || null;

const initialState = fromJS(token);

function userUpdate(state = initialState, { type, payload }) {
  switch (type) {
    case USER_LOGGED_IN:
      return payload;
    case USER_LOGGED_OUT:
      return {};
    default:
      return state;
  }
}

export default userUpdate;

Gotta say this lib is really neat now that I'm getting the hang of it! Going to try the loading example you mentioned soon hopefully as well! Just managed to fix the bug with the sagas as well... was related to the webpack settings for react-boilerplate!

@mjrussell
Copy link
Owner

So, for some reason the logout sets the user object to {}

Here's that code right there -

    case USER_LOGGED_OUT:
      return {};

@williamgranli
Copy link
Author

williamgranli commented Oct 13, 2016

Ah, makes sense.

You do it in both the basic and localStorage examples too, think I just copied it from there. Is that intended?

@mjrussell
Copy link
Owner

Its up to you how you want to set up your user reducer. The example is trying to be as simple as possible to explain the concepts. The wrapper by default has the predicate (to check if authenticated) see if the user object has data or if it is empty/null. But that can be easily overridden if you have a different structure.

@mjrussell
Copy link
Owner

@williamgranli can this be closed now?

@williamgranli
Copy link
Author

williamgranli commented Oct 15, 2016

Yeah! Thanks for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants