diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index 38933aea85..6f7c0d8e1e 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -1,3 +1,5 @@
+export const UUID_REGEX = /^[\dA-Fa-f]{8}(?:\b-[\dA-Fa-f]{4}){3}\b-[\dA-Fa-f]{12}$/;
+
export enum AssetAction {
ARCHIVE = 'archive',
UNARCHIVE = 'unarchive',
@@ -20,7 +22,9 @@ export enum AssetAction {
export enum AppRoute {
ADMIN_USERS = '/admin/users',
- ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
+ ADMIN_USERS_NEW = '/admin/users/new',
+ ADMIN_LIBRARIES = '/admin/library-management',
+ ADMIN_LIBRARIES_NEW = '/admin/library-management/new',
ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_STATS = '/admin/server-status',
ADMIN_QUEUES = '/admin/queues',
diff --git a/web/src/lib/modals/LibraryRenameModal.svelte b/web/src/lib/modals/LibraryRenameModal.svelte
deleted file mode 100644
index 0a7d675b11..0000000000
--- a/web/src/lib/modals/LibraryRenameModal.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts
index a7f2ba25e7..62371e2512 100644
--- a/web/src/lib/services/library.service.ts
+++ b/web/src/lib/services/library.service.ts
@@ -1,12 +1,10 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
-import LibraryCreateModal from '$lib/modals/LibraryCreateModal.svelte';
import LibraryExclusionPatternAddModal from '$lib/modals/LibraryExclusionPatternAddModal.svelte';
import LibraryExclusionPatternEditModal from '$lib/modals/LibraryExclusionPatternEditModal.svelte';
import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte';
-import LibraryRenameModal from '$lib/modals/LibraryRenameModal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
@@ -19,6 +17,7 @@ import {
updateLibrary,
type CreateLibraryDto,
type LibraryResponseDto,
+ type UpdateLibraryDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
@@ -38,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
title: $t('create_library'),
type: $t('command'),
icon: mdiPlusBoxOutline,
- onAction: () => handleShowLibraryCreateModal(),
+ onAction: () => goto(AppRoute.ADMIN_LIBRARIES_NEW),
shortcuts: { shift: true, key: 'n' },
};
@@ -46,11 +45,11 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
};
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
- const Rename: ActionItem = {
+ const Edit: ActionItem = {
icon: mdiPencilOutline,
type: $t('command'),
- title: $t('rename'),
- onAction: () => modalManager.show(LibraryRenameModal, { library }),
+ title: $t('edit'),
+ onAction: () => goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}/edit`),
shortcuts: { key: 'r' },
};
@@ -85,7 +84,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
shortcuts: { shift: true, key: 'r' },
};
- return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
+ return { Edit, Delete, AddFolder, AddExclusionPattern, Scan };
};
export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryResponseDto, folder: string) => {
@@ -150,7 +149,7 @@ const handleScanLibrary = async (library: LibraryResponseDto) => {
};
export const handleViewLibrary = async (library: LibraryResponseDto) => {
- await goto(`${AppRoute.ADMIN_LIBRARY_MANAGEMENT}/${library.id}`);
+ await goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}`);
};
export const handleCreateLibrary = async (dto: CreateLibraryDto) => {
@@ -160,33 +159,24 @@ export const handleCreateLibrary = async (dto: CreateLibraryDto) => {
const library = await createLibrary({ createLibraryDto: dto });
eventManager.emit('LibraryCreate', library);
toastManager.success($t('admin.library_created', { values: { library: library.name } }));
- return true;
+ return library;
} catch (error) {
handleError(error, $t('errors.unable_to_create_library'));
- return false;
}
};
-export const handleRenameLibrary = async (library: { id: string }, name?: string) => {
+export const handleUpdateLibrary = async (library: LibraryResponseDto, dto: UpdateLibraryDto) => {
const $t = await getFormatter();
- if (!name) {
- return false;
- }
-
try {
- const updatedLibrary = await updateLibrary({
- id: library.id,
- updateLibraryDto: { name },
- });
+ const updatedLibrary = await updateLibrary({ id: library.id, updateLibraryDto: dto });
eventManager.emit('LibraryUpdate', updatedLibrary);
toastManager.success($t('admin.library_updated'));
+ return true;
} catch (error) {
handleError(error, $t('errors.unable_to_update_library'));
return false;
}
-
- return true;
};
const handleDeleteLibrary = async (library: LibraryResponseDto) => {
@@ -357,7 +347,3 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
handleError(error, $t('errors.unable_to_update_library'));
}
};
-
-export const handleShowLibraryCreateModal = async () => {
- await modalManager.show(LibraryCreateModal, {});
-};
diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts
index 997a43fc7f..5fd4ff99fd 100644
--- a/web/src/lib/services/user-admin.service.ts
+++ b/web/src/lib/services/user-admin.service.ts
@@ -1,10 +1,9 @@
import { goto } from '$app/navigation';
+import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
-import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
-import UserEditModal from '$lib/modals/UserEditModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { user as authUser } from '$lib/stores/user.store';
import type { HeaderButtonActionItem } from '$lib/types';
@@ -39,7 +38,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
title: $t('create_user'),
type: $t('command'),
icon: mdiPlusBoxOutline,
- onAction: () => modalManager.show(UserCreateModal, {}),
+ onAction: () => goto(AppRoute.ADMIN_USERS_NEW),
shortcuts: { shift: true, key: 'n' },
};
@@ -50,7 +49,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
const Update: ActionItem = {
icon: mdiPencilOutline,
title: $t('edit'),
- onAction: () => modalManager.show(UserEditModal, { user }),
+ onAction: () => goto(`${AppRoute.ADMIN_USERS}/${user.id}/edit`),
};
const Delete: ActionItem = {
@@ -103,7 +102,7 @@ export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
const response = await createUserAdmin({ userAdminCreateDto: dto });
eventManager.emit('UserAdminCreate', response);
toastManager.success();
- return true;
+ return response;
} catch (error) {
handleError(error, $t('errors.unable_to_create_user'));
}
diff --git a/web/src/lib/sidebars/AdminSidebar.svelte b/web/src/lib/sidebars/AdminSidebar.svelte
index 919c072527..fa660d7e2f 100644
--- a/web/src/lib/sidebars/AdminSidebar.svelte
+++ b/web/src/lib/sidebars/AdminSidebar.svelte
@@ -9,7 +9,7 @@
-
+
diff --git a/web/src/params/id.ts b/web/src/params/id.ts
index 6b16a651d1..b7e93be6ae 100644
--- a/web/src/params/id.ts
+++ b/web/src/params/id.ts
@@ -1,6 +1,7 @@
+import { UUID_REGEX } from '$lib/constants';
import type { ParamMatcher } from '@sveltejs/kit';
/* Returns true if the given param matches UUID format */
export const match: ParamMatcher = (param: string) => {
- return /^[\dA-Fa-f]{8}(?:\b-[\dA-Fa-f]{4}){3}\b-[\dA-Fa-f]{12}$/.test(param);
+ return UUID_REGEX.test(param);
};
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index 77a3d402b2..3a1d4f49f9 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -166,7 +166,7 @@
title: $t('external_libraries'),
description: $t('admin.external_libraries_page_description'),
icon: mdiBookshelf,
- onAction: () => goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT),
+ onAction: () => goto(AppRoute.ADMIN_LIBRARIES),
},
{
title: $t('server_stats'),
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/(list)/+layout.svelte
similarity index 90%
rename from web/src/routes/admin/library-management/+page.svelte
rename to web/src/routes/admin/library-management/(list)/+layout.svelte
index e61b4433be..e741dbd610 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/(list)/+layout.svelte
@@ -4,27 +4,29 @@
import OnEvents from '$lib/components/OnEvents.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute } from '$lib/constants';
- import { getLibrariesActions, handleShowLibraryCreateModal, handleViewLibrary } from '$lib/services/library.service';
+ import { getLibrariesActions, handleViewLibrary } from '$lib/services/library.service';
import { locale } from '$lib/stores/preferences.store';
import { getBytesWithUnit } from '$lib/utils/byte-units';
import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
import { Button, CommandPaletteContext } from '@immich/ui';
+ import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
- import type { PageData } from './$types';
+ import type { LayoutData } from './$types';
type Props = {
- data: PageData;
+ children?: Snippet;
+ data: LayoutData;
};
- let { data }: Props = $props();
+ let { children, data }: Props = $props();
let libraries = $state(data.libraries);
let statistics = $state(data.statistics);
let owners = $state(data.owners);
const onLibraryCreate = async (library: LibraryResponseDto) => {
- await goto(`${AppRoute.ADMIN_LIBRARY_MANAGEMENT}/${library.id}`);
+ await goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}`);
};
const onLibraryUpdate = async (library: LibraryResponseDto) => {
@@ -100,10 +102,12 @@
{:else}
goto(AppRoute.ADMIN_LIBRARIES_NEW)}
class="mt-10 mx-auto"
/>
{/if}
+
+ {@render children?.()}
diff --git a/web/src/routes/admin/library-management/+page.ts b/web/src/routes/admin/library-management/(list)/+layout.ts
similarity index 93%
rename from web/src/routes/admin/library-management/+page.ts
rename to web/src/routes/admin/library-management/(list)/+layout.ts
index aa892f81ed..aee777a9e8 100644
--- a/web/src/routes/admin/library-management/+page.ts
+++ b/web/src/routes/admin/library-management/(list)/+layout.ts
@@ -1,7 +1,7 @@
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAllLibraries, getLibraryStatistics, getUserAdmin, searchUsersAdmin } from '@immich/sdk';
-import type { PageLoad } from './$types';
+import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
@@ -26,4 +26,4 @@ export const load = (async ({ url }) => {
title: $t('external_libraries'),
},
};
-}) satisfies PageLoad;
+}) satisfies LayoutLoad;
diff --git a/web/src/routes/admin/library-management/(list)/+page.svelte b/web/src/routes/admin/library-management/(list)/+page.svelte
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/lib/modals/LibraryCreateModal.svelte b/web/src/routes/admin/library-management/(list)/new/+page.svelte
similarity index 77%
rename from web/src/lib/modals/LibraryCreateModal.svelte
rename to web/src/routes/admin/library-management/(list)/new/+page.svelte
index d2d7f62c02..b236b83ce8 100644
--- a/web/src/lib/modals/LibraryCreateModal.svelte
+++ b/web/src/routes/admin/library-management/(list)/new/+page.svelte
@@ -1,5 +1,7 @@
diff --git a/web/src/routes/admin/library-management/(list)/new/+page.ts b/web/src/routes/admin/library-management/(list)/new/+page.ts
new file mode 100644
index 0000000000..da3756bcf6
--- /dev/null
+++ b/web/src/routes/admin/library-management/(list)/new/+page.ts
@@ -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, { admin: true });
+ const $t = await getFormatter();
+
+ return {
+ meta: {
+ title: $t('external_libraries'),
+ },
+ };
+}) satisfies PageLoad;
diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte
new file mode 100644
index 0000000000..62b4891459
--- /dev/null
+++ b/web/src/routes/admin/library-management/[id]/+layout.svelte
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
{library.name}
+
+
+
+
+
+
+
+ {#if library.importPaths.length === 0}
+ modalManager.show(LibraryFolderAddModal, { library })}
+ />
+ {:else}
+
+
+ {#each library.importPaths as folder (folder)}
+ {@const { Edit, Delete } = getLibraryFolderActions($t, library, folder)}
+
+
+ {folder}
+ |
+
+
+
+ |
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+ {#each library.exclusionPatterns as exclusionPattern (exclusionPattern)}
+ {@const { Edit, Delete } = getLibraryExclusionPatternActions($t, library, exclusionPattern)}
+
+
+ {exclusionPattern}
+ |
+
+
+
+ |
+
+ {/each}
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/web/src/routes/admin/library-management/[id]/+layout.ts b/web/src/routes/admin/library-management/[id]/+layout.ts
new file mode 100644
index 0000000000..d465d92c45
--- /dev/null
+++ b/web/src/routes/admin/library-management/[id]/+layout.ts
@@ -0,0 +1,29 @@
+import { AppRoute } from '$lib/constants';
+import { authenticate } from '$lib/utils/auth';
+import { getFormatter } from '$lib/utils/i18n';
+import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
+import { redirect } from '@sveltejs/kit';
+import type { LayoutLoad } from './$types';
+
+export const load = (async ({ params: { id }, url }) => {
+ await authenticate(url, { admin: true });
+
+ let library: LibraryResponseDto;
+
+ try {
+ library = await getLibrary({ id });
+ } catch {
+ redirect(302, AppRoute.ADMIN_LIBRARIES);
+ }
+
+ const statistics = await getLibraryStatistics({ id });
+ const $t = await getFormatter();
+
+ return {
+ library,
+ statistics,
+ meta: {
+ title: $t('admin.library_details'),
+ },
+ };
+}) satisfies LayoutLoad;
diff --git a/web/src/routes/admin/library-management/[id]/+page.svelte b/web/src/routes/admin/library-management/[id]/+page.svelte
index 6bffbe39aa..e69de29bb2 100644
--- a/web/src/routes/admin/library-management/[id]/+page.svelte
+++ b/web/src/routes/admin/library-management/[id]/+page.svelte
@@ -1,105 +0,0 @@
-
-
-
(library = newLibrary)}
- onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
-/>
-
-
-
-
-
-
-
{library.name}
-
-
-
-
-
-
-
- {#if library.importPaths.length === 0}
- modalManager.show(LibraryFolderAddModal, { library })}
- />
- {:else}
-
-
- {#each library.importPaths as folder (folder)}
- {@const { Edit, Delete } = getLibraryFolderActions($t, library, folder)}
-
-
- {folder}
- |
-
-
-
- |
-
- {/each}
-
-
- {/if}
-
-
-
-
-
- {#each library.exclusionPatterns as exclusionPattern (exclusionPattern)}
- {@const { Edit, Delete } = getLibraryExclusionPatternActions($t, library, exclusionPattern)}
-
-
- {exclusionPattern}
- |
-
-
-
- |
-
- {/each}
-
-
-
-
-
-
diff --git a/web/src/routes/admin/library-management/[id]/+page.ts b/web/src/routes/admin/library-management/[id]/+page.ts
index 77ce1eb1c8..10f0f33e11 100644
--- a/web/src/routes/admin/library-management/[id]/+page.ts
+++ b/web/src/routes/admin/library-management/[id]/+page.ts
@@ -1,26 +1,13 @@
-import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
-import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
-import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
-export const load = (async ({ params: { id }, url }) => {
+export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
- let library: LibraryResponseDto;
- try {
- library = await getLibrary({ id });
- } catch {
- redirect(302, AppRoute.ADMIN_LIBRARY_MANAGEMENT);
- }
-
- const statistics = await getLibraryStatistics({ id });
const $t = await getFormatter();
return {
- library,
- statistics,
meta: {
title: $t('admin.library_details'),
},
diff --git a/web/src/routes/admin/library-management/[id]/edit/+page.svelte b/web/src/routes/admin/library-management/[id]/edit/+page.svelte
new file mode 100644
index 0000000000..06e0af13e6
--- /dev/null
+++ b/web/src/routes/admin/library-management/[id]/edit/+page.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
diff --git a/web/src/routes/admin/library-management/[id]/edit/+page.ts b/web/src/routes/admin/library-management/[id]/edit/+page.ts
new file mode 100644
index 0000000000..10f0f33e11
--- /dev/null
+++ b/web/src/routes/admin/library-management/[id]/edit/+page.ts
@@ -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, { admin: true });
+
+ const $t = await getFormatter();
+
+ return {
+ meta: {
+ title: $t('admin.library_details'),
+ },
+ };
+}) satisfies PageLoad;
diff --git a/web/src/routes/admin/users/(list)/+layout.svelte b/web/src/routes/admin/users/(list)/+layout.svelte
new file mode 100644
index 0000000000..a8c281690d
--- /dev/null
+++ b/web/src/routes/admin/users/(list)/+layout.svelte
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+ | {$t('email')} |
+ {$t('name')} |
+ {$t('has_quota')} |
+
+
+
+ {#each users as user (user.id)}
+
+ |
+ {user.email}
+ |
+ {user.name} |
+
+
+ {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
+ {getByteUnitString(user.quotaSizeInBytes, $locale)}
+ {:else}
+
+ {/if}
+
+ |
+
+
+ |
+
+ {/each}
+
+
+
+ {@render children?.()}
+
+
diff --git a/web/src/routes/admin/users/+page.ts b/web/src/routes/admin/users/(list)/+layout.ts
similarity index 72%
rename from web/src/routes/admin/users/+page.ts
rename to web/src/routes/admin/users/(list)/+layout.ts
index 521f8573e1..cccc7e647a 100644
--- a/web/src/routes/admin/users/+page.ts
+++ b/web/src/routes/admin/users/(list)/+layout.ts
@@ -1,18 +1,18 @@
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { searchUsersAdmin } from '@immich/sdk';
-import type { PageLoad } from './$types';
+import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
- const allUsers = await searchUsersAdmin({ withDeleted: true });
+ const users = await searchUsersAdmin({ withDeleted: true });
const $t = await getFormatter();
return {
- allUsers,
+ users,
meta: {
title: $t('admin.user_management'),
},
};
-}) satisfies PageLoad;
+}) satisfies LayoutLoad;
diff --git a/web/src/routes/admin/users/(list)/+page.svelte b/web/src/routes/admin/users/(list)/+page.svelte
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/web/src/lib/modals/UserCreateModal.svelte b/web/src/routes/admin/users/(list)/new/+page.svelte
similarity index 92%
rename from web/src/lib/modals/UserCreateModal.svelte
rename to web/src/routes/admin/users/(list)/new/+page.svelte
index 7dd0449119..d4f753197a 100644
--- a/web/src/lib/modals/UserCreateModal.svelte
+++ b/web/src/routes/admin/users/(list)/new/+page.svelte
@@ -1,4 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
- | {$t('email')} |
- {$t('name')} |
- {$t('has_quota')} |
-
-
-
- {#each allUsers as user (user.id)}
-
- |
- {user.email}
- |
- {user.name} |
-
-
- {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
- {getByteUnitString(user.quotaSizeInBytes, $locale)}
- {:else}
-
- {/if}
-
- |
-
-
- |
-
- {/each}
-
-
-
-
-
diff --git a/web/src/routes/admin/users/[id]/+layout.svelte b/web/src/routes/admin/users/[id]/+layout.svelte
new file mode 100644
index 0000000000..d3fe5fdcaf
--- /dev/null
+++ b/web/src/routes/admin/users/[id]/+layout.svelte
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+ {#if user.deletedAt}
+
+ {/if}
+
+
+
+
+
+ {user.name}
+
+ {#if user.isAdmin}
+
+ {$t('admin.admin_user')}
+
+ {/if}
+
+
+
+
+
+
+ {$t('name')}
+ {user.name}
+
+
+ {$t('email')}
+ {user.email}
+
+
+ {$t('created_at')}
+ {userCreatedAtDateAndTime}
+
+
+ {$t('updated_at')}
+ {userUpdatedAtDateAndTime}
+
+
+ {$t('id')}
+ {user.id}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
+
+ {$t('storage_usage', {
+ values: {
+ used: getByteUnitString(usedBytes, $locale, 3),
+ available: getByteUnitString(availableBytes, $locale, 3),
+ },
+ })}
+
+ {:else}
+
+
+ {$t('unlimited')}
+
+ {/if}
+
+ {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
+
+ {/if}
+
+
+
+
+ {#each userSessions as session (session.id)}
+
+ {:else}
+ {$t('no_devices')}
+ {/each}
+
+
+
+
+ {@render children?.()}
+
+
+
diff --git a/web/src/routes/admin/users/[id]/+layout.ts b/web/src/routes/admin/users/[id]/+layout.ts
new file mode 100644
index 0000000000..32c41b0aca
--- /dev/null
+++ b/web/src/routes/admin/users/[id]/+layout.ts
@@ -0,0 +1,38 @@
+import { AppRoute, UUID_REGEX } from '$lib/constants';
+import { authenticate, requestServerInfo } from '$lib/utils/auth';
+import { getFormatter } from '$lib/utils/i18n';
+import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
+import { redirect } from '@sveltejs/kit';
+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(302, AppRoute.ADMIN_USERS);
+ }
+
+ const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []);
+ if (!user) {
+ redirect(302, AppRoute.ADMIN_USERS);
+ }
+
+ const [userPreferences, userStatistics, userSessions] = await Promise.all([
+ getUserPreferencesAdmin({ id: user.id }),
+ getUserStatisticsAdmin({ id: user.id }),
+ getUserSessionsAdmin({ id: user.id }),
+ ]);
+
+ const $t = await getFormatter();
+
+ return {
+ user,
+ userPreferences,
+ userStatistics,
+ userSessions,
+ meta: {
+ title: $t('admin.user_details'),
+ },
+ };
+}) satisfies LayoutLoad;
diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte
index c41afba97c..e69de29bb2 100644
--- a/web/src/routes/admin/users/[id]/+page.svelte
+++ b/web/src/routes/admin/users/[id]/+page.svelte
@@ -1,218 +0,0 @@
-
-
-
-
-
-
-
-
-
- {#if user.deletedAt}
-
- {/if}
-
-
-
-
-
- {user.name}
-
- {#if user.isAdmin}
-
- {$t('admin.admin_user')}
-
- {/if}
-
-
-
-
-
-
- {$t('name')}
- {user.name}
-
-
- {$t('email')}
- {user.email}
-
-
- {$t('created_at')}
- {userCreatedAtDateAndTime}
-
-
- {$t('updated_at')}
- {userUpdatedAtDateAndTime}
-
-
- {$t('id')}
- {user.id}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
-
- {$t('storage_usage', {
- values: {
- used: getByteUnitString(usedBytes, $locale, 3),
- available: getByteUnitString(availableBytes, $locale, 3),
- },
- })}
-
- {:else}
-
-
- {$t('unlimited')}
-
- {/if}
-
- {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
-
- {/if}
-
-
-
-
- {#each userSessions as session (session.id)}
-
- {:else}
- {$t('no_devices')}
- {/each}
-
-
-
-
-
-
diff --git a/web/src/routes/admin/users/[id]/+page.ts b/web/src/routes/admin/users/[id]/+page.ts
index bfc5bcefa9..775f9662d2 100644
--- a/web/src/routes/admin/users/[id]/+page.ts
+++ b/web/src/routes/admin/users/[id]/+page.ts
@@ -1,31 +1,12 @@
-import { AppRoute } from '$lib/constants';
-import { authenticate, requestServerInfo } from '$lib/utils/auth';
+import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
-import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
-import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
-export const load = (async ({ params, url }) => {
+export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
- await requestServerInfo();
- const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []);
- if (!user) {
- redirect(302, AppRoute.ADMIN_USERS);
- }
-
- const [userPreferences, userStatistics, userSessions] = await Promise.all([
- getUserPreferencesAdmin({ id: user.id }),
- getUserStatisticsAdmin({ id: user.id }),
- getUserSessionsAdmin({ id: user.id }),
- ]);
-
const $t = await getFormatter();
return {
- user,
- userPreferences,
- userStatistics,
- userSessions,
meta: {
title: $t('admin.user_details'),
},
diff --git a/web/src/lib/modals/UserEditModal.svelte b/web/src/routes/admin/users/[id]/edit/+page.svelte
similarity index 84%
rename from web/src/lib/modals/UserEditModal.svelte
rename to web/src/routes/admin/users/[id]/edit/+page.svelte
index 4b4878e46d..ea3bd827df 100644
--- a/web/src/lib/modals/UserEditModal.svelte
+++ b/web/src/routes/admin/users/[id]/edit/+page.svelte
@@ -1,10 +1,10 @@
diff --git a/web/src/routes/admin/users/[id]/edit/+page.ts b/web/src/routes/admin/users/[id]/edit/+page.ts
new file mode 100644
index 0000000000..64420e91da
--- /dev/null
+++ b/web/src/routes/admin/users/[id]/edit/+page.ts
@@ -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, { admin: true });
+
+ const $t = await getFormatter();
+
+ return {
+ meta: {
+ title: $t('admin.user_details'),
+ },
+ };
+}) satisfies PageLoad;