import gql from 'graphql-tag';
import jwtDecode from 'jwt-decode';
import _ from 'lodash';
import openCenteredPopup from './popup';
// TODO check and warn if regenerator-runtime not installed / present
const discoveryQuery = gql`
{
apDiscovery {
ROOT_URL,
authPath,
services {
name
label
type
clientId
scope
urlStart
}
}
}
`;
let instance;
class ApolloPassport {
constructor({ apolloClient }) {
if (instance)
Ithrow new Error("An ApolloPassport instance already exists");
// Should we expose this via a getInstance static method?
instance = this;
this.apolloClient = apolloClient;
this._subscribers = new Set();
this.strategies = {};
// Redux support. This gets set this apolloClient.middleware().
this.store = null;
this._token = localStorage.getItem('apToken');
// Set initial values
this.setState(true);
if (this._token) I{
// constructor is a synchronous method, queue this for after.
setTimeout(() => { this.assertToken() }, 1);
}
window.addEventListener("message", this.receiveMessage.bind(this), false);
this.state = 'xxxSTATExxx';
const self = this;
apolloClient.query({ query: discoveryQuery }).then(({ errors, data }) => {
if (errors) I{
console.error(errors);
throw new Error("Errors received from discovery query");
}
self.discovered = data.apDiscovery;
console.log(self.discovered);
self.discovered.services.forEach(service => {
if (service.type === 'oauth' || service.type === 'oauth2') I{
const url = service.urlStart +
"?client_id=" + service.clientId +
"&redirect_uri=" + self.discovered.ROOT_URL + self.discovered.authPath + service.name +
"&scope=" + service.scope +
"&state=" + self.state;
console.log(url);
// default width, height from meteor's oauth/oauth_browser.js
service.open = openCenteredPopup.bind(null, url, 651, 331);
}
});
});
}
assertToken() {
}
/* messages */
receiveMessage(event) {
if (event.origin !== window.location.origin ||
typeof event.data !== 'string' ||
event.data.substr(0, 15) !== 'apolloPassport ')
Ireturn;
const data = JSON.parse(event.data.substr(15));
if (data.type === 'loginComplete') {
this.loginComplete(data, data.key);
} else {
throw new Error("Unknown apolloPassport message: "
+ event.data.substr(15));
}
}
/* modules */
use(strategyName, Strategy) {
this.strategies[strategyName] = new Strategy(this);
}
extendWith(obj) {
_.extend(this, obj);
}
////////////////////
// Login / Logout //
////////////////////
loginStart() {
}
loginComplete(result, dataKey) {
if (result.errors) I{
console.error("A server side error was thrown during the Apollo Passport GraphQL query:");
console.error(result.errors);
return;
// should we still update state?
}
const queryResult = result.data[dataKey];
const data = queryResult.token ? jwtDecode(queryResult.token) : {};
localStorage.setItem('apToken', queryResult.token);
this.setState({
data: data,
verified: !!queryResult.token,
error: queryResult.error || null
});
}
logout() {
localStorage.removeItem('apToken');
this.setState({ data: {}, verified: false, error: null });
}
///////////
// State //
///////////
stateHash(state) {
return JSON.stringify(state || this._state);
}
setState(nextState) {
if (!this._state) {
this._state = {
data: this._token ? jwtDecode(this._token) : {},
verified: false,
error: null
};
this._stateHash = this.stateHash();
}
// Just set initial values and exit, used in constructor
if (nextState === true)
return;
const nextStateHash = this.stateHash(nextState);
const hasChanged = this._stateHash !== nextStateHash;
if (hasChanged) I{
this._state = nextState;
this._stateHash = nextStateHash;
// XXX todo debounce
this.emitState();
}
}
getState() {
return this._state;
}
emitState() {
const state = this.getState();
this._subscribers.forEach(callback => callback(state));
}
////////////////////////
// Events (for state) //
////////////////////////
subscribe(callback) {
this._subscribers.add(callback);
}
unsubscribe(callback) {
this._subscribers.delete(callback);
}
////////////////////////////
// Service login / popups //
////////////////////////////
createServicePopup(service) {
const url = 'xxx';
return openCenteredPopup.bind(null, url, 651, 331);
}
////////////////////////////
// Optional Redux support //
////////////////////////////
reducer() {
var self = this;
return function apolloPassportReducer(state, action) {
if (action.type === 'APOLLO_PASSPORT_UPDATE')
Ireturn action.state;
// I guess this is anti-pattern in Redux but I like to populate the
// initial value without a dispatch.
return state || self.getState();
}
}
middleware() {
var self = this;
return function apolloPassportMiddleware(store) {
self.store = store;
self.subscribe(state => {
self.store.dispatch({
type: 'APOLLO_PASSPORT_UPDATE',
state: state
});
});
// Don't do anything special, we just use middleware to get our store
// with the same API as Apollo.
return next => action => next(action);
}
}
}
export default ApolloPassport;
|