From a24c2edc76066d161ddce67032a9564c70b2ff98 Mon Sep 17 00:00:00 2001 From: Kirstie <39728053+epixieme@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:16:52 +0100 Subject: [PATCH] [686] - create code for refreshing google auth token (#690) * feat: Add refresh_token to googleLogin response - Added the `refresh_token` property to the `googleLogin` response type in `responses.ts`. - This change allows the backend to include a refresh token when a user logs in with Google. Refactor: Remove unused code in useLogin.tsx - Removed an extra whitespace in the `useLogin` hook in `useLogin.tsx`. - This change improves code readability and removes unnecessary code. Refactor: Comment out unused code in useApiClient.tsx - Commented out the unused `useNavigate` import and `ROUTES` import in `useApiClient.tsx`. - This change removes unused code and improves code cleanliness. Refactor: Remove unused parameters in apiCall function - Removed the `customCookies` parameter from the `apiCall` function in `useApiClient.tsx`. - This change removes unused code and simplifies the function signature. Fix: Update getAllConversations request headers - Updated the request headers in the `getAllConversations` function in `useApiClient.tsx` to include the access token. - This change ensures that the request is authenticated and authorized. Fix: Handle token refresh in useApiClient.tsx - Added a new `handleTokenRefresh` function in `useApiClient.tsx` to handle token refresh logic. - This change allows the frontend to refresh the access token when it expires. Fix: Update postGoogleLogin to store refresh token - Updated the `postGoogleLogin` function in `useApiClient.tsx` to store the refresh token in cookies. - This change ensures that the refresh token is available for token refresh requests. Fix: Remove unused code in postRegister function - Removed an extra line of code in the `postRegister` function in `useApiClient.tsx`. - This change removes unnecessary code and improves code readability. * feat: Add authorization header to createConversationInvite and deleteConversation API calls * refactor: Remove unused code and console logs in useApiClient hook * refactor: Remove unused code and console logs in useApiClient hook * refactor: Remove unused code and console logs in useApiClient hook * cleanup --------- Co-authored-by: Svenstar74 --- src/api/responses.ts | 1 + src/features/auth/hooks/useLogin.tsx | 2 +- src/pages/UserBPages/UserBSignUpPage.tsx | 3 +- src/shared/hooks/useApiClient.tsx | 121 +++++++++++++++-------- 4 files changed, 85 insertions(+), 42 deletions(-) diff --git a/src/api/responses.ts b/src/api/responses.ts index 015fe4b3..b8e42c69 100644 --- a/src/api/responses.ts +++ b/src/api/responses.ts @@ -55,6 +55,7 @@ export type Login = { export type googleLogin = { message: string; access_token: string; + refresh_token: string; user: { email: string; first_name: string; diff --git a/src/features/auth/hooks/useLogin.tsx b/src/features/auth/hooks/useLogin.tsx index ca254ee8..6f9c56f7 100644 --- a/src/features/auth/hooks/useLogin.tsx +++ b/src/features/auth/hooks/useLogin.tsx @@ -8,7 +8,7 @@ function useLogin() { const dispatch = useAppDispatch(); const quizIdB = useAppSelector((state) => state.auth.userB.quizId); const quizIdA = useAppSelector((state) => state.auth.userA.quizId); - + const apiClient = useApiClient(); const { showSuccessToast, showErrorToast } = useToastMessage(); diff --git a/src/pages/UserBPages/UserBSignUpPage.tsx b/src/pages/UserBPages/UserBSignUpPage.tsx index 7a88466f..d5979529 100644 --- a/src/pages/UserBPages/UserBSignUpPage.tsx +++ b/src/pages/UserBPages/UserBSignUpPage.tsx @@ -20,6 +20,7 @@ function UserBSignUpPage() { const { sessionId, quizId } = useAppSelector((state) => state.auth.userB); const { signUp } = useSignUp(); const [isLoading, setIsLoading] = useState(false); + const devMode = localStorage.getItem('devMode') === 'true'; async function signUpHandler(firstName: string, lastName: string, email: string, password: string) { setIsLoading(true); @@ -53,7 +54,7 @@ function UserBSignUpPage() {
- + {devMode && }
diff --git a/src/shared/hooks/useApiClient.tsx b/src/shared/hooks/useApiClient.tsx index fe993d4d..839ba0aa 100644 --- a/src/shared/hooks/useApiClient.tsx +++ b/src/shared/hooks/useApiClient.tsx @@ -3,7 +3,6 @@ import Cookies from 'js-cookie'; import { jwtDecode } from 'jwt-decode'; import { useAppSelector } from 'store/hooks'; -// import { useLogout } from 'features/auth'; import { useToastMessage } from 'shared/hooks'; import * as requests from 'api/requests'; import * as responses from 'api/responses'; @@ -26,24 +25,42 @@ const validateToken = (token: string): boolean => { function useApiClient() { const { showErrorToast } = useToastMessage(); - const sessionId = useAppSelector((state) => state.auth.userA.sessionId); const quizId = useAppSelector((state) => state.auth.userA.quizId); - async function apiCall(method: string, endpoint: string, headers: { [key: string]: string }, data?: any, withCredentials?: boolean) { + async function apiCall(method: string, endpoint: string, headers: { [key: string]: string }, data?: any, withCredentials?: boolean, customCookies?: { [key: string]: string }) { // Add sessionId to headers if (sessionId) { headers['X-Session-Id'] = sessionId; } + // customCookies is used to set cookies (In this instance it is the refresh token) for the request and remove them after the request is done. + if (customCookies) { + Object.entries(customCookies).forEach(([key, value]) => { + Cookies.set(key, value, { secure: true, sameSite: 'strict' }); + }); + } + // Get access token from cookies - const accessToken = Cookies.get('accessToken'); + let accessToken = Cookies.get('accessToken'); if (accessToken) { + // Check if the token is valid if (!validateToken(accessToken)) { - Cookies.remove('accessToken'); - // const newAccessToken = await postRefresh(); - // headers['Authorization'] = 'Bearer ' + newAccessToken; - } else { + // Attempt to refresh the access token + const newAccessToken = await handleTokenRefresh(); + + // If a new token is received, update it in the cookies and set the Authorization header + if (newAccessToken) { + accessToken = newAccessToken; + Cookies.set('accessToken', accessToken); + } else { + showErrorToast('Your session has expired. Please login again.'); + setTimeout(async () => { + window.location.reload(); + }, 2000); // Add a delay to allow the toast to show before redirecting + } + + // Set the Authorization header with the valid (or refreshed) token headers['Authorization'] = 'Bearer ' + accessToken; } } @@ -53,16 +70,25 @@ function useApiClient() { method, headers, data, - withCredentials, + withCredentials, // Always send credentials unless explicitly set to false }); + if (customCookies) { + Object.keys(customCookies).forEach((key) => { + Cookies.remove(key); + }); + } + return response; } async function postSession() { try { const response = await apiCall('post', '/session', {}); - return response.data; + if (response) { + return response.data; + } + throw new Error('Response is undefined'); } catch (error) { console.log(error); return { sessionId: '' }; @@ -108,9 +134,18 @@ function useApiClient() { async function postRegister({ firstName, lastName, email, password, quizId }: requests.PostRegister) { const response = await apiCall('post', '/register', {}, { firstName, lastName, email, password, quizId }); + const accessToken = response.data.access_token; // Store the access token for userA in cookies - const accessToken = response.data.access_token; + if (response) { + if (!response) { + throw new Error('Response is undefined'); + } + + Cookies.set('accessToken', accessToken, { secure: true }); + } else { + throw new Error('Response is undefined'); + } Cookies.set('accessToken', accessToken, { secure: true }); return response.data; @@ -137,26 +172,25 @@ function useApiClient() { Cookies.set('accessToken', accessToken, { secure: true }); } - // Store the refresh token in cookies - // const cookieHeader = response.headers['set-cookie']; - - // if (cookieHeader) { - // const refreshToken = cookieHeader[0].split(';')[0].split('=')[1]; - // Cookies.set('refreshToken', refreshToken, { expires: 365, secure: true }); - // } - + // Set refresh token from response headers (if backend returns it) + const cookieHeader = response.headers['set-cookie']; + if (cookieHeader) { + const refreshToken = cookieHeader[0].split(';')[0].split('=')[1]; + Cookies.set('refreshToken', refreshToken, { expires: 365, secure: true }); + } return response.data; } async function postGoogleLogin(credential: string, quizId: string) { - if (quizId) { - const response = await apiCall('post', '/auth/google', {}, { credential, quizId }, true); - return response.data; - } + const response = await apiCall('post', '/auth/google', {}, { credential, quizId }, true); + const { access_token, refresh_token } = response.data; + Cookies.set('accessToken', access_token, { secure: true }); + Cookies.set('refreshToken', refresh_token, { + secure: true, + sameSite: 'strict', + path: '/', + }); - const response = await apiCall('post', '/auth/google', {}, { credential }, true); - const { access_token } = response.data; - Cookies.set('accessToken', access_token, { secure: true, sameSite: 'strict' }); return response.data; } @@ -167,30 +201,37 @@ function useApiClient() { await apiCall('post', '/logout', {}); } + async function handleTokenRefresh(): Promise { + const newToken = await postRefresh(); + + if (newToken) { + Cookies.set('accessToken', newToken, { secure: true }); + return newToken; + } else { + showErrorToast('Your session has expired. Please login again.'); + setTimeout(async () => { + window.location.reload(); + }, 2000); // Add a delay to allow the toast to show before redirecting + } + } + async function postRefresh(): Promise { // Get the refresh token from cookies + Cookies.remove('accessToken'); const refreshToken = Cookies.get('refreshToken'); if (!refreshToken) { + showErrorToast('Refresh token not found. Please log in again.'); return ''; } try { - const response = await apiCall<{ access_token: string }>('post', '/refresh', { - Cookie: 'refreshToken=' + refreshToken, - }); + const response = await apiCall<{ access_token: string }>('post', '/refresh', {}, {}, true, { refresh_token: refreshToken }); // Update the access token in cookies const accessToken = response.data.access_token; Cookies.set('accessToken', accessToken, { secure: true }); - // Update the refresh token in cookies - const cookieHeader = response.headers['set-cookie']; - if (cookieHeader) { - const refreshToken = cookieHeader[0].split(';')[0].split('=')[1]; - Cookies.set('refreshToken', refreshToken, { expires: 365, secure: true }); - } - return accessToken; } catch (error) { if (error instanceof axios.AxiosError) { @@ -273,13 +314,13 @@ function useApiClient() { } async function createConversationInvite(invitedUserName: string) { - const response = await apiCall('post', '/conversation', {}, { invitedUserName }); + const response = await apiCall('post', '/conversation', { Authorization: `Bearer ${Cookies.get('accessToken')}` }, { invitedUserName }); return response.data; } async function getAllConversations() { - const response = await apiCall<{ conversations: responses.GetAllConversations[] }>('get', '/conversations', {}); + const response = await apiCall<{ conversations: responses.GetAllConversations[] }>('get', '/conversations', { Authorization: `Bearer ${Cookies.get('accessToken')}` }, {}); return response.data; } @@ -291,7 +332,7 @@ function useApiClient() { } async function deleteConversation(conversationId: string) { - await apiCall('delete', '/conversation/' + conversationId, {}); + await apiCall('delete', '/conversation/' + conversationId, { Authorization: `Bearer ${Cookies.get('accessToken')}` }); } async function putSingleConversation(data: requests.PutSingleConversation) { @@ -490,7 +531,7 @@ function useApiClient() { postSharedSolutions, getAlignmentSummary, postConversationConsent, - + handleTokenRefresh, postUserBVisit, }; }