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

[Feat] token 관리 #67

Merged
merged 3 commits into from
Nov 30, 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
13 changes: 8 additions & 5 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { RouterProvider } from 'react-router-dom';

import AppRouter from './appRouter';
import AuthProvider from './AuthProvider';
import ModalProvider from './ModalProvider';
import QueryProvider from './QueryProvider';

function App() {
return (
<QueryProvider>
<ModalProvider>
<RouterProvider router={AppRouter()} />
</ModalProvider>
</QueryProvider>
<AuthProvider>
<QueryProvider>
<ModalProvider>
<RouterProvider router={AppRouter()} />
</ModalProvider>
</QueryProvider>
</AuthProvider>
);
}

Expand Down
83 changes: 83 additions & 0 deletions src/app/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import axios from 'axios';
import type { ReactNode } from 'react';
import { createContext, useContext, useRef, useState } from 'react';

import api from '@/shared/api/baseApi';
import { customConfirm } from '@/shared/ui';

interface AuthContextType {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
reissueToken: () => Promise<void>;
logout: () => Promise<void>;
removeToken: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

const AuthProvider = ({ children }: { children: ReactNode }) => {
const [accessToken, setAccessToken] = useState<string | null>(
localStorage.getItem('accessToken'),
);
const isRefreshing = useRef(false);

const reissueToken = async () => {
if (isRefreshing.current) return;

isRefreshing.current = true;

try {
const response = await axios.post('/token/reissue', {}, { withCredentials: true });

const newAccessToken = (response.headers['authorization'] as string)?.split(' ')[1];

if (newAccessToken) {
setAccessToken(newAccessToken);
localStorage.setItem('accessToken', newAccessToken);
} else {
throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.');
}
} catch (error) {
console.error('토큰 재발급 실패:', error);
removeToken();
} finally {
// eslint-disable-next-line require-atomic-updates
isRefreshing.current = false;
}
};

const removeToken = () => {
setAccessToken(null);
localStorage.removeItem('accessToken');
customConfirm({ title: '로그인', text: '로그인 페이지로 이동합니다.', icon: 'info' });
};

const logout = async () => {
try {
await api.post('/user/logout');

setAccessToken(null);
localStorage.removeItem('accessToken');
} catch (error) {
console.error('로그아웃 실패:', error);
}
};

return (
<AuthContext.Provider
value={{ accessToken, setAccessToken, reissueToken, logout, removeToken }}
>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('AuthProvider Error');
}
return context;
};

export default AuthProvider;
33 changes: 28 additions & 5 deletions src/shared/api/baseApi.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import axios, { isAxiosError } from 'axios';
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';

import { useAuth } from '@/app/AuthProvider';

const api = axios.create({
baseURL: ``, // Backend API Domain 주소 넣기
baseURL: ``, // Backend API Domain 주소
timeout: 10_000,
withCredentials: true,
});

api.interceptors.request.use(config => {
const { accessToken } = useAuth();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});

api.interceptors.response.use(
response => response,
async error => {
if (isAxiosError(error)) {
return Promise.reject(error);
const { reissueToken } = useAuth();

if (error.response?.status === 401) {
try {
await reissueToken();
const { accessToken } = useAuth();

if (accessToken && error.config) {
error.config.headers.Authorization = `Bearer ${accessToken}`;
return api.request(error.config as AxiosRequestConfig);
}
} catch (reissueError) {
console.error('토큰 재발급 실패:', reissueError);
}
}

return Promise.reject(new Error('알 수 없는 에러 발생'));
return Promise.reject(error instanceof Error ? error : new Error('Unknown Error'));
},
);

Expand Down