Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add renew token query for apollo client (chrome-extension) #5200

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions packages/twenty-chrome-extension/src/db/auth.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,3 @@ export const exchangeAuthorizationCode = async (
return data.exchangeAuthorizationCode;
else return null;
};

// export const RenewToken = async (appToken: string): Promise<Tokens | null> => {
// const data = await callQuery<Tokens>(RENEW_TOKEN, { appToken });
// if (isDefined(data)) return data;
// else return null;
// };
36 changes: 36 additions & 0 deletions packages/twenty-chrome-extension/src/db/token.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ApolloClient, InMemoryCache } from '@apollo/client';

import { Tokens } from '~/db/types/auth.types';
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
import { isDefined } from '~/utils/isDefined';

export const renewToken = async (
appToken: string,
): Promise<{ renewToken: { tokens: Tokens } } | null> => {
const store = await chrome.storage.local.get();
const serverUrl = `${
isDefined(store.serverBaseUrl)
? store.serverBaseUrl
: import.meta.env.VITE_SERVER_BASE_URL
}/graphql`;

// Create new client to call refresh token graphql mutation
const client = new ApolloClient({
uri: serverUrl,
cache: new InMemoryCache({}),
});

const { data } = await client.mutate({
mutation: RENEW_TOKEN,
variables: {
appToken,
},
fetchPolicy: 'network-only',
});

if (isDefined(data)) {
return data;
} else {
return null;
}
};
17 changes: 17 additions & 0 deletions packages/twenty-chrome-extension/src/graphql/auth/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,20 @@ export const EXCHANGE_AUTHORIZATION_CODE = gql`
}
}
`;

export const RENEW_TOKEN = gql`
mutation RenewToken($appToken: String!) {
renewToken(appToken: $appToken) {
tokens {
accessToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
}
`;
20 changes: 0 additions & 20 deletions packages/twenty-chrome-extension/src/graphql/auth/queries.ts

This file was deleted.

137 changes: 93 additions & 44 deletions packages/twenty-chrome-extension/src/utils/apolloClient.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,121 @@
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client';
import {
ApolloClient,
from,
fromPromise,
HttpLink,
InMemoryCache,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

import { renewToken } from '~/db/token.db';
import { Tokens } from '~/db/types/auth.types';
import { isDefined } from '~/utils/isDefined';

const clearStore = () => {
chrome.storage.local.remove('loginToken');

chrome.storage.local.remove('accessToken');

chrome.storage.local.remove('refreshToken');

chrome.storage.local.remove(['loginToken', 'accessToken', 'refreshToken']);
chrome.storage.local.set({ isAuthenticated: false });
};

const getApolloClient = async () => {
const setStore = (tokens: Tokens) => {
if (isDefined(tokens.loginToken)) {
chrome.storage.local.set({
loginToken: tokens.loginToken,
});
}
chrome.storage.local.set({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
});
};

export const getServerUrl = async () => {
const store = await chrome.storage.local.get();
const serverUrl = `${
isDefined(store.serverBaseUrl)
? store.serverBaseUrl
: import.meta.env.VITE_SERVER_BASE_URL
}/graphql`;
return serverUrl;
};

const errorLink = onError(({ graphQLErrors, networkError }) => {
if (isDefined(graphQLErrors)) {
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
//TODO: replace this with renewToken mutation
clearStore();
return;
}
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
//TODO: replace this with renewToken mutation
clearStore();
break;
const getAuthToken = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.accessToken)) return `Bearer ${store.accessToken.token}`;
else return '';
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we return null instead to signify that we couldn't get it ? Seems that '' will create a silent error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to return null 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like HttpLink shouldn't be created if we don't have any accessToken, but instead redirect to the login page no ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the plan! We currently don't have any routing in place though its next on the agenda :)


const getApolloClient = async () => {
const store = await chrome.storage.local.get();

const authLink = setContext(async (_, { headers }) => {
const token = await getAuthToken();
return {
headers: {
...headers,
authorization: token,
},
};
});
const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => {
if (isDefined(graphQLErrors)) {
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
return fromPromise(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a problem here, because the server doesn't necessarily returns this exact error.

I'm looking into it, might be a problem of guard.

renewToken(store.refreshToken.token)
.then((response) => {
if (isDefined(response)) {
setStore(response.renewToken.tokens);
}
})
.catch(() => {
clearStore();
}),
).flatMap(() => forward(operation));
}
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
return fromPromise(
renewToken(store.refreshToken.token)
.then((response) => {
if (isDefined(response)) {
setStore(response.renewToken.tokens);
}
})
.catch(() => {
clearStore();
}),
).flatMap(() => forward(operation));
}
default:
// eslint-disable-next-line no-console
console.error(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${
graphQLError.locations
? JSON.stringify(graphQLError.locations)
: graphQLError.locations
}, Path: ${graphQLError.path}`,
);
break;
}
default:
// eslint-disable-next-line no-console
console.error(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${
graphQLError.locations
? JSON.stringify(graphQLError.locations)
: graphQLError.locations
}, Path: ${graphQLError.path}`,
);
break;
}
}
}

if (isDefined(networkError)) {
// eslint-disable-next-line no-console
console.error(`[Network error]: ${networkError}`);
}
});
if (isDefined(networkError)) {
// eslint-disable-next-line no-console
console.error(`[Network error]: ${networkError}`);
}
},
);

const httpLink = new HttpLink({
uri: serverUrl,
headers: isDefined(store.accessToken)
? {
Authorization: `Bearer ${store.accessToken.token}`,
}
: {},
uri: await getServerUrl(),
});

const client = new ApolloClient({
cache: new InMemoryCache(),
link: from([errorLink, httpLink]),
link: from([errorLink, authLink, httpLink]),
});

return client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,9 @@ export class TokenService {

assert(token, "This refresh token doesn't exist", NotFoundException);

const user = await this.userRepository.findOneBy({
id: jwtPayload.sub,
const user = await this.userRepository.findOne({
where: { id: jwtPayload.sub },
relations: ['appTokens'],
});

assert(user, 'User not found', NotFoundException);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class GraphQLHydrateRequestFromTokenMiddleware
'SignUp',
'RenewToken',
'IntrospectionQuery',
'ExchangeAuthorizationCode',
];

if (
Expand Down
Loading