diff --git a/src/pages/RedirectPage/RedirectPage.tsx b/src/pages/RedirectPage/RedirectPage.tsx index de91dd8e..0fb8d331 100644 --- a/src/pages/RedirectPage/RedirectPage.tsx +++ b/src/pages/RedirectPage/RedirectPage.tsx @@ -1,38 +1,31 @@ import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { setAccessToken } from '@/shared/utils/auth'; -// import { useLocation } from 'react-router-dom'; -// import { useSignUp } from '@/shared/apis/auth/queries'; import { ROUTES_CONFIG } from '@/router/routesConfig'; const RedirectPage = () => { - //Todo: 서버 이슈로 로그인 관련 로직 앱잼 끝나고 사용 - // const params = new URLSearchParams(useLocation().search); - // const authorizationCode = params.get('code'); + const { search } = useLocation(); const navigate = useNavigate(); - // const { data, error, isError } = useSignUp(authorizationCode); - - // useEffect(() => { - // if (data) { - // const { accessToken, refreshToken } = data || {}; - // if (accessToken && refreshToken) { - // localStorage.setItem('accessToken', accessToken); - // localStorage.setItem('refreshToken', refreshToken); - // navigate(ROUTES.home.path); - // } - // } - // if (error) { - // alert('다시 로그인 해주세요'); - // navigate(ROUTES.login.path, { replace: true }); - // } - // }, [error, navigate, data]); - useEffect(() => { - navigate(`${ROUTES_CONFIG.onboarding.path}?step=start`, { replace: true }); - }, [navigate]); + const params = new URLSearchParams(search); + const accessToken = params.get('accessToken'); + const isSignUp = params.get('isSignUp'); + + if (accessToken) { + setAccessToken(accessToken); + + if (isSignUp === 'true') { + navigate(`${ROUTES_CONFIG.home.path}`, { replace: true }); + } else { + navigate(`${ROUTES_CONFIG.onboarding.path}?step=start`, { replace: true }); + } + } + }, [navigate, search]); - return
RedirectPage
; + return <>; }; export default RedirectPage; diff --git a/src/router/ProtectedRoute.tsx b/src/router/ProtectedRoute.tsx new file mode 100644 index 00000000..146fc7be --- /dev/null +++ b/src/router/ProtectedRoute.tsx @@ -0,0 +1,17 @@ +import { Navigate, Outlet } from 'react-router-dom'; + +import { getAccessToken } from '@/shared/utils/auth'; + +import { ROUTES_CONFIG } from './routesConfig'; + +const ProtectedRoute = () => { + const accessToken = getAccessToken(); + if (!accessToken) { + alert('로그인 해주세요.'); + return ; + } + + return ; +}; + +export default ProtectedRoute; diff --git a/src/router/Router.tsx b/src/router/Router.tsx index b6b90268..6450d1fd 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -8,6 +8,7 @@ import OnboardingPage from '@/pages/OnboardingPage/OnboardingPage'; import RedirectPage from '@/pages/RedirectPage/RedirectPage'; import Layout from '@/shared/layout/Layout'; +import ProtectedRoute from './ProtectedRoute'; import { ROUTES_CONFIG } from './routesConfig'; const LoginPage = lazy(() => import('@/pages/LoginPage/LoginPage')); @@ -15,16 +16,6 @@ const HomePage = lazy(() => import('@/pages/HomePage/HomePage')); const TimerPage = lazy(() => import('@/pages/TimerPage/TimerPage')); const AllowedServicePage = lazy(() => import('@/pages/AllowedServicePage/AllowedServicePage')); -const ProtectedRoute = () => { - //Todo: 개발이 진행되면 실제 토큰 상태를 받아서 login page로 이동 시킴 - // const accessToken = getAccessTotken(); - // if (!accessToken) { - // alert('로그인 해주세요'); - // return ; - // } - return ; -}; - const router: Router = createBrowserRouter([ { //public 라우트들 diff --git a/src/router/routesConfig.ts b/src/router/routesConfig.ts index df653f61..5fac3f1d 100644 --- a/src/router/routesConfig.ts +++ b/src/router/routesConfig.ts @@ -17,7 +17,7 @@ export const ROUTES_CONFIG = { }, redirect: { title: 'Redirect', - path: '/redirect', + path: 'auth/redirect', }, timer: { title: 'Timer', diff --git a/src/shared/apisV2/auth/auth.api.ts b/src/shared/apisV2/auth/auth.api.ts new file mode 100644 index 00000000..05a22011 --- /dev/null +++ b/src/shared/apisV2/auth/auth.api.ts @@ -0,0 +1,28 @@ +import axios from 'axios'; + +import { authClient } from '@/shared/apis/client'; + +import { getAccessToken } from '@/shared/utils/auth'; + +import { reissueRes } from '@/shared/types/api/auth'; + +const AUTH_URL = { + PATCH_REISSUE_TOKEN: 'api/v2/users/reissue', + POST_LOGOUT: 'api/v2/users/logout', +}; + +export const patchReissueToken = async (): Promise => { + const accessToken = getAccessToken(); + + const { data } = await axios.patch(AUTH_URL.PATCH_REISSUE_TOKEN, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + return data; +}; + +export const postLogout = async () => { + await authClient.post(AUTH_URL.POST_LOGOUT); +}; diff --git a/src/shared/apisV2/auth/auth.queries.ts b/src/shared/apisV2/auth/auth.queries.ts new file mode 100644 index 00000000..222fad0d --- /dev/null +++ b/src/shared/apisV2/auth/auth.queries.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postLogout } from './auth.api'; + +export const usePostLogout = () => { + return useMutation({ + mutationFn: postLogout, + }); +}; diff --git a/src/shared/apisV2/client.ts b/src/shared/apisV2/client.ts new file mode 100644 index 00000000..c05d0e5e --- /dev/null +++ b/src/shared/apisV2/client.ts @@ -0,0 +1,72 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +import { getAccessToken, reloginWithoutLogout, setAccessToken } from '@/shared/utils/auth'; + +import { patchReissueToken } from './auth/auth.api'; + +const API_URL = `${import.meta.env.VITE_BASE_URL}`; + +const defaultConfig: AxiosRequestConfig = { + baseURL: API_URL, + headers: { 'Content-Type': 'application/json' }, +}; + +// 기본 설정을 적용한 axios 인스턴스 생성 함수 +const createBaseClient = (additionalConfig: AxiosRequestConfig = {}): AxiosInstance => { + const clientConfig = { + ...defaultConfig, + ...additionalConfig, + }; + const baseClient = axios.create(clientConfig); + return baseClient; +}; + +// 인증 설정을 추가하는 함수 (토큰) +const addAuthInterceptor = (axiosClient: AxiosInstance) => { + axiosClient.interceptors.request.use(async (config) => { + const accessToken = getAccessToken(); + config.headers.Authorization = `Bearer ${accessToken}`; + config.withCredentials = true; + return config; + }); + + axiosClient.interceptors.response.use( + (response) => { + return response; + }, + async (e) => { + const prevRequest = e.config; + if (e.response.status === 401 && !prevRequest.sent) { + prevRequest.sent = true; + // 401 에러가 떴을 때 토큰 재발급 + try { + const { data } = await patchReissueToken(); + setAccessToken(data.accessToken); + return axiosClient(prevRequest); + } catch (reissueError) { + reloginWithoutLogout(); + } + } + return Promise.reject(e); + }, + ); +}; + +// 클라이언트 생성 함수 +const createAxiosClient = (additionalConfig: AxiosRequestConfig = {}, withAuth: boolean = false): AxiosInstance => { + const axiosClient = createBaseClient(additionalConfig); + + if (withAuth) { + addAuthInterceptor(axiosClient); + } + + return axiosClient; +}; + +// 일반 요청 클라이언트 (토큰 불필요) +const nonAuthClient: AxiosInstance = createAxiosClient(); + +// 인증 요청 클라이언트 (토큰 필요) +const authClient: AxiosInstance = createAxiosClient({}, true); + +export { authClient, nonAuthClient }; diff --git a/src/shared/apisV2/queryClient.ts b/src/shared/apisV2/queryClient.ts new file mode 100644 index 00000000..5305ce59 --- /dev/null +++ b/src/shared/apisV2/queryClient.ts @@ -0,0 +1,12 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + throwOnError: true, + }, + mutations: { + throwOnError: true, + }, + }, +}); diff --git a/src/shared/types/api/auth.ts b/src/shared/types/api/auth.ts new file mode 100644 index 00000000..5c848197 --- /dev/null +++ b/src/shared/types/api/auth.ts @@ -0,0 +1,7 @@ +export interface reissueRes { + status: number; + message: string; + data: { + accessToken: string; + }; +} diff --git a/src/shared/utils/auth.ts b/src/shared/utils/auth.ts new file mode 100644 index 00000000..59a8862c --- /dev/null +++ b/src/shared/utils/auth.ts @@ -0,0 +1,15 @@ +import { ROUTES_CONFIG } from '@/router/routesConfig'; + +export const getAccessToken = () => { + const accessToken = localStorage.getItem('accessToken'); + return accessToken; +}; + +export const setAccessToken = (accessToken: string) => { + localStorage.setItem('accessToken', accessToken); +}; + +export const reloginWithoutLogout = () => { + localStorage.removeItem('accessToken'); + location.href = ROUTES_CONFIG.login.path; +}; diff --git a/src/shared/utils/token/index.ts b/src/shared/utils/token/index.ts deleted file mode 100644 index 0a026cb0..00000000 --- a/src/shared/utils/token/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const getAccessTotken = () => { - const accessToken = localStorage.getItem('accessToken'); - return accessToken; -}; - -export const getRefreshToken = () => { - const refresh = localStorage.getItem('refreshToken'); - return refresh; -}; - -export const getAllToken = () => { - const accessToken = localStorage.getItem('accessToken'); - const refreshToken = localStorage.getItem('refreshToken'); - return { accessToken, refreshToken }; -}; - -export const setAllToken = (accessToken: string, refreshToken: string) => { - localStorage.setItem('accessToken', accessToken); - localStorage.setItem('refreshToken', refreshToken); -}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe2..6a8bacbc 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + readonly VITE_BASE_URL: string; + readonly VITE_GOOGLE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}