Skip to content

Commit

Permalink
useLogin 마이그레이션 (#621)
Browse files Browse the repository at this point in the history
* feat: 로그인, refresh api 추가

* feat: jwt 유효 기간을 확인하고 refresh 후 api 요청

* fix: 누락된 return문 추가

* refactor: useLogin 수정

* refactor: useFetch 제거

* feat: jwt 유효시간 확인 유틸 추가

* feat: auth 관련 api 분리

* feat: 토큰 제거시 쿼리무효화

* feat: default retry 1 설정

* refactor: useLogin 제거

* feat: 로그인 에러 발생 이후 removeQuery

* feat: 로컬 로그인 기능 추가

* feat: 로그인 api then/catch 적용

* Merge branch 'dev/FE' into feat/614

* fix: mockData 중복 태그 제거

* feat: 인증 관련 오류 발생시 로그아웃

* feat: 로그아웃 api 추가

* fix: 로그아웃 api PATCH로 변경

* fix: 로그아웃 msw 수정
  • Loading branch information
guridaek authored Oct 10, 2023
1 parent 34e8da0 commit 7a8c087
Show file tree
Hide file tree
Showing 34 changed files with 325 additions and 414 deletions.
14 changes: 2 additions & 12 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import React, { Suspense, useState } from 'react';
import React, { Suspense } from 'react';
import { styled } from 'styled-components';
import { Outlet } from 'react-router-dom';
import ToastProvider from './contexts/ToastContext';
import ChannelService from './ChannelService';
import { CHANNEL_SERVICE_KEY } from './constants';
import { useLogin } from './hooks/useLogin';
import LoadingPage from './pages/LoadingPage';
import ModalProvider from './contexts/ModalContext';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './hooks/query/queryClient';

const App = () => {
const { checkLoginToken } = useLogin();
const [isLoading, setIsLoading] = useState<boolean>(true);

checkLoginToken().finally(() => {
setIsLoading(false);
});

ChannelService.loadScript();

if (CHANNEL_SERVICE_KEY) {
Expand All @@ -26,9 +18,7 @@ const App = () => {
});
}

return isLoading ? (
<LoadingPage />
) : (
return (
<ToastProvider>
<ModalProvider>
<S.AppContainer>
Expand Down
46 changes: 23 additions & 23 deletions frontend/src/apis/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ export const getNotification = () => {
return request.get<GetNotificationResponse>(`/notifications`, true);
};

export const getOtherRunnerProfile = (userId: number) => {
return request.get<GetRunnerProfileResponse>(`/profile/runner/${userId}`, false);
};

export const getOtherSupporterProfile = (userId: number) => {
return request.get<GetRunnerProfileResponse>(`/profile/supporter/${userId}`, false);
};

export const getOtherSupporterPost = (supporterId: number) => {
const params = new URLSearchParams([
['supporterId', supporterId.toString()],
['reviewStatus', 'DONE'],
]);

return request.get<GetRunnerPostResponse>(`/posts/runner/search?${params.toString()}`, false);
};

export const postRunnerPostCreation = (formData: CreateRunnerPostRequest) => {
const body = JSON.stringify(formData);
return request.post<void>(`/posts/runner`, body);
Expand All @@ -92,6 +109,11 @@ export const postFeedbackToSupporter = (formData: PostFeedbackRequest) => {
return request.post<void>(`/feedback/supporter`, body);
};

export const postMissionBranchCreation = (repoName: string) => {
const body = JSON.stringify({ repoName });
return request.post<void>(`/branch`, body);
};

export const patchReviewCancelation = (runnerPostId: number) => {
return request.patch<void>(`/posts/runner/${runnerPostId}/cancelation`);
};
Expand Down Expand Up @@ -125,26 +147,4 @@ export const deleteRunnerPost = (runnerPostId: number) => {

export const deleteNotification = (notificationsId: number) => {
return request.delete<void>(`/notifications/${notificationsId}`);
};

export const postMissionBranchCreation = (repoName: string) => {
const body = JSON.stringify({ repoName });
return request.post<void>(`/branch`, body);
};

export const getOtherRunnerProfile = (userId: number) => {
return request.get<GetRunnerProfileResponse>(`/profile/runner/${userId}`, false);
};

export const getOtherSupporterProfile = (userId: number) => {
return request.get<GetRunnerProfileResponse>(`/profile/supporter/${userId}`, false);
};

export const getOtherSupporterPost = (supporterId: number) => {
const params = new URLSearchParams([
['supporterId', supporterId.toString()],
['reviewStatus', 'DONE'],
]);

return request.get<GetRunnerPostResponse>(`/posts/runner/search?${params.toString()}`, false);
};
};
81 changes: 81 additions & 0 deletions frontend/src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ACCESS_TOKEN_LOCAL_STORAGE_KEY, BATON_BASE_URL } from '@/constants';
import { queryClient } from '@/hooks/query/queryClient';
import { getRestMinute } from '@/utils/jwt';

export const getAccessToken = () => localStorage.getItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY);

const removeAccessToken = () => {
localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY);
};

export const isLogin = () => Boolean(getAccessToken());

export const logout = () => {
patchRefreshToken();

removeAccessToken();

queryClient.removeQueries({ queryKey: ['headerProfile'] });
};

const saveAccessToken = (response: Response) => {
if (!response.ok) {
removeAccessToken();
queryClient.resetQueries({ queryKey: ['headerProfile'] });

return;
}

const jwt = response.headers.get('Authorization');

if (jwt) {
localStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, jwt);
queryClient.resetQueries({ queryKey: ['headerProfile'] });
}
};

export const issueLoginToken = async (authCode: string) => {
const response = await fetch(`${BATON_BASE_URL}/oauth/login/github?code=${authCode}`, {
method: 'GET',
headers: {
credentials: 'include',
},
});

saveAccessToken(response);

return response;
};

export const postRefreshToken = async () => {
const response = await fetch(`${BATON_BASE_URL}/oauth/refresh`, {
method: 'POST',
headers: {
Authorization: `Bearer ${getAccessToken()}`,
credentials: 'include',
},
});

saveAccessToken(response);

return response;
};

export const patchRefreshToken = async () => {
const response = await fetch(`${BATON_BASE_URL}/oauth/logout`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
});

return response;
};

export const checkLoginToken = async () => {
const jwt = getAccessToken();

if (!jwt || getRestMinute(jwt) > 2) return;

return await postRefreshToken();
};
12 changes: 3 additions & 9 deletions frontend/src/apis/error.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ACCESS_TOKEN_LOCAL_STORAGE_KEY } from '@/constants';
import { ERROR_DESCRIPTION, ERROR_TITLE } from '@/constants/message';
import { APIError, CustomApiError } from '@/types/error';

const removeAccessToken = () => localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY);
import { logout } from './auth';

export const validateResponse = async (response: Response) => {
if (response.ok) return;
Expand All @@ -11,13 +9,9 @@ export const validateResponse = async (response: Response) => {
const apiError: APIError = await response.json();

if (apiError.errorCode.includes('JW') || apiError.errorCode.includes('OA')) {
removeAccessToken();
logout();

const authErrorCode = ['JW007', 'JW008', 'JW009', 'JW010'];
throw new CustomApiError(
ERROR_TITLE.NO_PERMISSION,
authErrorCode.includes(apiError.errorCode) ? ERROR_DESCRIPTION.TOKEN_EXPIRATION : ERROR_DESCRIPTION.NO_TOKEN,
);
throw new CustomApiError(ERROR_TITLE.NO_PERMISSION, ERROR_DESCRIPTION.NO_TOKEN);
}
} catch (error) {
throw new CustomApiError(ERROR_TITLE.REQUEST, `${response.status}: ` + ERROR_DESCRIPTION.UNEXPECTED);
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/apis/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ACCESS_TOKEN_LOCAL_STORAGE_KEY, BATON_BASE_URL } from '@/constants';
import { BATON_BASE_URL } from '@/constants';
import { Method } from '@/types/api';
import { throwErrorBadRequest, validateResponse } from './error';

const getAccessToken = () => localStorage.getItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY);
import { checkLoginToken, getAccessToken } from './auth';

const parseJson = async (response: Response): Promise<any> => {
await validateResponse(response);
Expand All @@ -20,7 +19,9 @@ const fetchJson = async <T>(url: string, options?: RequestInit): Promise<T> => {
.then(async (response) => parseJson(response));
};

const fetchApi = <T>(url: string, method: Method, isAuth: boolean, body?: BodyInit) => {
const fetchApi = async <T>(url: string, method: Method, isAuth: boolean, body?: BodyInit) => {
if (isAuth) await checkLoginToken();

return fetchJson<T>(url, {
method,
...(isAuth && {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/ProfileDropdown/ProfileDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import React from 'react';
import styled from 'styled-components';
import MyPageIcon from '@/assets/my-page-icon.svg';
import LogoutIcon from '@/assets/logout-icon.svg';
import { useLogin } from '@/hooks/useLogin';
import { usePageRouter } from '@/hooks/usePageRouter';
import { logout } from '@/apis/auth';

const ProfileDropdown = () => {
const { logout } = useLogin();
const { goToMainPage, goToRunnerMyPage, goToSupporterMyPage } = usePageRouter();

const handleClickRunnerMyPage = () => {
Expand All @@ -21,7 +20,6 @@ const ProfileDropdown = () => {
logout();

goToMainPage();
window.location.reload();
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { Candidate } from '@/types/supporterCandidate';
import React, { useContext, useEffect, useState } from 'react';
import { styled } from 'styled-components';
import SupporterCardItem from '../SupporterCardItem/SupporterCardItem';
import { useParams } from 'react-router-dom';
import { ToastContext } from '@/contexts/ToastContext';
import { useLogin } from '@/hooks/useLogin';
import { usePageRouter } from '@/hooks/usePageRouter';
import { ERROR_DESCRIPTION, ERROR_TITLE } from '@/constants/message';
import { useProposedSupporterList } from '@/hooks/query/useProposedSupporterList';
import { isLogin } from '@/apis/auth';

const SupporterCardList = () => {
const { runnerPostId } = useParams();

const { isLogin } = useLogin();
const { showErrorToast } = useContext(ToastContext);
const { goToLoginPage } = usePageRouter();

const { data: supporterList } = useProposedSupporterList(Number(runnerPostId));

useEffect(() => {
if (!isLogin) {
if (!isLogin()) {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: ERROR_DESCRIPTION.NO_TOKEN });
goToLoginPage();

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/hooks/query/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
retry: 1,
},
mutations: {
retry: 1,
},
},
});
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useFeedbackToSupporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ export const useFeedbackToSupporter = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '피드백 작성에 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/hooks/query/useHeaderProfile.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { getHeaderProfile } from '@/apis/apis';
import { ERROR_DESCRIPTION, ERROR_TITLE } from '@/constants/message';
import { ToastContext } from '@/contexts/ToastContext';
import { useQuery } from '@tanstack/react-query';
import { useContext, useEffect } from 'react';
import { usePageRouter } from '../usePageRouter';
import { isLogin } from '@/apis/auth';
import { queryClient } from './queryClient';

export const useHeaderProfile = () => {
const { showErrorToast } = useContext(ToastContext);
const { goToLoginPage } = usePageRouter();

export const useHeaderProfile = (isLogin: boolean) => {
const queryResult = useQuery({
queryKey: ['headerProfile'],
queryFn: getHeaderProfile,
enabled: isLogin,
enabled: isLogin(),
});

useEffect(() => {
if (queryResult.error) {
showErrorToast({ title: ERROR_TITLE.EXPIRATION, description: ERROR_DESCRIPTION.TOKEN_EXPIRATION });
goToLoginPage();

queryClient.removeQueries({ queryKey: ['headerProfile'] });
}
}, [queryResult.error]);

return {
data: queryResult.data as NonNullable<typeof queryResult.data>,
};
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useMissionBranchCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export const useMissionBranchCreation = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '브랜치 생성에 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/query/useMyPostList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const useMyPostList = (isRunner: boolean, reviewStatus?: ReviewStatus) =>
initialPageParam: 1,

getNextPageParam: (nextPage) => {
if (nextPage.pageInfo.isLast) undefined;
if (nextPage.pageInfo.isLast) return undefined;

return nextPage.pageInfo.currentPage + 1;
},
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useMyRunnerProfileEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export const useRunnerProfileEdit = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '프로필 수정에 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useMySupporterProfileEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export const useRunnerProfileEdit = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '프로필 수정에 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useReviewCancelation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const useReviewCancelation = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '리뷰 취소 요청이 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useReviewComplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export const useReviewComplete = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '리뷰 완료 요청이 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/query/useReviewSuggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ export const useReviewSuggestion = () => {
onError: () => {
showErrorToast({ title: ERROR_TITLE.REQUEST, description: '리뷰 제안 요청이 실패했어요' });
},

retry: 1,
});

return queryResult;
Expand Down
Loading

0 comments on commit 7a8c087

Please sign in to comment.