lynndylanhurley / redux-auth

Complete token authentication system for react + redux that supports isomorphic rendering.
Do What The F*ck You Want To Public License
2.13k stars 259 forks source link

Using redux-auth fetch to call async api server side #32

Open chriskozlowski opened 8 years ago

chriskozlowski commented 8 years ago

I am successfully using this library inside of an express/redux/react app to authenticate against a separate rails app with your devise_token_auth gem. Its awesome and works great! I have run into a snag though when trying to get server side rendering of the redux/react app going.

Basically, the promise returned by the redux-auth wrapped version of fetch is not hitting the rails api server and is not resolving. This causes the express server to hang. If I switch it out for "straight-up" isomorphic-fetch, the request is made to the rails api and the promise resolves - I have to disable auth for this to work though. Obviously not a usable solution in production lol.

Any idea why this might be? Is there something special I need to do for redux/auth's fetch to be able to do its job? Could it be that it can't get at the auth data in redux when on the server?

lynndylanhurley commented 8 years ago

@chriskozlowski - can you post the code for the request?

chriskozlowski commented 8 years ago

@lynndylanhurley Sure! I can't post the project as a repo but here are some excerpts. I could create a test repo if needed but that would take a little more time to set up. In case I wasn't totally clear above, the redux-auth fetch does work client side just fine. This issue only happened to me on the server. Made me wonder if the redux-auth fetch needs something that is only in the browser environment...

The component:

class ProjectsAppRoot extends Component {

  static fetchData(store, props) {
    return store.dispatch(fetchProjects());
  }
...

The action creator:

import { fetch } from 'redux-auth';
// import fetch from 'isomorphic-fetch'; <-- This will work but doesn't send auth headers 

export function fetchProjects() {
  return (dispatch) => {
    dispatch(requestProjects());

    return fetch('http://127.0.0.1/api/v1/projects', { method: 'get' }) <-- This does not resolve
      .then(response => response.json())
      .then(json => dispatch(receiveProjects(json)));
  };
}

The SSR middleware

store.dispatch(configure(
    {
      apiUrl: 'http://127.0.0.1',
      tokenValidationPath: '/auth/validate_token',
      authProviderPaths: {
        google: '/auth/google_oauth2',
      },
    },
    {
      isServer: global.__SERVER__,
      cookies: request.headers.cookie,
      currentLocation: url,
    }
  )).then(({ redirectPath, blank } = {}) => {
    store.dispatch(match(url, (error, redirectLocation, routerState) => {
      if (redirectLocation) {
        response.redirect(redirectLocation.pathname + redirectLocation.search);
      } else if (error) {
        console.error('ROUTER ERROR:', pretty.render(error));
        response.status(500);
        hydrateOnClient();
      } else {
        const rootComponent = (
        <div>
          <Provider store={store}>
            <div>
              <AuthGlobals />
              <ReduxRouter />
            </div>
          </Provider>
        </div>
        );

        const fetchingComponents = routerState.components
        // if you use react-redux, your components will be wrapped, unwrap them
        .map(component => component.WrappedComponent ? component.WrappedComponent : component)
        // now grab the fetchData functions from all (unwrapped) components that have it
        .filter(component => component.fetchData);

        // Call the fetchData functions and collect the promises they return
        const fetchPromises = fetchingComponents.map(component => {
          let promise = component.fetchData(store, routerState) // <-- We don't make it past here bc the promise hangs
          return promise;
        });

        Promise.all(fetchPromises)
          .then(() => {           
            const status = getStatusFromRoutes(routerState.routes);
            if (status) {
              response.status(status);
            }
            response.send('<!doctype html>\n' +
              renderToString(
                <Html store={ store } component={ rootComponent } assets={ global.webpackIsomorphicTools.assets() } />
              )
            );
          });
      }
    }));
  });
lynndylanhurley commented 8 years ago

What is the result of dispatch(receiveProjects(json))? Are you sure that it returns something resolvable? For example, should you be doing something like this instead?

    return fetch('http://127.0.0.1/api/v1/projects', { method: 'get' }) <-- This does not resolve
      .then(response => response.json())
      .then(json => Promise.resolve(dispatch(receiveProjects(json))));
chriskozlowski commented 8 years ago

Thanks for your help on this!

receiveProjects() is just a plain object action creator that signals the api call succeeded and the response data can be merged into the redux state:

export function receiveProject(payload) {
  return { type: RECEIVE_PROJECT, payload };
}

Actually, while working this out, I actually commented out those additional promise then statements so it was isolating just the fetch call and the result was the same:

return fetch('http://127.0.0.1/api/v1/projects', { method: 'get' }) <-- This does not resolve
  //.then(response => response.json())
  //.then(json => dispatch(receiveProjects(json)));

The hope was to just get the request to happen. It definitely isn't firing off as nothing even shows in the rails log. Of course the response data would not ultimately be merged into the state so I wouldn't see it in the rendered html but I was just hoping to get it to render and then add those additional promise then statements back in.

Take a look at this code from utils/session-storage.js. This file has the retrieveData function that looks like it tries to load the auth info from localStorage or a cookie depending on whats available. This function is used by the redux-auth fetch function to generate the auth headers:

// even though this code shouldn't be used server-side, node will throw
// errors if "window" is used
var root = Function("return this")() || (42, eval)("this");
... and then later ...
export function retrieveData (key) {
  var val = null;

  switch (root.authState.currentSettings.storage) {
    case "localStorage":
      val = root.localStorage && root.localStorage.getItem(key);
      break;

    default:
      val = Cookies.get(key);
      break;
  }

  // if value is a simple string, the parser will fail. in that case, simply
  // unescape the quotes and return the string.
  try {
    // return parsed json response
    return JSON.parse(val);
  } catch (err) {
    // unescape quotes
    return unescapeQuotes(val);
  }

Seems like this might not work in the server side use case?

I tried your suggestion just to be sure...no luck.

lynndylanhurley commented 8 years ago

Actually now that I think about it, the bundled fetch was meant to be used in the client, normally in the componentDidMount phase of the component lifecycle. Is there a reason that the request needs to be made server-side in your app?

See these posts for more details:

chriskozlowski commented 8 years ago

Totally makes sense where you're coming from there. Here is the story on that: We are building a public website and a private internal app that both use the same rails api (CORS). My goal is to get them both to authenticate with the rails api with different user classes and permissions which is why your libraries are a great fit. Both the public website and internal app are redux/react based. I have been posting about the internal app which I suppose doesn't absolutely need to SSR the page with data preloaded (although I'd prefer it). However, I will definitely need it for the public site so I render out the full content for SEO reasons.

I wonder if others would want to be able to hit an external api server side like this too? I would be willing to help improve the fetch wrapper to support this type of usage.

yury-dymov commented 8 years ago

I implemented server-side fetch in my fork: https://github.com/yury-dymov/redux-oauth

Implementation details: 1) I added new server action to update headers in redux without touching user; 2) In fetch in getAuthHeaders and updateAuthCredentials I am checking if it server-side and updating session-storage.js instead of cookies/local storage and dispatching new action

lynndylanhurley commented 8 years ago

@yury-dymov - do you want to merge that stuff into this repo?

yury-dymov commented 8 years ago

@lynndylanhurley in my fork I dropped multiple endpoint support and it might take too much time from my side to merge my changes and test if I haven't forgot something to support correctly all your features, which I am not using.

But I will be more than happy if other folks, who want to have that functionality in their projects will provide PR based on my code.

lynndylanhurley commented 8 years ago

I wonder if others would want to be able to hit an external api server side like this too? I would be willing to help improve the fetch wrapper to support this type of usage.

@chriskozlowski - I'll accept a PR for this if you have time.

chriskozlowski commented 8 years ago

@lynndylanhurley - I will put this on our list to work on once we ship what we're working on now. About 1-2 months. Gettin' married next week so time is a little tight!

lynndylanhurley commented 8 years ago

Congrats 💯