From 87552357be668936631be4eb22560441d2e25225 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 28 Aug 2026 12:35:51 -0700 Subject: [PATCH] fix(account): stop signing in deactivated accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchAccountAsync detected a deactivated account, dispatched resetAccount and fetchAccountFailed({ reason: 'ACCOUNT_DEACTIVATED' }), then fell through to setLocalStorageAccountAndUser, fetchAccountSucceeded and signedIn — so the account was fully restored (and cached) anyway. The `return` existed in the old JS saga and was dropped in the JS -> TS port in #10447. The sign-in form's own check does block, but it left the hedgehog session from the login intact, so reloading the page ran fetchAccountAsync and signed the user straight in. Clear the session there too. Affects web and mobile (shared saga). Reported by a deactivated user who sent a screenshot of their own feed. Co-Authored-By: Claude Opus 5 --- .../common/src/store/account/sagas.test.ts | 69 +++++++++++++++++++ packages/common/src/store/account/sagas.ts | 1 + .../src/common/store/pages/signon/sagas.ts | 4 ++ 3 files changed, 74 insertions(+) create mode 100644 packages/common/src/store/account/sagas.test.ts diff --git a/packages/common/src/store/account/sagas.test.ts b/packages/common/src/store/account/sagas.test.ts new file mode 100644 index 00000000000..c5485242c33 --- /dev/null +++ b/packages/common/src/store/account/sagas.test.ts @@ -0,0 +1,69 @@ +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { describe, it, vitest } from 'vitest' + +import { getWalletAccountSaga } from '~/api' +import { AccountUserMetadata } from '~/models' + +import { fetchAccountAsync } from './sagas' +import { fetchAccountFailed, fetchAccountSucceeded, signedIn } from './slice' + +const wallet = '0xc12f8e8a40b90e5aedf58fb729fa543e9a020cb0' + +const makeAccount = (isDeactivated: boolean) => + ({ + user: { + user_id: 1, + handle: 'test', + name: 'Test', + is_deactivated: isDeactivated + }, + playlists: [], + playlist_library: { contents: [] }, + track_save_count: 0 + }) as unknown as AccountUserMetadata + +const runFetchAccount = (account: AccountUserMetadata) => { + const sdk = { + services: { + audiusWalletClient: { + getAddresses: vitest.fn().mockResolvedValue([wallet]) + } + } + } + + return expectSaga(fetchAccountAsync, { + shouldMarkAccountAsLoading: true + }).provide([ + [matchers.getContext('audiusSdk'), vitest.fn().mockResolvedValue(sdk)], + [matchers.getContext('audiusBackendInstance'), {}], + [matchers.getContext('remoteConfigInstance'), { setUserId: vitest.fn() }], + [ + matchers.getContext('localStorage'), + { + getAudiusUserWalletOverride: vitest.fn().mockResolvedValue(null), + getItem: vitest.fn().mockResolvedValue(null), + setAudiusAccount: vitest.fn(), + setAudiusAccountUser: vitest.fn() + } + ], + [ + matchers.getContext('queryClient'), + { setQueryData: vitest.fn(), getQueryData: vitest.fn() } + ], + [matchers.call.fn(getWalletAccountSaga), account] + ]) +} + +describe('fetchAccountAsync', () => { + // Regression test: the deactivated branch used to fall through to + // fetchAccountSucceeded/signedIn, so a deactivated user was signed back in + // on any app load despite the sign-in form rejecting them. + it('does not sign in a deactivated account', async () => { + await runFetchAccount(makeAccount(true)) + .put(fetchAccountFailed({ reason: 'ACCOUNT_DEACTIVATED' })) + .not.put.actionType(fetchAccountSucceeded.type) + .not.put.actionType(signedIn.type) + .silentRun() + }) +}) diff --git a/packages/common/src/store/account/sagas.ts b/packages/common/src/store/account/sagas.ts index 1a44491a176..6b0c0dec04d 100644 --- a/packages/common/src/store/account/sagas.ts +++ b/packages/common/src/store/account/sagas.ts @@ -216,6 +216,7 @@ export function* fetchAccountAsync({ reason: 'ACCOUNT_DEACTIVATED' }) ) + return } const guestEmailFromLocalStorage = yield* call( diff --git a/packages/web/src/common/store/pages/signon/sagas.ts b/packages/web/src/common/store/pages/signon/sagas.ts index e1d83ec3728..703363e5a34 100644 --- a/packages/web/src/common/store/pages/signon/sagas.ts +++ b/packages/web/src/common/store/pages/signon/sagas.ts @@ -839,6 +839,10 @@ function* signIn(action: ReturnType) { yield* put( make(Name.SIGN_IN_WITH_DEACTIVATED_ACCOUNT, { handle: user.handle }) ) + // The hedgehog login above already persisted a session. Clear it, or a + // refresh will restore the deactivated account via fetchAccount. + const authService = yield* getContext('authService') + yield* call([authService, authService.signOut]) yield* put(signOnActions.signInFailed('Account is deactivated')) yield* put(toastActions.toast({ content: messages.deactivatedAccount })) return