Remove apollo link state

This commit is contained in:
Chocobozzz
2019-08-12 16:04:16 +02:00
parent 3fa2bd35d8
commit 6d221212ef
22 changed files with 415 additions and 148 deletions

View File

@@ -9,15 +9,15 @@
</template>
<script lang="ts">
import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator';
import { AUTH_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
import Footer from '@/components/Footer.vue';
import Logo from '@/components/Logo.vue';
import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator';
import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
import Footer from '@/components/Footer.vue';
import Logo from '@/components/Logo.vue';
@Component({
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
@@ -45,9 +45,9 @@ export default class App extends Vue {
private initializeCurrentUser() {
const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
const token = localStorage.getItem(AUTH_TOKEN);
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
if (userId && userEmail && token) {
if (userId && userEmail && accessToken) {
return this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {

View File

@@ -1,27 +1,32 @@
export const currentUser = {
defaults: {
currentUser: {
__typename: 'CurrentUser',
id: null,
email: null,
isLoggedIn: false,
},
},
import { ApolloCache } from 'apollo-cache';
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
resolvers: {
Mutation: {
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
const data = {
export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
cache.writeData({
data: {
currentUser: {
__typename: 'CurrentUser',
id: null,
email: null,
isLoggedIn: false,
},
},
});
return {
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
const data = {
Mutation: {
currentUser: {
id,
email,
isLoggedIn,
__typename: 'CurrentUser',
},
};
},
};
cache.writeData({ data });
},
cache.writeData({ data });
},
},
};
};

View File

@@ -60,19 +60,18 @@
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogout } from '@/vue-apollo';
import { deleteUserData } from '@/utils/auth';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { ICurrentUser } from '@/types/current-user.model';
import Logo from '@/components/Logo.vue';
import SearchField from '@/components/SearchField.vue';
import { Component, Vue, Watch } from 'vue-property-decorator';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { logout } from '@/utils/auth';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { ICurrentUser } from '@/types/current-user.model';
import Logo from '@/components/Logo.vue';
import SearchField from '@/components/SearchField.vue';
@Component({
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
@@ -111,18 +110,7 @@ export default class NavBar extends Vue {
}
async logout() {
await this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: null,
email: null,
isLoggedIn: false,
},
});
deleteUserData();
onLogout(this.$apollo);
await logout(this.$apollo.provider.defaultClient);
return this.$router.push({ path: '/' });
}

View File

@@ -1,4 +1,5 @@
export const AUTH_TOKEN = 'auth-token';
export const AUTH_ACCESS_TOKEN = 'auth-access-token';
export const AUTH_REFRESH_TOKEN = 'auth-refresh-token';
export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_EMAIL = 'auth-user-email';
export const AUTH_USER_ACTOR = 'auth-user-actor';

View File

@@ -3,7 +3,8 @@ import gql from 'graphql-tag';
export const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token,
accessToken,
refreshToken,
user {
id,
}
@@ -33,3 +34,12 @@ mutation ResendConfirmationEmail($email: String!) {
resendConfirmationEmail(email: $email)
}
`;
export const REFRESH_TOKEN = gql`
mutation RefreshToken($refreshToken: String!) {
refreshToken(refreshToken: $refreshToken) {
accessToken,
refreshToken,
}
}
`;

View File

@@ -12,7 +12,8 @@ mutation CreateUser($email: String!, $password: String!) {
export const VALIDATE_USER = gql`
mutation ValidateUser($token: String!) {
validateUser(token: $token) {
token,
accessToken,
refreshToken,
user {
id,
email,

View File

@@ -1,13 +1,13 @@
import { NavigationGuard } from 'vue-router';
import { UserRouteName } from '@/router/user';
import { LoginErrorCode } from '@/types/login-error-code.model';
import { AUTH_TOKEN } from '@/constants';
import { AUTH_ACCESS_TOKEN } from '@/constants';
export const authGuardIfNeeded: NavigationGuard = async function (to, from, next) {
if (to.meta.requiredAuth !== true) return next();
// We can't use "currentUser" from apollo here because we may not have loaded the user from the local storage yet
if (!localStorage.getItem(AUTH_TOKEN)) {
if (!localStorage.getItem(AUTH_ACCESS_TOKEN)) {
return next({
name: UserRouteName.LOGIN,
query: {

7
js/src/types/apollo.ts Normal file
View File

@@ -0,0 +1,7 @@
import { ServerError, ServerParseError } from 'apollo-link-http-common';
function isServerError(err: Error | ServerError | ServerParseError | undefined): err is ServerError {
return !!err && (err as ServerError).statusCode !== undefined;
}
export { isServerError };

View File

@@ -1,7 +1,10 @@
import { ICurrentUser } from '@/types/current-user.model';
export interface ILogin {
user: ICurrentUser;
token: string;
export interface IToken {
accessToken: string;
refreshToken: string;
}
export interface ILogin extends IToken {
user: ICurrentUser;
}

View File

@@ -1,14 +1,38 @@
import { AUTH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { ILogin } from '@/types/login.model';
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { ILogin, IToken } from '@/types/login.model';
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogout } from '@/vue-apollo';
import ApolloClient from 'apollo-client';
export function saveUserData(obj: ILogin) {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_TOKEN, obj.token);
saveTokenData(obj);
}
export function saveTokenData(obj: IToken) {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
}
export function deleteUserData() {
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_TOKEN]) {
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN]) {
localStorage.removeItem(key);
}
}
export function logout(apollo: ApolloClient<any>) {
apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: null,
email: null,
isLoggedIn: false,
},
});
deleteUserData();
onLogout();
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="container">
<div class="container" v-if="config">
<section class="hero is-link" v-if="!currentUser.id || !loggedPerson">
<div class="hero-body">
<div class="container">

View File

@@ -17,13 +17,14 @@
</template>
<script lang="ts">
import { VALIDATE_USER } from '@/graphql/user';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants';
import { RouteName } from '@/router';
import { UserRouteName } from '@/router/user';
import { VALIDATE_USER } from '@/graphql/user';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { AUTH_USER_ID } from '@/constants';
import { RouteName } from '@/router';
import { UserRouteName } from '@/router/user';
import { saveTokenData } from '@/utils/auth';
@Component
@Component
export default class Validate extends Vue {
@Prop({ type: String, required: true }) token!: string;
@@ -62,7 +63,8 @@ export default class Validate extends Vue {
saveUserData({ validateUser: login }) {
localStorage.setItem(AUTH_USER_ID, login.user.id);
localStorage.setItem(AUTH_TOKEN, login.token);
saveTokenData(login)
}
}
</script>

View File

@@ -1,15 +1,18 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { ApolloLink } from 'apollo-link';
import { ApolloLink, Observable } from 'apollo-link';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { onError } from 'apollo-link-error';
import { createLink } from 'apollo-absinthe-upload-link';
import { AUTH_TOKEN } from './constants';
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
import { withClientState } from 'apollo-link-state';
import { currentUser } from '@/apollo/user';
import merge from 'lodash/merge';
import { ApolloClient } from 'apollo-client';
import { DollarApollo } from 'vue-apollo/types/vue-apollo';
import { buildCurrentUserResolver } from '@/apollo/user';
import { isServerError } from '@/types/apollo';
import { inspect } from 'util';
import { REFRESH_TOKEN } from '@/graphql/auth';
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
import { logout, saveTokenData } from '@/utils/auth';
// Install the vue plugin
Vue.use(VueApollo);
@@ -44,14 +47,11 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
},
});
const cache = new InMemoryCache({ fragmentMatcher });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem(AUTH_TOKEN);
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : null,
authorization: generateTokenHeader(),
},
});
@@ -64,21 +64,54 @@ const uploadLink = createLink({
uri: httpEndpoint,
});
const stateLink = withClientState({
...merge(currentUser),
cache,
let refreshingTokenPromise: Promise<boolean> | undefined;
let alreadyRefreshedToken = false;
const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
if (isServerError(networkError) && networkError.statusCode === 401 && !alreadyRefreshedToken) {
if (!refreshingTokenPromise) refreshingTokenPromise = refreshAccessToken();
return promiseToObservable(refreshingTokenPromise).flatMap(() => {
refreshingTokenPromise = undefined;
alreadyRefreshedToken = true;
const context = operation.getContext();
const oldHeaders = context.headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: generateTokenHeader(),
},
});
return forward(operation);
});
}
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
);
}
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const link = stateLink.concat(authMiddleware).concat(uploadLink);
const link = authMiddleware
.concat(errorLink)
.concat(uploadLink);
const cache = new InMemoryCache({ fragmentMatcher });
const apolloClient = new ApolloClient({
cache,
link,
connectToDevTools: true,
resolvers: {
currentUser: buildCurrentUserResolver(cache),
},
});
apolloClient.onResetStore(stateLink.writeDefaults as any);
export const apolloProvider = new VueApollo({
defaultClient: apolloClient,
errorHandler(error) {
@@ -93,13 +126,65 @@ export function onLogin(apolloClient) {
}
// Manually call this when user log out
export async function onLogout(apolloClient: DollarApollo<any>) {
export async function onLogout() {
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.provider.defaultClient.resetStore();
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
}
}
async function refreshAccessToken() {
// Remove invalid access token, so the next request is not authenticated
localStorage.removeItem(AUTH_ACCESS_TOKEN);
const refreshToken = localStorage.getItem(AUTH_REFRESH_TOKEN);
console.log('Refreshing access token.');
try {
const res = await apolloClient.mutate({
mutation: REFRESH_TOKEN,
variables: {
refreshToken,
},
});
saveTokenData(res.data.refreshToken);
return true;
} catch (err) {
return false;
}
}
function generateTokenHeader() {
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
return token ? `Bearer ${token}` : null;
}
// Thanks: https://github.com/apollographql/apollo-link/issues/747#issuecomment-502676676
const promiseToObservable = <T> (promise: Promise<T>) => {
return new Observable<T>((subscriber) => {
promise.then(
(value) => {
if (subscriber.closed) {
return;
}
subscriber.next(value);
subscriber.complete();
},
(err) => {
console.error('Cannot refresh token.', err);
subscriber.error(err);
logout(apolloClient);
},
);
});
};