diff --git a/src/lib/app/auth.svelte.ts b/src/lib/app/auth.svelte.ts index 1e3d5dcb..18552fa5 100644 --- a/src/lib/app/auth.svelte.ts +++ b/src/lib/app/auth.svelte.ts @@ -288,8 +288,9 @@ class Profile { * Remove a profile by calling the server logout endpoint. * The server handles token cleanup and session management. * - * @returns LogoutResult indicating success/failure and any warnings - * @throws Error if the server logout fails (local state is NOT cleared) + * @returns LogoutResult indicating success/failure and any warnings. + * Local state is NOT cleared if the server logout fails, except on 401 + * (session already expired server-side), which is treated as logged out. */ async remove(id: string): Promise { const profileToRemove = this.meta.profiles.find((p) => p.id === id) @@ -314,7 +315,11 @@ class Profile { } } - if (!response.ok) { + if (response.status === 401) { + // Session already gone on the server — treat logout as complete and + // fall through to clear local state, otherwise the profile is stuck. + console.warn('[auth] Session already expired; clearing local profile') + } else if (!response.ok) { // Server returned an error - don't clear local state let errorMsg = `Server returned status ${response.status}` try { diff --git a/src/routes/api/auth/auth.test.ts b/src/routes/api/auth/auth.test.ts index c2f1ea72..1150a95f 100644 --- a/src/routes/api/auth/auth.test.ts +++ b/src/routes/api/auth/auth.test.ts @@ -731,7 +731,7 @@ describe('POST /api/auth/logout', () => { vi.clearAllMocks() }) - it('returns 401 if not authenticated', async () => { + it('succeeds idempotently and clears cookie if not authenticated', async () => { const cookies = createMockCookies() const event = createMockEvent({ @@ -741,7 +741,11 @@ describe('POST /api/auth/logout', () => { }) const response = await logoutHandler(event) - expect(response.status).toBe(401) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.success).toBe(true) + expect(cookies.delete).toHaveBeenCalledWith('coves_session', { path: '/' }) }) it('returns 403 for cross-origin requests', async () => { diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts index 4fae5610..f4c63005 100644 --- a/src/routes/api/auth/logout/+server.ts +++ b/src/routes/api/auth/logout/+server.ts @@ -26,7 +26,10 @@ export const POST: RequestHandler = async ({ } if (!locals.auth.authenticated) { - return json({ error: 'Not authenticated' }, { status: 401 }) + // Session already expired or invalid — logout is idempotent. Clear any + // stale cookie and report success so the client can drop its local state. + cookies.delete('coves_session', { path: '/' }) + return json({ success: true, session: null }) } // Call Go /oauth/logout to revoke the session on the backend (best effort).