feat: align auth api with front convention (#370)
* feat: align auth api with front convention * fix: email password auth * fix: proper file naming * Fix login --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -3,6 +3,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { ApolloProvider } from './providers/apollo/ApolloProvider';
|
||||
import { AppThemeProvider } from './providers/theme/AppThemeProvider';
|
||||
import { UserProvider } from './providers/user/UserProvider';
|
||||
import { App } from './App';
|
||||
|
||||
export function AppWrapper() {
|
||||
@ -11,7 +12,9 @@ export function AppWrapper() {
|
||||
<BrowserRouter>
|
||||
<AppThemeProvider>
|
||||
<StrictMode>
|
||||
<App />
|
||||
<UserProvider>
|
||||
<App />
|
||||
</UserProvider>
|
||||
</StrictMode>
|
||||
</AppThemeProvider>
|
||||
</BrowserRouter>
|
||||
|
||||
57
front/src/modules/apollo/hooks/useApolloFactory.ts
Normal file
57
front/src/modules/apollo/hooks/useApolloFactory.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { CommentThreadTarget } from '~/generated/graphql';
|
||||
|
||||
import { ApolloFactory } from '../services/apollo.factory';
|
||||
|
||||
export function useApolloFactory() {
|
||||
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
|
||||
|
||||
const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
|
||||
|
||||
const apolloClient = useMemo(() => {
|
||||
apolloRef.current = new ApolloFactory({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
CommentThread: {
|
||||
fields: {
|
||||
commentThreadTargets: {
|
||||
merge(
|
||||
existing: CommentThreadTarget[] = [],
|
||||
incoming: CommentThreadTarget[],
|
||||
) {
|
||||
return [...incoming];
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
onTokenPairChange(tokenPair) {
|
||||
setTokenPair(tokenPair);
|
||||
},
|
||||
onUnauthenticatedError() {
|
||||
setTokenPair(null);
|
||||
},
|
||||
});
|
||||
|
||||
return apolloRef.current.getClient();
|
||||
}, [setTokenPair]);
|
||||
|
||||
useEffect(() => {
|
||||
if (apolloRef.current) {
|
||||
apolloRef.current.updateTokenPair(tokenPair);
|
||||
}
|
||||
}, [tokenPair]);
|
||||
|
||||
return apolloClient;
|
||||
}
|
||||
44
front/src/modules/apollo/hooks/useApolloMocked.ts
Normal file
44
front/src/modules/apollo/hooks/useApolloMocked.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
createHttpLink,
|
||||
from,
|
||||
InMemoryCache,
|
||||
} from '@apollo/client';
|
||||
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
export default function useApolloMocked() {
|
||||
const mockedClient = useMemo(() => {
|
||||
const apiLink = createHttpLink({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
});
|
||||
|
||||
const mockLink = new ApolloLink((operation, forward) => {
|
||||
return forward(operation).map((response) => {
|
||||
if (operation.operationName === 'GetCompanies') {
|
||||
return { data: { companies: mockedCompaniesData } };
|
||||
}
|
||||
if (operation.operationName === 'GetCurrentUser') {
|
||||
return { data: { users: [mockedUsersData[0]] } };
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
return new ApolloClient({
|
||||
link: from([mockLink, apiLink]),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return mockedClient;
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
|
||||
export interface ApolloManager<TCacheShape> {
|
||||
getClient(): ApolloClient<TCacheShape>;
|
||||
updateTokenPair(tokenPair: AuthTokenPair | null): void;
|
||||
}
|
||||
@ -14,13 +14,12 @@ import { RetryLink } from '@apollo/client/link/retry';
|
||||
import { Observable } from '@apollo/client/utilities';
|
||||
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
import { tokenService } from '@/auth/services/TokenService';
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
|
||||
import { assertNotNull } from '../../modules/utils/assert';
|
||||
import { promiseToObservable } from '../../modules/utils/promise-to-observable';
|
||||
|
||||
import { ApolloManager } from './interfaces/apollo-manager.interface';
|
||||
import { loggerLink } from './logger';
|
||||
import { loggerLink } from '../../utils/apollo-logger';
|
||||
import { assertNotNull } from '../../utils/assert';
|
||||
import { promiseToObservable } from '../../utils/promise-to-observable';
|
||||
import { ApolloManager } from '../interfaces/apolloManager.interface';
|
||||
|
||||
const logger = loggerLink(() => 'Twenty');
|
||||
|
||||
@ -35,17 +34,20 @@ const resolvePendingRequests = () => {
|
||||
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
||||
onError?: (err: GraphQLErrors | undefined) => void;
|
||||
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
||||
onUnauthenticatedError?: () => void;
|
||||
}
|
||||
|
||||
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
private client: ApolloClient<TCacheShape>;
|
||||
private tokenPair: AuthTokenPair | null = null;
|
||||
|
||||
constructor(opts: Options<TCacheShape>) {
|
||||
const {
|
||||
uri,
|
||||
onError: onErrorCb,
|
||||
onNetworkError,
|
||||
onTokenPairChange,
|
||||
onUnauthenticatedError,
|
||||
...options
|
||||
} = opts;
|
||||
@ -56,13 +58,11 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
});
|
||||
|
||||
const authLink = setContext(async (_, { headers }) => {
|
||||
const credentials = tokenService.getTokenPair();
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: credentials?.accessToken
|
||||
? `Bearer ${credentials?.accessToken}`
|
||||
authorization: this.tokenPair?.accessToken.token
|
||||
? `Bearer ${this.tokenPair?.accessToken.token}`
|
||||
: '',
|
||||
},
|
||||
};
|
||||
@ -93,8 +93,9 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
forward$ = promiseToObservable(
|
||||
renewToken(uri)
|
||||
.then(() => {
|
||||
renewToken(uri, this.tokenPair)
|
||||
.then((tokens) => {
|
||||
onTokenPairChange?.(tokens);
|
||||
resolvePendingRequests();
|
||||
return true;
|
||||
})
|
||||
@ -161,6 +162,10 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
});
|
||||
}
|
||||
|
||||
updateTokenPair(tokenPair: AuthTokenPair | null) {
|
||||
this.tokenPair = tokenPair;
|
||||
}
|
||||
|
||||
getClient() {
|
||||
return this.client;
|
||||
}
|
||||
@ -3,11 +3,12 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useChallengeMutation, useVerifyMutation } from '~/generated/graphql';
|
||||
|
||||
import { tokenService } from '../services/TokenService';
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { isAuthenticatingState } from '../states/isAuthenticatingState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
export function useAuth() {
|
||||
const [, setTokenPair] = useRecoilState(tokenPairState);
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||
|
||||
@ -50,14 +51,14 @@ export function useAuth() {
|
||||
throw new Error('No verify result');
|
||||
}
|
||||
|
||||
tokenService.setTokenPair(verifyResult.data?.verify.tokens);
|
||||
setTokenPair(verifyResult.data?.verify.tokens);
|
||||
|
||||
setIsAuthenticating(false);
|
||||
setCurrentUser(verifyResult.data?.verify.user);
|
||||
|
||||
return verifyResult.data?.verify;
|
||||
},
|
||||
[setCurrentUser, setIsAuthenticating, verify],
|
||||
[setCurrentUser, setIsAuthenticating, setTokenPair, verify],
|
||||
);
|
||||
|
||||
const handleLogin = useCallback(
|
||||
@ -70,8 +71,8 @@ export function useAuth() {
|
||||
);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
tokenService.removeTokenPair();
|
||||
}, []);
|
||||
setTokenPair(null);
|
||||
}, [setTokenPair]);
|
||||
|
||||
return {
|
||||
challenge: handleChallenge,
|
||||
|
||||
26
front/src/modules/auth/hooks/useFetchCurrentUser.ts
Normal file
26
front/src/modules/auth/hooks/useFetchCurrentUser.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect } from 'react';
|
||||
import jwt from 'jwt-decode';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
export function useFetchCurrentUser() {
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const [tokenPair] = useRecoilState(tokenPairState);
|
||||
const userId = tokenPair?.accessToken.token
|
||||
? jwt<{ sub: string }>(tokenPair.accessToken.token).sub
|
||||
: null;
|
||||
const { data } = useGetCurrentUserQuery({
|
||||
variables: { uuid: userId },
|
||||
});
|
||||
const user = data?.users?.[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setCurrentUser(user);
|
||||
}
|
||||
}, [user, setCurrentUser]);
|
||||
}
|
||||
@ -1,21 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
export function useIsLogged(): boolean {
|
||||
const [value, setValue] = useState<string | undefined>(
|
||||
cookieStorage.getItem('accessToken'),
|
||||
);
|
||||
const [tokenPair] = useRecoilState(tokenPairState);
|
||||
|
||||
useEffect(() => {
|
||||
const updateValue = (newValue: string | undefined) => setValue(newValue);
|
||||
|
||||
cookieStorage.addEventListener('accessToken', updateValue);
|
||||
|
||||
return () => {
|
||||
cookieStorage.removeEventListener('accessToken', updateValue);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !!value;
|
||||
return !!tokenPair;
|
||||
}
|
||||
|
||||
@ -5,17 +5,14 @@ import {
|
||||
InMemoryCache,
|
||||
UriFunction,
|
||||
} from '@apollo/client';
|
||||
import jwt from 'jwt-decode';
|
||||
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
import { loggerLink } from '@/utils/apollo-logger';
|
||||
import {
|
||||
AuthTokenPair,
|
||||
RenewTokenDocument,
|
||||
RenewTokenMutation,
|
||||
RenewTokenMutationVariables,
|
||||
} from '~/generated/graphql';
|
||||
import { loggerLink } from '~/providers/apollo/logger';
|
||||
|
||||
import { tokenService } from './TokenService';
|
||||
|
||||
const logger = loggerLink(() => 'Twenty-Refresh');
|
||||
|
||||
@ -60,29 +57,15 @@ const renewTokenMutation = async (
|
||||
* @param uri string | UriFunction | undefined
|
||||
* @returns TokenPair
|
||||
*/
|
||||
export const renewToken = async (uri: string | UriFunction | undefined) => {
|
||||
const tokenPair = tokenService.getTokenPair();
|
||||
|
||||
export const renewToken = async (
|
||||
uri: string | UriFunction | undefined,
|
||||
tokenPair: AuthTokenPair | undefined | null,
|
||||
) => {
|
||||
if (!tokenPair) {
|
||||
throw new Error('Refresh token is not defined');
|
||||
}
|
||||
|
||||
const data = await renewTokenMutation(uri, tokenPair.refreshToken);
|
||||
const data = await renewTokenMutation(uri, tokenPair.refreshToken.token);
|
||||
|
||||
tokenService.setTokenPair(data.renewToken.tokens);
|
||||
|
||||
return data.renewToken;
|
||||
};
|
||||
|
||||
export const getUserIdFromToken: () => string | null = () => {
|
||||
const accessToken = cookieStorage.getItem('accessToken');
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt<{ sub: string }>(accessToken).sub;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return data.renewToken.tokens;
|
||||
};
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
|
||||
export class TokenService {
|
||||
getTokenPair() {
|
||||
const accessToken = cookieStorage.getItem('accessToken');
|
||||
const refreshToken = cookieStorage.getItem('refreshToken');
|
||||
|
||||
if (!accessToken || !refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
setTokenPair(tokens: AuthTokenPair) {
|
||||
cookieStorage.setItem('accessToken', tokens.accessToken.token, {
|
||||
secure: true,
|
||||
});
|
||||
cookieStorage.setItem('refreshToken', tokens.refreshToken.token, {
|
||||
secure: true,
|
||||
});
|
||||
}
|
||||
|
||||
removeTokenPair() {
|
||||
cookieStorage.removeItem('accessToken');
|
||||
cookieStorage.removeItem('refreshToken');
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenService = new TokenService();
|
||||
@ -1,27 +0,0 @@
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
|
||||
import { getUserIdFromToken } from '../AuthService';
|
||||
|
||||
it('getUserIdFromToken returns null when the token is not present', async () => {
|
||||
const userId = getUserIdFromToken();
|
||||
expect(userId).toBeNull();
|
||||
});
|
||||
|
||||
it('getUserIdFromToken returns null when the token is not valid', async () => {
|
||||
cookieStorage.setItem('accessToken', 'xxx-invalid-access');
|
||||
const userId = getUserIdFromToken();
|
||||
expect(userId).toBeNull();
|
||||
});
|
||||
|
||||
it('getUserIdFromToken returns the right userId when the token is valid', async () => {
|
||||
cookieStorage.setItem(
|
||||
'accessToken',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTI0ODgsImV4cCI6MTY4Njk5Mjc4OH0.IO7U5G14IrrQriw3JjrKVxmZgd6XKL6yUIwuNe_R55E',
|
||||
);
|
||||
const userId = getUserIdFromToken();
|
||||
expect(userId).toBe('374fe3a5-df1e-4119-afe0-2a62a2ba481e');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cookieStorage.clear();
|
||||
});
|
||||
@ -1,43 +0,0 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { tokenService } from '../TokenService';
|
||||
|
||||
const tokenPair = {
|
||||
accessToken: {
|
||||
token:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6MTY4Njk5MzQ4Mn0.F_FD6nJ5fssR_47v2XFhtzqjr-wrEQpqaWVq8iIlLJw',
|
||||
expiresAt: '2023-06-17T09:18:02.942Z',
|
||||
},
|
||||
refreshToken: {
|
||||
token:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6OTQ2Mjk5MzE4MiwianRpIjoiNzBmMWNhMjctOTYxYi00ZGZlLWEwOTUtMTY2OWEwOGViMTVjIn0.xEdX9dOGzrPHrPsivQYB9ipYGJH-mJ7GSIVPacmIzfY',
|
||||
expiresAt: '2023-09-15T09:13:02.952Z',
|
||||
},
|
||||
};
|
||||
|
||||
it('getTokenPair is fullfiled when token is present', () => {
|
||||
tokenService.setTokenPair(tokenPair);
|
||||
|
||||
// Otherwise the test will fail because Cookies-js seems to be async but functions aren't promises
|
||||
setTimeout(() => {
|
||||
expect(tokenService.getTokenPair()).toBe({
|
||||
accessToken: tokenPair.accessToken,
|
||||
refreshToken: tokenPair.refreshToken,
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
|
||||
it('getTokenPair is null when token is not set', () => {
|
||||
expect(tokenService.getTokenPair()).toBeNull();
|
||||
});
|
||||
|
||||
it('removeTokenPair clean cookie storage', () => {
|
||||
tokenService.setTokenPair(tokenPair);
|
||||
tokenService.removeTokenPair();
|
||||
expect(tokenService.getTokenPair()).toBeNull();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Cookies.remove('accessToken');
|
||||
Cookies.remove('refreshToken');
|
||||
});
|
||||
29
front/src/modules/auth/states/tokenPairState.ts
Normal file
29
front/src/modules/auth/states/tokenPairState.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { atom, AtomEffect } from 'recoil';
|
||||
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
|
||||
const cookieStorageEffect =
|
||||
(key: string): AtomEffect<AuthTokenPair | null> =>
|
||||
({ setSelf, onSet }) => {
|
||||
const savedValue = cookieStorage.getItem(key);
|
||||
if (savedValue != null) {
|
||||
setSelf(JSON.parse(savedValue));
|
||||
}
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
if (!newValue) {
|
||||
cookieStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
isReset
|
||||
? cookieStorage.removeItem(key)
|
||||
: cookieStorage.setItem(key, JSON.stringify(newValue));
|
||||
});
|
||||
};
|
||||
|
||||
export const tokenPairState = atom<AuthTokenPair | null>({
|
||||
key: 'tokenPairState',
|
||||
default: null,
|
||||
effects: [cookieStorageEffect('tokenPair')],
|
||||
});
|
||||
@ -1,12 +1,6 @@
|
||||
import Cookies, { CookieAttributes } from 'js-cookie';
|
||||
|
||||
type Listener = (
|
||||
newValue: string | undefined,
|
||||
oldValue: string | undefined,
|
||||
) => void;
|
||||
|
||||
class CookieStorage {
|
||||
private listeners: Record<string, Listener[]> = {};
|
||||
private keys: Set<string> = new Set();
|
||||
|
||||
getItem(key: string): string | undefined {
|
||||
@ -14,49 +8,18 @@ class CookieStorage {
|
||||
}
|
||||
|
||||
setItem(key: string, value: string, attributes?: CookieAttributes): void {
|
||||
const oldValue = this.getItem(key);
|
||||
|
||||
this.keys.add(key);
|
||||
Cookies.set(key, value, attributes);
|
||||
this.dispatch(key, value, oldValue);
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
const oldValue = this.getItem(key);
|
||||
|
||||
this.keys.delete(key);
|
||||
Cookies.remove(key);
|
||||
this.dispatch(key, undefined, oldValue);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.keys.forEach((key) => this.removeItem(key));
|
||||
}
|
||||
|
||||
private dispatch(
|
||||
key: string,
|
||||
newValue: string | undefined,
|
||||
oldValue: string | undefined,
|
||||
): void {
|
||||
if (this.listeners[key]) {
|
||||
this.listeners[key].forEach((callback) => callback(newValue, oldValue));
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener(key: string, callback: Listener): void {
|
||||
if (!this.listeners[key]) {
|
||||
this.listeners[key] = [];
|
||||
}
|
||||
this.listeners[key].push(callback);
|
||||
}
|
||||
|
||||
removeEventListener(key: string, callback: Listener): void {
|
||||
if (this.listeners[key]) {
|
||||
this.listeners[key] = this.listeners[key].filter(
|
||||
(listener) => listener !== callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cookieStorage = new CookieStorage();
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
|
||||
import useApolloMocked from '@/apollo/hooks/useApolloMocked';
|
||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
|
||||
import { apolloClient } from './apollo-client';
|
||||
import { mockClient } from './mock-client';
|
||||
|
||||
export const ApolloProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const apolloClient = useApolloFactory();
|
||||
const mockedClient = useApolloMocked();
|
||||
|
||||
const [isMockMode] = useRecoilState(isMockModeState);
|
||||
|
||||
return (
|
||||
<ApolloProviderBase client={isMockMode ? mockClient : apolloClient}>
|
||||
<ApolloProviderBase client={isMockMode ? mockedClient : apolloClient}>
|
||||
{children}
|
||||
</ApolloProviderBase>
|
||||
);
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { tokenService } from '@/auth/services/TokenService';
|
||||
import { CommentThreadTarget } from '~/generated/graphql';
|
||||
|
||||
import { ApolloFactory } from './apollo.factory';
|
||||
|
||||
const apollo = new ApolloFactory({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
CommentThread: {
|
||||
fields: {
|
||||
commentThreadTargets: {
|
||||
merge(
|
||||
existing: CommentThreadTarget[] = [],
|
||||
incoming: CommentThreadTarget[],
|
||||
) {
|
||||
return [...incoming];
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
onUnauthenticatedError() {
|
||||
tokenService.removeTokenPair();
|
||||
},
|
||||
});
|
||||
|
||||
export const apolloClient = apollo.getClient();
|
||||
@ -1,36 +0,0 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
createHttpLink,
|
||||
from,
|
||||
InMemoryCache,
|
||||
} from '@apollo/client';
|
||||
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
const apiLink = createHttpLink({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
});
|
||||
|
||||
const mockLink = new ApolloLink((operation, forward) => {
|
||||
return forward(operation).map((response) => {
|
||||
if (operation.operationName === 'GetCompanies') {
|
||||
return { data: { companies: mockedCompaniesData } };
|
||||
}
|
||||
if (operation.operationName === 'Verify') {
|
||||
return { data: { user: [mockedUsersData[0]], tokens: {} } };
|
||||
}
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
export const mockClient = new ApolloClient({
|
||||
link: from([mockLink, apiLink]),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
});
|
||||
9
front/src/providers/user/UserProvider.tsx
Normal file
9
front/src/providers/user/UserProvider.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { useFetchCurrentUser } from '@/auth/hooks/useFetchCurrentUser';
|
||||
|
||||
export const UserProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
useFetchCurrentUser();
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@ -11,7 +11,6 @@ import { GET_CURRENT_USER } from '@/users/services';
|
||||
import {
|
||||
GetCompaniesQuery,
|
||||
GetPeopleQuery,
|
||||
GetUsersQuery,
|
||||
SearchCompanyQuery,
|
||||
SearchUserQuery,
|
||||
} from '~/generated/graphql';
|
||||
@ -73,19 +72,9 @@ export const graphqlMocks = [
|
||||
);
|
||||
}),
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', (req, res, ctx) => {
|
||||
const customWhere = {
|
||||
...req.variables.where,
|
||||
id: {
|
||||
equals: req.variables.uuid,
|
||||
},
|
||||
};
|
||||
|
||||
const returnedMockedData = filterAndSortData<
|
||||
GetUsersQuery['findManyUser'][0]
|
||||
>(mockedUsersData, customWhere, req.variables.orderBy, req.variables.limit);
|
||||
return res(
|
||||
ctx.data({
|
||||
users: returnedMockedData,
|
||||
users: [mockedUsersData[0]],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
||||
@ -4,6 +4,7 @@ import { ApolloProvider } from '@apollo/client';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||
import { UserProvider } from '~/providers/user/UserProvider';
|
||||
|
||||
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
||||
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
||||
@ -17,11 +18,13 @@ export function getRenderWrapperForPage(
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<ApolloProvider client={mockedClient}>
|
||||
<MemoryRouter initialEntries={[currentPath]}>
|
||||
<FullHeightStorybookLayout>
|
||||
<DefaultLayout>{children}</DefaultLayout>
|
||||
</FullHeightStorybookLayout>
|
||||
</MemoryRouter>
|
||||
<UserProvider>
|
||||
<MemoryRouter initialEntries={[currentPath]}>
|
||||
<FullHeightStorybookLayout>
|
||||
<DefaultLayout>{children}</DefaultLayout>
|
||||
</FullHeightStorybookLayout>
|
||||
</MemoryRouter>
|
||||
</UserProvider>
|
||||
</ApolloProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user