From bd74c58da440b4c1fe5ed089602312f291366da2 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Thu, 13 Aug 2026 15:19:59 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=20=ED=9B=84=20?= =?UTF-8?q?=EC=A3=BD=EC=9D=80=20=ED=86=A0=ED=81=B0=EC=9D=B4=20=EB=82=A8?= =?UTF-8?q?=EC=95=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EA=B0=80=20=ED=99=88=EC=9C=BC=EB=A1=9C=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 탈퇴 성공 시 웹 브라우저에서도 서버 액션으로 httpOnly 인증 쿠키 폐기 - 쿠키 폐기 서버 액션을 clearAuthCookies 로 추출해 로그아웃과 공유 - proxy 의 쿠키 폐기 발동 조건에 withdrawn-account action 추가 closes #467 Co-Authored-By: Claude Fable 5 --- apps/web/src/app/mypage/_actions/logout.ts | 7 ++----- .../mypage/_common/_actions/clearAuthCookies.ts | 14 ++++++++++++++ .../src/app/mypage/withdraw/_hooks/useDeleteMe.ts | 9 ++++++++- apps/web/src/proxy.ts | 14 ++++++++------ 4 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/app/mypage/_common/_actions/clearAuthCookies.ts diff --git a/apps/web/src/app/mypage/_actions/logout.ts b/apps/web/src/app/mypage/_actions/logout.ts index 991c4c38..c7108f78 100644 --- a/apps/web/src/app/mypage/_actions/logout.ts +++ b/apps/web/src/app/mypage/_actions/logout.ts @@ -4,10 +4,9 @@ import { cookies } from 'next/headers'; import { deleteFcmToken } from '@/apis/deleteFcmToken'; import { serverApi } from '@/apis/server'; +import { clearAuthCookies } from '@/app/mypage/_common/_actions/clearAuthCookies'; import { ENDPOINTS } from '@/consts/api'; -const AUTH_COOKIE_NAMES = ['access_token', 'refresh_token', 'device_id'] as const; - export const logout = async () => { const cookieStore = await cookies(); const deviceId = cookieStore.get('device_id')?.value; @@ -26,7 +25,5 @@ export const logout = async () => { // 백엔드 실패해도 로컬 쿠키는 삭제 } - AUTH_COOKIE_NAMES.forEach(cookieName => { - if (cookieStore.has(cookieName)) cookieStore.delete(cookieName); - }); + await clearAuthCookies(); }; diff --git a/apps/web/src/app/mypage/_common/_actions/clearAuthCookies.ts b/apps/web/src/app/mypage/_common/_actions/clearAuthCookies.ts new file mode 100644 index 00000000..623f71f6 --- /dev/null +++ b/apps/web/src/app/mypage/_common/_actions/clearAuthCookies.ts @@ -0,0 +1,14 @@ +'use server'; + +import { cookies } from 'next/headers'; + +const AUTH_COOKIE_NAMES = ['access_token', 'refresh_token', 'device_id'] as const; + +/** 브라우저 httpOnly 인증 쿠키는 JS 로 지울 수 없어 서버 액션으로 폐기한다 */ +export const clearAuthCookies = async () => { + const cookieStore = await cookies(); + + AUTH_COOKIE_NAMES.forEach(cookieName => { + if (cookieStore.has(cookieName)) cookieStore.delete(cookieName); + }); +}; diff --git a/apps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.ts b/apps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.ts index 1d1f6c5b..0714f367 100644 --- a/apps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.ts +++ b/apps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.ts @@ -1,8 +1,10 @@ import { WEBBRIDGE_MESSAGE_TYPE } from '@piki/core'; +import * as Sentry from '@sentry/nextjs'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; +import { clearAuthCookies } from '@/app/mypage/_common/_actions/clearAuthCookies'; import { ROUTES } from '@/consts/route'; import { isGlobalNetError } from '@/utils/apiError'; import { deleteCookie } from '@/utils/cookie'; @@ -17,14 +19,19 @@ export const useDeleteMe = () => { const { mutate: deleteMeMutation, isPending: isDeleteMePending } = useMutation({ mutationFn: deleteMe, - onSuccess: () => { + onSuccess: async () => { if (isWebview()) { deleteCookie('access_token'); deleteCookie('refresh_token'); WebBridge.postMessage({ type: WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_LOGOUT }); + } else { + /** 웹인 경우 쿠키 삭제 */ + await clearAuthCookies(); } + Sentry.setUser(null); + queryClient.clear(); router.replace(ROUTES.ROOT); }, diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 11f13070..6b308509 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -217,8 +217,8 @@ const handleTokenRefresh = async (request: NextRequest) => { } }; -/** 세션 만료 신호로 진입한 로그인 - 토큰 폐기 후 페이지 진입 */ -const handleSessionExpired = (request: NextRequest) => { +/** 세션 만료·탈퇴 신호로 진입한 로그인 - 토큰 폐기 후 페이지 진입 */ +const handleAuthReset = (request: NextRequest) => { /** 페이지 렌더링 시 죽은 토큰을 보지 않도록 남은 쿠키에서 삭제 */ const remainingCookies = request.cookies .getAll() @@ -255,10 +255,12 @@ export const proxy = async (request: NextRequest) => { /** 퍼블릭 영역 */ if (routeType === 'PUBLIC') { - const isSessionExpired = - pathname === ROUTES.LOGIN && - request.nextUrl.searchParams.get(QUERY_ACTION.KEY) === QUERY_ACTION.VALUE.SESSION_EXPIRED; - if (isSessionExpired) return handleSessionExpired(request); + const loginAction = + pathname === ROUTES.LOGIN ? request.nextUrl.searchParams.get(QUERY_ACTION.KEY) : null; + const isAuthReset = + loginAction === QUERY_ACTION.VALUE.SESSION_EXPIRED || + loginAction === QUERY_ACTION.VALUE.WITHDRAWN_ACCOUNT; + if (isAuthReset) return handleAuthReset(request); const needsRestore = pathname === ROUTES.LOGIN &&