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:
Jérémy M
2023-06-24 07:43:54 +02:00
committed by GitHub
parent c6708b2c1f
commit 31145c5518
23 changed files with 222 additions and 293 deletions

View File

@ -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,

View 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]);
}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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();

View File

@ -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();
});

View File

@ -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');
});

View 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')],
});