From b7e4c2d9f53de3a6dc4c38bdb8d7c7608d266228 Mon Sep 17 00:00:00 2001 From: pory-gone Date: Sun, 21 Sep 2025 14:50:01 +0200 Subject: [PATCH 1/4] cancel all pending requests --- components/account.js | 3 ++- components/nav/common.js | 4 +++- lib/apollo.js | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/components/account.js b/components/account.js index dd9d3248dd..278fb91e98 100644 --- a/components/account.js +++ b/components/account.js @@ -11,6 +11,7 @@ import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } f const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8') export const nextAccount = async () => { + if (typeof window !== 'undefined') window.logoutInProgress = true const { status } = await fetch('/api/next-account', { credentials: 'include' }) // if status is 302, this means the server was able to switch us to the next available account return status === 302 @@ -69,7 +70,7 @@ const AccountListRow = ({ account, selected, ...props }) => { const onClick = async (e) => { // prevent navigation e.preventDefault() - + if (typeof window !== 'undefined') window.logoutInProgress = true // update pointer cookie const options = cookieOptions({ httpOnly: false }) const anon = account.id === USER_ID.anon diff --git a/components/nav/common.js b/components/nav/common.js index 3c9f619705..a475c99e63 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -305,7 +305,9 @@ function LogoutObstacle ({ onClose }) { router.reload() return } - + window.logoutInProgress = true + document.cookie = 'next-auth.session-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' + document.cookie = 'multi_auth.user-id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' // order is important because we need to be logged in to delete push subscription on server const pushSubscription = await swRegistration?.pushManager.getSubscription() if (pushSubscription) { diff --git a/lib/apollo.js b/lib/apollo.js index b05d1a9440..fb2738466b 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -4,6 +4,7 @@ import { decodeCursor, LIMIT } from './cursor' import { COMMENTS_LIMIT, SSR } from './constants' import { RetryLink } from '@apollo/client/link/retry' import { isMutationOperation, isQueryOperation } from '@apollo/client/utilities' +import { setContext } from '@apollo/client/link/context' function isFirstPage (cursor, existingThings, limit = LIMIT) { if (cursor) { @@ -46,8 +47,19 @@ const retryLink = new RetryLink({ }) function getClient (uri) { + const authContextLink = setContext((_, { headers }) => { + if (SSR) return { headers } + if (typeof window !== 'undefined' && window.logoutInProgress) { + const newHeaders = { ...headers } + delete newHeaders.authorization + delete newHeaders.cookie + return { headers: newHeaders } + } + return { headers } + }) const link = from([ retryLink, + authContextLink, split( // batch zaps if wallet is enabled so they can be executed serially in a single request operation => operation.operationName === 'act' && operation.variables.act === 'TIP' && operation.getContext().batch, From 31126d3255bf733b4f5e62924371edfab8b80897 Mon Sep 17 00:00:00 2001 From: pory-gone Date: Sun, 21 Sep 2025 14:50:01 +0200 Subject: [PATCH 2/4] cancel all pending requests --- components/nav/common.js | 4 ++-- lib/auth.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/nav/common.js b/components/nav/common.js index a475c99e63..4eee1cf5b5 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -22,6 +22,7 @@ import { useWalletIndicator } from '@/wallets/client/hooks' import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account' import { useShowModal } from '@/components/modal' import { numWithUnits } from '@/lib/format' +import { clearAuthCookies } from '@/lib/auth' export function Brand ({ className }) { return ( @@ -306,8 +307,7 @@ function LogoutObstacle ({ onClose }) { return } window.logoutInProgress = true - document.cookie = 'next-auth.session-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' - document.cookie = 'multi_auth.user-id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' + clearAuthCookies() // order is important because we need to be logged in to delete push subscription on server const pushSubscription = await swRegistration?.pushManager.getSubscription() if (pushSubscription) { diff --git a/lib/auth.js b/lib/auth.js index 70dec13abf..1aa2f7af06 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -35,6 +35,12 @@ export const cookieOptions = (args) => ({ ...args }) +export const clearAuthCookies = () => { + const expiredDate = 'Thu, 01 Jan 1970 00:00:00 GMT' + document.cookie = `${SESSION_COOKIE}=; path=/; expires=${expiredDate}` + document.cookie = `${MULTI_AUTH_POINTER}=; path=/; expires=${expiredDate}` +} + export function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) { const httpOnlyOptions = cookieOptions() const jsOptions = { ...httpOnlyOptions, httpOnly: false } From e12d3d1f81eaf6835dc41d9f732b56d736d90146 Mon Sep 17 00:00:00 2001 From: pory-gone Date: Sun, 21 Sep 2025 19:59:48 +0200 Subject: [PATCH 3/4] implementaton of abortlink --- components/account.js | 5 +++-- components/nav/common.js | 6 ++--- lib/apollo.js | 47 +++++++++++++++++++++++++++++----------- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/components/account.js b/components/account.js index 278fb91e98..56434d97a0 100644 --- a/components/account.js +++ b/components/account.js @@ -7,11 +7,12 @@ import useCookie from '@/components/use-cookie' import Link from 'next/link' import AddIcon from '@/svgs/add-fill.svg' import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth' +import { abortPendingRequests } from '@/lib/apollo' const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8') export const nextAccount = async () => { - if (typeof window !== 'undefined') window.logoutInProgress = true + abortPendingRequests() // Cancel any pending requests during account switch const { status } = await fetch('/api/next-account', { credentials: 'include' }) // if status is 302, this means the server was able to switch us to the next available account return status === 302 @@ -70,7 +71,7 @@ const AccountListRow = ({ account, selected, ...props }) => { const onClick = async (e) => { // prevent navigation e.preventDefault() - if (typeof window !== 'undefined') window.logoutInProgress = true + abortPendingRequests() // Cancel pending requests during account switch // update pointer cookie const options = cookieOptions({ httpOnly: false }) const anon = account.id === USER_ID.anon diff --git a/components/nav/common.js b/components/nav/common.js index 4eee1cf5b5..d8422a7846 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -22,7 +22,7 @@ import { useWalletIndicator } from '@/wallets/client/hooks' import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account' import { useShowModal } from '@/components/modal' import { numWithUnits } from '@/lib/format' -import { clearAuthCookies } from '@/lib/auth' +import { abortPendingRequests } from '@/lib/apollo' export function Brand ({ className }) { return ( @@ -306,8 +306,8 @@ function LogoutObstacle ({ onClose }) { router.reload() return } - window.logoutInProgress = true - clearAuthCookies() + // Abort all pending GraphQL requests to prevent race condition + abortPendingRequests() // order is important because we need to be logged in to delete push subscription on server const pushSubscription = await swRegistration?.pushManager.getSubscription() if (pushSubscription) { diff --git a/lib/apollo.js b/lib/apollo.js index fb2738466b..b3f589edbe 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -1,10 +1,9 @@ -import { ApolloClient, InMemoryCache, HttpLink, makeVar, split, from } from '@apollo/client' +import { ApolloClient, InMemoryCache, HttpLink, makeVar, split, from, ApolloLink } from '@apollo/client' import { BatchHttpLink } from '@apollo/client/link/batch-http' import { decodeCursor, LIMIT } from './cursor' import { COMMENTS_LIMIT, SSR } from './constants' import { RetryLink } from '@apollo/client/link/retry' import { isMutationOperation, isQueryOperation } from '@apollo/client/utilities' -import { setContext } from '@apollo/client/link/context' function isFirstPage (cursor, existingThings, limit = LIMIT) { if (cursor) { @@ -30,6 +29,36 @@ export default function getApolloClient () { } export const meAnonSats = {} +let globalAbortController = null +export function getGlobalAbortController () { + if (!globalAbortController || globalAbortController.signal.aborted) { + globalAbortController = new AbortController() + } + return globalAbortController +} + +export function abortPendingRequests () { + if (globalAbortController && !globalAbortController.signal.aborted) { + globalAbortController.abort('Logout in progress') + console.log('Aborted pending GraphQL requests during logout') + } +} + +const createAbortLink = () => new ApolloLink((operation, forward) => { + if (SSR) { + return forward(operation) + } + + const abortController = getGlobalAbortController() + operation.setContext((context) => ({ + ...context, + fetchOptions: { + ...context.fetchOptions, + signal: abortController.signal + } + })) + return forward(operation) +}) const retryLink = new RetryLink({ delay: { @@ -47,19 +76,11 @@ const retryLink = new RetryLink({ }) function getClient (uri) { - const authContextLink = setContext((_, { headers }) => { - if (SSR) return { headers } - if (typeof window !== 'undefined' && window.logoutInProgress) { - const newHeaders = { ...headers } - delete newHeaders.authorization - delete newHeaders.cookie - return { headers: newHeaders } - } - return { headers } - }) + const abortLink = createAbortLink() + const link = from([ retryLink, - authContextLink, + abortLink, split( // batch zaps if wallet is enabled so they can be executed serially in a single request operation => operation.operationName === 'act' && operation.variables.act === 'TIP' && operation.getContext().batch, From 439f941d7c5a5c90bd6eaf52de047a3ba7af9682 Mon Sep 17 00:00:00 2001 From: pory-gone Date: Sun, 21 Sep 2025 20:09:07 +0200 Subject: [PATCH 4/4] implementaton of abortlink --- lib/auth.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index 1aa2f7af06..70dec13abf 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -35,12 +35,6 @@ export const cookieOptions = (args) => ({ ...args }) -export const clearAuthCookies = () => { - const expiredDate = 'Thu, 01 Jan 1970 00:00:00 GMT' - document.cookie = `${SESSION_COOKIE}=; path=/; expires=${expiredDate}` - document.cookie = `${MULTI_AUTH_POINTER}=; path=/; expires=${expiredDate}` -} - export function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) { const httpOnlyOptions = cookieOptions() const jsOptions = { ...httpOnlyOptions, httpOnly: false }