Skip to content

Commit fc4c223

Browse files
authored
FIx, Feat: 로그인 기능 버그 수정, 권한별 접근 페이지 기능 추가 (#56)
* Fix: reissue 요청 수정 * Fix: 무한 response intercept 수정 - 요청 queue 포함 * feat: 권한별 접근 페이지 기능 추가 - PrivateRoute 추가하여 로그인된 사용자만 접근 가능하도록 설정 - AuthCallbackPage 임의로 접근시 home, login redirection 추가 - 로그인된 사용자는 login 접근 불가 - TODO - [ ] : intercepters.response에서 reissue 실패하는 경우 기존 api 함수에 catch로 에러 전달이 안되는 것 같음(추가로 문제 상황을 확인) --------- Co-authored-by: nirii00 <[email protected]>
1 parent bf32263 commit fc4c223

File tree

10 files changed

+232
-106
lines changed

10 files changed

+232
-106
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: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,100 @@ 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) => {
13-
const accessToken = useAuthStore((state) => state.accessToken);
14-
console.log(config.url);
15-
console.log(accessToken);
36+
const accessToken = useAuthStore.getState().accessToken;
37+
1638
if (config.url !== '/auth/reissue' && accessToken) {
1739
config.headers.Authorization = `Bearer ${accessToken}`;
18-
console.log('intercepter', config.headers);
1940
}
41+
2042
return config;
2143
},
22-
(error) => {
23-
const logout = useAuthStore((state) => state.logout);
24-
logout();
25-
window.location.replace('/login');
26-
return Promise.reject(error);
27-
},
44+
(error) => Promise.reject(error),
2845
);
2946

3047
client.interceptors.response.use(
3148
(response) => response,
3249
async (error) => {
33-
const setAccessToken = useAuthStore((state) => state.setAccessToken);
34-
const logout = useAuthStore((state) => state.logout);
35-
50+
const setAccessToken = useAuthStore.getState().setAccessToken;
51+
const logout = useAuthStore.getState().logout;
3652
const originalRequest = error.config;
3753

38-
if (!originalRequest) {
54+
if (!originalRequest) return Promise.reject(error);
55+
56+
if (
57+
originalRequest.url === '/auth/reissue' ||
58+
originalRequest.url.includes('/api/auth/token?state=')
59+
) {
3960
return Promise.reject(error);
4061
}
4162

4263
if (
43-
(error.response.status === 401 ||
44-
error.response.status === 403 ||
45-
error.response.data.message === 'Unauthorized') &&
64+
(error.response?.status === 401 || error.response?.status === 403) &&
4665
!originalRequest._retry
4766
) {
4867
originalRequest._retry = true;
4968

69+
if (isRefreshing) {
70+
try {
71+
return new Promise((resolve, reject) => {
72+
failedQueue.push({
73+
resolve: (token: string) => {
74+
originalRequest.headers.Authorization = `Bearer ${token}`;
75+
resolve(client(originalRequest));
76+
},
77+
reject: (err: unknown) => reject(err),
78+
});
79+
});
80+
} catch (e) {
81+
return Promise.reject(e);
82+
}
83+
}
84+
85+
isRefreshing = true;
86+
5087
try {
5188
const response = await getNewToken();
5289
const newToken = response?.data.accessToken;
5390

54-
if (!newToken) throw new Error('Failed to Refresh Token');
91+
if (!newToken) throw new Error('Failed to refresh token');
5592

5693
setAccessToken(newToken);
57-
originalRequest.headers.Authorization = `Bearer ${newToken}`;
94+
processQueue(null, newToken);
5895

96+
isRefreshing = false;
97+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
5998
return client(originalRequest);
6099
} catch (e) {
100+
processQueue(e, null);
101+
isRefreshing = false;
61102
logout();
62-
window.location.replace('/login');
63103
return Promise.reject(e);
64104
}
65105
}

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+
}

src/pages/Auth/index.tsx

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-unused-expressions */
21
import { useEffect } from 'react';
32
import { useNavigate } from 'react-router';
43

@@ -10,73 +9,75 @@ const AuthCallbackPage = () => {
109
const redirectURL = new URLSearchParams(window.location.search).get('redirect');
1110

1211
const login = useAuthStore((state) => state.login);
12+
const logout = useAuthStore((state) => state.logout);
1313
const setAccessToken = useAuthStore((state) => state.setAccessToken);
1414
const setZipCode = useAuthStore((state) => state.setZipCode);
15-
1615
const navigate = useNavigate();
1716

17+
const handleError = (error: unknown) => {
18+
console.error('AuthCallback Error:', error);
19+
logout();
20+
navigate('/login', { replace: true });
21+
};
22+
1823
const setUserInfo = async (stateToken: string) => {
1924
try {
2025
const response = await getUserToken(stateToken);
21-
if (!response) throw new Error('Error Fetching userInfo');
26+
console.log(response);
27+
if (!response) throw new Error('Error fetching user token');
2228

2329
const userInfo = response.data;
24-
if (userInfo) {
25-
login();
26-
userInfo.accessToken && setAccessToken(userInfo.accessToken);
27-
28-
if (redirectURL == 'home') {
29-
const zipCodeResponse = await getMydata();
30-
if (!zipCodeResponse) throw new Error('Error Fetching userInfo');
31-
const zipCode = zipCodeResponse.data.data.zipCode;
32-
zipCode && setZipCode(zipCode);
33-
34-
console.log(
35-
'isLoggedIn',
36-
useAuthStore.getState().isLoggedIn,
37-
'access',
38-
useAuthStore.getState().accessToken,
39-
'zipCode',
40-
useAuthStore.getState().zipCode,
41-
);
42-
} else if (redirectURL === 'onboarding') {
43-
const createZipCodeResponse = await postZipCode();
44-
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');
45-
const zipCode = createZipCodeResponse.data.data.zipCode;
46-
console.log(createZipCodeResponse);
47-
const newAccessToken = createZipCodeResponse.headers['Authorization'];
48-
setZipCode(zipCode);
49-
setAccessToken(newAccessToken);
50-
console.log(
51-
'isLoggedIn',
52-
useAuthStore.getState().isLoggedIn,
53-
'access',
54-
useAuthStore.getState().accessToken,
55-
'zipCode',
56-
useAuthStore.getState().zipCode,
57-
);
58-
}
59-
} else {
60-
navigate('/login');
30+
if (!userInfo) throw new Error('Invalid user info');
31+
32+
login();
33+
if (userInfo.accessToken) setAccessToken(userInfo.accessToken);
34+
35+
switch (redirectURL) {
36+
case 'home':
37+
{
38+
const zipCodeResponse = await getMydata();
39+
if (!zipCodeResponse) throw new Error('Error fetching user data');
40+
setZipCode(zipCodeResponse.data.data.zipCode);
41+
}
42+
break;
43+
44+
case 'onboarding':
45+
{
46+
const createZipCodeResponse = await postZipCode();
47+
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');
48+
49+
setZipCode(createZipCodeResponse.data.data.zipCode);
50+
const newAccessToken = createZipCodeResponse.headers['authorization']?.split(' ')[1];
51+
if (!newAccessToken) throw new Error('Missing new access token');
52+
53+
setAccessToken(newAccessToken);
54+
}
55+
break;
56+
57+
default:
58+
navigate('/notFound');
59+
return;
6160
}
61+
navigate(redirectURL === 'onboarding' ? '/onboarding' : '/');
6262
} catch (error) {
63-
console.error(error);
63+
handleError(error);
6464
}
6565
};
6666

67-
const redirection = () => {
68-
if (redirectURL === 'onboarding') navigate('/onboarding');
69-
else if (redirectURL === 'home') navigate('/');
70-
else navigate('/notFound');
71-
};
72-
7367
useEffect(() => {
74-
if (stateToken) {
75-
setUserInfo(stateToken as string);
76-
redirection();
77-
} else navigate('/notFound');
78-
}, []);
79-
return <></>;
68+
if (!stateToken) {
69+
navigate('/notFound');
70+
return;
71+
}
72+
73+
const fetchData = async () => {
74+
await setUserInfo(stateToken as string);
75+
};
76+
77+
fetchData();
78+
}, [stateToken, navigate]);
79+
80+
return null;
8081
};
8182

8283
export default AuthCallbackPage;

0 commit comments

Comments
 (0)