Skip to content

Commit

Permalink
Merge pull request #32 from Moaguide-develop/feat/token
Browse files Browse the repository at this point in the history
feat: 리프레시토큰 적용
  • Loading branch information
jiohjung98 authored Sep 25, 2024
2 parents a03b9ef + dfd4a1e commit 12384a8
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 15 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"qs": "^6.13.0",
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-cookie": "^7.2.0",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"react-query": "^3.39.3",
Expand All @@ -35,6 +36,7 @@
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/cookies": "^0.9.0",
"@types/lodash": "^4.17.7",
"@types/node": "^20",
"@types/qs": "^6.9.15",
Expand Down
4 changes: 2 additions & 2 deletions src/components/mypage/CertifyPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ const CertifyPassword = ({ setStep }: CertifyPasswordType) => {
{isValid ? (
<button
onClick={handleCheckPassword}
className={`cursor-pointer bg-gradient2 flex justify-center items-center text-white rounded-[12px] text-title2 px-5 py-[14px] w-full mt-0 sm:mt-[50px] ${
className={`cursor-pointer bg-gradient2 flex justify-center items-center text-white rounded-[12px] text-title2 px-5 py-[14px] w-full mt-0 sm:mt-[40px] ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
disabled={isSubmitting}
>
다음으로
</button>
) : (
<button className="flex justify-center items-center bg-gray100 text-gray400 rounded-[12px] text-title2 px-5 py-[14px] w-full mt-0 sm:mt-[50px] ">
<button className="flex justify-center items-center bg-gray100 text-gray400 rounded-[12px] text-title2 px-5 py-[14px] w-full mt-0 sm:mt-[40px]">
다음으로
</button>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/sign/SignLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const SignLayout = () => {
)}
</div>
<button
className="w-[320px] bg-gradient-to-r font-bold text-lg from-purple-500 to-indigo-500 text-white py-3 rounded-lg mb-4"
className="w-[320px] bg-gradient2 font-bold text-lg text-white py-3 rounded-lg mb-4"
onClick={throttledHandleLogin}
>
로그인
Expand Down
47 changes: 44 additions & 3 deletions src/service/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AuthHeaders, NicknameCheckResponse, SendCodeResponse, VerifyCodeResponse } from '@/type/auth';
import { setToken, removeToken } from '@/utils/localStorage';
import { setToken, removeToken, getToken } from '@/utils/localStorage';
import { useMemberStore } from '@/store/user.store';
import { axiosInstance, basicAxiosInstance } from './axiosInstance';
import { axiosInstance, basicAxiosInstance, refreshAxiosInstance } from './axiosInstance';

// 토큰 사용하지 않는 API 함수들
export const sendVerificationCode = async (phone: string): Promise<SendCodeResponse> => {
Expand Down Expand Up @@ -80,16 +80,19 @@ export const login = async (email: string, password: string) => {
console.log('로그인 성공:', response.data);

const token = response.headers['authorization'] || response.headers['Authorization'];
console.log(token);
const accessToken = token.replace('Bearer ', '');
setToken(accessToken);

const refreshToken = response.headers['Set-Cookie'] || response.headers['set-cookie'];

const { setMember } = useMemberStore.getState();
const userInfo = response.data.user;
setMember({
memberEmail: userInfo.email,
memberNickName: userInfo.nickname,
memberPhone: userInfo.phonenumber,
subscribe: '1개월 플랜', // 임시
subscribe: '1개월 플랜',
});

return response.data;
Expand Down Expand Up @@ -159,4 +162,42 @@ export const getUserEmail = async (token: string) => {
console.error('이메일 정보 요청 실패:', error);
throw error;
}
};

export const refreshAccessToken = async () => {
try {
const response = await refreshAxiosInstance.post('/token/refresh', null);

const newToken = response.headers['Authorization'] || response.headers['authorization'];

if (newToken) {
const accessToken = newToken.replace('Bearer ', '');
setToken(accessToken);
return accessToken;
} else {
throw new Error('새로운 액세스 토큰을 받지 못했습니다.');
}
} catch (error) {
console.error('리프레시 토큰 요청 오류:', error);
removeToken();
throw error;
}
};

export const deleteUser = async () => {
try {
const response = await axiosInstance.delete('/user/Withdrawal');

if (response.status === 200) {
console.log(response.data.message);
removeToken();

const { clearMember } = useMemberStore.getState();
clearMember();
} else {
console.error('회원탈퇴 실패', response.status);
}
} catch (error) {
console.error('회원탈퇴 오류:', error);
}
};
57 changes: 49 additions & 8 deletions src/service/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// src/api/axiosInstance.ts

import axios from 'axios';
import { getToken } from '@/utils/localStorage';
import { getToken, setToken, removeToken } from '@/utils/localStorage';
import { refreshAccessToken } from './auth';


const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;

Expand All @@ -11,6 +11,19 @@ export const axiosInstance = axios.create({
withCredentials: true,
});

// 토큰을 사용하지 않는 Axios 인스턴스
export const basicAxiosInstance = axios.create({
baseURL: backendUrl,
withCredentials: true,
});

// 리프레시 토큰 요청을 위한 Axios 인스턴스
export const refreshAxiosInstance = axios.create({
baseURL: backendUrl,
withCredentials: true,
});


axiosInstance.interceptors.request.use(
(config) => {
const token = getToken();
Expand All @@ -22,8 +35,36 @@ axiosInstance.interceptors.request.use(
(error) => Promise.reject(error)
);

// 토큰을 사용하지 않는 Axios 인스턴스
export const basicAxiosInstance = axios.create({
baseURL: backendUrl,
withCredentials: true,
});

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;

// 토큰이 만료되어 401 에러가 발생했을 경우
if (error.response && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 중복 요청 방지 플래그 설정

try {
// 리프레시 토큰으로 새 액세스 토큰 발급
const newToken = await refreshAccessToken();
if (newToken) {
// 새 토큰으로 헤더를 갱신한 후, 실패한 요청 재시도
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return axiosInstance(originalRequest);
}
} catch (refreshError) {
console.error('리프레시 토큰 갱신 오류:', refreshError);

// 리프레시 토큰 갱신 실패 시 로그아웃 처리 또는 에러 핸들링
removeToken(); // 만료된 토큰 제거
window.location.href = '/sign'; // 로그인 페이지로 리디렉션
return Promise.reject(refreshError); // 무한 루프 방지
}
}

return Promise.reject(error);
}
);
19 changes: 19 additions & 0 deletions src/type/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface CookieGetOptions {
doNotParse?: boolean;
doNotUpdate?: boolean;
}
export interface CookieSetOptions {
path?: string;
expires?: Date;
maxAge?: number;
domain?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: boolean | 'none' | 'lax' | 'strict';
}
export interface CookieChangeOptions {
name: string;
value?: any;
options?: CookieSetOptions;
}

17 changes: 17 additions & 0 deletions src/utils/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Cookies } from 'react-cookie';

import { CookieGetOptions, CookieSetOptions } from '@/type/cookies';

const cookies = new Cookies();

export const setCookie = (name: string, value: string, options?: CookieSetOptions) => {
return cookies.set(name, value, { ...options });
};

export const getCookie = (name: string, options?: CookieGetOptions) => {
return cookies.get(name, options);
};

export const removeCookie = (name: string, options?: CookieSetOptions) => {
return cookies.remove(name, options);
};
Loading

0 comments on commit 12384a8

Please sign in to comment.