kylecorbelli / redux-token-auth

Redux actions and reducers to integrate easily with Devise Token Auth
MIT License
154 stars 80 forks source link

Using generateRequireSignInWrapper throws error #53

Open Rinbo opened 5 years ago

Rinbo commented 5 years ago

Using the generateRequireSignInWrapper gives me this error message:

Could not find "store" in either the context or props of "Connect(GatedPage)". Either wrap the root component in a Provider, or explicitly pass "store" as a prop to "Connect(GatedPage)".

Here is my code:

index.js

import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware, compose } from "redux";
import { Provider } from "react-redux";
import reduxThunk from "redux-thunk";
import { verifyCredentials } from "./apis/redux-token-auth-config";

import reducers from "./reducers";
import App from "./components/App";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  reducers,
  composeEnhancers(applyMiddleware(reduxThunk))
);
verifyCredentials(store);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

app.js

import React from "react";
import "semantic-ui-css/semantic.min.css";
import { Router, Route, Switch } from "react-router-dom";
import history from "../history";
import { generateRequireSignInWrapper } from "redux-token-auth";

import LandingPage from "./LandingPage";
import CardCreate from "./CardsCreate";
import CardShow from "./CardShow";
import SignInPage from "./SignInPage";

const requireSignIn = generateRequireSignInWrapper({
  redirectPathIfNotSignedIn: "/signin"
});

const Routes = () => (
  <div className="ui container">
    <Router history={history}>
      <div>
        <Switch>
          <Route path="/" exact component={requireSignIn(LandingPage)} />
          <Route path="/cards/new" exact component={CardCreate} />
          <Route path="/cards/:id" exact component={CardShow} />
          <Route path="/signin" exact component={SignInPage} />
        </Switch>
      </div>
    </Router>
  </div>
);

const App = () => {
  return Routes();
};

export default App;
suyesh commented 5 years ago

@Rinbo Did you get this working?

Rinbo commented 5 years ago

@suyesh nope.

suyesh commented 5 years ago

@RinboI tried everything and nothing worked. Going to good old create from scratch mode.

Rinbo commented 5 years ago

@suyesh Yeah, same here.

tochman commented 5 years ago

@Rinbo I would need your advice on how to solve this issue.

Rinbo commented 5 years ago

@tochman I threw out this module and implemented my own middleware instead:

import axios from "axios";

let HEADERS = ["access-token", "token-type", "client", "expiry", "uid"];

const tokenMiddleware = () => store => next => action => {
  if (!action) {
    action = { type: "" };
  }

  let customHeaders = []
  let validateAction = "VALIDATE_TOKEN"
  let logoutAction = "SIGN_OUT"

  HEADERS = [...new Set([...HEADERS, ...customHeaders])];
  if (action.type === validateAction) {
    HEADERS.forEach(
      token =>
        (axios.defaults.headers.common[token] = localStorage.getItem(token))
    );
  } else if (action.type === logoutAction) {
    HEADERS.forEach(token => localStorage.removeItem(token));
  } else {
    let { headers } = action;
    if (headers) {
      if (headers["access-token"]) {
        HEADERS.forEach(token => {
          axios.defaults.headers.common[token] = headers[token];
          localStorage.setItem(token, headers[token]);
        });
      }
    }
  }
  return next(action);
};

export default tokenMiddleware;

Then I applied it to store in root index.js:

...
import reduxThunk from "redux-thunk";
import tokenMiddelware from "./tokenMiddleware";

import reducers from "./reducers";
import App from "./components/App";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  reducers,
  composeEnhancers(applyMiddleware(reduxThunk, tokenMiddelware()))
);
...

Then in authActions i made the following function:

export const validateUser = () => async dispatch => {  
  dispatch({ type: "VALIDATE_TOKEN" });
  const headers = axios.defaults.headers.common;
  try {
    const response = await endpoint.get("api/v1/auth/validate_token", headers);
    const user = response.data.data;
    dispatch(setHeaders(response.headers));
    dispatch({ type: "SIGN_IN", payload: user });
  } catch (err) {
    console.log(err, "Missing Token. Please Log in.");
    dispatch({ type: "LOADED" });
  }
};

The setHeaders function just relays the headers that gets returned from the rails app to the middleware (via the action key), so that they are always available to the axios module (needed for every request).

Then you can make your own custom protected routes (assuming you are using react-router-dom), in which I call the validateUser action in ComponentDidMount. For my part I made three custom routes. AdminRoute, ProtectedRoute (for signed in users) and AuthRoute (which automatically redirects a signed in user away from the signup page). The syntax is a little obscure but here is the ProtectedRoute.js:

import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import { validateUser } from "../actions/authActions";

export class ProtectedRoute extends React.Component {
  componentDidMount = () => {
    this.props.validateUser();
  };

  render() {
    const {
      isSignedIn,
      isDOMLoaded,
      component: Component,
      ...rest
    } = this.props;

    if (isDOMLoaded) {
      return (
        <Route
          {...rest}
          render={props =>
            isSignedIn ? (
              <Component {...props} />
            ) : (
              <Redirect
                to={{ pathname: "/auth", state: { from: props.location } }}
              />
            )
          }
        />
      );
    } else {
      return null;
    }
  }
}

const MapStateToProps = state => {
  return {
    isSignedIn: state.currentUser.isSignedIn,
    isDOMLoaded: state.currentUser.isDOMLoaded
  };
};

export default connect(
  MapStateToProps,
  { validateUser }
)(ProtectedRoute);

You can probably omit IsDOMLoaded. Don't remember why I included that.

Then in your app.js where you define your routes:

import React from "react";
import { Router, Switch, Route } from "react-router-dom";
import history from "../history";

...
import AdminRoute from "./AdminRoute";
import ProtectedRoute from "./ProtectedRoute";
import AuthRoute from "./AuthRoute";
import LandingPage from "./LandingPage";
import Signup from "./auth/Signup";
...

const Routes = () => (
  <div className="ui container">
    <Router history={history}>
      <div>
        <Header />
        <Switch>
          <ProtectedRoute path="/" exact component={LandingPage} />
          <ProtectedRoute path="/profile" exact component={UserProfile} />
          ...
          <AdminRoute path="/admin" exact component={Dashboard} />
          ...     
          <AuthRoute path="/auth" exact component={AuthPage} />
          ...     
        </Switch>
        <Footer />
      </div>
    </Router>
  </div>
);

const App = () => {
  return Routes();
};

export default App;
tochman commented 5 years ago

Awesome solution.