From 5f749fea63ebc87114dff22ad72e3f570eec7eea Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:23:59 +0200 Subject: [PATCH] fix: sign out of one account without dropping the others #1201 Logout switched to the first account in the list. After a restore that is often an account still waiting for sign-in, which has no client, so logout took the full sign-out path and dropped every account. It now switches to the first account that is connected. Boot also counted waiting accounts when deciding whether any account could be restored, so a reload with only waiting accounts left showed a connection error instead of the login page. --- .../auth-store-archive-restore.test.ts | 24 +++++++++++++++++++ stores/auth-store.ts | 17 ++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/stores/__tests__/auth-store-archive-restore.test.ts b/stores/__tests__/auth-store-archive-restore.test.ts index 50d429a84..47f51b689 100644 --- a/stores/__tests__/auth-store-archive-restore.test.ts +++ b/stores/__tests__/auth-store-archive-restore.test.ts @@ -95,3 +95,27 @@ it('stops waiting once the account is signed in', () => { expect(alice).toMatchObject({ label: 'Alice', avatarColor: '#16a34a', isConnected: true }); expect(alice?.awaitingSignIn).toBeUndefined(); }); + +it('signs out to an account that is signed in, not to one awaiting sign-in', async () => { + restoreAccounts(archived); + useAccountStore.setState((s) => ({ + accounts: [...s.accounts, { ...signedIn, id: 'carol@x.test@mail.x.test', username: 'carol@x.test', email: 'carol@x.test', label: 'Carol', cookieSlot: 2, isDefault: false }] as never, + })); + await useAuthStore.getState().checkAuth(); + // Wait for the background restore of the other remembered account. + await vi.waitFor(() => expect(useAccountStore.getState().getAccountById('carol@x.test@mail.x.test')?.isConnected).toBe(true)); + + await useAuthStore.getState().logout(); + expect(useAuthStore.getState().activeAccountId).toBe('carol@x.test@mail.x.test'); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + expect(useAccountStore.getState().accounts.map(a => a.id)).toEqual(['alice@x.test@mail.x.test', 'carol@x.test@mail.x.test']); +}); + +it('treats a browser left with only accounts awaiting sign-in as signed out, not as a connection failure', async () => { + useAccountStore.setState({ accounts: [{ ...signedIn, rememberMe: false }] as never }); + restoreAccounts(archived.slice(1)); + await useAuthStore.getState().checkAuth(); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + expect(useAuthStore.getState().error).not.toBe('connection_failed'); + expect(useAccountStore.getState().accounts.map(a => a.id)).toEqual(['alice@x.test@mail.x.test']); +}); diff --git a/stores/auth-store.ts b/stores/auth-store.ts index eb8076bfe..5a79f6ba4 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -2112,7 +2112,13 @@ export const useAuthStore = create()( // Check if there are remaining accounts to switch to. Read the store // afresh: `accountStore` is the state from before removeAccount(). - const nextAccount = wasDemoMode ? undefined : useAccountStore.getState().accounts[0]; + // Prefer an account that is signed in here: one restored from the + // account list backup (or otherwise not connected) has no client, and + // picking it signs out every account left. + const remainingAccounts = useAccountStore.getState().accounts; + const nextAccount = wasDemoMode + ? undefined + : remainingAccounts.find((a) => clients.has(a.id)) ?? remainingAccounts[0]; const droppedAccounts: AccountEntry[] = []; if (nextAccount) { @@ -2762,8 +2768,13 @@ export const useAuthStore = create()( } } - // No accounts could be restored - if (accounts.some((account) => accountStore.getAccountById(account.id))) { + // No accounts could be restored. Accounts restored from the account + // list backup were never signed in here, so they say nothing about + // the connection. + if (accounts.some((account) => { + const entry = accountStore.getAccountById(account.id); + return entry && !entry.awaitingSignIn; + })) { set({ isAuthenticated: false, isLoading: false,