- Access 토큰 만료시 Refresh 토큰으로 Access 토큰 재갱신 요청 로직 구현 중
This commit is contained in:
6
src/data/response/account/RefreshAccessTokenResponse.ts
Normal file
6
src/data/response/account/RefreshAccessTokenResponse.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { BaseResponse } from "../BaseResponse";
|
||||||
|
|
||||||
|
export class RefreshAccessTokenResponse extends BaseResponse {
|
||||||
|
accessToken!: string;
|
||||||
|
refreshToken!: string;
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export default function Layout() {
|
|||||||
>
|
>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
{ authData?.isLogedIn ? <Header /> : null}
|
{ authData ? <Header /> : null}
|
||||||
{/* <Header /> */}
|
{/* <Header /> */}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import type {
|
|||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
InternalAxiosRequestConfig,
|
InternalAxiosRequestConfig,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import type { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
|
||||||
|
|
||||||
export class BaseNetwork {
|
export class BaseNetwork {
|
||||||
protected instance: AxiosInstance;
|
protected instance: AxiosInstance;
|
||||||
|
|
||||||
|
private isRefreshing = false;
|
||||||
|
private refreshQueue: Array<(token:string) => void> = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
|
baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
|
||||||
@@ -48,20 +53,86 @@ export class BaseNetwork {
|
|||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse) => response,
|
(response: AxiosResponse) => response,
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
const message =
|
const status = error.response?.status;
|
||||||
(error.response?.data as any)?.message ||
|
const errorCode = (error.response?.data as any)?.code;
|
||||||
error.message ||
|
const originalRequest = error.config as AxiosRequestConfig & {
|
||||||
"Unknown error";
|
_retry?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject({
|
if (
|
||||||
status: error.response?.status,
|
status === 401
|
||||||
message,
|
&& errorCode === 'AccessTokenExpired'
|
||||||
raw: error,
|
&& !originalRequest._retry
|
||||||
});
|
) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
return this.handleRefreshToken(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleRefreshToken(originalRequest: AxiosRequestConfig) {
|
||||||
|
|
||||||
|
const authData = useAuthStore.getState().authData;
|
||||||
|
const refreshToken = authData?.refreshToken;
|
||||||
|
|
||||||
|
if (!authData || !refreshToken) {
|
||||||
|
useAuthStore.getState().logout();
|
||||||
|
return Promise.reject("no refresh token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRefreshing) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.refreshQueue.push((newToken: string) => {
|
||||||
|
originalRequest.headers = {
|
||||||
|
...originalRequest.headers,
|
||||||
|
Authorization: `Bearer ${newToken}`
|
||||||
|
};
|
||||||
|
resolve(this.instance(originalRequest));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRefreshing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.get<RefreshAccessTokenResponse>(
|
||||||
|
'/account/refresh-access-token',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${refreshToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const newAccessToken = response.data.accessToken;
|
||||||
|
const newRefreshToken = response.data.refreshToken;
|
||||||
|
|
||||||
|
useAuthStore.getState().login({
|
||||||
|
...authData,
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refreshQueue.forEach((cb) => cb(newAccessToken));
|
||||||
|
this.refreshQueue = [];
|
||||||
|
originalRequest.headers = {
|
||||||
|
...originalRequest.headers,
|
||||||
|
Authorization: `Bearer ${newAccessToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.instance(originalRequest);
|
||||||
|
} catch (err) {
|
||||||
|
useAuthStore.getState().logout();
|
||||||
|
window.location.href = '/login?expired=1'
|
||||||
|
return Promise.reject(err);
|
||||||
|
} finally {
|
||||||
|
this.isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 기본 CRUD 메서드
|
* 기본 CRUD 메서드
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { create } from 'zustand';
|
|||||||
interface AuthStoreProps {
|
interface AuthStoreProps {
|
||||||
authData: AuthData | undefined;
|
authData: AuthData | undefined;
|
||||||
login: (data: AuthData) => void;
|
login: (data: AuthData) => void;
|
||||||
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthStoreProps>((set) => ({
|
export const useAuthStore = create<AuthStoreProps>((set) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user