diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts index 88f6b375..0a4b5389 100644 --- a/apps/server/src/lib/auth.ts +++ b/apps/server/src/lib/auth.ts @@ -262,7 +262,7 @@ export function getSessionCookieOptions(): { return { httpOnly: true, // JavaScript cannot access this cookie secure: process.env.NODE_ENV === 'production', // HTTPS only in production - sameSite: 'lax', // Sent on same-site requests including cross-origin fetches + sameSite: 'lax', // Sent for same-site requests and top-level navigations, but not cross-origin fetch/XHR maxAge: SESSION_MAX_AGE_MS, path: '/', }; diff --git a/apps/server/src/routes/auth/index.ts b/apps/server/src/routes/auth/index.ts index 9c838b58..e4ff2c45 100644 --- a/apps/server/src/routes/auth/index.ts +++ b/apps/server/src/routes/auth/index.ts @@ -233,10 +233,7 @@ export function createAuthRoutes(): Router { // Using res.cookie() with maxAge: 0 is more reliable than clearCookie() // in cross-origin development environments res.cookie(cookieName, '', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', + ...getSessionCookieOptions(), maxAge: 0, expires: new Date(0), }); diff --git a/apps/server/tests/unit/lib/auth.test.ts b/apps/server/tests/unit/lib/auth.test.ts index 70f50def..8708062f 100644 --- a/apps/server/tests/unit/lib/auth.test.ts +++ b/apps/server/tests/unit/lib/auth.test.ts @@ -277,7 +277,7 @@ describe('auth.ts', () => { const options = getSessionCookieOptions(); expect(options.httpOnly).toBe(true); - expect(options.sameSite).toBe('strict'); + expect(options.sameSite).toBe('lax'); expect(options.path).toBe('/'); expect(options.maxAge).toBeGreaterThan(0); }); diff --git a/apps/ui/src/app.tsx b/apps/ui/src/app.tsx index 57a7d08f..31a71e85 100644 --- a/apps/ui/src/app.tsx +++ b/apps/ui/src/app.tsx @@ -3,7 +3,6 @@ import { RouterProvider } from '@tanstack/react-router'; import { createLogger } from '@automaker/utils/logger'; import { router } from './utils/router'; import { SplashScreen } from './components/splash-screen'; -import { LoadingState } from './components/ui/loading-state'; import { useSettingsSync } from './hooks/use-settings-sync'; import { useCursorStatusInit } from './hooks/use-cursor-status-init'; import './styles/global.css'; diff --git a/apps/ui/src/components/views/login-view.tsx b/apps/ui/src/components/views/login-view.tsx index 94b83c35..4d436f09 100644 --- a/apps/ui/src/components/views/login-view.tsx +++ b/apps/ui/src/components/views/login-view.tsx @@ -125,14 +125,25 @@ async function checkAuthStatusSafe(): Promise<{ authenticated: boolean }> { */ async function checkServerAndSession( dispatch: React.Dispatch, - setAuthState: (state: { isAuthenticated: boolean; authChecked: boolean }) => void + setAuthState: (state: { isAuthenticated: boolean; authChecked: boolean }) => void, + signal?: AbortSignal ): Promise { for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + // Return early if the component has unmounted + if (signal?.aborted) { + return; + } + dispatch({ type: 'SERVER_CHECK_RETRY', attempt }); try { const result = await checkAuthStatusSafe(); + // Return early if the component has unmounted + if (signal?.aborted) { + return; + } + if (result.authenticated) { // Server is reachable and we're authenticated setAuthState({ isAuthenticated: true, authChecked: true }); @@ -148,10 +159,13 @@ async function checkServerAndSession( console.debug(`Server check attempt ${attempt}/${MAX_RETRIES} failed:`, error); if (attempt === MAX_RETRIES) { - dispatch({ - type: 'SERVER_ERROR', - message: 'Unable to connect to server. Please check that the server is running.', - }); + // Return early if the component has unmounted + if (!signal?.aborted) { + dispatch({ + type: 'SERVER_ERROR', + message: 'Unable to connect to server. Please check that the server is running.', + }); + } return; } @@ -225,7 +239,12 @@ export function LoginView() { if (initialCheckDone.current) return; initialCheckDone.current = true; - checkServerAndSession(dispatch, setAuthState); + const controller = new AbortController(); + checkServerAndSession(dispatch, setAuthState, controller.signal); + + return () => { + controller.abort(); + }; }, [setAuthState]); // When we enter checking_setup phase, check setup status @@ -255,7 +274,8 @@ export function LoginView() { const handleRetry = () => { initialCheckDone.current = false; dispatch({ type: 'RETRY_SERVER_CHECK' }); - checkServerAndSession(dispatch, setAuthState); + const controller = new AbortController(); + checkServerAndSession(dispatch, setAuthState, controller.signal); }; // =============================================================================