230 lines
6.3 KiB
TypeScript
230 lines
6.3 KiB
TypeScript
import Vue from "vue";
|
|
import VueApollo from "@vue/apollo-option";
|
|
import { onError } from "@apollo/client/link/error";
|
|
import { createLink } from "apollo-absinthe-upload-link";
|
|
import {
|
|
ApolloClient,
|
|
ApolloLink,
|
|
defaultDataIdFromObject,
|
|
fromPromise,
|
|
InMemoryCache,
|
|
NormalizedCacheObject,
|
|
split,
|
|
} from "@apollo/client/core";
|
|
import { RetryLink } from "@apollo/client/link/retry";
|
|
import { Socket as PhoenixSocket } from "phoenix";
|
|
import * as AbsintheSocket from "@absinthe/socket";
|
|
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
|
|
import { getMainDefinition } from "@apollo/client/utilities";
|
|
import fetch from "unfetch";
|
|
import buildCurrentUserResolver from "@/apollo/user";
|
|
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
|
import { logout } from "@/utils/auth";
|
|
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from "./api/_entrypoint";
|
|
import {
|
|
possibleTypes,
|
|
typePolicies,
|
|
refreshAccessToken,
|
|
} from "./apollo/utils";
|
|
import { GraphQLError } from "graphql";
|
|
|
|
// Install the vue plugin
|
|
Vue.use(VueApollo);
|
|
|
|
let isRefreshing = false;
|
|
let pendingRequests: any[] = [];
|
|
|
|
// Endpoints
|
|
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
|
const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`;
|
|
const webSocketPrefix = process.env.NODE_ENV === "production" ? "wss" : "ws";
|
|
const wsEndpoint = `${webSocketPrefix}${httpServer.substring(
|
|
httpServer.indexOf(":")
|
|
)}/graphql_socket`;
|
|
|
|
function generateTokenHeader() {
|
|
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
|
|
|
return token ? `Bearer ${token}` : null;
|
|
}
|
|
|
|
const authMiddleware = new ApolloLink((operation, forward) => {
|
|
// add the authorization to the headers
|
|
operation.setContext({
|
|
headers: {
|
|
authorization: generateTokenHeader(),
|
|
},
|
|
});
|
|
|
|
if (forward) return forward(operation);
|
|
|
|
return null;
|
|
});
|
|
|
|
const customFetch = async (uri: string, options: any) => {
|
|
const response = await fetch(uri, options);
|
|
if (response.status >= 400) {
|
|
return Promise.reject(response.status);
|
|
}
|
|
return response;
|
|
};
|
|
|
|
const uploadLink = createLink({
|
|
uri: httpEndpoint,
|
|
fetch: customFetch,
|
|
});
|
|
|
|
const phoenixSocket = new PhoenixSocket(wsEndpoint, {
|
|
params: () => {
|
|
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
|
if (token) {
|
|
return { token };
|
|
}
|
|
return {};
|
|
},
|
|
});
|
|
|
|
const absintheSocket = AbsintheSocket.create(phoenixSocket);
|
|
const wsLink = createAbsintheSocketLink(absintheSocket);
|
|
|
|
const link = split(
|
|
// split based on operation type
|
|
({ query }) => {
|
|
const definition = getMainDefinition(query);
|
|
return (
|
|
definition.kind === "OperationDefinition" &&
|
|
definition.operation === "subscription"
|
|
);
|
|
},
|
|
wsLink,
|
|
uploadLink
|
|
);
|
|
|
|
const resolvePendingRequests = () => {
|
|
pendingRequests.map((callback) => callback());
|
|
pendingRequests = [];
|
|
};
|
|
|
|
const isAuthError = (graphQLError: GraphQLError | undefined) => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
return graphQLError && [403, 401].includes(graphQLError.status_code);
|
|
};
|
|
|
|
const errorLink = onError(
|
|
({ graphQLErrors, networkError, forward, operation }) => {
|
|
console.debug("We have an apollo error", [graphQLErrors, networkError]);
|
|
if (
|
|
graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) ||
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
networkError === 401
|
|
) {
|
|
console.debug("It's a authorization error (statusCode 401)");
|
|
let forwardOperation;
|
|
|
|
if (!isRefreshing) {
|
|
console.debug("Setting isRefreshing to true");
|
|
isRefreshing = true;
|
|
|
|
forwardOperation = fromPromise(
|
|
refreshAccessToken(apolloClient)
|
|
.then((res) => {
|
|
if (res !== true) {
|
|
// failed to refresh the token
|
|
throw "Failed to refresh the token";
|
|
}
|
|
resolvePendingRequests();
|
|
|
|
const context = operation.getContext();
|
|
const oldHeaders = context.headers;
|
|
|
|
operation.setContext({
|
|
headers: {
|
|
...oldHeaders,
|
|
authorization: generateTokenHeader(),
|
|
},
|
|
});
|
|
return true;
|
|
})
|
|
.catch((e) => {
|
|
console.debug("Something failed, let's logout", e);
|
|
pendingRequests = [];
|
|
// don't perform a logout since we don't have any working access/refresh tokens
|
|
logout(apolloClient, false);
|
|
return;
|
|
})
|
|
.finally(() => {
|
|
isRefreshing = false;
|
|
})
|
|
).filter((value) => Boolean(value));
|
|
} else {
|
|
forwardOperation = fromPromise(
|
|
new Promise((resolve) => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
pendingRequests.push(() => resolve());
|
|
})
|
|
);
|
|
}
|
|
|
|
return forwardOperation.flatMap(() => forward(operation));
|
|
}
|
|
|
|
if (graphQLErrors) {
|
|
graphQLErrors.map(
|
|
(graphQLError: GraphQLError & { status_code?: number }) => {
|
|
if (graphQLError?.status_code !== 401) {
|
|
console.log(
|
|
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
if (networkError) {
|
|
console.error(`[Network error]: ${networkError}`);
|
|
}
|
|
}
|
|
);
|
|
|
|
const retryLink = new RetryLink();
|
|
|
|
const fullLink = authMiddleware
|
|
.concat(retryLink)
|
|
.concat(errorLink)
|
|
.concat(link);
|
|
|
|
const cache = new InMemoryCache({
|
|
addTypename: true,
|
|
typePolicies,
|
|
possibleTypes,
|
|
dataIdFromObject: (object: any) => {
|
|
if (object.__typename === "Address") {
|
|
return object.origin_id;
|
|
}
|
|
return defaultDataIdFromObject(object);
|
|
},
|
|
});
|
|
|
|
const apolloClient = new ApolloClient<NormalizedCacheObject>({
|
|
cache,
|
|
link: fullLink,
|
|
connectToDevTools: true,
|
|
resolvers: buildCurrentUserResolver(cache),
|
|
});
|
|
|
|
export default new VueApollo({
|
|
defaultClient: apolloClient,
|
|
errorHandler(error) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
"%cError",
|
|
"background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;",
|
|
error.message
|
|
);
|
|
console.error(error);
|
|
},
|
|
});
|