Skip to content

Commit 1afdeae

Browse files
committed
refactor(auth): consolidate logout logic and state cleanup
- Clears TanStack Query cache and cancels pending queries to prevent data leaks. - Ensures Intercom messenger is hidden and shut down on logout. - Resets organization state and removes associated data from local storage. - Enhances the logout function to support dependency injection for better testability. - Updates the logout page to use the centralized cleanup logic.
1 parent 358028a commit 1afdeae

3 files changed

Lines changed: 57 additions & 15 deletions

File tree

src/Exceptionless.Web/ClientApp/src/lib/features/auth/api.svelte.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { env } from '$env/dynamic/public';
22
import { getIntercomTokenSessionKey, intercomTokenRefreshIntervalMs } from '$features/intercom/config';
3+
import { organization } from '$features/organizations/context.svelte';
34
import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
4-
import { createQuery } from '@tanstack/svelte-query';
5+
import { hide as hideIntercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk';
6+
import { createQuery, type QueryClient } from '@tanstack/svelte-query';
57

68
import type { Login, TokenResult } from './models';
79

@@ -93,10 +95,23 @@ export async function login(email: string, password: string) {
9395
return response;
9496
}
9597

96-
export async function logout() {
97-
const client = useFetchClient();
98-
await client.get('auth/logout', { expectedStatusCodes: [200, 401] });
99-
accessToken.current = '';
98+
export async function logout(queryClient?: QueryClient, client = useFetchClient()) {
99+
await client.get('auth/logout', { expectedStatusCodes: [200, 401, 403] });
100+
101+
await queryClient?.cancelQueries();
102+
queryClient?.clear();
103+
104+
if (typeof window !== 'undefined' && 'Intercom' in window && typeof window.Intercom === 'function') {
105+
hideIntercom();
106+
shutdownIntercom();
107+
}
108+
109+
organization.current = undefined;
110+
if (typeof localStorage !== 'undefined') {
111+
localStorage.removeItem('organization');
112+
}
113+
114+
accessToken.current = null;
100115
}
101116

102117
export async function resetPassword(passwordResetToken: string, password: string) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FetchClient } from '@exceptionless/fetchclient';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { logout } from './api.svelte';
5+
6+
describe('logout', () => {
7+
beforeEach(() => {
8+
// Mock localStorage for server-side tests
9+
Object.defineProperty(globalThis, 'localStorage', {
10+
configurable: true,
11+
value: {
12+
removeItem: vi.fn()
13+
},
14+
writable: true
15+
});
16+
});
17+
18+
it('uses the provided client instance for the logout request', async () => {
19+
const mockClient = {
20+
get: vi.fn().mockResolvedValue({ ok: true, status: 200 }),
21+
isLoading: false
22+
} as unknown as FetchClient;
23+
24+
await logout(undefined, mockClient);
25+
26+
expect(mockClient.get).toHaveBeenCalledWith('auth/logout', { expectedStatusCodes: [200, 401, 403] });
27+
});
28+
});

src/Exceptionless.Web/ClientApp/src/routes/(auth)/logout/+page.svelte

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import { accessToken, logout } from '$features/auth/index.svelte';
1010
import { useFetchClientStatus } from '$shared/api/api.svelte';
1111
import { useFetchClient } from '@exceptionless/fetchclient';
12+
import { useQueryClient } from '@tanstack/svelte-query';
1213
13-
let isAuthenticated = $derived(accessToken.current !== null);
14+
let isAuthenticated = $derived(!!accessToken.current);
1415
1516
$effect(() => {
1617
if (!isAuthenticated) {
@@ -20,25 +21,23 @@
2021
2122
const client = useFetchClient();
2223
const clientStatus = useFetchClientStatus(client);
24+
const queryClient = useQueryClient();
2325
2426
let message = $state<string>();
25-
async function onLogout() {
27+
async function onLogout(event: SubmitEvent) {
28+
event.preventDefault();
29+
2630
if (client.isLoading) {
2731
return;
2832
}
2933
30-
const response = await client.get('auth/logout');
31-
if (response.ok) {
32-
await logout();
33-
await goto(resolve('/(auth)/login'));
34-
} else {
35-
message = 'An error occurred while logging out, please try again.';
36-
}
34+
await logout(queryClient, client);
35+
await goto(resolve('/(auth)/login'));
3736
}
3837
</script>
3938

4039
<Card.Root class="mx-auto w-sm">
41-
<Card.Header class="min-w-[382px]">
40+
<Card.Header class="min-w-95.5">
4241
<Logo />
4342
</Card.Header>
4443
<Card.Content>

0 commit comments

Comments
 (0)