Skip to content

Commit

Permalink
[ Feat ] 로그인 api를 연동합니다 (#253)
Browse files Browse the repository at this point in the history
* feat: 인증관련 유틸함수 추가

* feat: ProtectedRouter 추가

* feat: redirect 페이지 구글 로그인 인증 로직 추가

* feat: axios instance 토큰 재발급 로직 추가

* feat: 인증 관련 api 함수 작성

- 로그아웃은 다른 페이지 연동 이후에 작성 필요

* feat: vite env 타입 추가

* feat: getAccessToken 경로 오류 수정
  • Loading branch information
suwonthugger authored Jan 31, 2025
1 parent 8c9663b commit 77da498
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 57 deletions.
45 changes: 19 additions & 26 deletions src/pages/RedirectPage/RedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>RedirectPage</div>;
return <></>;
};

export default RedirectPage;
17 changes: 17 additions & 0 deletions src/router/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to={ROUTES_CONFIG.login.path} replace />;
}

return <Outlet />;
};

export default ProtectedRoute;
11 changes: 1 addition & 10 deletions src/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,14 @@ 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'));
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 <Navigate to="/login" replace />;
// }
return <Outlet />;
};

const router: Router = createBrowserRouter([
{
//public 라우트들
Expand Down
2 changes: 1 addition & 1 deletion src/router/routesConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const ROUTES_CONFIG = {
},
redirect: {
title: 'Redirect',
path: '/redirect',
path: 'auth/redirect',
},
timer: {
title: 'Timer',
Expand Down
28 changes: 28 additions & 0 deletions src/shared/apisV2/auth/auth.api.ts
Original file line number Diff line number Diff line change
@@ -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<reissueRes> => {
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);
};
9 changes: 9 additions & 0 deletions src/shared/apisV2/auth/auth.queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useMutation } from '@tanstack/react-query';

import { postLogout } from './auth.api';

export const usePostLogout = () => {
return useMutation({
mutationFn: postLogout,
});
};
72 changes: 72 additions & 0 deletions src/shared/apisV2/client.ts
Original file line number Diff line number Diff line change
@@ -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 };
12 changes: 12 additions & 0 deletions src/shared/apisV2/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true,
},
mutations: {
throwOnError: true,
},
},
});
7 changes: 7 additions & 0 deletions src/shared/types/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface reissueRes {
status: number;
message: string;
data: {
accessToken: string;
};
}
15 changes: 15 additions & 0 deletions src/shared/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -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;
};
20 changes: 0 additions & 20 deletions src/shared/utils/token/index.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_BASE_URL: string;
readonly VITE_GOOGLE_URL: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}

0 comments on commit 77da498

Please sign in to comment.