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
11 changes: 10 additions & 1 deletion apps/web/app/_lib/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
85 changes: 73 additions & 12 deletions apps/web/middleware.ts
Original file line number Diff line number Diff line change
@@ -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),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 3. 백엔드 쿠키(Set-Cookie) 포워딩
const backendSetCookies = response.headers.getSetCookie()
for (const cookie of backendSetCookies) {
res.headers.append('set-cookie', cookie)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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