This commit is contained in:
Jason Rasmussen 2026-08-15 13:39:58 +02:00 committed by GitHub
commit 6fc48627b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 264 additions and 230 deletions

View file

@ -2,14 +2,12 @@
import { page } from '$app/state';
import { focusTrap } from '$lib/actions/focus-trap';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
import AvatarEditModal from '$lib/modals/AvatarEditModal.svelte';
import HelpAndFeedbackModal from '$lib/modals/HelpAndFeedbackModal.svelte';
import { Route } from '$lib/route';
import { userInteraction } from '$lib/stores/user.svelte';
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, Icon, IconButton, modalManager } from '@immich/ui';
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import UserAvatar from '../UserAvatar.svelte';
@ -20,11 +18,7 @@
let { onClose }: Props = $props();
let info: ServerAboutResponseDto | undefined = $state();
onMount(async () => {
info = userInteraction.aboutInfo ?? (await getAboutInfo());
});
const info = $derived(serverManager.about);
</script>
<div

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { ImmichProduct } from '$lib/constants';
import { getLicenseLink as getProductLink } from '$lib/utils/license-utils';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { Button, Icon } from '@immich/ui';
import { mdiAccount, mdiCheckCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -38,6 +38,8 @@
</div>
</div>
<Button shape="round" href={getProductLink(ImmichProduct.Client)} fullWidth>{$t('purchase_button_select')}</Button>
<Button shape="round" href={licenseManager.asHref(ImmichProduct.Client)} fullWidth
>{$t('purchase_button_select')}</Button
>
</div>
</div>

View file

@ -1,19 +1,17 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
import { Button, Heading, LoadingSpinner } from '@immich/ui';
import { t } from 'svelte-i18n';
import UserPurchaseOptionCard from './IndividualPurchaseOptionCard.svelte';
import ServerPurchaseOptionCard from './ServerPurchaseOptionCard.svelte';
interface Props {
onActivate: () => void;
showTitle?: boolean;
showMessage?: boolean;
}
let { onActivate, showTitle = true, showMessage = true }: Props = $props();
let { showTitle = true, showMessage = true }: Props = $props();
let productKey = $state('');
let isLoading = $state(false);
@ -22,11 +20,7 @@
productKey = productKey.trim();
isLoading = true;
const activationKey = await getActivationKey(productKey);
await activateProduct(productKey, activationKey);
onActivate();
authManager.isPurchased = true;
await licenseManager.activate(productKey);
} catch (error) {
handleError(error, $t('purchase_failed_activation'));
} finally {

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { ImmichProduct } from '$lib/constants';
import { getLicenseLink } from '$lib/utils/license-utils';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { Button, Icon } from '@immich/ui';
import { mdiCheckCircleOutline, mdiServer } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -38,6 +38,8 @@
</div>
</div>
<Button shape="round" href={getLicenseLink(ImmichProduct.Server)} fullWidth>{$t('purchase_button_select')}</Button>
<Button shape="round" href={licenseManager.asHref(ImmichProduct.Server)} fullWidth
>{$t('purchase_button_select')}</Button
>
</div>
</div>

View file

@ -3,6 +3,7 @@
import { OpenQueryParam } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import PurchaseModal from '$lib/modals/PurchaseModal.svelte';
import { Route } from '$lib/route';
import { getAccountAge } from '$lib/utils/auth';
@ -70,7 +71,7 @@
</script>
<div class="license-status ps-4 text-sm">
{#if authManager.isPurchased && authManager.preferences.purchase.showSupportBadge}
{#if licenseManager.license && authManager.preferences.purchase.showSupportBadge}
<button
onclick={() => goto(Route.userSettings({ isOpen: OpenQueryParam.PURCHASE_SETTINGS }))}
class="mt-2 w-full"
@ -78,7 +79,7 @@
>
<SupporterBadge size="small" effect="always" />
</button>
{:else if !authManager.isPurchased && showBuyButton && getAccountAge() > 14}
{:else if !licenseManager.license && showBuyButton && getAccountAge() > 14}
<button
type="button"
onclick={openPurchaseModal}

View file

@ -1,18 +1,12 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { releaseManager } from '$lib/managers/release-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
import { userInteraction } from '$lib/stores/user.svelte';
import { websocketStore } from '$lib/stores/websocket';
import { semverToName } from '$lib/utils';
import { requestServerInfo } from '$lib/utils/auth';
import {
getAboutInfo,
getVersionHistory,
type ReleaseEventV1,
type ServerAboutResponseDto,
type ServerVersionHistoryResponseDto,
} from '@immich/sdk';
import { getVersionHistory, type ReleaseEventV1, type ServerVersionHistoryResponseDto } from '@immich/sdk';
import { Icon, modalManager, Text } from '@immich/ui';
import { mdiAlert, mdiNewBox } from '@mdi/js';
import { onMount } from 'svelte';
@ -20,21 +14,19 @@
const { serverVersion, connected } = websocketStore;
let info: ServerAboutResponseDto | undefined = $state();
let versions: ServerVersionHistoryResponseDto[] = $state([]);
let versions: ServerVersionHistoryResponseDto[] = $state(userInteraction.versions ?? []);
onMount(async () => {
if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) {
info = userInteraction.aboutInfo;
versions = userInteraction.versions;
if (userInteraction.versions && $serverVersion) {
return;
}
await requestServerInfo();
[info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]);
userInteraction.aboutInfo = info;
versions = await getVersionHistory();
userInteraction.versions = versions;
});
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
let isMain = $derived(
serverManager.about?.sourceRef === 'main' && serverManager.about.repository === 'immich-app/immich',
);
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
const getReleaseInfo = (release?: ReleaseEventV1) => {
@ -74,11 +66,12 @@
{#if $connected && version}
<button
type="button"
onclick={() => info && modalManager.show(ServerAboutModal, { versions, info })}
onclick={() =>
serverManager.about && modalManager.show(ServerAboutModal, { versions, info: serverManager.about })}
class="flex place-content-center place-items-center gap-1 dark:text-immich-gray"
>
{#if isMain}
<Icon icon={mdiAlert} size="1.5em" color="#ffcc4d" /> {info?.sourceRef}
<Icon icon={mdiAlert} size="1.5em" color="#ffcc4d" /> {serverManager.about?.sourceRef}
{:else}
{version}
{/if}

View file

@ -1,36 +1,25 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { requestServerInfo } from '$lib/utils/auth';
import { getByteUnitString } from '$lib/utils/byte-units';
import { LoadingSpinner, Meter } from '@immich/ui';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
let hasQuota = $derived(authManager.user.quotaSizeInBytes !== null);
let availableBytes = $derived(
(hasQuota && authManager.authenticated
? authManager.user.quotaSizeInBytes
: userInteraction.serverInfo?.diskSizeRaw) || 0,
(hasQuota && authManager.authenticated ? authManager.user.quotaSizeInBytes : serverManager.storage?.diskSizeRaw) ||
0,
);
let usedBytes = $derived(
(hasQuota && authManager.authenticated
? authManager.user.quotaUsageInBytes
: userInteraction.serverInfo?.diskUseRaw) || 0,
(hasQuota && authManager.authenticated ? authManager.user.quotaUsageInBytes : serverManager.storage?.diskUseRaw) ||
0,
);
const thresholds = [
{ from: 0.8, className: 'bg-warning' },
{ from: 0.95, className: 'bg-danger' },
];
onMount(async () => {
if (userInteraction.serverInfo && authManager.authenticated) {
return;
}
await requestServerInfo();
});
</script>
<div
@ -42,7 +31,7 @@
},
})}
>
{#if userInteraction.serverInfo}
{#if serverManager.storage}
<Meter
size="tiny"
class="bg-light-200"

View file

@ -1,5 +1,4 @@
import {
getAboutInfo,
getMyPreferences,
getMyUser,
logout,
@ -14,7 +13,6 @@ import { Route } from '$lib/route';
import { isSharedLinkRoute } from '$lib/utils/navigation';
class AuthManager {
isPurchased = $state(false);
isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {});
@ -65,16 +63,6 @@ class AuthManager {
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
@ -102,8 +90,6 @@ class AuthManager {
}
if (redirectUri.startsWith('/')) {
this.isPurchased = false;
this.reset();
eventManager.emit('AuthLogout');

View file

@ -30,6 +30,8 @@ export type Events = {
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
LicenseActivated: [];
ApiKeyCreate: [ApiKeyResponseDto];
ApiKeyUpdate: [ApiKeyResponseDto];
ApiKeyDelete: [ApiKeyResponseDto];

View file

@ -0,0 +1,117 @@
import {
deleteServerLicense,
deleteUserLicense,
getMyUser,
getServerLicense,
isHttpError,
setServerLicense,
setUserLicense,
type LicenseResponseDto,
} from '@immich/sdk';
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
import { ImmichProduct } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
export type License = { type: ImmichProduct; licenseKey?: string; activatedAt?: string };
class LicenseManager {
#serverLicense = $state<LicenseResponseDto>();
#userLicense = $derived(authManager.authenticated ? authManager.user.license : undefined);
license = $derived.by<License | undefined>(() => {
if (this.#serverLicense) {
return { type: ImmichProduct.Server, ...this.#serverLicense };
}
if (serverManager.about?.licensed) {
return { type: ImmichProduct.Server };
}
if (this.#userLicense?.activatedAt) {
return { type: ImmichProduct.Client, ...this.#userLicense };
}
});
constructor() {
eventManager.on({
AuthLogout: () => this.reset(),
});
}
async load() {
await serverManager.ready();
if (this.license) {
await this.refresh();
}
}
async activate(licenseKey: string, activationKey?: string | null) {
const isServerActivation = authManager.user.isAdmin && licenseKey.includes('IMSV');
const licenseKeyDto = { licenseKey, activationKey: activationKey || (await this.#getActivationKey(licenseKey)) };
// Send server key to user activation if user is not admin
const response = isServerActivation
? await setServerLicense({ licenseKeyDto })
: await setUserLicense({ licenseKeyDto });
await this.refresh();
eventManager.emit('LicenseActivated');
return response;
}
async refresh() {
const [user] = await Promise.all([getMyUser(), serverManager.load()]);
authManager.setUser(user);
this.#serverLicense = user.isAdmin && serverManager.about?.licensed ? await this.#getServerLicense() : undefined;
}
async removeUserLicense() {
await deleteUserLicense();
authManager.setUser({ ...authManager.user, license: null });
}
async removeServerLicense() {
await deleteServerLicense();
this.#serverLicense = undefined;
await serverManager.load();
}
reset() {
this.#serverLicense = undefined;
}
async #getActivationKey(licenseKey: string) {
const response = await fetch(new URL(`/api/v1/activate/${licenseKey}`, PUBLIC_IMMICH_PAY_HOST).href);
if (!response.ok) {
throw new Error('Failed to fetch activation key');
}
return response.text();
}
async #getServerLicense() {
try {
return await getServerLicense();
} catch (error) {
if (isHttpError(error) && error.status === 404) {
return;
}
throw error;
}
}
asHref(product: ImmichProduct) {
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
url.searchParams.append('productId', product);
url.searchParams.append('instanceUrl', serverConfigManager.value.externalDomain || globalThis.origin);
return url.href;
}
}
export const licenseManager = new LicenseManager();

View file

@ -0,0 +1,57 @@
import { getAboutInfo, getStorage, type ServerAboutResponseDto, type ServerStorageResponseDto } from '@immich/sdk';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
class ServerManager {
#about = $state<ServerAboutResponseDto>();
#storage = $state<ServerStorageResponseDto>();
#loading: Promise<void> | undefined;
get about() {
return this.#about;
}
get storage() {
return this.#storage;
}
constructor() {
eventManager.on({
AuthUserLoaded: () => this.load(),
AuthLogout: () => this.#reset(),
});
if (authManager.authenticated) {
void this.load();
}
}
load() {
this.#loading ??= this.#refresh()
.catch((error) => console.error(`[ServerManager] failed to load server information: ${error}`, error))
.finally(() => (this.#loading = undefined));
return this.#loading;
}
async ready() {
if (this.#about && this.#storage) {
return;
}
await this.load();
}
async #refresh() {
const [about, storage] = await Promise.all([getAboutInfo(), getStorage()]);
this.#about = about;
this.#storage = storage;
}
#reset() {
this.#about = undefined;
this.#storage = undefined;
}
}
export const serverManager = new ServerManager();

View file

@ -1,4 +1,5 @@
<script lang="ts">
import OnEvents from '$lib/components/OnEvents.svelte';
import PurchaseActivationSuccess from '$lib/components/shared-components/purchasing/PurchaseActivationSuccess.svelte';
import PurchaseContent from '$lib/components/shared-components/purchasing/PurchaseContent.svelte';
@ -13,17 +14,14 @@
let showProductActivated = $state(false);
</script>
<OnEvents onLicenseActivated={() => (showProductActivated = true)} />
<Modal title=" " {onClose} size="large">
<ModalBody>
{#if showProductActivated}
<PurchaseActivationSuccess onDone={onClose} />
{:else}
<PurchaseContent
onActivate={() => {
showProductActivated = true;
}}
showMessage={false}
/>
<PurchaseContent showMessage={false} />
{/if}
</ModalBody>
</Modal>

View file

@ -1,23 +1,14 @@
import type {
AlbumResponseDto,
ServerAboutResponseDto,
ServerStorageResponseDto,
ServerVersionHistoryResponseDto,
} from '@immich/sdk';
import type { AlbumResponseDto, ServerVersionHistoryResponseDto } from '@immich/sdk';
import { eventManager } from '$lib/managers/event-manager.svelte';
interface UserInteractions {
recentAlbums?: AlbumResponseDto[];
versions?: ServerVersionHistoryResponseDto[];
aboutInfo?: ServerAboutResponseDto;
serverInfo?: ServerStorageResponseDto;
}
const defaultUserInteraction: UserInteractions = {
recentAlbums: undefined,
versions: undefined,
aboutInfo: undefined,
serverInfo: undefined,
};
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);

View file

@ -1,9 +1,7 @@
import { getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { userInteraction } from '$lib/stores/user.svelte';
export interface AuthOptions {
admin?: true;
@ -27,15 +25,6 @@ export const authenticate = async (url: URL, options?: AuthOptions) => {
}
};
export const requestServerInfo = async () => {
if (!authManager.authenticated) {
return;
}
const data = await getStorage();
userInteraction.serverInfo = data;
};
export const getAccountAge = (): number => {
if (!authManager.authenticated) {
return 0;

View file

@ -1,30 +0,0 @@
import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk';
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
import type { ImmichProduct } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
export const activateProduct = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
// TODO is this needed?
await authManager.load();
const isServerActivation = authManager.user.isAdmin && licenseKey.search('IMSV') !== -1;
const licenseKeyDto = { licenseKey, activationKey };
// Send server key to user activation if user is not admin
return isServerActivation ? setServerLicense({ licenseKeyDto }) : setUserLicense({ licenseKeyDto });
};
export const getActivationKey = async (licenseKey: string): Promise<string> => {
const response = await fetch(new URL(`/api/v1/activate/${licenseKey}`, PUBLIC_IMMICH_PAY_HOST).href);
if (!response.ok) {
throw new Error('Failed to fetch activation key');
}
return response.text();
};
export const getLicenseLink = (license: ImmichProduct) => {
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
url.searchParams.append('productId', license);
url.searchParams.append('instanceUrl', serverConfigManager.value.externalDomain || globalThis.origin);
return url.href;
};

View file

@ -1,10 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/PurchaseActivationSuccess.svelte';
import LicenseContent from '$lib/components/shared-components/purchasing/PurchaseContent.svelte';
import SupporterBadge from './SupporterBadge.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { Route } from '$lib/route';
import { Alert, Container, Stack } from '@immich/ui';
import { mdiAlertCircleOutline } from '@mdi/js';
@ -19,6 +20,8 @@
let showLicenseActivated = $state(false);
</script>
<OnEvents onLicenseActivated={() => (showLicenseActivated = true)} />
<UserPageLayout title={data.meta.title}>
<Container size="medium" center>
<Stack gap={4} class="mt-4">
@ -26,18 +29,14 @@
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
{/if}
{#if authManager.isPurchased}
{#if licenseManager.license}
<SupporterBadge logoSize="lg" centered />
{/if}
{#if showLicenseActivated || data.isActivated === true}
<LicenseActivationSuccess onDone={() => goto(Route.photos(), { replaceState: false })} />
{:else}
<LicenseContent
onActivate={() => {
showLicenseActivated = true;
}}
/>
<LicenseContent />
{/if}
</Stack>
</Container>

View file

@ -1,7 +1,6 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
@ -9,24 +8,16 @@ export const load = (async ({ url }) => {
const $t = await getFormatter();
const licenseKey = url.searchParams.get('licenseKey');
let activationKey = url.searchParams.get('activationKey');
let isActivated: boolean | undefined = undefined;
let isActivated: boolean | undefined;
try {
if (licenseKey && !activationKey) {
activationKey = await getActivationKey(licenseKey);
if (licenseKey) {
try {
const { activatedAt } = await licenseManager.activate(licenseKey, url.searchParams.get('activationKey'));
isActivated = activatedAt !== '';
} catch (error) {
isActivated = false;
console.error(`Failed to activate license key: ${error}`, error);
}
if (licenseKey && activationKey) {
const response = await activateProduct(licenseKey, activationKey);
if (response.activatedAt !== '') {
isActivated = true;
authManager.isPurchased = true;
}
}
} catch (error) {
isActivated = false;
console.log('error navigating to /buy', error);
}
return {

View file

@ -3,60 +3,20 @@
import PurchaseContent from '$lib/components/shared-components/purchasing/PurchaseContent.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
import { dateFormats } from '$lib/constants';
import { dateFormats, ImmichProduct } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { licenseManager } from '$lib/managers/license-manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { handleError } from '$lib/utils/handle-error';
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
import {
deleteUserLicense as deleteIndividualProductKey,
deleteServerLicense as deleteServerProductKey,
getAboutInfo,
getMyUser,
getServerLicense,
isHttpError,
type LicenseResponseDto,
} from '@immich/sdk';
import { Button, Icon, modalManager } from '@immich/ui';
import { mdiKey } from '@mdi/js';
import { onMount } from 'svelte';
import { copyToClipboard, handlePromiseError } from '$lib/utils';
import { t } from 'svelte-i18n';
let isServerProduct = $state(false);
let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
const license = $derived(licenseManager.license);
const checkPurchaseInfo = async () => {
const serverInfo = await getAboutInfo();
isServerProduct = serverInfo.licensed;
const response = await getMyUser();
if (response.license) {
authManager.setUser(response);
}
if (isServerProduct && authManager.user.isAdmin) {
serverPurchaseInfo = await getServerPurchaseInfo();
}
};
const getServerPurchaseInfo = async () => {
try {
return await getServerLicense();
} catch (error) {
if (isHttpError(error) && error.status === 404) {
return null;
}
throw error;
}
};
onMount(async () => {
if (!authManager.isPurchased) {
return;
}
await checkPurchaseInfo();
});
$effect.pre(() => handlePromiseError(licenseManager.load()));
const removeIndividualProductKey = async () => {
try {
@ -70,8 +30,7 @@
return;
}
await deleteIndividualProductKey();
authManager.isPurchased = false;
await licenseManager.removeUserLicense();
} catch (error) {
handleError(error, $t('errors.failed_to_remove_product_key'));
}
@ -89,22 +48,16 @@
return;
}
await deleteServerProductKey();
authManager.isPurchased = false;
await licenseManager.removeServerLicense();
} catch (error) {
handleError(error, $t('errors.failed_to_remove_product_key'));
}
};
const onProductActivated = async () => {
authManager.isPurchased = true;
await checkPurchaseInfo();
};
</script>
<section class="my-4">
<div class="sm:ms-8" in:fade={{ duration: 500 }}>
{#if authManager.isPurchased}
{#if license}
<!-- BADGE TOGGLE -->
<div class="mb-4">
<SettingSwitch
@ -116,7 +69,7 @@
</div>
<!-- PRODUCT KEY INFO CARD -->
{#if isServerProduct}
{#if license.type === ImmichProduct.Server}
<div
class="flex place-content-center gap-4 rounded-xl border border-immich-dark-primary/20 bg-gray-50 p-6 pe-12 dark:bg-immich-dark-primary/15"
>
@ -127,11 +80,11 @@
{$t('purchase_server_title')}
</p>
{#if authManager.user.isAdmin && serverPurchaseInfo?.activatedAt}
{#if license.activatedAt}
<p class="col-start-2 mt-1 text-sm dark:text-white">
{$t('purchase_activated_time', {
values: {
date: new Date(serverPurchaseInfo.activatedAt).toLocaleString($locale, dateFormats.settings),
date: new Date(license.activatedAt).toLocaleString($locale, dateFormats.settings),
},
})}
</p>
@ -158,11 +111,11 @@
<p class="text-lg font-semibold text-primary">
{$t('purchase_individual_title')}
</p>
{#if authManager.user.license?.activatedAt}
{#if license.activatedAt}
<p class="col-start-2 mt-1 text-sm dark:text-white">
{$t('purchase_activated_time', {
values: {
date: new Date(authManager.user.license?.activatedAt).toLocaleString($locale, dateFormats.settings),
date: new Date(license.activatedAt).toLocaleString($locale, dateFormats.settings),
},
})}
</p>
@ -170,14 +123,20 @@
</div>
</div>
<div class="mt-4 text-right">
<div class="mt-4 flex justify-between text-right">
<Button shape="round" size="small" color="danger" onclick={removeIndividualProductKey}
>{$t('purchase_button_remove_key')}</Button
>
{#if license.licenseKey}
{@const licenseKey = license.licenseKey}
<Button shape="round" size="small" onclick={() => copyToClipboard(licenseKey)}>
{$t('copy_to_clipboard')}
</Button>
{/if}
</div>
{/if}
{:else}
<PurchaseContent onActivate={onProductActivated} showTitle={false} />
<PurchaseContent showTitle={false} />
{/if}
</div>
</section>

View file

@ -1,12 +1,11 @@
import { getAllLibraries, getLibraryStatistics, getUserAdmin, searchUsersAdmin } from '@immich/sdk';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ url, depends }) => {
depends('app:libraries');
await authenticate(url, { admin: true });
await requestServerInfo();
const allUsers = await searchUsersAdmin({ withDeleted: false });
const $t = await getFormatter();

View file

@ -1,13 +1,12 @@
import { getQueue, getQueueJobs, QueueJobStatus } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { fromQueueSlug, Route } from '$lib/route';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
const name = fromQueueSlug(params.name);
if (!name) {

View file

@ -1,11 +1,10 @@
import { searchUsersAdmin } from '@immich/sdk';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
const users = await searchUsersAdmin({ withDeleted: true });
const $t = await getFormatter();

View file

@ -1,13 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
import { Route } from '$lib/route';
import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { Field, FormModal, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
import { t } from 'svelte-i18n';
$effect.pre(() => void serverManager.load());
let success = $state(false);
let email = $state('');
@ -23,7 +25,7 @@
let quotaSizeInBytes = $derived(quotaSize === null ? null : convertToBytes(Number(quotaSize), ByteUnit.GiB));
let quotaSizeWarning = $derived(
quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
quotaSizeInBytes && serverManager.storage && quotaSizeInBytes > serverManager.storage.diskSizeRaw,
);
const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0);

View file

@ -2,13 +2,12 @@ import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin,
import { redirect } from '@sveltejs/kit';
import { UUID_REGEX } from '$lib/constants';
import { Route } from '$lib/route';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
if (!UUID_REGEX.test(params.id)) {
redirect(307, Route.users());

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverManager } from '$lib/managers/server-manager.svelte';
import { Route } from '$lib/route';
import { handleUpdateUserAdmin } from '$lib/services/user-admin.service';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
import { Field, FormModal, Input, Link, NumberInput, Switch, Text } from '@immich/ui';
import { mdiAccountEditOutline } from '@mdi/js';
@ -16,6 +16,8 @@
let { data }: Props = $props();
$effect.pre(() => void serverManager.load());
const user = $derived(data.user);
let { isAdmin, name, email } = $derived(user);
let storageLabel = $derived(user.storageLabel || '');
@ -30,8 +32,8 @@
let quotaSizeWarning = $derived(
previousQuota !== quotaSizeBytes &&
!!quotaSizeBytes &&
userInteraction.serverInfo &&
quotaSizeBytes > userInteraction.serverInfo.diskSizeRaw,
serverManager.storage &&
quotaSizeBytes > serverManager.storage.diskSizeRaw,
);
const onClose = async () => {