Skip to content

Commit

Permalink
[Feat] token 관리 (#67)
Browse files Browse the repository at this point in the history
* feat: AuthProvider

* feat: 토큰 발급 실패 시, 팝업 알림

* feat: 로그아웃
  • Loading branch information
he2e2 authored Nov 30, 2024
1 parent e369392 commit f2c5378
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 10 deletions.
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

1 comment on commit f2c5378

@github-actions
Copy link

Choose a reason for hiding this comment

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

⚡ Lighthouse report for http://localhost:3000/

Category Score
🔴 Performance 38
🟢 Accessibility 95
🟢 Best Practices 100
🟠 SEO 83

Detailed Metrics

Metric Value
🔴 First Contentful Paint 43.7 s
🔴 Largest Contentful Paint 72.9 s
🟠 Total Blocking Time 330 ms
🟢 Cumulative Layout Shift 0
🔴 Speed Index 56.7 s

Please sign in to comment.