From f2c537834797f54231e370dab6fdc30b9a9a400f Mon Sep 17 00:00:00 2001 From: Cho-heejung <66050038+he2e2@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:04:08 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20token=20=EA=B4=80=EB=A6=AC=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: AuthProvider * feat: 토큰 발급 실패 시, 팝업 알림 * feat: 로그아웃 --- src/app/App.tsx | 13 +++--- src/app/AuthProvider.tsx | 83 +++++++++++++++++++++++++++++++++++++++ src/shared/api/baseApi.ts | 33 +++++++++++++--- 3 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/app/AuthProvider.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 1634b18..5f78661 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,16 +1,19 @@ import { RouterProvider } from 'react-router-dom'; import AppRouter from './appRouter'; +import AuthProvider from './AuthProvider'; import ModalProvider from './ModalProvider'; import QueryProvider from './QueryProvider'; function App() { return ( - - - - - + + + + + + + ); } diff --git a/src/app/AuthProvider.tsx b/src/app/AuthProvider.tsx new file mode 100644 index 0000000..5f8e5b9 --- /dev/null +++ b/src/app/AuthProvider.tsx @@ -0,0 +1,83 @@ +import axios from 'axios'; +import type { ReactNode } from 'react'; +import { createContext, useContext, useRef, useState } from 'react'; + +import api from '@/shared/api/baseApi'; +import { customConfirm } from '@/shared/ui'; + +interface AuthContextType { + accessToken: string | null; + setAccessToken: (token: string | null) => void; + reissueToken: () => Promise; + logout: () => Promise; + removeToken: () => void; +} + +const AuthContext = createContext(undefined); + +const AuthProvider = ({ children }: { children: ReactNode }) => { + const [accessToken, setAccessToken] = useState( + localStorage.getItem('accessToken'), + ); + const isRefreshing = useRef(false); + + const reissueToken = async () => { + if (isRefreshing.current) return; + + isRefreshing.current = true; + + try { + const response = await axios.post('/token/reissue', {}, { withCredentials: true }); + + const newAccessToken = (response.headers['authorization'] as string)?.split(' ')[1]; + + if (newAccessToken) { + setAccessToken(newAccessToken); + localStorage.setItem('accessToken', newAccessToken); + } else { + throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.'); + } + } catch (error) { + console.error('토큰 재발급 실패:', error); + removeToken(); + } finally { + // eslint-disable-next-line require-atomic-updates + isRefreshing.current = false; + } + }; + + const removeToken = () => { + setAccessToken(null); + localStorage.removeItem('accessToken'); + customConfirm({ title: '로그인', text: '로그인 페이지로 이동합니다.', icon: 'info' }); + }; + + const logout = async () => { + try { + await api.post('/user/logout'); + + setAccessToken(null); + localStorage.removeItem('accessToken'); + } catch (error) { + console.error('로그아웃 실패:', error); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('AuthProvider Error'); + } + return context; +}; + +export default AuthProvider; diff --git a/src/shared/api/baseApi.ts b/src/shared/api/baseApi.ts index cd9a46b..e70cdc8 100644 --- a/src/shared/api/baseApi.ts +++ b/src/shared/api/baseApi.ts @@ -1,19 +1,42 @@ -import axios, { isAxiosError } from 'axios'; +import type { AxiosRequestConfig } from 'axios'; +import axios from 'axios'; + +import { useAuth } from '@/app/AuthProvider'; const api = axios.create({ - baseURL: ``, // Backend API Domain 주소 넣기 + baseURL: ``, // Backend API Domain 주소 timeout: 10_000, withCredentials: true, }); +api.interceptors.request.use(config => { + const { accessToken } = useAuth(); + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + return config; +}); + api.interceptors.response.use( response => response, async error => { - if (isAxiosError(error)) { - return Promise.reject(error); + const { reissueToken } = useAuth(); + + if (error.response?.status === 401) { + try { + await reissueToken(); + const { accessToken } = useAuth(); + + if (accessToken && error.config) { + error.config.headers.Authorization = `Bearer ${accessToken}`; + return api.request(error.config as AxiosRequestConfig); + } + } catch (reissueError) { + console.error('토큰 재발급 실패:', reissueError); + } } - return Promise.reject(new Error('알 수 없는 에러 발생')); + return Promise.reject(error instanceof Error ? error : new Error('Unknown Error')); }, );