Skip to content
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
56 changes: 31 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router';
import useViewport from './hooks/useViewport';
import Layout from './layouts/Layout';
import MobileLayout from './layouts/MobileLayout';
import PrivateRoute from './layouts/PrivateRoute';
import AdminPage from './pages/Admin';
import FilteredLetterManage from './pages/Admin/FilteredLetter';
import FilteringManage from './pages/Admin/Filtering';
Expand Down Expand Up @@ -30,39 +31,44 @@ const App = () => {
return (
<Routes>
<Route element={<MobileLayout />}>
<Route index element={<Home />} />
<Route path="login" element={<LoginPage />} />
<Route path="landing" element={<Landing />} />
<Route path="*" element={<NotFoundPage />} />
<Route path="auth-callback" element={<AuthCallbackPage />} />
<Route index element={<Home />} />
<Route path="onboarding" element={<OnboardingPage />} />
<Route path="letter">
<Route element={<Layout />}>
<Route path="random" element={<RandomLettersPage />} />
<Route path="box" element={<LetterBoxPage />} />
<Route path="box/:id" element={<LetterBoxDetailPage />} />

<Route element={<PrivateRoute />}>
<Route path="letter">
<Route element={<Layout />}>
<Route path="random" element={<RandomLettersPage />} />
<Route path="box" element={<LetterBoxPage />} />
<Route path="box/:id" element={<LetterBoxDetailPage />} />
</Route>
<Route path="write" element={<WritePage />} />
<Route path=":id" element={<LetterDetailPage />} />
</Route>
<Route path="write" element={<WritePage />} />
<Route path=":id" element={<LetterDetailPage />} />
</Route>
<Route path="board">
<Route element={<Layout />}>
<Route path="rolling/:id" element={<RollingPaperPage />} />
<Route path="letter" element={<LetterBoardPage />} />
<Route path="board">
<Route element={<Layout />}>
<Route path="rolling/:id" element={<RollingPaperPage />} />
<Route path="letter" element={<LetterBoardPage />} />
</Route>
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
</Route>
<Route path="mypage" element={<Layout />}>
<Route index element={<MyPage />} />
<Route path="board" element={<LetterBoardPage />} />
<Route path="notifications" element={<NotificationsPage />} />
</Route>
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
</Route>
<Route path="mypage" element={<Layout />}>
<Route index element={<MyPage />} />
<Route path="board" element={<LetterBoardPage />} />
<Route path="notifications" element={<NotificationsPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
<Route path="auth-callback" element={<AuthCallbackPage />} />
</Route>

<Route path="admin" element={<AdminPage />}>
<Route path="report" element={<ReportManage />} />
<Route path="badwords" element={<FilteringManage />} />
<Route path="filtered-letter" element={<FilteredLetterManage />} />
<Route element={<PrivateRoute />}>
<Route path="admin" element={<AdminPage />}>
<Route path="report" element={<ReportManage />} />
<Route path="badwords" element={<FilteringManage />} />
<Route path="filtered-letter" element={<FilteredLetterManage />} />
</Route>
</Route>
</Routes>
);
Expand Down
6 changes: 4 additions & 2 deletions src/apis/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export const getUserToken = async (stateToken: string) => {
if (userInfo) {
return userInfo;
}
return response;
} catch (error) {
console.error(error);
throw error;
}
};

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

export const getNewToken = async () => {
try {
const response = await client.get('/api/reissue', { withCredentials: true });
const response = await client.post('/api/reissue', {}, { withCredentials: true });
if (!response) throw new Error('getNewToken: no response data');
return response;
} catch (error) {
Expand All @@ -40,7 +42,7 @@ export const getNewToken = async () => {
export const getMydata = async () => {
try {
const response = await client.get('/api/members/me');
if (!response) throw new Error('getNewTOken: no response data');
if (!response) throw new Error('getNewToken: no response data');
return response;
} catch (error) {
console.error(error);
Expand Down
80 changes: 60 additions & 20 deletions src/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,100 @@ import { getNewToken } from './auth';

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

type FailedRequest = {
resolve: (token: string) => void;
reject: (error: unknown) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error타입으로 수정하셔도 좋을거 같습니다!!

};

let isRefreshing = false;
let failedQueue: FailedRequest[] = [];

const processQueue = (error: unknown, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
if (token) {
prom.resolve(token);
}
}
});

failedQueue = [];
};

client.interceptors.request.use(
(config) => {
const accessToken = useAuthStore((state) => state.accessToken);
console.log(config.url);
console.log(accessToken);
const accessToken = useAuthStore.getState().accessToken;

if (config.url !== '/auth/reissue' && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
console.log('intercepter', config.headers);
}

return config;
},
(error) => {
const logout = useAuthStore((state) => state.logout);
logout();
window.location.replace('/login');
return Promise.reject(error);
},
(error) => Promise.reject(error),
);

client.interceptors.response.use(
(response) => response,
async (error) => {
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const logout = useAuthStore((state) => state.logout);

const setAccessToken = useAuthStore.getState().setAccessToken;
const logout = useAuthStore.getState().logout;
const originalRequest = error.config;

if (!originalRequest) {
if (!originalRequest) return Promise.reject(error);

if (
originalRequest.url === '/auth/reissue' ||
originalRequest.url.includes('/api/auth/token?state=')
) {
return Promise.reject(error);
}

if (
(error.response.status === 401 ||
error.response.status === 403 ||
error.response.data.message === 'Unauthorized') &&
(error.response?.status === 401 || error.response?.status === 403) &&
!originalRequest._retry
) {
originalRequest._retry = true;

if (isRefreshing) {
try {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(client(originalRequest));
},
reject: (err: unknown) => reject(err),
});
});
} catch (e) {
return Promise.reject(e);
}
}

isRefreshing = true;

try {
const response = await getNewToken();
const newToken = response?.data.accessToken;

if (!newToken) throw new Error('Failed to Refresh Token');
if (!newToken) throw new Error('Failed to refresh token');

setAccessToken(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
processQueue(null, newToken);

isRefreshing = false;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
processQueue(e, null);
isRefreshing = false;
logout();
window.location.replace('/login');
return Promise.reject(e);
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/layouts/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import { useNavigate, Outlet } from 'react-router';

import useAuthStore from '@/stores/authStore';

export default function PrivateRoute() {
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const navigate = useNavigate();
const [shouldRender, setShouldRender] = useState(false);

useEffect(() => {
if (!isLoggedIn) {
navigate('/login', { replace: true });
} else {
setShouldRender(true);
}
}, [isLoggedIn, navigate]);

if (!shouldRender) {
return null;
}

return <Outlet />;
}
107 changes: 54 additions & 53 deletions src/pages/Auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { useEffect } from 'react';
import { useNavigate } from 'react-router';

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

const login = useAuthStore((state) => state.login);
const logout = useAuthStore((state) => state.logout);
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const setZipCode = useAuthStore((state) => state.setZipCode);

const navigate = useNavigate();

const handleError = (error: unknown) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error 타입으로도 지정할 수 있다고 하네용22

console.error('AuthCallback Error:', error);
logout();
navigate('/login', { replace: true });
};

const setUserInfo = async (stateToken: string) => {
try {
const response = await getUserToken(stateToken);
if (!response) throw new Error('Error Fetching userInfo');
console.log(response);
if (!response) throw new Error('Error fetching user token');

const userInfo = response.data;
if (userInfo) {
login();
userInfo.accessToken && setAccessToken(userInfo.accessToken);

if (redirectURL == 'home') {
const zipCodeResponse = await getMydata();
if (!zipCodeResponse) throw new Error('Error Fetching userInfo');
const zipCode = zipCodeResponse.data.data.zipCode;
zipCode && setZipCode(zipCode);

console.log(
'isLoggedIn',
useAuthStore.getState().isLoggedIn,
'access',
useAuthStore.getState().accessToken,
'zipCode',
useAuthStore.getState().zipCode,
);
} else if (redirectURL === 'onboarding') {
const createZipCodeResponse = await postZipCode();
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');
const zipCode = createZipCodeResponse.data.data.zipCode;
console.log(createZipCodeResponse);
const newAccessToken = createZipCodeResponse.headers['Authorization'];
setZipCode(zipCode);
setAccessToken(newAccessToken);
console.log(
'isLoggedIn',
useAuthStore.getState().isLoggedIn,
'access',
useAuthStore.getState().accessToken,
'zipCode',
useAuthStore.getState().zipCode,
);
}
} else {
navigate('/login');
if (!userInfo) throw new Error('Invalid user info');

login();
if (userInfo.accessToken) setAccessToken(userInfo.accessToken);

switch (redirectURL) {
case 'home':
{
const zipCodeResponse = await getMydata();
if (!zipCodeResponse) throw new Error('Error fetching user data');
setZipCode(zipCodeResponse.data.data.zipCode);
}
break;

case 'onboarding':
{
const createZipCodeResponse = await postZipCode();
if (!createZipCodeResponse) throw new Error('Error creating ZipCode');

setZipCode(createZipCodeResponse.data.data.zipCode);
const newAccessToken = createZipCodeResponse.headers['authorization']?.split(' ')[1];
if (!newAccessToken) throw new Error('Missing new access token');

setAccessToken(newAccessToken);
}
break;

default:
navigate('/notFound');
return;
}
navigate(redirectURL === 'onboarding' ? '/onboarding' : '/');
} catch (error) {
console.error(error);
handleError(error);
}
};

const redirection = () => {
if (redirectURL === 'onboarding') navigate('/onboarding');
else if (redirectURL === 'home') navigate('/');
else navigate('/notFound');
};

useEffect(() => {
if (stateToken) {
setUserInfo(stateToken as string);
redirection();
} else navigate('/notFound');
}, []);
return <></>;
if (!stateToken) {
navigate('/notFound');
return;
}

const fetchData = async () => {
await setUserInfo(stateToken as string);
};

fetchData();
}, [stateToken, navigate]);

return null;
};

export default AuthCallbackPage;
Loading