From 108e2a87463cf5737acf9aa40a1158bb3c4cfae3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 12 Mar 2025 13:29:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20refresh=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/api/proxy/[...url]/route.ts | 37 +++++-- apps/cs/src/app/index.tsx | 6 +- .../entities/auth/login/api/login-service.ts | 7 +- .../entities/auth/login/lib/token-manager.ts | 4 +- apps/cs/src/shared/provider/auth-provider.tsx | 97 +++++++++++++++++++ .../provider/{cookie => }/cookie-provider.tsx | 2 +- apps/cs/src/shared/provider/index.ts | 9 +- .../provider/{query => }/query-provider.tsx | 0 packages/common/network/client.ts | 16 ++- 9 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 apps/cs/src/shared/provider/auth-provider.tsx rename apps/cs/src/shared/provider/{cookie => }/cookie-provider.tsx (92%) rename apps/cs/src/shared/provider/{query => }/query-provider.tsx (100%) diff --git a/apps/cs/app/api/proxy/[...url]/route.ts b/apps/cs/app/api/proxy/[...url]/route.ts index 2f1a9140f..5397e5fe6 100644 --- a/apps/cs/app/api/proxy/[...url]/route.ts +++ b/apps/cs/app/api/proxy/[...url]/route.ts @@ -9,21 +9,21 @@ import { import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -export async function POST( +async function handler( request: Request, - props: { params: Promise<{ url: string[] }> }, + params: { url: string[] }, + method: "GET" | "POST" | "PUT" | "DELETE", ) { - const params = await props.params; - + const cookieStore = await cookies(); const headers: Record = { "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; - - const cookieStore = await cookies(); + let body = {}; + const isRefreshHandler = params.url.includes("refresh"); // silent refresh - if (params.url.includes("refresh")) { + if (isRefreshHandler) { const refreshToken = cookieStore.get(REFRESH_TOKEN); headers[CLAIM_NAME] = refreshToken?.value ?? ""; @@ -33,9 +33,12 @@ export async function POST( } try { - const body = await request.json(); + if (!isRefreshHandler) { + body = await request.json(); + } + const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, { - method: "POST", + method, headers, body: JSON.stringify(body), }); @@ -79,3 +82,19 @@ export async function POST( ); } } + +export async function POST( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "POST"); +} + +export async function PUT( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "PUT"); +} diff --git a/apps/cs/src/app/index.tsx b/apps/cs/src/app/index.tsx index 4600d03c2..672023d15 100644 --- a/apps/cs/src/app/index.tsx +++ b/apps/cs/src/app/index.tsx @@ -1,13 +1,15 @@ import "./styles/base.css"; import "./styles/components.css"; -import { CookieProvider, QueryProvider } from "@/shared/provider"; +import { AuthProvider, CookieProvider, QueryProvider } from "@/shared/provider"; const App = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( - {children} + + {children} + diff --git a/apps/cs/src/entities/auth/login/api/login-service.ts b/apps/cs/src/entities/auth/login/api/login-service.ts index e6db440cf..722470184 100644 --- a/apps/cs/src/entities/auth/login/api/login-service.ts +++ b/apps/cs/src/entities/auth/login/api/login-service.ts @@ -31,7 +31,7 @@ export class LoginApiService implements LoginService { const tokenId = data[AUTH_USER_ID]; if (!accessToken || !tokenId) { - throw new Error("Missing required authentication tokens"); + throw new Error("[login] Missing required authentication tokens"); } tokenManager.setToken(CLAIM_NAME, accessToken); @@ -62,13 +62,16 @@ export class LoginApiService implements LoginService { const tokenId = data[AUTH_USER_ID]; if (!accessToken || !tokenId) { - throw new Error("Missing required authentication tokens"); + throw new Error( + "[silentRefresh] Missing required authentication tokens", + ); } tokenManager.setToken(CLAIM_NAME, accessToken); tokenManager.setToken(AUTH_USER_ID, tokenId); } catch (error) { console.warn("Token has expired, logging out."); + tokenManager.clearAuthToken(); if (error instanceof AxiosError) { throw new Error( `Silent refresh failed: ${error.response?.data?.message || error.message}`, diff --git a/apps/cs/src/entities/auth/login/lib/token-manager.ts b/apps/cs/src/entities/auth/login/lib/token-manager.ts index e72e00567..4429c7d1b 100644 --- a/apps/cs/src/entities/auth/login/lib/token-manager.ts +++ b/apps/cs/src/entities/auth/login/lib/token-manager.ts @@ -40,12 +40,12 @@ export class TokenManager { this.refreshTimer = setTimeout(() => { callback().catch((error) => { console.error("Silent refresh failed:", error); - this.clearAuth(); + this.clearAuthToken(); }); }, expirationTime - 60000); } - public clearAuth(): void { + public clearAuthToken(): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; diff --git a/apps/cs/src/shared/provider/auth-provider.tsx b/apps/cs/src/shared/provider/auth-provider.tsx new file mode 100644 index 000000000..7c827e948 --- /dev/null +++ b/apps/cs/src/shared/provider/auth-provider.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { createContext, useContext, useState } from "react"; +import { client } from "../api/client"; +import { ENDPOINT } from "../config/api"; +import { AUTH_USER_ID } from "../config/env"; +import { getCookie } from "../lib/cookie"; +import { AxiosError } from "axios"; +import { LoginApi } from "@/entities/auth/login"; + +interface IUserInfo { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; +} + +export interface AuthContextType { + userInfo: IUserInfo | null; +} + +export const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [userInfo, setUserInfo] = useState(null); + + useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryFn: async () => { + try { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/${getCookie(AUTH_USER_ID)}`, + ); + setUserInfo(response.data); + return response; + } catch (error) { + console.error(error); + if (error instanceof AxiosError) { + if (error.response?.status === 401) { + try { + await LoginApi.silentRefresh(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + } + } + } + } + }, + }); + + const authContextValue: AuthContextType = { + userInfo, + }; + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/apps/cs/src/shared/provider/cookie/cookie-provider.tsx b/apps/cs/src/shared/provider/cookie-provider.tsx similarity index 92% rename from apps/cs/src/shared/provider/cookie/cookie-provider.tsx rename to apps/cs/src/shared/provider/cookie-provider.tsx index 2e7bf4c2f..c50050cf5 100644 --- a/apps/cs/src/shared/provider/cookie/cookie-provider.tsx +++ b/apps/cs/src/shared/provider/cookie-provider.tsx @@ -1,7 +1,7 @@ "use client"; import { createContext, useContext } from "react"; -import { getCookie, setCookie, removeCookie } from "../../lib/cookie"; +import { getCookie, setCookie, removeCookie } from "../lib/cookie"; export interface CookieContextType { getCookie: (name: string) => string | undefined; diff --git a/apps/cs/src/shared/provider/index.ts b/apps/cs/src/shared/provider/index.ts index 7ef95c20c..8d018ff08 100644 --- a/apps/cs/src/shared/provider/index.ts +++ b/apps/cs/src/shared/provider/index.ts @@ -1,6 +1,3 @@ -export { - CookieContext, - CookieProvider, - useCookie, -} from "./cookie/cookie-provider"; -export { QueryProvider } from "./query/query-provider"; +export { AuthProvider, useAuth } from "./auth-provider"; +export { CookieProvider, useCookie } from "./cookie-provider"; +export { QueryProvider } from "./query-provider"; diff --git a/apps/cs/src/shared/provider/query/query-provider.tsx b/apps/cs/src/shared/provider/query-provider.tsx similarity index 100% rename from apps/cs/src/shared/provider/query/query-provider.tsx rename to apps/cs/src/shared/provider/query-provider.tsx diff --git a/packages/common/network/client.ts b/packages/common/network/client.ts index c61ce01e9..705c45d5d 100644 --- a/packages/common/network/client.ts +++ b/packages/common/network/client.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import Cookies from "js-cookie"; import { AUTH_USER_ID, CLAIM_NAME } from "./env"; @@ -27,7 +27,19 @@ class Client { const userId = Cookies.get(AUTH_USER_ID); if (!token || !userId) { - throw new Error("Missing required authentication tokens"); + throw new AxiosError( + "[Client] Missing required authentication tokens", + "UNAUTHORIZED", + config, + null, + { + data: { message: "Missing required authentication tokens" }, + status: 401, + statusText: "Unauthorized", + headers: this.axiosInstance.defaults.headers, + config: config, + } as AxiosResponse + ); } config.headers[CLAIM_NAME] = token;