From 1c250906edebb1b39b83af43e49ccc9a96f139b5 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sat, 30 May 2026 14:33:26 +0800 Subject: [PATCH] fix: keep terminal stable across auth refresh --- .../account/__tests__/paymentSecurity.test.ts | 9 +++++++++ .../components/dashboard/ScanTerminalDashboard.tsx | 12 ++++++++++++ .../__tests__/terminalGridPolicy.test.ts | 11 +++++++++++ frontend/middleware.ts | 7 +++++-- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/frontend/components/account/__tests__/paymentSecurity.test.ts b/frontend/components/account/__tests__/paymentSecurity.test.ts index 982b82da..1a5c5d0f 100644 --- a/frontend/components/account/__tests__/paymentSecurity.test.ts +++ b/frontend/components/account/__tests__/paymentSecurity.test.ts @@ -119,6 +119,15 @@ export function runTests() { middlewareSource.indexOf("await refreshMiddlewareSession(request)"), "middleware must redirect no-cookie page requests without calling Supabase auth", ); + const terminalGateSource = middlewareSource.slice( + middlewareSource.indexOf("async function handleTerminalGate"), + middlewareSource.indexOf("async function handleSupabaseAuthGate"), + ); + assert( + terminalGateSource.includes("return response;") && + !terminalGateSource.includes("return redirectToLogin(request, pathname);\n}"), + "terminal middleware must not navigate long-lived dashboards away when a session cookie exists but claims refresh is transiently unavailable", + ); assert( middlewareSource.includes("unauthorizedSupabaseSessionResponse()"), "middleware must reject no-cookie protected API requests without calling Supabase auth", diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 684b1d12..6dbf7d96 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -954,6 +954,18 @@ function ScanTerminalScreen() { data: { subscription }, } = supabase.auth.onAuthStateChange(async (event, session) => { if (event === "SIGNED_OUT") { + try { + const { + data: { session: currentSession }, + } = await supabase.auth.getSession(); + const accessToken = + currentSession?.access_token || session?.access_token || null; + if (accessToken) { + const payload = await loadAuthProfile(accessToken); + setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload)); + return; + } + } catch {} setProAccess(createEmptyAccess(false)); } else if (event === "TOKEN_REFRESHED" || event === "SIGNED_IN") { try { diff --git a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts index 1b4fd816..a5096c40 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts @@ -65,4 +65,15 @@ export function runTests() { chartCanvasSource.includes('compact ? "min-h-[120px]" : "min-h-[220px]"'), "compact grid charts must not force desktop minimum heights that get clipped inside 3x3 terminal slots", ); + const signedOutBlock = dashboardSource.slice( + dashboardSource.indexOf('if (event === "SIGNED_OUT")'), + dashboardSource.indexOf('} else if (event === "TOKEN_REFRESHED"'), + ); + assert( + signedOutBlock.includes("await supabase.auth.getSession()") && + signedOutBlock.includes("mergeAccessStateWithAuthPayload(prev, payload)") && + signedOutBlock.indexOf("await supabase.auth.getSession()") < + signedOutBlock.indexOf("setProAccess(createEmptyAccess(false))"), + "terminal auth listener must re-check the current Supabase session before clearing access on SIGNED_OUT events", + ); } diff --git a/frontend/middleware.ts b/frontend/middleware.ts index 79ee73ac..b125a592 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -104,8 +104,11 @@ async function handleTerminalGate(request: NextRequest): Promise { return response; } - // Layer 1: Not logged in → redirect to /auth/login?next=/terminal - return redirectToLogin(request, pathname); + // A session cookie exists, but the edge/server refresh can occasionally fail + // during a long-lived terminal tab. Do not navigate an active dashboard away + // on a transient claims failure; the client can still verify via bearer auth + // and render the in-product access gate if the session is truly gone. + return response; } async function handleSupabaseAuthGate(request: NextRequest) {