refactor: auth manager (#27638)

This commit is contained in:
Jason Rasmussen
2026-04-14 08:49:24 -04:00
committed by GitHub
parent daed3f0966
commit 1ba0989e15
77 changed files with 387 additions and 379 deletions
@@ -1,4 +1,4 @@
import { user } from '$lib/stores/user.store';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
@@ -157,7 +157,7 @@ class ActivityManager {
const [liked] = await getActivities({
albumId,
assetId,
userId: get(user).id,
userId: authManager.user.id,
$type: ReactionType.Like,
level: assetId ? undefined : ReactionLevel.Album,
});
@@ -1,7 +1,8 @@
import { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { resetSavedUser, user } from '$lib/stores/user.store';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { AssetVisibility } from '@immich/sdk';
import { timelineAssetFactory } from '@test-data/factories/asset-factory';
import { preferencesFactory } from '@test-data/factories/preferences-factory';
import { userAdminFactory } from '@test-data/factories/user-factory';
describe('AssetMultiSelectManager', () => {
@@ -32,14 +33,15 @@ describe('AssetMultiSelectManager', () => {
const cleanup = $effect.root(() => {
expect(sut.isAllUserOwned).toBe(false);
user.set(user1);
authManager.setUser(user1);
authManager.setPreferences(preferencesFactory.build());
expect(sut.isAllUserOwned).toBe(true);
user.set(user2);
authManager.setUser(user2);
expect(sut.isAllUserOwned).toBe(false);
});
cleanup();
resetSavedUser();
authManager.reset();
});
});
@@ -1,17 +1,14 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { user } from '$lib/stores/user.store';
import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk';
import { AssetVisibility } from '@immich/sdk';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store';
export type AssetMultiSelectOptions = {
resetOnNavigate?: boolean;
};
export class AssetMultiSelectManager {
#selectedMap = new SvelteMap<string, TimelineAsset>();
#user = fromStore<UserAdminResponseDto | undefined>(user);
#userId = $derived(this.#user.current?.id);
selectAll = $state(false);
startAsset = $state<TimelineAsset | null>(null);
@@ -23,12 +20,16 @@ export class AssetMultiSelectManager {
selectionActive = $derived(this.#selectedMap.size > 0);
assets = $derived(Array.from(this.#selectedMap.values()));
ownedAssets = $derived(this.#userId ? this.assets.filter((asset) => asset.ownerId === this.#userId) : this.assets);
ownedAssets = $derived(
authManager.authenticated ? this.assets.filter((asset) => asset.ownerId === authManager.user.id) : this.assets,
);
isAllTrashed = $derived(this.assets.every((asset) => asset.isTrashed));
isAllArchived = $derived(this.assets.every((asset) => asset.visibility === AssetVisibility.Archive));
isAllFavorite = $derived(this.assets.every((asset) => asset.isFavorite));
isAllUserOwned = $derived(this.assets.every((asset) => asset.ownerId === this.#userId));
isAllUserOwned = $derived(
authManager.authenticated && this.assets.every((asset) => asset.ownerId === authManager.user.id),
);
#unsubscribe?: () => void;
@@ -44,7 +45,9 @@ export class AssetMultiSelectManager {
}
getOwnedAssets() {
return this.#userId ? this.assets.filter((asset) => asset.ownerId === this.#userId) : this.assets;
return authManager.authenticated
? this.assets.filter((asset) => asset.ownerId === authManager.user.id)
: this.assets;
}
hasSelectedAsset(assetId: string) {
+88 -11
View File
@@ -1,31 +1,86 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { Route } from '$lib/route';
import { isSharedLinkRoute } from '$lib/utils/navigation';
import { getAboutInfo, logout, type UserAdminResponseDto } from '@immich/sdk';
import {
getAboutInfo,
getMyPreferences,
getMyUser,
logout,
type UserAdminResponseDto,
type UserPreferencesResponseDto,
} from '@immich/sdk';
class AuthManager {
isPurchased = $state(false);
isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {});
constructor() {
eventManager.on({
AuthUserLoaded: (user) => this.onAuthUserLoaded(user),
});
#user = $state<UserAdminResponseDto>();
#preferences = $state<UserPreferencesResponseDto>();
get authenticated() {
return !!(this.#user && this.#preferences);
}
private async onAuthUserLoaded(user: UserAdminResponseDto) {
if (user.license?.activatedAt) {
authManager.isPurchased = true;
get user() {
if (!this.#user) {
throw new TypeError('AuthManager.user is undefined');
}
return this.#user;
}
get preferences() {
if (!this.#preferences) {
throw new TypeError('AuthManager.preferences is undefined');
}
return this.#preferences;
}
async load() {
if (authManager.authenticated) {
return;
}
const serverInfo = await getAboutInfo().catch(() => undefined);
if (serverInfo?.licensed) {
authManager.isPurchased = true;
if (!this.#hasAuthCookie()) {
return;
}
return this.refresh();
}
async refresh() {
try {
const [user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
this.#preferences = preferences;
this.#user = user;
if (user.license?.activatedAt) {
this.isPurchased = true;
} else {
// check server status
const serverInfo = await getAboutInfo().catch(() => {});
if (serverInfo?.licensed) {
this.isPurchased = true;
}
}
eventManager.emit('AuthUserLoaded', user);
} catch {
// noop
}
}
setUser(user: UserAdminResponseDto) {
this.#user = user;
}
setPreferences(preferences: UserPreferencesResponseDto) {
this.#preferences = preferences;
}
async logout() {
@@ -50,9 +105,31 @@ class AuthManager {
}
} finally {
this.isPurchased = false;
this.reset();
eventManager.emit('AuthLogout');
}
}
reset() {
this.#user = undefined;
this.#preferences = undefined;
}
#hasAuthCookie() {
if (!browser) {
return;
}
for (const cookie of document.cookie.split('; ')) {
const [name] = cookie.split('=');
if (name === 'immich_is_authenticated') {
return true;
}
}
return false;
}
}
export const authManager = new AuthManager();
@@ -1,11 +1,10 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { user } from '$lib/stores/user.store';
import { asLocalTimeISO } from '$lib/utils/date-time';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
import { DateTime } from 'luxon';
import { get } from 'svelte/store';
type MemoryIndex = {
memoryIndex: number;
@@ -31,7 +30,7 @@ class MemoryManager {
});
// loaded event might have already happened
if (get(user)) {
if (authManager.authenticated) {
void this.initialize();
}
}