From d2c615f8eff2309276da3f6c103291a1852f5008 Mon Sep 17 00:00:00 2001 From: devleejb Date: Mon, 23 Sep 2024 14:44:45 +0900 Subject: [PATCH 1/4] Fix refresh token race condition --- frontend/src/App.tsx | 39 +--------------------------------- frontend/src/hooks/api/user.ts | 39 +++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd0b543e..bcceea47 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,7 @@ import * as Sentry from "@sentry/react"; import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import axios from "axios"; import { useEffect, useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { RouterProvider, createBrowserRouter, @@ -21,10 +21,7 @@ import { useGetSettingsQuery } from "./hooks/api/settings"; import { useErrorHandler } from "./hooks/useErrorHandler"; import AuthProvider from "./providers/AuthProvider"; import { routes } from "./routes"; -import { logout, setAccessToken } from "./store/authSlice"; import { selectConfig } from "./store/configSlice"; -import { store } from "./store/store"; -import { setUserData } from "./store/userSlice"; import { isAxios404Error, isAxios500Error } from "./utils/axios.default"; if (import.meta.env.PROD) { @@ -61,7 +58,6 @@ function SettingLoader() { function App() { const config = useSelector(selectConfig); - const dispatch = useDispatch(); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); const theme = useMemo(() => { const defaultMode = prefersDarkMode ? "dark" : "light"; @@ -108,39 +104,6 @@ function App() { }); }, [handleError]); - useEffect(() => { - const handleRefreshTokenExpiration = () => { - dispatch(logout()); - dispatch(setUserData(null)); - }; - - const interceptor = axios.interceptors.response.use( - (response) => response, - async (error) => { - if (error.response?.status === 401 && !error.config._retry) { - if (error.config.url === "/auth/refresh") { - handleRefreshTokenExpiration(); - return Promise.reject(error); - } else { - error.config._retry = true; - const refreshToken = store.getState().auth.refreshToken; - const response = await axios.post("/auth/refresh", { refreshToken }); - const newAccessToken = response.data.newAccessToken; - dispatch(setAccessToken(newAccessToken)); - axios.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`; - error.config.headers["Authorization"] = `Bearer ${newAccessToken}`; - return axios(error.config); - } - } - return Promise.reject(error); - } - ); - - return () => { - axios.interceptors.response.eject(interceptor); - }; - }, [dispatch]); - return ( diff --git a/frontend/src/hooks/api/user.ts b/frontend/src/hooks/api/user.ts index 992f9838..887d7ca8 100644 --- a/frontend/src/hooks/api/user.ts +++ b/frontend/src/hooks/api/user.ts @@ -1,8 +1,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { logout, selectAuth } from "../../store/authSlice"; +import { logout, selectAuth, setAccessToken } from "../../store/authSlice"; import { User, setUserData } from "../../store/userSlice"; import { GetUserResponse, UpdateUserRequest } from "./types/user"; @@ -13,10 +13,43 @@ export const generateGetUserQueryKey = (accessToken: string) => { export const useGetUserQuery = () => { const dispatch = useDispatch(); const authStore = useSelector(selectAuth); + const [axiosIntercepterAdded, setAxiosIntercepterAdded] = useState(false); + + useEffect(() => { + const interceptor = axios.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401 && !error.config._retry) { + if (error.config.url === "/auth/refresh") { + dispatch(logout()); + dispatch(setUserData(null)); + return Promise.reject(error); + } else { + error.config._retry = true; + const { refreshToken } = authStore; + const response = await axios.post("/auth/refresh", { refreshToken }); + const newAccessToken = response.data.newAccessToken; + dispatch(setAccessToken(newAccessToken)); + axios.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`; + error.config.headers["Authorization"] = `Bearer ${newAccessToken}`; + return axios(error.config); + } + } + return Promise.reject(error); + } + ); + + setAxiosIntercepterAdded(true); + + return () => { + setAxiosIntercepterAdded(false); + axios.interceptors.response.eject(interceptor); + }; + }, [authStore, dispatch]); const query = useQuery({ queryKey: generateGetUserQueryKey(authStore.accessToken || ""), - enabled: Boolean(authStore.accessToken), + enabled: Boolean(axiosIntercepterAdded && authStore.accessToken), queryFn: async () => { axios.defaults.headers.common["Authorization"] = `Bearer ${authStore.accessToken}`; const res = await axios.get("/users"); From 81cf8c87679f024c516087cb27c5b95a7aaa022c Mon Sep 17 00:00:00 2001 From: devleejb Date: Mon, 23 Sep 2024 15:14:22 +0900 Subject: [PATCH 2/4] Change the login loading condition --- frontend/src/providers/AuthProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 1a1be25c..4d98a849 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -9,14 +9,14 @@ interface AuthProviderProps { function AuthProvider(props: AuthProviderProps) { const { children } = props; - const { data: user, isSuccess, isLoading } = useGetUserQuery(); + const { data: user, isSuccess, isLoading, isPending } = useGetUserQuery(); const shouldChangeNickname = useMemo( () => isSuccess && !user.nickname, [isSuccess, user?.nickname] ); return ( - + {shouldChangeNickname ? : children} ); From f9ae2216efb030f52f735989c1929df6447980ad Mon Sep 17 00:00:00 2001 From: devleejb Date: Mon, 23 Sep 2024 15:20:33 +0900 Subject: [PATCH 3/4] Improve variable readability --- frontend/src/providers/AuthProvider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 4d98a849..49db667d 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -14,9 +14,10 @@ function AuthProvider(props: AuthProviderProps) { () => isSuccess && !user.nickname, [isSuccess, user?.nickname] ); + const isAuthLoading = isLoading || isPending; return ( - + {shouldChangeNickname ? : children} ); From 7e27671e8b3f658ecde0ec7d4131288afee2fda5 Mon Sep 17 00:00:00 2001 From: devleejb Date: Mon, 23 Sep 2024 15:20:53 +0900 Subject: [PATCH 4/4] Fix typo --- frontend/src/hooks/api/user.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/api/user.ts b/frontend/src/hooks/api/user.ts index 887d7ca8..67df493b 100644 --- a/frontend/src/hooks/api/user.ts +++ b/frontend/src/hooks/api/user.ts @@ -13,7 +13,7 @@ export const generateGetUserQueryKey = (accessToken: string) => { export const useGetUserQuery = () => { const dispatch = useDispatch(); const authStore = useSelector(selectAuth); - const [axiosIntercepterAdded, setAxiosIntercepterAdded] = useState(false); + const [axiosInterceptorAdded, setAxiosInterceptorAdded] = useState(false); useEffect(() => { const interceptor = axios.interceptors.response.use( @@ -39,17 +39,17 @@ export const useGetUserQuery = () => { } ); - setAxiosIntercepterAdded(true); + setAxiosInterceptorAdded(true); return () => { - setAxiosIntercepterAdded(false); + setAxiosInterceptorAdded(false); axios.interceptors.response.eject(interceptor); }; }, [authStore, dispatch]); const query = useQuery({ queryKey: generateGetUserQueryKey(authStore.accessToken || ""), - enabled: Boolean(axiosIntercepterAdded && authStore.accessToken), + enabled: Boolean(axiosInterceptorAdded && authStore.accessToken), queryFn: async () => { axios.defaults.headers.common["Authorization"] = `Bearer ${authStore.accessToken}`; const res = await axios.get("/users");