Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

페이지 기능: 로그인 이후 토큰 인증에 따라 로그인 만료, 로그인 연장 처리 #182

Merged
merged 13 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ModalContextProvider } from '@contexts/ModalContext';
import { CookieProvider } from '@providers/CookieProvider';
import MockProvider from '@providers/MockProvider';
import QueryProvider from '@providers/QueryProvider';
import RefreshTokenProvider from '@providers/RefreshTokenProvider';
import StoreProvider from '@providers/StoreProvider';
import ToastProvider from '@providers/ToastProvider';

Expand Down Expand Up @@ -42,7 +43,9 @@ export default function RootLayout({
<StoreProvider>
<ToastProvider>
<ModalContextProvider>
{children}
<RefreshTokenProvider>
{children}
</RefreshTokenProvider>
</ModalContextProvider>
</ToastProvider>
</StoreProvider>
Expand Down
15 changes: 4 additions & 11 deletions src/app/my-page/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,27 @@
import { useEffect, useState } from 'react';
import { useCookies } from 'react-cookie';

import { useQueryClient } from '@tanstack/react-query';
import classNames from 'classnames/bind';
import Link from 'next/link';
import { useRouter } from 'next/navigation';

import LinkArrow from '@components/icons/LinkArrow';
import { REQUIRED_LOGIN } from '@constants/requiredUser';
import useLoggedOut from '@hooks/useLoggedOut';
import BottomNav from '@shared/bottom-nav/BottomNav';
import Confirmation from '@shared/confirmation/Confirmation';
import Spacing from '@shared/spacing/Spacing';
import Text from '@shared/text/Text';
import Title from '@shared/title/Title';
import { useAppSelector, useAppDispatch } from '@stores/hooks';
import { clearUserId } from '@stores/slices/userSlice';
import { useAppSelector } from '@stores/hooks';

import styles from './page.module.scss';

const cx = classNames.bind(styles);

function MyProfilePage() {
const router = useRouter();
const dispatch = useAppDispatch();
const query = useQueryClient();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [cookies, removeCookie] = useCookies(['token']);
const logout = useLoggedOut();

// eslint-disable-next-line max-len
const userId = useAppSelector((state) => { return state.user.id; }, (prev, curr) => { return prev === curr; });
Expand All @@ -41,10 +37,7 @@ function MyProfilePage() {
// 로그아웃
const handleLoggedOut = () => {
// TODO: 먼저 로그아웃 모달이 뜨도록 할지 논의필요
dispatch(clearUserId());
removeCookie('token', { path: '/' });
query.clear();
router.push('/');
logout();
};

const topMargin = 96;
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useIntervalRefreshToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import useRefreshToken from '@remote/queries/auth/useRefreshToken';

const JWT_EXPIRY_TIME = 14 * 60 * 1000;

function useIntervalRefreshToken() {
const { mutate: refresh } = useRefreshToken();
let refreshTokenInterval: NodeJS.Timeout;

const startRefreshTokenInterval = () => {
refreshTokenInterval = setInterval(() => {
refresh();
}, JWT_EXPIRY_TIME);
};

const refreshTokenClear = () => {
clearInterval(refreshTokenInterval);
};

return { startRefreshTokenInterval, refreshTokenClear };
}

export default useIntervalRefreshToken;
26 changes: 26 additions & 0 deletions src/hooks/useLoggedOut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCookies } from 'react-cookie';

import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';

import { useAppDispatch } from '@stores/hooks';
import { clearUserId } from '@stores/slices/userSlice';

function useLoggedOut() {
const router = useRouter();
const dispatch = useAppDispatch();
const query = useQueryClient();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [cookies, removeCookie] = useCookies(['token']);

const logout = (redirectPath = '/') => {
dispatch(clearUserId());
removeCookie('token', { path: '/' });
query.clear();
router.push(redirectPath);
};

return logout;
}

export default useLoggedOut;
15 changes: 14 additions & 1 deletion src/mocks/authHandler/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { http, HttpResponse } from 'msw';

import { MOCK_LOGIN_DATA, MOCK_TOKEN_DATA } from './mocks';

export const authHandlers = [
/* ----- 회원가입 api ----- */
http.post(`${process.env.NEXT_PUBLIC_BASE_URL}/member/join`, () => {
Expand All @@ -9,6 +11,17 @@ export const authHandlers = [

/* ----- 로그인 api ----- */
http.post(`${process.env.NEXT_PUBLIC_BASE_URL}/member/login`, () => {
return HttpResponse.json('로그인 성공!!');
return HttpResponse.json(MOCK_LOGIN_DATA);
}),

/* ----- refresh token api success ----- */
http.post(`${process.env.NEXT_PUBLIC_BASE_URL}/auth/validToken`, () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 두개로 나누지 않고 조건문으로 분기 처리할 수 있지 않나요?

return HttpResponse.json(MOCK_TOKEN_DATA);
}),

/* ----- refresh token api error ----- */
http.post(`${process.env.NEXT_PUBLIC_BASE_URL}/auth/validToken`, () => {
return HttpResponse.json(Error);
}),

];
29 changes: 29 additions & 0 deletions src/mocks/authHandler/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* ----- 로그인 MOCK DATA ----- */
export const MOCK_LOGIN_DATA = {
status: 200,
code: 'BMC002',
message: '로그인 성공',
value: {
memberNo: 7251,
id: 'stest0123',
email: '[email protected]',
password: null,
gender: 'woman',
age: '30',
createdAt: '2024-02-19',
createdBy: 'admin',
modifiedAt: '2024-02-19',
modifiedBy: 'admin',
jwtToken: 'test-abcdefg1234--abc',
},
};

/* ----- 토큰 MOCK DATA ----- */
export const MOCK_TOKEN_DATA = {
status: 202,
code: 'success',
message: '토큰 인증 성공',
value: {
jwtToken: 'success-test-abcdefg1234--abc',
},
};
32 changes: 32 additions & 0 deletions src/providers/RefreshTokenProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react/jsx-no-useless-fragment */
/* eslint-disable no-console */

'use client';

import { useEffect } from 'react';

import useIntervalRefreshToken from '@hooks/useIntervalRefreshToken';
import { useAppSelector } from '@stores/hooks';

function RefreshTokenProvider({ children }: { children: React.ReactNode }) {
const { startRefreshTokenInterval, refreshTokenClear } = useIntervalRefreshToken();
const userId = useAppSelector((state) => { return state.user.id; });

useEffect(() => {
if (userId !== null) {
startRefreshTokenInterval();
Copy link
Collaborator

@bottlewook bottlewook Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 가지고 있던 토큰을 이용해서 새로운 토큰을 발급 받을 예정이신거죠?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 지용님이랑 얘기해봤는데 헤더 Authentication에 담아 보내주면 body에 새로운 토큰을 넣어서 보내주신다고 하십니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 알겠습니다!

console.log('RefreshTokenProvider mounted');
}

// cleanup 함수를 이용하여 컴포넌트가 unmount 될 때 clearInterval 호출
return () => {
refreshTokenClear();
console.log('RefreshTokenProvider unmounted');
};
}, [userId]);

return <>{children}</>;
}

export default RefreshTokenProvider;
15 changes: 9 additions & 6 deletions src/remote/api/common.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable no-param-reassign */
import { useCookies } from 'react-cookie';

import axios, { InternalAxiosRequestConfig, AxiosError, AxiosResponse } from 'axios';

import { axiosInstance } from '@remote/api/instance.api';
Expand All @@ -10,13 +12,14 @@ axiosInstance.interceptors.request.use(
* request 직전 공통으로 진행할 작업
*/
if (config && config.headers) {
// TODO: 인증할 때 받은 토큰을 쿠키에 저장했다면 가져옵니다.
// 인증할 때 받은 토큰을 쿠키에 저장했다면 가져옵니다.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [cookies, setCookie] = useCookies(['token']);

// const token = getCookie('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// config.headers['Content-Type'] = 'application/json';
// }
if (cookies.token) {
config.headers.Authorization = `Bearer ${cookies.token}`;
config.headers['Content-Type'] = 'application/json';
}
}
if (process.env.NODE_ENV === 'development') {
const { method, url } = config;
Expand Down
8 changes: 7 additions & 1 deletion src/remote/api/requests/auth/auth.post.api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
ChangePassword,
FindId, FindPassword, ISignIn, ISignUp, UserInfoType,
FindId, FindPassword, RefreshTokenType, ISignIn, ISignUp, UserInfoType,
} from '../../types/auth';
import { ICommon } from '../../types/common';
import { postRequest, putRequest } from '../requests.api';
Expand All @@ -25,6 +25,12 @@ export const login = async ({
return response;
};

export const refreshToken = async () => {
const response = await postRequest<RefreshTokenType, null>('/auth/validToken');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 api는 지용님이 만들어주실 예정인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 그러실 것 같아요. 개발 예정이라고 하셨습니다!


return response;
};

export const findId = async ({
email,
}: FindId) => {
Expand Down
2 changes: 1 addition & 1 deletion src/remote/api/requests/requests.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getRequest = async <T>(
/* post 요청 */
export const postRequest = async <T, D>(
url: string,
data: D,
data?: D,
config?: AxiosRequestConfig,
): Promise<T> => {
const response = await axiosInstance.post<T>(
Expand Down
7 changes: 7 additions & 0 deletions src/remote/api/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ export interface IUserInfo {
}

export type UserInfoType = ICommon<IUserInfo>;

// refreshToken res
export interface IRefreshToken {
jwtToken: string
}

export type RefreshTokenType = ICommon<IRefreshToken>;
33 changes: 33 additions & 0 deletions src/remote/queries/auth/useRefreshToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable no-console */
import { useCookies } from 'react-cookie';

import { useMutation } from '@tanstack/react-query';

import useLoggedOut from '@hooks/useLoggedOut';
import { refreshToken } from '@remote/api/requests/auth/auth.post.api';
import { RefreshTokenType } from '@remote/api/types/auth';

function useRefreshToken() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [cookies, setCookie, removeCookie] = useCookies(['token']);
const handleLogout = useLoggedOut();

const onSuccess = (data: RefreshTokenType) => {
const { jwtToken } = data.value;
const cookieOptions = { path: '/', maxAge: 60 * 15 };

setCookie('token', jwtToken, cookieOptions);
};

const onError = () => {
handleLogout('/login');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 로그인 해주세요 표시를 토스트로 보여줘도 될 것 같네요
하나 만들겠습니다!

};

return useMutation({
mutationFn: refreshToken,
onSuccess,
onError,
});
}

export default useRefreshToken;
Loading