Skip to content

Commit bf7f2f5

Browse files
committed
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-final-project/WEB2_3_9crops_FE into 49-feat-write/randomLetter/letterDetail-nd
2 parents a78f3ee + fc4c223 commit bf7f2f5

File tree

10 files changed

+258
-130
lines changed

10 files changed

+258
-130
lines changed

src/App.tsx

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router';
33
import useViewport from './hooks/useViewport';
44
import Layout from './layouts/Layout';
55
import MobileLayout from './layouts/MobileLayout';
6+
import PrivateRoute from './layouts/PrivateRoute';
67
import AdminPage from './pages/Admin';
78
import FilteredLetterManage from './pages/Admin/FilteredLetter';
89
import FilteringManage from './pages/Admin/Filtering';
@@ -30,39 +31,44 @@ const App = () => {
3031
return (
3132
<Routes>
3233
<Route element={<MobileLayout />}>
33-
<Route index element={<Home />} />
3434
<Route path="login" element={<LoginPage />} />
3535
<Route path="landing" element={<Landing />} />
36+
<Route path="*" element={<NotFoundPage />} />
37+
<Route path="auth-callback" element={<AuthCallbackPage />} />
38+
<Route index element={<Home />} />
3639
<Route path="onboarding" element={<OnboardingPage />} />
37-
<Route path="letter">
38-
<Route element={<Layout />}>
39-
<Route path="random" element={<RandomLettersPage />} />
40-
<Route path="box" element={<LetterBoxPage />} />
41-
<Route path="box/:id" element={<LetterBoxDetailPage />} />
40+
41+
<Route element={<PrivateRoute />}>
42+
<Route path="letter">
43+
<Route element={<Layout />}>
44+
<Route path="random" element={<RandomLettersPage />} />
45+
<Route path="box" element={<LetterBoxPage />} />
46+
<Route path="box/:id" element={<LetterBoxDetailPage />} />
47+
</Route>
48+
<Route path="write" element={<WritePage />} />
49+
<Route path=":id" element={<LetterDetailPage />} />
4250
</Route>
43-
<Route path="write" element={<WritePage />} />
44-
<Route path=":id" element={<LetterDetailPage />} />
45-
</Route>
46-
<Route path="board">
47-
<Route element={<Layout />}>
48-
<Route path="rolling/:id" element={<RollingPaperPage />} />
49-
<Route path="letter" element={<LetterBoardPage />} />
51+
<Route path="board">
52+
<Route element={<Layout />}>
53+
<Route path="rolling/:id" element={<RollingPaperPage />} />
54+
<Route path="letter" element={<LetterBoardPage />} />
55+
</Route>
56+
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
57+
</Route>
58+
<Route path="mypage" element={<Layout />}>
59+
<Route index element={<MyPage />} />
60+
<Route path="board" element={<LetterBoardPage />} />
61+
<Route path="notifications" element={<NotificationsPage />} />
5062
</Route>
51-
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
52-
</Route>
53-
<Route path="mypage" element={<Layout />}>
54-
<Route index element={<MyPage />} />
55-
<Route path="board" element={<LetterBoardPage />} />
56-
<Route path="notifications" element={<NotificationsPage />} />
5763
</Route>
58-
<Route path="*" element={<NotFoundPage />} />
59-
<Route path="auth-callback" element={<AuthCallbackPage />} />
6064
</Route>
6165

62-
<Route path="admin" element={<AdminPage />}>
63-
<Route path="report" element={<ReportManage />} />
64-
<Route path="badwords" element={<FilteringManage />} />
65-
<Route path="filtered-letter" element={<FilteredLetterManage />} />
66+
<Route element={<PrivateRoute />}>
67+
<Route path="admin" element={<AdminPage />}>
68+
<Route path="report" element={<ReportManage />} />
69+
<Route path="badwords" element={<FilteringManage />} />
70+
<Route path="filtered-letter" element={<FilteredLetterManage />} />
71+
</Route>
6672
</Route>
6773
</Routes>
6874
);

src/apis/auth.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export const getUserToken = async (stateToken: string) => {
1212
if (userInfo) {
1313
return userInfo;
1414
}
15+
return response;
1516
} catch (error) {
1617
console.error(error);
18+
throw error;
1719
}
1820
};
1921

@@ -29,7 +31,7 @@ export const postZipCode = async () => {
2931

3032
export const getNewToken = async () => {
3133
try {
32-
const response = await client.get('/api/reissue', { withCredentials: true });
34+
const response = await client.post('/api/reissue', {}, { withCredentials: true });
3335
if (!response) throw new Error('getNewToken: no response data');
3436
return response;
3537
} catch (error) {
@@ -40,7 +42,7 @@ export const getNewToken = async () => {
4042
export const getMydata = async () => {
4143
try {
4244
const response = await client.get('/api/members/me');
43-
if (!response) throw new Error('getNewTOken: no response data');
45+
if (!response) throw new Error('getNewToken: no response data');
4446
return response;
4547
} catch (error) {
4648
console.error(error);

src/apis/client.ts

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,104 @@ import { getNewToken } from './auth';
66

77
const client = axios.create({
88
baseURL: import.meta.env.VITE_API_URL,
9+
headers: { 'Content-Type': 'application/json' },
910
});
1011

12+
type FailedRequest = {
13+
resolve: (token: string) => void;
14+
reject: (error: unknown) => void;
15+
};
16+
17+
let isRefreshing = false;
18+
let failedQueue: FailedRequest[] = [];
19+
20+
const processQueue = (error: unknown, token: string | null = null) => {
21+
failedQueue.forEach((prom) => {
22+
if (error) {
23+
prom.reject(error);
24+
} else {
25+
if (token) {
26+
prom.resolve(token);
27+
}
28+
}
29+
});
30+
31+
failedQueue = [];
32+
};
33+
1134
client.interceptors.request.use(
1235
(config) => {
1336
const accessToken = useAuthStore.getState().accessToken;
1437
if (config.url !== '/auth/reissue' && accessToken) {
1538
config.headers.Authorization = `Bearer ${accessToken}`;
1639
}
40+
1741
return config;
1842
},
19-
(error) => {
20-
const logout = useAuthStore((state) => state.logout);
21-
logout();
22-
window.location.replace('/login');
43+
(error) => Promise.reject(error),
44+
);
45+
46+
client.interceptors.response.use(
47+
(response) => response,
48+
async (error) => {
49+
const setAccessToken = useAuthStore.getState().setAccessToken;
50+
const logout = useAuthStore.getState().logout;
51+
const originalRequest = error.config;
52+
53+
if (!originalRequest) return Promise.reject(error);
54+
55+
if (
56+
originalRequest.url === '/auth/reissue' ||
57+
originalRequest.url.includes('/api/auth/token?state=')
58+
) {
59+
return Promise.reject(error);
60+
}
61+
62+
if (
63+
(error.response?.status === 401 || error.response?.status === 403) &&
64+
!originalRequest._retry
65+
) {
66+
originalRequest._retry = true;
67+
68+
if (isRefreshing) {
69+
try {
70+
return new Promise((resolve, reject) => {
71+
failedQueue.push({
72+
resolve: (token: string) => {
73+
originalRequest.headers.Authorization = `Bearer ${token}`;
74+
resolve(client(originalRequest));
75+
},
76+
reject: (err: unknown) => reject(err),
77+
});
78+
});
79+
} catch (e) {
80+
return Promise.reject(e);
81+
}
82+
}
83+
84+
isRefreshing = true;
85+
86+
try {
87+
const response = await getNewToken();
88+
const newToken = response?.data.accessToken;
89+
90+
if (!newToken) throw new Error('Failed to refresh token');
91+
92+
setAccessToken(newToken);
93+
processQueue(null, newToken);
94+
95+
isRefreshing = false;
96+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
97+
return client(originalRequest);
98+
} catch (e) {
99+
processQueue(e, null);
100+
isRefreshing = false;
101+
logout();
102+
return Promise.reject(e);
103+
}
104+
}
23105
return Promise.reject(error);
24106
},
25107
);
26108

27-
// client.interceptors.response.use(
28-
// (response) => response,
29-
// async (error) => {
30-
// const setAccessToken = useAuthStore.getState().setAccessToken;
31-
// const logout = useAuthStore.getState().logout;
32-
33-
// const originalRequest = error.config;
34-
35-
// if (!originalRequest) {
36-
// return Promise.reject(error);
37-
// }
38-
39-
// if (
40-
// (error.response.status === 401 ||
41-
// error.response.status === 403 ||
42-
// error.response.data.message === 'Unauthorized') &&
43-
// !originalRequest._retry
44-
// ) {
45-
// originalRequest._retry = true;
46-
47-
// try {
48-
// const response = await getNewToken();
49-
// const newToken = response?.data.accessToken;
50-
51-
// if (!newToken) throw new Error('Failed to Refresh Token');
52-
53-
// setAccessToken(newToken);
54-
// originalRequest.headers.Authorization = `Bearer ${newToken}`;
55-
56-
// return client(originalRequest);
57-
// } catch (e) {
58-
// logout();
59-
// window.location.replace('/login');
60-
// return Promise.reject(e);
61-
// }
62-
// }
63-
// return Promise.reject(error);
64-
// },
65-
// );
66-
67109
export default client;

src/layouts/PrivateRoute.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect, useState } from 'react';
2+
import { useNavigate, Outlet } from 'react-router';
3+
4+
import useAuthStore from '@/stores/authStore';
5+
6+
export default function PrivateRoute() {
7+
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
8+
const navigate = useNavigate();
9+
const [shouldRender, setShouldRender] = useState(false);
10+
11+
useEffect(() => {
12+
if (!isLoggedIn) {
13+
navigate('/login', { replace: true });
14+
} else {
15+
setShouldRender(true);
16+
}
17+
}, [isLoggedIn, navigate]);
18+
19+
if (!shouldRender) {
20+
return null;
21+
}
22+
23+
return <Outlet />;
24+
}

0 commit comments

Comments
 (0)