mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge c63cfa061a into ffc83eae36
This commit is contained in:
commit
6fc48627b1
24 changed files with 264 additions and 230 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export type Events = {
|
|||
|
||||
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
|
||||
|
||||
LicenseActivated: [];
|
||||
|
||||
ApiKeyCreate: [ApiKeyResponseDto];
|
||||
ApiKeyUpdate: [ApiKeyResponseDto];
|
||||
ApiKeyDelete: [ApiKeyResponseDto];
|
||||
|
|
|
|||
117
web/src/lib/managers/license-manager.svelte.ts
Normal file
117
web/src/lib/managers/license-manager.svelte.ts
Normal 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();
|
||||
57
web/src/lib/managers/server-manager.svelte.ts
Normal file
57
web/src/lib/managers/server-manager.svelte.ts
Normal 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();
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue