fix(R61): 增加 session hint cookie 消除 bootstrap refresh 401

这个提交包含在:
2026-04-12 19:29:10 +08:00
父节点 dabb03e55b
当前提交 c3f1152824
共修改 2 个文件,包含 41 行新增和 2 行删除
+24 -2
查看文件
@@ -58,6 +58,17 @@ function isWindowAvailable() {
return typeof window !== "undefined"; 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() { function readMustChangePasswordFromStorage() {
return isWindowAvailable() && window.localStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY) === "1"; 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; sessionGenerationRef.current += 1;
bootstrapPromiseRef.current = null; bootstrapPromiseRef.current = null;
refreshRequestRef.current = null; refreshRequestRef.current = null;
@@ -189,6 +200,12 @@ export function AuthProvider({ children }: PropsWithChildren) {
})); }));
}, [clearProactiveRefreshTimer]); }, [clearProactiveRefreshTimer]);
const clearSession = useCallback(() => {
clearSessionHint();
markUnauthenticatedReady();
void clearPwaRuntimeCaches();
}, [markUnauthenticatedReady]);
const commitAuthenticatedUser = useCallback( const commitAuthenticatedUser = useCallback(
(accessToken: string, me: MeResponse, mustChangePassword: boolean) => { (accessToken: string, me: MeResponse, mustChangePassword: boolean) => {
const permissions = resolvePermissionsForRoles(me.roleCodes ?? []); const permissions = resolvePermissionsForRoles(me.roleCodes ?? []);
@@ -301,6 +318,11 @@ export function AuthProvider({ children }: PropsWithChildren) {
return; return;
} }
if (!hasSessionHint() && (!isWindowAvailable() || window.navigator.onLine !== false)) {
markUnauthenticatedReady();
return;
}
if (!bootstrapPromiseRef.current) { if (!bootstrapPromiseRef.current) {
bootstrapPromiseRef.current = (async () => { bootstrapPromiseRef.current = (async () => {
try { try {
@@ -323,7 +345,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
} }
await bootstrapPromiseRef.current; await bootstrapPromiseRef.current;
}, [activateOfflineSnapshot, clearSession, runRefresh, scheduleProactiveRefresh]); }, [activateOfflineSnapshot, clearSession, markUnauthenticatedReady, runRefresh, scheduleProactiveRefresh]);
const login = useCallback( const login = useCallback(
async (identifier: string, password: string) => { async (identifier: string, password: string) => {
+17
查看文件
@@ -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) { function oidcCookieOptions(config) {
return { return {
httpOnly: true, httpOnly: true,
@@ -420,6 +430,7 @@ async function authRoutes(fastify) {
refreshToken, refreshToken,
refreshCookieOptions(fastify.config), refreshCookieOptions(fastify.config),
); );
reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config));
const accessToken = fastify.issueAccessToken(employee); const accessToken = fastify.issueAccessToken(employee);
return sendData(reply, request, { return sendData(reply, request, {
@@ -540,6 +551,7 @@ async function authRoutes(fastify) {
} }
reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config));
reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config));
throw new AppError({ throw new AppError({
code: "AUTH_005", code: "AUTH_005",
message: "Refresh Token 无效/已撤销", message: "Refresh Token 无效/已撤销",
@@ -607,6 +619,7 @@ async function authRoutes(fastify) {
newRefreshToken, newRefreshToken,
refreshCookieOptions(fastify.config), refreshCookieOptions(fastify.config),
); );
reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config));
const accessToken = fastify.issueAccessToken(employee); const accessToken = fastify.issueAccessToken(employee);
return sendData(reply, request, { return sendData(reply, request, {
@@ -857,6 +870,7 @@ async function authRoutes(fastify) {
refreshToken, refreshToken,
refreshCookieOptions(fastify.config), refreshCookieOptions(fastify.config),
); );
reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config));
reply.clearCookie(oidcCookieName, oidcCookieOptions(fastify.config)); reply.clearCookie(oidcCookieName, oidcCookieOptions(fastify.config));
const origin = resolvePrimaryAppOrigin(fastify.config, request); const origin = resolvePrimaryAppOrigin(fastify.config, request);
@@ -894,6 +908,7 @@ async function authRoutes(fastify) {
} }
reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config));
reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config));
return sendData(reply, request, {}); return sendData(reply, request, {});
}); });
@@ -1019,6 +1034,7 @@ async function authRoutes(fastify) {
refreshToken, refreshToken,
refreshCookieOptions(fastify.config), refreshCookieOptions(fastify.config),
); );
reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config));
const accessToken = fastify.issueAccessToken(updatedEmployee); const accessToken = fastify.issueAccessToken(updatedEmployee);
return sendData(reply, request, { return sendData(reply, request, {
@@ -1245,6 +1261,7 @@ async function authRoutes(fastify) {
} }
reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config)); reply.clearCookie(fastify.config.refreshCookieName, refreshCookieOptions(fastify.config));
reply.clearCookie("has_session", sessionHintCookieOptions(fastify.config));
return sendData(reply, request, {}); return sendData(reply, request, {});
}); });
} }