mirror of
https://github.com/immich-app/immich.git
synced 2025-05-15 05:02:33 +02:00
refactor: event manager (#17862)
* refactor: event manager * refactor: event manager
This commit is contained in:
parent
e822e3eca9
commit
d0014bdf94
12 changed files with 127 additions and 47 deletions
e2e/src/web/specs
web/src
lib
components/shared-components/navigation-bar
stores
auth-manager.svelte.tsevent-manager.svelte.tsfolders.svelte.tsmemory.store.svelte.tssearch.svelte.tsuser.store.tsuser.svelte.tswebsocket.ts
utils
routes/auth/change-password
|
@ -25,7 +25,7 @@ test.describe('Registration', () => {
|
||||||
|
|
||||||
// login
|
// login
|
||||||
await expect(page).toHaveTitle(/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('Email').fill('admin@immich.app');
|
||||||
await page.getByLabel('Password').fill('password');
|
await page.getByLabel('Password').fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
@ -59,7 +59,7 @@ test.describe('Registration', () => {
|
||||||
await context.clearCookies();
|
await context.clearCookies();
|
||||||
|
|
||||||
// login
|
// login
|
||||||
await page.goto('/auth/login');
|
await page.goto('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('password');
|
await page.getByLabel('Password').fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
@ -72,7 +72,7 @@ test.describe('Registration', () => {
|
||||||
await page.getByRole('button', { name: 'Change password' }).click();
|
await page.getByRole('button', { name: 'Change password' }).click();
|
||||||
|
|
||||||
// login with new password
|
// 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('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('new-password');
|
await page.getByLabel('Password').fill('new-password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
|
|
@ -10,11 +10,13 @@
|
||||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
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 { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { userInteraction } from '$lib/stores/user.svelte';
|
import { userInteraction } from '$lib/stores/user.svelte';
|
||||||
import { handleLogout } from '$lib/utils/auth';
|
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||||
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
|
|
||||||
import { Button, IconButton } from '@immich/ui';
|
import { Button, IconButton } from '@immich/ui';
|
||||||
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
@ -23,8 +25,6 @@
|
||||||
import ThemeButton from '../theme-button.svelte';
|
import ThemeButton from '../theme-button.svelte';
|
||||||
import UserAvatar from '../user-avatar.svelte';
|
import UserAvatar from '../user-avatar.svelte';
|
||||||
import AccountInfoPanel from './account-info-panel.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 {
|
interface Props {
|
||||||
showUploadButton?: boolean;
|
showUploadButton?: boolean;
|
||||||
|
@ -38,11 +38,6 @@
|
||||||
let shouldShowHelpPanel = $state(false);
|
let shouldShowHelpPanel = $state(false);
|
||||||
let innerWidth: number = $state(0);
|
let innerWidth: number = $state(0);
|
||||||
|
|
||||||
const onLogout = async () => {
|
|
||||||
const { redirectUri } = await logout();
|
|
||||||
await handleLogout(redirectUri);
|
|
||||||
};
|
|
||||||
|
|
||||||
let info: ServerAboutResponseDto | undefined = $state();
|
let info: ServerAboutResponseDto | undefined = $state();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -183,7 +178,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shouldShowAccountInfoPanel}
|
{#if shouldShowAccountInfoPanel}
|
||||||
<AccountInfoPanel {onLogout} />
|
<AccountInfoPanel onLogout={() => authManager.logout()} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
33
web/src/lib/stores/auth-manager.svelte.ts
Normal file
33
web/src/lib/stores/auth-manager.svelte.ts
Normal file
|
@ -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();
|
54
web/src/lib/stores/event-manager.svelte.ts
Normal file
54
web/src/lib/stores/event-manager.svelte.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void;
|
||||||
|
|
||||||
|
class EventManager<EventMap extends Record<string, unknown[]>> {
|
||||||
|
private listeners: {
|
||||||
|
[K in keyof EventMap]?: {
|
||||||
|
listener: Listener<EventMap, K>;
|
||||||
|
once?: boolean;
|
||||||
|
}[];
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
on<T extends keyof EventMap>(key: T, listener: (...params: EventMap[T]) => void) {
|
||||||
|
return this.addListener(key, listener, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
once<T extends keyof EventMap>(key: T, listener: (...params: EventMap[T]) => void) {
|
||||||
|
return this.addListener(key, listener, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
off<K extends keyof EventMap>(key: K, listener: Listener<EventMap, K>) {
|
||||||
|
if (this.listeners[key]) {
|
||||||
|
this.listeners[key] = this.listeners[key].filter((item) => item.listener !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T extends keyof EventMap>(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<T extends keyof EventMap>(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': [];
|
||||||
|
}>();
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||||
import {
|
import {
|
||||||
getAssetsByOriginalPath,
|
getAssetsByOriginalPath,
|
||||||
getUniqueOriginalPaths,
|
getUniqueOriginalPaths,
|
||||||
|
@ -16,6 +17,10 @@ class FoldersStore {
|
||||||
uniquePaths = $state<string[]>([]);
|
uniquePaths = $state<string[]>([]);
|
||||||
assets = $state<AssetCache>({});
|
assets = $state<AssetCache>({});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
eventManager.on('auth.logout', () => this.clearCache());
|
||||||
|
}
|
||||||
|
|
||||||
async fetchUniquePaths() {
|
async fetchUniquePaths() {
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||||
import {
|
import {
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
|
@ -24,6 +25,10 @@ export type MemoryAsset = MemoryIndex & {
|
||||||
};
|
};
|
||||||
|
|
||||||
class MemoryStoreSvelte {
|
class MemoryStoreSvelte {
|
||||||
|
constructor() {
|
||||||
|
eventManager.on('auth.logout', () => this.clearCache());
|
||||||
|
}
|
||||||
|
|
||||||
memories = $state<MemoryResponseDto[]>([]);
|
memories = $state<MemoryResponseDto[]>([]);
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private memoryAssets = $derived.by(() => {
|
private memoryAssets = $derived.by(() => {
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||||
|
|
||||||
class SearchStore {
|
class SearchStore {
|
||||||
savedSearchTerms = $state<string[]>([]);
|
savedSearchTerms = $state<string[]>([]);
|
||||||
isSearchEnabled = $state(false);
|
isSearchEnabled = $state(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
eventManager.on('auth.logout', () => this.clearCache());
|
||||||
|
}
|
||||||
|
|
||||||
clearCache() {
|
clearCache() {
|
||||||
this.savedSearchTerms = [];
|
this.savedSearchTerms = [];
|
||||||
this.isSearchEnabled = false;
|
this.isSearchEnabled = false;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
|
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
@ -14,3 +15,5 @@ export const resetSavedUser = () => {
|
||||||
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
||||||
purchaseStore.setPurchaseStatus(false);
|
purchaseStore.setPurchaseStatus(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
eventManager.on('auth.logout', () => resetSavedUser());
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { eventManager } from '$lib/stores/event-manager.svelte';
|
||||||
import type {
|
import type {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
ServerAboutResponseDto,
|
ServerAboutResponseDto,
|
||||||
|
@ -19,8 +20,10 @@ const defaultUserInteraction: UserInteractions = {
|
||||||
serverInfo: undefined,
|
serverInfo: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetUserInteraction = () => {
|
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
Object.assign(userInteraction, defaultUserInteraction);
|
Object.assign(userInteraction, defaultUserInteraction);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
|
eventManager.on('auth.logout', () => reset());
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AppRoute } from '$lib/constants';
|
import { authManager } from '$lib/stores/auth-manager.svelte';
|
||||||
import { handleLogout } from '$lib/utils/auth';
|
|
||||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||||
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
|
@ -50,7 +49,7 @@ websocket
|
||||||
.on('disconnect', () => websocketStore.connected.set(false))
|
.on('disconnect', () => websocketStore.connected.set(false))
|
||||||
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
|
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
|
||||||
.on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
|
.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));
|
.on('connect_error', (e) => console.log('Websocket Connect Error', e));
|
||||||
|
|
||||||
export const openWebsocketConnection = () => {
|
export const openWebsocketConnection = () => {
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { browser } from '$app/environment';
|
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 { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
|
||||||
import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store';
|
import { userInteraction } from '$lib/stores/user.svelte';
|
||||||
import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte';
|
|
||||||
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
@ -91,19 +87,3 @@ export const getAccountAge = (): number => {
|
||||||
|
|
||||||
return Number(accountAge);
|
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { authManager } from '$lib/stores/auth-manager.svelte';
|
||||||
import { resetSavedUser, user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { logout, updateMyUser } from '@immich/sdk';
|
import { updateMyUser } from '@immich/sdk';
|
||||||
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
|
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
@ -25,9 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateMyUser({ userUpdateMeDto: { password } });
|
await updateMyUser({ userUpdateMeDto: { password } });
|
||||||
await goto(AppRoute.AUTH_LOGIN);
|
await authManager.logout();
|
||||||
resetSavedUser();
|
|
||||||
await logout();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue