From c3f1152824f6b4d8c5704d75094cec84ad5983d8 Mon Sep 17 00:00:00 2001 From: shunli Date: Sun, 12 Apr 2026 19:29:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(R61):=20=E5=A2=9E=E5=8A=A0=20session=20hint?= =?UTF-8?q?=20cookie=20=E6=B6=88=E9=99=A4=20bootstrap=20refresh=20401?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/auth/AuthContext.tsx | 26 ++++++++++++++++++++++++-- server/src/routes/auth.js | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/client/src/auth/AuthContext.tsx b/client/src/auth/AuthContext.tsx index fe683d0c..b2e18888 100644 --- a/client/src/auth/AuthContext.tsx +++ b/client/src/auth/AuthContext.tsx @@ -58,6 +58,17 @@ function isWindowAvailable() { return typeof window !== "undefined"; } +function hasSessionHint() { + return typeof document !== "undefined" && document.cookie.includes("has_session=1"); +} + +function clearSessionHint() { + if (typeof document === "undefined") { + return; + } + document.cookie = "has_session=; path=/; max-age=0"; +} + function readMustChangePasswordFromStorage() { return isWindowAvailable() && window.localStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY) === "1"; } @@ -168,7 +179,7 @@ export function AuthProvider({ children }: PropsWithChildren) { } }, []); - const clearSession = useCallback(() => { + const markUnauthenticatedReady = useCallback(() => { sessionGenerationRef.current += 1; bootstrapPromiseRef.current = null; refreshRequestRef.current = null; @@ -189,6 +200,12 @@ export function AuthProvider({ children }: PropsWithChildren) { })); }, [clearProactiveRefreshTimer]); + const clearSession = useCallback(() => { + clearSessionHint(); + markUnauthenticatedReady(); + void clearPwaRuntimeCaches(); + }, [markUnauthenticatedReady]); + const commitAuthenticatedUser = useCallback( (accessToken: string, me: MeResponse, mustChangePassword: boolean) => { const permissions = resolvePermissionsForRoles(me.roleCodes ?? []); @@ -301,6 +318,11 @@ export function AuthProvider({ children }: PropsWithChildren) { return; } + if (!hasSessionHint() && (!isWindowAvailable() || window.navigator.onLine !== false)) { + markUnauthenticatedReady(); + return; + } + if (!bootstrapPromiseRef.current) { bootstrapPromiseRef.current = (async () => { try { @@ -323,7 +345,7 @@ export function AuthProvider({ children }: PropsWithChildren) { } await bootstrapPromiseRef.current; - }, [activateOfflineSnapshot, clearSession, runRefresh, scheduleProactiveRefresh]); + }, [activateOfflineSnapshot, clearSession, markUnauthenticatedReady, runRefresh, scheduleProactiveRefresh]); const login = useCallback( async (identifier: string, password: string) => { diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js index ee86516d..492e09d4 100644 --- a/server/src/routes/auth.js +++ b/server/src/routes/auth.js @@ -118,6 +118,16 @@ function refreshCookieOptions(config) { }; } +function sessionHintCookieOptions(config) { + return { + httpOnly: false, + sameSite: "strict", + secure: config.cookieSecure, + path: "/", + maxAge: config.refreshTtlSeconds, + }; +} + function oidcCookieOptions(config) { return { httpOnly: true, @@ -420,6 +430,7 @@ async function authRoutes(fastify) { refreshToken, refreshCookieOptions(fastify.config), ); + reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config)); const accessToken = fastify.issueAccessToken(employee); return sendData(reply, request, { @@ -540,6 +551,7 @@ async function authRoutes(fastify) { } reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); + reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config)); throw new AppError({ code: "AUTH_005", message: "Refresh Token 无效/已撤销", @@ -607,6 +619,7 @@ async function authRoutes(fastify) { newRefreshToken, refreshCookieOptions(fastify.config), ); + reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config)); const accessToken = fastify.issueAccessToken(employee); return sendData(reply, request, { @@ -857,6 +870,7 @@ async function authRoutes(fastify) { refreshToken, refreshCookieOptions(fastify.config), ); + reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config)); reply.clearCookie(oidcCookieName, oidcCookieOptions(fastify.config)); const origin = resolvePrimaryAppOrigin(fastify.config, request); @@ -894,6 +908,7 @@ async function authRoutes(fastify) { } reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); + reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config)); return sendData(reply, request, {}); }); @@ -1019,6 +1034,7 @@ async function authRoutes(fastify) { refreshToken, refreshCookieOptions(fastify.config), ); + reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config)); const accessToken = fastify.issueAccessToken(updatedEmployee); return sendData(reply, request, { @@ -1245,6 +1261,7 @@ async function authRoutes(fastify) { } reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); + reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config)); return sendData(reply, request, {}); }); }