Open sibelius opened 8 years ago
Just provide auth
middleware to network layer with a token
thunk. This function will be called every time, when Relay makes request. You may store your token in global var, localStorage. In my project I use https://www.npmjs.com/package/store
Relay.injectNetworkLayer(new RelayNetworkLayer([
authMiddleware({
token: () => window.MyGlobalVarWithToken, // store.get('jwt'),
}),
]));
thanks, I will use AsyncStorage.
calling it every time is not a problem?
I don't sure that token: async () => await storage('token'),
will work at all.
But if does, then you'll get delays before relay send queries to server.
So if possible, try to put token in some variable with sync access.
BTW don't forget about tokenRefreshPromise
option in authMiddleware. This promise will be called only if your server returns 401 response. After resolving of token, it implicitly make re-request.
At the end, you may write your own middleware for working with your token logic. As example you may see this simple MW: https://github.com/nodkz/react-relay-network-layer/blob/master/src/middleware/perf.js
You may change req.headers = {}
before call next(req)
.
thanks for all the help, I will try to write a middleware
Few minutes ago I publish new version, where was added allowEmptyToken
options to authMiddleware
. If allowEmptyToken: true,
and token is empty, request proceed without Authorization
header.
This can help you, if you do requests to graphql server without token and don't want send empty auth header.
it could be very useful if we could just call a function on authMiddleware to save the token or remove it
Assumption:
Suppose, such function exists. So you should somehow
save reference to this function in some global variable. When you log in, you take this var with function and call it. So I can not imagine this somehow
realization should be implemented.
Current realization:
Let this variable save not reference to function which you wish, let it keep token. So just provide this variable to arrow function for token
option. When you log in, you just write token to this global var. And with next Relay request it reads token from this global var.
I try keep MW realization as simple, as possible. If I begin store token internally, I should also provide functions for reading and removing token ;).
I think it is better to use Relay.Environment to reset relay data on login, thanks for all the support
How I actually use auth middleware:
class ClientApi {
...
getRelayNetworkLayer = () => {
return new RelayNetworkLayer([
authMiddleware({
token: () => this._token,
tokenRefreshPromise: () => this._getTokenFromServer(this.url('/auth/jwt/new')),
}),
...
}
refresh token promise has the following view
_getTokenFromServer = (url, data = {}) => {
console.log('[AUTH] LOAD NEW CABINET JWT');
const opts = {
method: 'POST',
headers: {
'Accept': 'application/json', // eslint-disable-line
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
return fetch(url, opts)
.then(res => {
if (res.status > 299) { throw new Error(`Wrong response status ${res.status}`); }
return res;
})
.then(res => res.json())
.then(json => {
this._token = json.token;
this._saveTokenToStorage();
return this._token;
})
.catch(err => console.error('[AUTH] ERROR can not load new cabinet jwt', err));
};
So if new user makes request to server, it responds with 401 header. AuthMW implicitly catchs this header and run _getTokenFromServer
. Internally it makes new request to obtain new token from server, and if succeed, store it to localStorage and pass new token to AuthMW. AuthMW makes re-request and seamlessly pass data to Relay.
So app logic does not know anything about authorization.
Our ClientApi
also contains following methods loginByOauth
(providerName), loginByEmail(...), logout(). And instance of ClientApi stored in global variable.
Our ClientApi class was written inspired by https://github.com/reindexio/reindex-js/blob/master/src/index.js
for every 401 it will try to get the token?
how _getTokenFromServer
get data parameter?
the problem with this approach is that you do not clean the Relay cache, so if an user logged in, then logout, and another user login, this second user could have access to some left data from the first user
Yep, for every 401 request it tries to refresh token calling _getTokenFromServer
, and made re-request. If second request produce again 401 error it will be thrown upper.
FYI you may open dialog window with login and password, send it server, get token, and after that resolve Promise from _getTokenFromServer.
data
parameter is undefined for this case. I use method _getTokenFromServer
from another methods like this ClientApi.switchUser(), loginByEmail(), logout(), loginByOauth(). This methods provide some data for auth server for generating proper token.
In my case I just reload page after logout()
. For switchUser()
after obtaining token, I just force reload some counters after call, in my case no need clear entire relay store.
reload a page is not a good user experience, and it does not work on react native either
Also trying to solve this issue on React Native. Did anyone successfully implement a solution for pre-register -> post-login Relay?
Happy to keep this (work in progress) React Native sample public as a template, but how to fix the middleware implementation? https://github.com/allpwrfulroot/testMiddleware
Update: condensed the code question to a gist https://gist.github.com/allpwrfulroot/6b6b58ee2a3efebf138f00714c63630b
@sibelius, to address the problem avoiding page refresh, the solution provided by Relay is to create a new Store (see RelayEnvironment) which clean all the cache and avoid one user to see data it should not. react-router-relay and Relay.Renderer allow you to provide this environment prop.
This could be useful to easily reset a RelayEnvironment
class RelayStore {
constructor() {
this._env = new Environment();
this._networkLayer = null;
this._taskScheduler = null;
RelayNetworkDebug.init(this._env);
}
reset(networkLayer) {
// invariant(
// !this._env.getStoreData().getChangeEmitter().hasActiveListeners() &&
// !this._env.getStoreData().getMutationQueue().hasPendingMutations() &&
// !this._env.getStoreData().getPendingQueryTracker().hasPendingQueries(),
// 'RelayStore.reset(): Cannot reset the store while there are active ' +
// 'Relay Containers or pending mutations/queries.'
// );
if (networkLayer !== undefined) {
this._networkLayer = networkLayer;
}
this._env = new Environment();
if (this._networkLayer !== null) {
this._env.injectNetworkLayer(this._networkLayer);
}
if (this._taskScheduler !== null) {
this._env.injectTaskScheduler(this._taskScheduler);
}
RelayNetworkDebug.init(this._env);
}
// Map existing RelayEnvironment methods
getStoreData() {
return this._env.getStoreData();
}
injectNetworkLayer(networkLayer) {
this._networkLayer = networkLayer;
this._env.injectNetworkLayer(networkLayer);
}
injectTaskScheduler(taskScheduler) {
this._taskScheduler = taskScheduler;
this._env.injectTaskScheduler(taskScheduler);
}
primeCache(...args) {
return this._env.primeCache(...args);
}
forceFetch(...args) {
return this._env.forceFetch(...args);
}
read(...args) {
return this._env.read(...args);
}
readAll(...args) {
return this._env.readAll(...args);
}
readQuery(...args) {
return this._env.readQuery(...args);
}
observe(...args) {
return this._env.observe(...args);
}
getFragmentResolver(...args) {
return this._env.getFragmentResolver(...args);
}
applyUpdate(...args) {
return this._env.applyUpdate(...args);
}
commitUpdate(...args) {
return this._env.commitUpdate(...args);
}
/**
* @deprecated
*
* Method renamed to commitUpdate
*/
update(...args) {
return this._env.update(...args);
}
}
const relayStore = new RelayStore();
just call:
RelayStore.reset(networkLayer)
I prefer to solve the problem by simply storing the instance of the environment in a singleton and provide a method to refresh the environment as it avoids a lot of code duplication. Here is my way to deal with this:
import Relay from 'react-relay';
import {
RelayNetworkLayer,
authMiddleware,
urlMiddleware,
} from 'react-relay-network-layer';
let instance = null;
const tokenRefreshPromise = () => ...;
const refresh = () => {
instance = new Relay.Environment();
instance.injectNetworkLayer(new RelayNetworkLayer([
urlMiddleware({
url: '<some_url>',
batchUrl: '<some_batch_url>',
}),
next => req => next(
// Retrieve token info and assign it to req
),
authMiddleware({
allowEmptyToken: true,
token: req => req.token,
tokenRefreshPromise,
}),
], { disableBatchQuery: true }));
return instance;
};
const getInstance = () => (instance || refresh());
export default {
getCurrent: getInstance.bind(this),
refresh: refresh.bind(this),
};
Then you can import this file as Store and pass it to your RelayRenderer as Store.getInstance() and when you need to refresh your data after a logout for instance, just include Store in your file and call Store.refresh(), it generates a new instance and updates it everywhere.
This is how I'm doing it:
networkLayer.js
const networkLayer = new RelayNetworkLayer([
urlMiddleware({
url: _ => '/graphql',
}),
authMiddleware({
token: () => localStorage.getItem('id_token'),
}),
], {disableBatchQuery: true});
Root.js
import React from 'react';
import Relay from 'react-relay';
import {
Router,
browserHistory,
applyRouterMiddleware,
} from 'react-router';
import useRelay from 'react-router-relay';
import ReactGA from 'react-ga';
import networkLayer from 'shared/utils/networkLayer';
import routes from './routes';
class Root extends React.Component {
static childContextTypes = {
logout: React.PropTypes.func,
};
state = {
environment: null,
};
getChildContext () {
return {
logout: _ => this.logout(),
};
}
componentWillMount () {
ReactGA.initialize(process.env.GOOGLE_ANALYTICS_KEY);
const environment = new Relay.Environment();
environment.injectNetworkLayer(networkLayer);
this.setState({environment});
}
logout () {
const environment = new Relay.Environment();
environment.injectNetworkLayer(networkLayer);
this.setState({environment});
}
logPageView = _ => {
ReactGA.set({ page: window.location.pathname });
ReactGA.pageview(window.location.pathname);
}
render () {
return <Router
render={applyRouterMiddleware(useRelay)}
history={browserHistory}
environment={this.state.environment}
routes={routes}
onUpdate={this.logPageView}
key={Math.random()}
/>;
}
}
LoginPage.js
This page has a method that gets passed to a login form. The login form calls an AuthenticateUserMutation
, and uses this function as a callback:
loginUser = data => {
localStorage.setItem('id_token', data.jwtToken);
localStorage.setItem('user_uuid', data.userId);
this.context.setLoggedIn(true);
this.context.setUserId(data.userId);
this.context.router.push('/profile');
}
LoginForm.js
processLogin = response => {
const { authenticateUser: { authenticateUserResult } } = response;
if (authenticateUserResult && authenticateUserResult.userId) {
this.props.loginUser(authenticateUserResult);
} else {
this.setState({ loginError: 'Email and/or Password is incorrect' });
}
}
loginUser = ({ email, password }) => {
this.props.relay.commitUpdate(
new AuthenticateUserMutation({ email, password }),
{ onSuccess: this.processLogin },
);
}
core/relay.ts
import { getAuth } from "firebase/auth";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
/**
* Initializes a new instance of Relay environment.
* @see https://relay.dev/docs/
*/
export function createRelay(): Environment {
// Configure a network layer that fetches data from the GraphQL API
// https://relay.dev/docs/guides/network-layer/
const network = Network.create(async function fetchFn(operation, variables) {
const auth = getAuth();
const headers = new Headers({ ["Content-Type"]: "application/json" });
// When the user is authenticated append the ID token to the request
if (auth.currentUser) {
const idToken = await auth.currentUser.getIdToken();
headers.set("Authorization", `Bearer ${idToken}`);
}
const res = await fetch("/api", {
method: "POST",
headers,
credentials: "include",
body: JSON.stringify({ query: operation.text, variables }),
});
if (!res.ok) {
throw new HttpError(res.status, res.statusText);
}
return await res.json();
});
// Initialize Relay records store
const recordSource = new RecordSource();
const store = new Store(recordSource);
return new Environment({ store, network, handlerProvider: null });
}
Then, somewhere at the root level:
import { useMemo } as React from "react";
import { RelayEnvironmentProvider } from "react-relay";
import { createRelay } from "../core/relay.js";
import { AppRoutes } from "../routes/index.js";
/**
* The top-level (root) React component.
*/
export function App(): JSX.Element {
const relay = useMemo(() => createRelay(), []);
return (
<RelayEnvironmentProvider environment={relay}>
<AppRoutes />
</RelayEnvironmentProvider>
);
}
I want to set the auth token after login/register process using the token received
is it possible? do u have an example?