diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts
index e89f17a4e9c..74bee64e0a9 100644
--- a/e2e/src/web/specs/auth.e2e-spec.ts
+++ b/e2e/src/web/specs/auth.e2e-spec.ts
@@ -25,7 +25,7 @@ test.describe('Registration', () => {
// login
await expect(page).toHaveTitle(/Login/);
- await page.goto('/auth/login');
+ await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('admin@immich.app');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
@@ -59,7 +59,7 @@ test.describe('Registration', () => {
await context.clearCookies();
// login
- await page.goto('/auth/login');
+ await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
@@ -72,7 +72,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Change password' }).click();
// login with new password
- await expect(page).toHaveURL('/auth/login');
+ await expect(page).toHaveURL('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click();
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
index f7f9b877f34..90f6b3c55b1 100644
--- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
@@ -10,11 +10,13 @@
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute } from '$lib/constants';
+ import { authManager } from '$lib/stores/auth-manager.svelte';
+ import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
+ import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { user } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
- import { handleLogout } from '$lib/utils/auth';
- import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
+ import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui';
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
import { onMount } from 'svelte';
@@ -23,8 +25,6 @@
import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.svelte';
- import { sidebarStore } from '$lib/stores/sidebar.svelte';
- import { mobileDevice } from '$lib/stores/mobile-device.svelte';
interface Props {
showUploadButton?: boolean;
@@ -38,11 +38,6 @@
let shouldShowHelpPanel = $state(false);
let innerWidth: number = $state(0);
- const onLogout = async () => {
- const { redirectUri } = await logout();
- await handleLogout(redirectUri);
- };
-
let info: ServerAboutResponseDto | undefined = $state();
onMount(async () => {
@@ -183,7 +178,7 @@
{/if}
{#if shouldShowAccountInfoPanel}
-
+ authManager.logout()} />
{/if}
diff --git a/web/src/lib/stores/auth-manager.svelte.ts b/web/src/lib/stores/auth-manager.svelte.ts
new file mode 100644
index 00000000000..72c966df0bf
--- /dev/null
+++ b/web/src/lib/stores/auth-manager.svelte.ts
@@ -0,0 +1,33 @@
+import { goto } from '$app/navigation';
+import { AppRoute } from '$lib/constants';
+import { eventManager } from '$lib/stores/event-manager.svelte';
+import { logout } from '@immich/sdk';
+
+class AuthManager {
+ async logout() {
+ let redirectUri;
+
+ try {
+ const response = await logout();
+ if (response.redirectUri) {
+ redirectUri = response.redirectUri;
+ }
+ } catch (error) {
+ console.log('Error logging out:', error);
+ }
+
+ redirectUri = redirectUri ?? AppRoute.AUTH_LOGIN;
+
+ try {
+ if (redirectUri.startsWith('/')) {
+ await goto(redirectUri);
+ } else {
+ globalThis.location.href = redirectUri;
+ }
+ } finally {
+ eventManager.emit('auth.logout');
+ }
+ }
+}
+
+export const authManager = new AuthManager();
diff --git a/web/src/lib/stores/event-manager.svelte.ts b/web/src/lib/stores/event-manager.svelte.ts
new file mode 100644
index 00000000000..09e9b45c3cd
--- /dev/null
+++ b/web/src/lib/stores/event-manager.svelte.ts
@@ -0,0 +1,54 @@
+type Listener, K extends keyof EventMap> = (...params: EventMap[K]) => void;
+
+class EventManager> {
+ private listeners: {
+ [K in keyof EventMap]?: {
+ listener: Listener;
+ once?: boolean;
+ }[];
+ } = {};
+
+ on(key: T, listener: (...params: EventMap[T]) => void) {
+ return this.addListener(key, listener, false);
+ }
+
+ once(key: T, listener: (...params: EventMap[T]) => void) {
+ return this.addListener(key, listener, true);
+ }
+
+ off(key: K, listener: Listener) {
+ if (this.listeners[key]) {
+ this.listeners[key] = this.listeners[key].filter((item) => item.listener !== listener);
+ }
+
+ return this;
+ }
+
+ emit(key: T, ...params: EventMap[T]) {
+ if (!this.listeners[key]) {
+ return;
+ }
+
+ for (const { listener } of this.listeners[key]) {
+ listener(...params);
+ }
+
+ // remove one time listeners
+ this.listeners[key] = this.listeners[key].filter((item) => !item.once);
+ }
+
+ private addListener(key: T, listener: (...params: EventMap[T]) => void, once: boolean) {
+ if (!this.listeners[key]) {
+ this.listeners[key] = [];
+ }
+
+ this.listeners[key].push({ listener, once });
+
+ return this;
+ }
+}
+
+export const eventManager = new EventManager<{
+ 'user.login': [];
+ 'auth.logout': [];
+}>();
diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts
index fb59687a381..c6fc7808b2d 100644
--- a/web/src/lib/stores/folders.svelte.ts
+++ b/web/src/lib/stores/folders.svelte.ts
@@ -1,3 +1,4 @@
+import { eventManager } from '$lib/stores/event-manager.svelte';
import {
getAssetsByOriginalPath,
getUniqueOriginalPaths,
@@ -16,6 +17,10 @@ class FoldersStore {
uniquePaths = $state([]);
assets = $state({});
+ constructor() {
+ eventManager.on('auth.logout', () => this.clearCache());
+ }
+
async fetchUniquePaths() {
if (this.initialized) {
return;
diff --git a/web/src/lib/stores/memory.store.svelte.ts b/web/src/lib/stores/memory.store.svelte.ts
index 7173b43d06b..ef3f87a3aa8 100644
--- a/web/src/lib/stores/memory.store.svelte.ts
+++ b/web/src/lib/stores/memory.store.svelte.ts
@@ -1,3 +1,4 @@
+import { eventManager } from '$lib/stores/event-manager.svelte';
import { asLocalTimeISO } from '$lib/utils/date-time';
import {
type AssetResponseDto,
@@ -24,6 +25,10 @@ export type MemoryAsset = MemoryIndex & {
};
class MemoryStoreSvelte {
+ constructor() {
+ eventManager.on('auth.logout', () => this.clearCache());
+ }
+
memories = $state([]);
private initialized = false;
private memoryAssets = $derived.by(() => {
diff --git a/web/src/lib/stores/search.svelte.ts b/web/src/lib/stores/search.svelte.ts
index 7d012922ca0..f334f534600 100644
--- a/web/src/lib/stores/search.svelte.ts
+++ b/web/src/lib/stores/search.svelte.ts
@@ -1,7 +1,13 @@
+import { eventManager } from '$lib/stores/event-manager.svelte';
+
class SearchStore {
savedSearchTerms = $state([]);
isSearchEnabled = $state(false);
+ constructor() {
+ eventManager.on('auth.logout', () => this.clearCache());
+ }
+
clearCache() {
this.savedSearchTerms = [];
this.isSearchEnabled = false;
diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts
index 5bffc08b803..fe2288c252c 100644
--- a/web/src/lib/stores/user.store.ts
+++ b/web/src/lib/stores/user.store.ts
@@ -1,3 +1,4 @@
+import { eventManager } from '$lib/stores/event-manager.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store';
@@ -14,3 +15,5 @@ export const resetSavedUser = () => {
preferences.set(undefined as unknown as UserPreferencesResponseDto);
purchaseStore.setPurchaseStatus(false);
};
+
+eventManager.on('auth.logout', () => resetSavedUser());
diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts
index 71b2cdd847c..093d90e4b5f 100644
--- a/web/src/lib/stores/user.svelte.ts
+++ b/web/src/lib/stores/user.svelte.ts
@@ -1,3 +1,4 @@
+import { eventManager } from '$lib/stores/event-manager.svelte';
import type {
AlbumResponseDto,
ServerAboutResponseDto,
@@ -19,8 +20,10 @@ const defaultUserInteraction: UserInteractions = {
serverInfo: undefined,
};
-export const resetUserInteraction = () => {
+export const userInteraction = $state(defaultUserInteraction);
+
+const reset = () => {
Object.assign(userInteraction, defaultUserInteraction);
};
-export const userInteraction = $state(defaultUserInteraction);
+eventManager.on('auth.logout', () => reset());
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index d398ca52a9d..90228a5cbde 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -1,5 +1,4 @@
-import { AppRoute } from '$lib/constants';
-import { handleLogout } from '$lib/utils/auth';
+import { authManager } from '$lib/stores/auth-manager.svelte';
import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client';
@@ -50,7 +49,7 @@ websocket
.on('disconnect', () => websocketStore.connected.set(false))
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
.on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
- .on('on_session_delete', () => handleLogout(AppRoute.AUTH_LOGIN))
+ .on('on_session_delete', () => authManager.logout())
.on('connect_error', (e) => console.log('Websocket Connect Error', e));
export const openWebsocketConnection = () => {
diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts
index 22b92dd988e..9b78c345e2a 100644
--- a/web/src/lib/utils/auth.ts
+++ b/web/src/lib/utils/auth.ts
@@ -1,11 +1,7 @@
import { browser } from '$app/environment';
-import { goto } from '$app/navigation';
-import { foldersStore } from '$lib/stores/folders.svelte';
-import { memoryStore } from '$lib/stores/memory.store.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
-import { searchStore } from '$lib/stores/search.svelte';
-import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store';
-import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte';
+import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
+import { userInteraction } from '$lib/stores/user.svelte';
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon';
@@ -91,19 +87,3 @@ export const getAccountAge = (): number => {
return Number(accountAge);
};
-
-export const handleLogout = async (redirectUri: string) => {
- try {
- if (redirectUri.startsWith('/')) {
- await goto(redirectUri);
- } else {
- globalThis.location.href = redirectUri;
- }
- } finally {
- resetSavedUser();
- resetUserInteraction();
- foldersStore.clearCache();
- memoryStore.clearCache();
- searchStore.clearCache();
- }
-};
diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte
index 33d354552e8..16a6ffc6773 100644
--- a/web/src/routes/auth/change-password/+page.svelte
+++ b/web/src/routes/auth/change-password/+page.svelte
@@ -1,9 +1,8 @@