From a3478ace447d0a9c99c8266e3c0723c0ae29d5f5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 24 Mar 2026 09:15:06 -0400 Subject: [PATCH] fix(ui): guard auth success on registered account --- ui/src/hooks/use-cliproxy-auth-flow.ts | 26 +++++++++++- .../hooks/use-cliproxy-auth-flow.test.tsx | 42 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index a2a9b3bd..34431809 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -133,6 +133,7 @@ export function useCliproxyAuthFlow() { const data = (await response.json()) as { status?: string; error?: string; + account?: unknown; url?: string; auth_url?: string; verification_url?: string; @@ -141,8 +142,22 @@ export function useCliproxyAuthFlow() { pollFailureCountRef.current = 0; if (data.status === 'ok') { + const hasAccount = typeof data.account === 'object' && data.account !== null; + if (!hasAccount) { + stopPolling(); + const errorMsg = 'Authenticated account could not be registered'; + toast.error(errorMsg); + setState((prev) => ({ + ...prev, + isAuthenticating: false, + error: errorMsg, + })); + return; + } + stopPolling(); queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] }); queryClient.invalidateQueries({ queryKey: ['account-quota'] }); toast.success(`${provider} authentication successful`); openedAuthUrlRef.current = false; @@ -270,8 +285,10 @@ export function useCliproxyAuthFlow() { return; } const success = data.success === true; - if (response.ok && success) { + const hasAccount = typeof data.account === 'object' && data.account !== null; + if (response.ok && success && hasAccount) { queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] }); queryClient.invalidateQueries({ queryKey: ['account-quota'] }); // Note: No toast here - DeviceCodeDialog's useDeviceCode hook handles success toast // via deviceCodeCompleted WebSocket event to avoid duplicate toasts @@ -279,7 +296,11 @@ export function useCliproxyAuthFlow() { setState(INITIAL_STATE); } else { const errorMsg = - typeof data.error === 'string' ? data.error : 'Authentication failed'; + typeof data.error === 'string' + ? data.error + : success + ? 'Authenticated account could not be registered' + : 'Authentication failed'; toast.error(errorMsg); setState((prev) => ({ ...prev, @@ -416,6 +437,7 @@ export function useCliproxyAuthFlow() { if (response.ok && success && hasAccount) { stopPolling(); queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] }); queryClient.invalidateQueries({ queryKey: ['account-quota'] }); toast.success(`${currentProvider} authentication successful`); setState(INITIAL_STATE); diff --git a/ui/tests/unit/hooks/use-cliproxy-auth-flow.test.tsx b/ui/tests/unit/hooks/use-cliproxy-auth-flow.test.tsx index 8498ce9f..1ad319b1 100644 --- a/ui/tests/unit/hooks/use-cliproxy-auth-flow.test.tsx +++ b/ui/tests/unit/hooks/use-cliproxy-auth-flow.test.tsx @@ -196,4 +196,46 @@ describe('useCliproxyAuthFlow', () => { expect(toast.error).toHaveBeenCalledWith('Authenticated account could not be registered'); expect(toast.success).not.toHaveBeenCalled(); }); + + it('treats status ok responses without an account as failures', async () => { + vi.stubGlobal( + 'fetch', + vi.fn((input: RequestInfo | URL) => { + const url = String(input); + + if (url.includes('/start-url')) { + return Promise.resolve( + createJsonResponse({ + success: true, + authUrl: 'https://auth.example/status-only', + state: 'state-status-only', + }) + ); + } + + if (url.includes('/status?state=state-status-only')) { + return Promise.resolve(createJsonResponse({ status: 'ok' })); + } + + return Promise.reject(new Error(`Unexpected fetch: ${url}`)); + }) + ); + + const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper }); + + await act(async () => { + await result.current.startAuth('codex', { startEndpoint: 'start-url' }); + }); + + await act(async () => { + vi.advanceTimersByTime(3000); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(result.current.error).toBe('Authenticated account could not be registered'); + expect(result.current.isAuthenticating).toBe(false); + expect(toast.error).toHaveBeenCalledWith('Authenticated account could not be registered'); + expect(toast.success).not.toHaveBeenCalled(); + }); });