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 { ApolloProvider } from './providers/apollo/ApolloProvider';
|
||||||
import { AppThemeProvider } from './providers/theme/AppThemeProvider';
|
import { AppThemeProvider } from './providers/theme/AppThemeProvider';
|
||||||
|
import { UserProvider } from './providers/user/UserProvider';
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
|
|
||||||
export function AppWrapper() {
|
export function AppWrapper() {
|
||||||
@ -11,7 +12,9 @@ export function AppWrapper() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<UserProvider>
|
||||||
|
<App />
|
||||||
|
</UserProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
</AppThemeProvider>
|
</AppThemeProvider>
|
||||||
</BrowserRouter>
|
</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 { ApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { AuthTokenPair } from '~/generated/graphql';
|
||||||
|
|
||||||
export interface ApolloManager<TCacheShape> {
|
export interface ApolloManager<TCacheShape> {
|
||||||
getClient(): ApolloClient<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 { Observable } from '@apollo/client/utilities';
|
||||||
|
|
||||||
import { renewToken } from '@/auth/services/AuthService';
|
import { renewToken } from '@/auth/services/AuthService';
|
||||||
import { tokenService } from '@/auth/services/TokenService';
|
import { AuthTokenPair } from '~/generated/graphql';
|
||||||
|
|
||||||
import { assertNotNull } from '../../modules/utils/assert';
|
import { loggerLink } from '../../utils/apollo-logger';
|
||||||
import { promiseToObservable } from '../../modules/utils/promise-to-observable';
|
import { assertNotNull } from '../../utils/assert';
|
||||||
|
import { promiseToObservable } from '../../utils/promise-to-observable';
|
||||||
import { ApolloManager } from './interfaces/apollo-manager.interface';
|
import { ApolloManager } from '../interfaces/apolloManager.interface';
|
||||||
import { loggerLink } from './logger';
|
|
||||||
|
|
||||||
const logger = loggerLink(() => 'Twenty');
|
const logger = loggerLink(() => 'Twenty');
|
||||||
|
|
||||||
@ -35,17 +34,20 @@ const resolvePendingRequests = () => {
|
|||||||
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
||||||
onError?: (err: GraphQLErrors | undefined) => void;
|
onError?: (err: GraphQLErrors | undefined) => void;
|
||||||
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||||
|
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
||||||
onUnauthenticatedError?: () => void;
|
onUnauthenticatedError?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||||
private client: ApolloClient<TCacheShape>;
|
private client: ApolloClient<TCacheShape>;
|
||||||
|
private tokenPair: AuthTokenPair | null = null;
|
||||||
|
|
||||||
constructor(opts: Options<TCacheShape>) {
|
constructor(opts: Options<TCacheShape>) {
|
||||||
const {
|
const {
|
||||||
uri,
|
uri,
|
||||||
onError: onErrorCb,
|
onError: onErrorCb,
|
||||||
onNetworkError,
|
onNetworkError,
|
||||||
|
onTokenPairChange,
|
||||||
onUnauthenticatedError,
|
onUnauthenticatedError,
|
||||||
...options
|
...options
|
||||||
} = opts;
|
} = opts;
|
||||||
@ -56,13 +58,11 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const authLink = setContext(async (_, { headers }) => {
|
const authLink = setContext(async (_, { headers }) => {
|
||||||
const credentials = tokenService.getTokenPair();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
authorization: credentials?.accessToken
|
authorization: this.tokenPair?.accessToken.token
|
||||||
? `Bearer ${credentials?.accessToken}`
|
? `Bearer ${this.tokenPair?.accessToken.token}`
|
||||||
: '',
|
: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -93,8 +93,9 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
if (!isRefreshing) {
|
if (!isRefreshing) {
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
forward$ = promiseToObservable(
|
forward$ = promiseToObservable(
|
||||||
renewToken(uri)
|
renewToken(uri, this.tokenPair)
|
||||||
.then(() => {
|
.then((tokens) => {
|
||||||
|
onTokenPairChange?.(tokens);
|
||||||
resolvePendingRequests();
|
resolvePendingRequests();
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
@ -161,6 +162,10 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTokenPair(tokenPair: AuthTokenPair | null) {
|
||||||
|
this.tokenPair = tokenPair;
|
||||||
|
}
|
||||||
|
|
||||||
getClient() {
|
getClient() {
|
||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
@ -3,11 +3,12 @@ import { useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { useChallengeMutation, useVerifyMutation } from '~/generated/graphql';
|
import { useChallengeMutation, useVerifyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
import { tokenService } from '../services/TokenService';
|
|
||||||
import { currentUserState } from '../states/currentUserState';
|
import { currentUserState } from '../states/currentUserState';
|
||||||
import { isAuthenticatingState } from '../states/isAuthenticatingState';
|
import { isAuthenticatingState } from '../states/isAuthenticatingState';
|
||||||
|
import { tokenPairState } from '../states/tokenPairState';
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
|
const [, setTokenPair] = useRecoilState(tokenPairState);
|
||||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||||
|
|
||||||
@ -50,14 +51,14 @@ export function useAuth() {
|
|||||||
throw new Error('No verify result');
|
throw new Error('No verify result');
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenService.setTokenPair(verifyResult.data?.verify.tokens);
|
setTokenPair(verifyResult.data?.verify.tokens);
|
||||||
|
|
||||||
setIsAuthenticating(false);
|
setIsAuthenticating(false);
|
||||||
setCurrentUser(verifyResult.data?.verify.user);
|
setCurrentUser(verifyResult.data?.verify.user);
|
||||||
|
|
||||||
return verifyResult.data?.verify;
|
return verifyResult.data?.verify;
|
||||||
},
|
},
|
||||||
[setCurrentUser, setIsAuthenticating, verify],
|
[setCurrentUser, setIsAuthenticating, setTokenPair, verify],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLogin = useCallback(
|
const handleLogin = useCallback(
|
||||||
@ -70,8 +71,8 @@ export function useAuth() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
const handleLogout = useCallback(() => {
|
||||||
tokenService.removeTokenPair();
|
setTokenPair(null);
|
||||||
}, []);
|
}, [setTokenPair]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenge: handleChallenge,
|
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 {
|
export function useIsLogged(): boolean {
|
||||||
const [value, setValue] = useState<string | undefined>(
|
const [tokenPair] = useRecoilState(tokenPairState);
|
||||||
cookieStorage.getItem('accessToken'),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return !!tokenPair;
|
||||||
const updateValue = (newValue: string | undefined) => setValue(newValue);
|
|
||||||
|
|
||||||
cookieStorage.addEventListener('accessToken', updateValue);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cookieStorage.removeEventListener('accessToken', updateValue);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return !!value;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,17 +5,14 @@ import {
|
|||||||
InMemoryCache,
|
InMemoryCache,
|
||||||
UriFunction,
|
UriFunction,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
import jwt from 'jwt-decode';
|
|
||||||
|
|
||||||
import { cookieStorage } from '@/utils/cookie-storage';
|
import { loggerLink } from '@/utils/apollo-logger';
|
||||||
import {
|
import {
|
||||||
|
AuthTokenPair,
|
||||||
RenewTokenDocument,
|
RenewTokenDocument,
|
||||||
RenewTokenMutation,
|
RenewTokenMutation,
|
||||||
RenewTokenMutationVariables,
|
RenewTokenMutationVariables,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { loggerLink } from '~/providers/apollo/logger';
|
|
||||||
|
|
||||||
import { tokenService } from './TokenService';
|
|
||||||
|
|
||||||
const logger = loggerLink(() => 'Twenty-Refresh');
|
const logger = loggerLink(() => 'Twenty-Refresh');
|
||||||
|
|
||||||
@ -60,29 +57,15 @@ const renewTokenMutation = async (
|
|||||||
* @param uri string | UriFunction | undefined
|
* @param uri string | UriFunction | undefined
|
||||||
* @returns TokenPair
|
* @returns TokenPair
|
||||||
*/
|
*/
|
||||||
export const renewToken = async (uri: string | UriFunction | undefined) => {
|
export const renewToken = async (
|
||||||
const tokenPair = tokenService.getTokenPair();
|
uri: string | UriFunction | undefined,
|
||||||
|
tokenPair: AuthTokenPair | undefined | null,
|
||||||
|
) => {
|
||||||
if (!tokenPair) {
|
if (!tokenPair) {
|
||||||
throw new Error('Refresh token is not defined');
|
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.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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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';
|
import Cookies, { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
type Listener = (
|
|
||||||
newValue: string | undefined,
|
|
||||||
oldValue: string | undefined,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
class CookieStorage {
|
class CookieStorage {
|
||||||
private listeners: Record<string, Listener[]> = {};
|
|
||||||
private keys: Set<string> = new Set();
|
private keys: Set<string> = new Set();
|
||||||
|
|
||||||
getItem(key: string): string | undefined {
|
getItem(key: string): string | undefined {
|
||||||
@ -14,49 +8,18 @@ class CookieStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setItem(key: string, value: string, attributes?: CookieAttributes): void {
|
setItem(key: string, value: string, attributes?: CookieAttributes): void {
|
||||||
const oldValue = this.getItem(key);
|
|
||||||
|
|
||||||
this.keys.add(key);
|
this.keys.add(key);
|
||||||
Cookies.set(key, value, attributes);
|
Cookies.set(key, value, attributes);
|
||||||
this.dispatch(key, value, oldValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem(key: string): void {
|
removeItem(key: string): void {
|
||||||
const oldValue = this.getItem(key);
|
|
||||||
|
|
||||||
this.keys.delete(key);
|
this.keys.delete(key);
|
||||||
Cookies.remove(key);
|
Cookies.remove(key);
|
||||||
this.dispatch(key, undefined, oldValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.keys.forEach((key) => this.removeItem(key));
|
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();
|
export const cookieStorage = new CookieStorage();
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
|
||||||
|
import useApolloMocked from '@/apollo/hooks/useApolloMocked';
|
||||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
|
|
||||||
import { apolloClient } from './apollo-client';
|
|
||||||
import { mockClient } from './mock-client';
|
|
||||||
|
|
||||||
export const ApolloProvider: React.FC<React.PropsWithChildren> = ({
|
export const ApolloProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
const apolloClient = useApolloFactory();
|
||||||
|
const mockedClient = useApolloMocked();
|
||||||
|
|
||||||
const [isMockMode] = useRecoilState(isMockModeState);
|
const [isMockMode] = useRecoilState(isMockModeState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProviderBase client={isMockMode ? mockClient : apolloClient}>
|
<ApolloProviderBase client={isMockMode ? mockedClient : apolloClient}>
|
||||||
{children}
|
{children}
|
||||||
</ApolloProviderBase>
|
</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 {
|
import {
|
||||||
GetCompaniesQuery,
|
GetCompaniesQuery,
|
||||||
GetPeopleQuery,
|
GetPeopleQuery,
|
||||||
GetUsersQuery,
|
|
||||||
SearchCompanyQuery,
|
SearchCompanyQuery,
|
||||||
SearchUserQuery,
|
SearchUserQuery,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
@ -73,19 +72,9 @@ export const graphqlMocks = [
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', (req, res, ctx) => {
|
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(
|
return res(
|
||||||
ctx.data({
|
ctx.data({
|
||||||
users: returnedMockedData,
|
users: [mockedUsersData[0]],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ApolloProvider } from '@apollo/client';
|
|||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||||
|
import { UserProvider } from '~/providers/user/UserProvider';
|
||||||
|
|
||||||
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
||||||
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
||||||
@ -17,11 +18,13 @@ export function getRenderWrapperForPage(
|
|||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<ApolloProvider client={mockedClient}>
|
<ApolloProvider client={mockedClient}>
|
||||||
<MemoryRouter initialEntries={[currentPath]}>
|
<UserProvider>
|
||||||
<FullHeightStorybookLayout>
|
<MemoryRouter initialEntries={[currentPath]}>
|
||||||
<DefaultLayout>{children}</DefaultLayout>
|
<FullHeightStorybookLayout>
|
||||||
</FullHeightStorybookLayout>
|
<DefaultLayout>{children}</DefaultLayout>
|
||||||
</MemoryRouter>
|
</FullHeightStorybookLayout>
|
||||||
|
</MemoryRouter>
|
||||||
|
</UserProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user