Compare commits

...

3 Commits

Author SHA1 Message Date
Mees Frensel
4865938f55 properly reload settings 2026-03-18 11:15:05 +01:00
Mees Frensel
3b20bf4a6a Merge branch 'main' into feat/deeplink-api-creation 2026-03-18 10:37:49 +01:00
Mees Frensel
096927a835 feat(web): allow deeplinking into API key creation modal 2026-02-23 13:02:43 +01:00
10 changed files with 123 additions and 24 deletions

View File

@@ -10,7 +10,7 @@
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { user } from '$lib/stores/user.store';
import { oauth } from '$lib/utils';
import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
import { getApiKeys, type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
import {
mdiAccountGroupOutline,
mdiAccountOutline,
@@ -26,6 +26,7 @@
mdiServerOutline,
mdiTwoFactorAuthentication,
} from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
import SettingAccordion from '../shared-components/settings/setting-accordion.svelte';
@@ -38,11 +39,16 @@
import UserProfileSettings from './user-profile-settings.svelte';
interface Props {
keys?: ApiKeyResponseDto[];
sessions?: SessionResponseDto[];
}
let { keys = $bindable([]), sessions = $bindable([]) }: Props = $props();
let { sessions = $bindable([]) }: Props = $props();
let keys: ApiKeyResponseDto[] = $state([]);
onMount(async () => {
keys = await getApiKeys();
});
let oauthOpen =
oauth.isCallback(globalThis.location) ||

View File

@@ -62,6 +62,7 @@ export enum SessionStorageKey {
// TODO split into user settings vs system settings
export enum OpenQueryParam {
API_KEYS = 'api-keys',
OAUTH = 'oauth',
JOB = 'job',
STORAGE_TEMPLATE = 'storage-template',

View File

@@ -116,6 +116,7 @@ export const Route = {
// settings
userSettings: (params?: { isOpen?: OpenQueryParam }) => '/user-settings' + asQueryString(params),
newApiKey: (params?: { permissions?: string }) => '/user-settings/new-api-key' + asQueryString(params),
// system
systemSettings: (params?: { isOpen?: OpenQueryParam }) => '/admin/system-settings' + asQueryString(params),

View File

@@ -1,6 +1,7 @@
import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte';
import ApiKeyCreateModal from '$lib/modals/ApiKeyCreateModal.svelte';
import ApiKeyUpdateModal from '$lib/modals/ApiKeyUpdateModal.svelte';
import { Route } from '$lib/route';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
@@ -19,7 +20,7 @@ export const getApiKeysActions = ($t: MessageFormatter) => {
const Create: ActionItem = {
title: $t('new_api_key'),
icon: mdiPlus,
onAction: () => modalManager.show(ApiKeyCreateModal, {}),
onAction: () => goto(Route.newApiKey()),
};
return { Create };

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import { getKeyboardActions } from '$lib/services/keyboard.service';
import { Container } from '@immich/ui';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
children?: Snippet;
data: PageData;
};
let { children, data }: Props = $props();
const { KeyboardShortcuts } = $derived(getKeyboardActions($t));
</script>
<UserPageLayout title={data.meta.title} actions={[KeyboardShortcuts]}>
<Container size="medium" center>
{@render children?.()}
</Container>
</UserPageLayout>

View File

@@ -0,0 +1,15 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const $t = await getFormatter();
return {
meta: {
title: $t('settings'),
},
};
}) satisfies PageLoad;

View File

@@ -1,9 +1,5 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte';
import { getKeyboardActions } from '$lib/services/keyboard.service';
import { Container } from '@immich/ui';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
@@ -11,12 +7,6 @@
};
let { data }: Props = $props();
const { KeyboardShortcuts } = $derived(getKeyboardActions($t));
</script>
<UserPageLayout title={data.meta.title} actions={[KeyboardShortcuts]}>
<Container size="medium" center>
<UserSettingsList keys={data.keys} sessions={data.sessions} />
</Container>
</UserPageLayout>
<UserSettingsList sessions={data.sessions} />

View File

@@ -1,20 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getApiKeys, getSessions } from '@immich/sdk';
import { getSessions } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const keys = await getApiKeys();
const sessions = await getSessions();
const $t = await getFormatter();
return {
keys,
sessions,
meta: {
title: $t('settings'),
},
};
}) satisfies PageLoad;

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
import { OpenQueryParam } from '$lib/constants';
import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
import { Route } from '$lib/route';
import { handleCreateApiKey } from '$lib/services/api-key.service';
import { Permission } from '@immich/sdk';
import { Field, FormModal, Input } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { t } from 'svelte-i18n';
const validPermissions = new Set(Object.values(Permission));
const queryPermissions = (page.url.searchParams.get('permissions') || '')
.split(' ')
.filter((x) => x !== '')
.map((value) => {
if (value === Permission.All || !validPermissions.has(value as Permission)) {
console.warn(`Invalid permission value: ${value}`);
}
return value as Permission;
});
let name = $state('API Key');
let selectedPermissions = $state<Permission[]>(queryPermissions);
const isAllPermissions = $derived(selectedPermissions.length === Object.keys(Permission).length - 1);
const onClose = async () => {
await goto(Route.userSettings({ isOpen: OpenQueryParam.API_KEYS }));
};
let secret: string | undefined = $state();
const onSubmit = async () => {
const permissions = isAllPermissions ? [Permission.All] : selectedPermissions;
const response = await handleCreateApiKey({ name, permissions });
if (response) {
secret = response.secret;
}
};
</script>
{#if !secret}
<FormModal title={$t('new_api_key')} icon={mdiKeyVariant} {onClose} {onSubmit} submitText={$t('create')} size="giant">
<div class="mb-4 flex flex-col gap-2">
<Field label={$t('name')}>
<Input bind:value={name} />
</Field>
</div>
<ApiKeyPermissionsPicker bind:selectedPermissions />
</FormModal>
{:else}
<ApiKeySecretModal {secret} {onClose} />
{/if}

View File

@@ -0,0 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const $t = await getFormatter();
return {
meta: {
title: $t('settings'),
},
};
}) satisfies PageLoad;