From 39e7d61c8a6d94542d144e144305df9b8c6efa71 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 16 Jun 2026 18:24:23 +0800 Subject: [PATCH] fix(auth): re-check auth status after logout; clear NextAuth session after sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) Sidebar logout flow: - After AuthReset, also dispatch AuthStatusCheckSubmitted so the auth machine re-verifies storage (not just resets to initialState). This makes sure the redirect to /chat reflects the actual storage state, not just the forcibly-cleared context. 2) OAuthSessionSync: - After dispatching AuthGoogleSyncSubmitted / AuthFacebookSyncSubmitted, call signOut({ redirect: false }) to drop the NextAuth session. The OAuth idToken / accessToken has already been handed to the backend; clearing the browser-side session prevents lingering OAuth credential exposure. Fire-and-forget — once status flips to "unauthenticated", the useEffect early-returns so no re-entry. Note: auth-machine.ts lost a 3-line stale comment block about XState v5 type inference for inline assign; bundled with this commit. --- src/app/sidebar/components/sidebar-screen.tsx | 5 +++++ src/stores/auth/auth-machine.ts | 3 --- src/stores/auth/oauth-session-sync.tsx | 13 ++++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/app/sidebar/components/sidebar-screen.tsx b/src/app/sidebar/components/sidebar-screen.tsx index c542d118..5f67b668 100644 --- a/src/app/sidebar/components/sidebar-screen.tsx +++ b/src/app/sidebar/components/sidebar-screen.tsx @@ -52,7 +52,12 @@ export function SidebarScreen() { const isNowLoggedOut = user.currentUser == null; prevUserRef.current = user.currentUser; if (wasLoggedIn && isNowLoggedOut) { + // 1) AuthReset:清场(清 email/password/username 等敏感字段) authDispatch({ type: "AuthReset" }); + // 2) AuthStatusCheckSubmitted:通过 storage 重新验证实际 auth 状态 + // (userLogoutActor 已清空 userStorage,checkAuthStatusActor 会返 "notLoggedIn") + // 这一步确保 redirect 到 /chat 后,chat-screen 看到的是真实 storage 状态而非 AuthReset 直接置的 initialState + authDispatch({ type: "AuthStatusCheckSubmitted" }); router.replace(ROUTES.chat); } }, [user.currentUser, authDispatch, router]); diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index bc0b1df4..cc0c8e4e 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -32,9 +32,6 @@ export type { AuthEvent } from "./auth-events"; // splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat // 跳完派 `AuthClearPendingRedirect` 置 false // checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。 -// -// 注:onDone 用了内联assign(不是 helper function)—— XState v5 的 TypeScript -// 推断对 helper return type 不友好;内联能保留 `state.matches("loadingXxx")` 的类型。 // ============================================================ export const authMachine = setup({ types: { diff --git a/src/stores/auth/oauth-session-sync.tsx b/src/stores/auth/oauth-session-sync.tsx index 8d228ce5..2dec85e0 100644 --- a/src/stores/auth/oauth-session-sync.tsx +++ b/src/stores/auth/oauth-session-sync.tsx @@ -8,6 +8,9 @@ * → 派发 `AuthGoogleSyncSubmitted` / `AuthFacebookSyncSubmitted` 事件 * → auth machine 调 `authRepository.googleLogin/facebookLogin` 换业务 token * → 业务 token 写入本地 storage + * → **清掉 NextAuth session**(`signOut({ redirect: false })`): + * idToken / accessToken 已交给后端换业务 token,浏览器侧不再需要保留 OAuth 凭证 + * 避免凭证在 cookie / sessionStorage 里持续暴露 * * 此组件无可见 UI(返回 null),仅作为 NextAuth session 与 auth state * machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后: @@ -16,7 +19,7 @@ * */ import { useEffect } from "react"; -import { useSession } from "next-auth/react"; +import { signOut, useSession } from "next-auth/react"; import { useAuthDispatch, useAuthState } from "./auth-context"; @@ -35,20 +38,24 @@ export function OAuthSessionSync() { // 3) 已经有该 provider 的业务 token(防止重复 sync 刷后端 user 表) if (loginStatus === session.provider) return; - // 4) Google:派发 idToken sync 事件 + // 4) Google:派发 idToken sync 事件 → 清 NextAuth session if (session.provider === "google") { if (!session.idToken) return; dispatch({ type: "AuthGoogleSyncSubmitted", idToken: session.idToken }); + // OAuth token 已交给后端 —— 清掉 NextAuth session(fire-and-forget) + // 清完后 useSession 返 null / unauthenticated → 下次 effect 早 return,不会重入 + void signOut({ redirect: false }); return; } - // 5) Facebook:派发 accessToken sync 事件 + // 5) Facebook:派发 accessToken sync 事件 → 清 NextAuth session if (session.provider === "facebook") { if (!session.accessToken) return; dispatch({ type: "AuthFacebookSyncSubmitted", accessToken: session.accessToken, }); + void signOut({ redirect: false }); } }, [status, session, loginStatus, dispatch]);