diff --git a/apps/web/app/_lib/axiosInstance.ts b/apps/web/app/_lib/axiosInstance.ts index 694497a5..388d4307 100644 --- a/apps/web/app/_lib/axiosInstance.ts +++ b/apps/web/app/_lib/axiosInstance.ts @@ -6,6 +6,7 @@ import axios, { import * as Sentry from '@sentry/nextjs' import { getToken } from '@/_apis/services/login' import { getCookie } from '@/_utils/getCookie' +import { setCookie } from 'cookies-next' const axiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -53,7 +54,15 @@ axiosInstance.interceptors.response.use( originalRequest._retry = true try { - const { accessToken: newAccessToken } = await getToken() + const { accessToken: newAccessToken, accessTokenExpiresIn } = + await getToken() + + setCookie('accessToken', newAccessToken, { + path: '/', + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + expires: new Date(Date.now() + accessTokenExpiresIn), + }) if (originalRequest.headers) { originalRequest.headers.Authorization = newAccessToken diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 3f53e0c6..bcabd2f0 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,32 +1,93 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { CLIENT_PATH } from 'app/_constants/path' +import { API_PATH, CLIENT_PATH } from '@/_constants/path' export async function middleware(request: NextRequest) { - const cookieStore = await cookies() - const accessToken = cookieStore.get('accessToken')?.value + const accessToken = request.cookies.get('accessToken')?.value + const refreshToken = request.cookies.get('refreshToken')?.value - if (!accessToken) { + // [Case 1] 액세스 토큰이 유효한 경우 -> 가장 먼저 통과시킴 (Early Return) + if (accessToken) { + return NextResponse.next() + } + + // [Case 2] 토큰이 아예 없는 경우 -> 곧바로 로그인 페이지로 (Early Return) + if (!refreshToken) { return NextResponse.redirect(new URL(CLIENT_PATH.LOGIN, request.url)) } - return NextResponse.next() + // [Case 3] 액세스 토큰 만료 & 리프레시 토큰 존재 -> 갱신 시도 + return await handleTokenRefresh(request, refreshToken) } export const config = { matcher: [ '/likes', '/profile', - - // '/places/new', - // '/places/new/success', - // '/places/new/fail', - '/requests', '/requests/:path*', - '/events/lucky-draw', '/events/gifticon', '/events/gifticon/:path*', ], } + +const handleTokenRefresh = async ( + request: NextRequest, + refreshToken: string, +) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${API_PATH.AUTH.TOKEN}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `refreshToken=${refreshToken}`, + }, + }, + ) + + if (!response.ok) throw new Error('Token refresh failed') + + // 구조 분해 할당을 한 번에 처리하여 코드 간소화 + const { + data: { accessToken: newAccessToken, accessTokenExpiresIn }, + } = await response.json() + + // 1. [서버 컴포넌트 동기화] Request Header 조작 + const requestHeaders = new Headers(request.headers) + requestHeaders.set('Authorization', `Bearer ${newAccessToken}`) + + const res = NextResponse.next({ + request: { + headers: requestHeaders, + }, + }) + + // 2. [브라우저 동기화] 쿠키 세팅 + res.cookies.set('accessToken', newAccessToken, { + path: '/', + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + expires: new Date(Date.now() + accessTokenExpiresIn), + }) + + // 3. 백엔드 쿠키(Set-Cookie) 포워딩 + const backendSetCookies = response.headers.getSetCookie() + for (const cookie of backendSetCookies) { + res.headers.append('set-cookie', cookie) + } + + return res + } catch (error) { + console.error('Middleware Token Refresh Error:', error) + + // 갱신 실패 시 로그인 리다이렉트 및 만료 토큰 정리 + const redirectRes = NextResponse.redirect( + new URL(CLIENT_PATH.LOGIN, request.url), + ) + redirectRes.cookies.delete('refreshToken') + + return redirectRes + } +}