feat: new user route

This commit is contained in:
Jason Rasmussen
2025-12-19 13:44:22 -05:00
parent 5b80323326
commit 48d5e4118b
9 changed files with 46 additions and 19 deletions

View File

@@ -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 { export enum AssetAction {
ARCHIVE = 'archive', ARCHIVE = 'archive',
UNARCHIVE = 'unarchive', UNARCHIVE = 'unarchive',
@@ -20,6 +22,7 @@ export enum AssetAction {
export enum AppRoute { export enum AppRoute {
ADMIN_USERS = '/admin/users', ADMIN_USERS = '/admin/users',
ADMIN_USERS_NEW = '/admin/users/new',
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management', ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
ADMIN_SETTINGS = '/admin/system-settings', ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_STATS = '/admin/server-status', ADMIN_STATS = '/admin/server-status',

View File

@@ -1,8 +1,8 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte'; import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte'; import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserEditModal from '$lib/modals/UserEditModal.svelte'; import UserEditModal from '$lib/modals/UserEditModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte'; import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
@@ -39,7 +39,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
title: $t('create_user'), title: $t('create_user'),
type: $t('command'), type: $t('command'),
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
onAction: () => modalManager.show(UserCreateModal, {}), onAction: () => goto(AppRoute.ADMIN_USERS_NEW),
shortcuts: { shift: true, key: 'n' }, shortcuts: { shift: true, key: 'n' },
}; };
@@ -103,7 +103,7 @@ export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
const response = await createUserAdmin({ userAdminCreateDto: dto }); const response = await createUserAdmin({ userAdminCreateDto: dto });
eventManager.emit('UserAdminCreate', response); eventManager.emit('UserAdminCreate', response);
toastManager.success(); toastManager.success();
return true; return response;
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_create_user')); handleError(error, $t('errors.unable_to_create_user'));
} }

View File

@@ -1,6 +1,7 @@
import { UUID_REGEX } from '$lib/constants';
import type { ParamMatcher } from '@sveltejs/kit'; import type { ParamMatcher } from '@sveltejs/kit';
/* Returns true if the given param matches UUID format */ /* Returns true if the given param matches UUID format */
export const match: ParamMatcher = (param: string) => { 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);
}; };

View File

@@ -7,14 +7,16 @@
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
import { Button, CommandPaletteContext, Icon } from '@immich/ui'; import { Button, CommandPaletteContext, Icon } from '@immich/ui';
import { mdiInfinity } from '@mdi/js'; import { mdiInfinity } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { LayoutData } from './$types';
type Props = { type Props = {
data: PageData; children?: Snippet;
data: LayoutData;
}; };
let { data }: Props = $props(); let { children, data }: Props = $props();
let allUsers: UserAdminResponseDto[] = $state(data.allUsers); let allUsers: UserAdminResponseDto[] = $state(data.allUsers);
@@ -91,3 +93,5 @@
</section> </section>
</section> </section>
</AdminPageLayout> </AdminPageLayout>
{@render children?.()}

View File

@@ -1,7 +1,7 @@
import { authenticate, requestServerInfo } from '$lib/utils/auth'; import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { searchUsersAdmin } from '@immich/sdk'; import { searchUsersAdmin } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => { export const load = (async ({ url }) => {
await authenticate(url, { admin: true }); await authenticate(url, { admin: true });
@@ -15,4 +15,4 @@ export const load = (async ({ url }) => {
title: $t('admin.user_management'), title: $t('admin.user_management'),
}, },
}; };
}) satisfies PageLoad; }) satisfies LayoutLoad;

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { handleCreateUserAdmin } from '$lib/services/user-admin.service'; import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
import { userInteraction } from '$lib/stores/user.svelte'; import { userInteraction } from '$lib/stores/user.svelte';
@@ -18,12 +20,6 @@
} from '@immich/ui'; } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
type Props = {
onClose: () => void;
};
let { onClose }: Props = $props();
let success = $state(false); let success = $state(false);
let email = $state(''); let email = $state('');
@@ -46,6 +42,10 @@
const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : ''); const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : '');
const valid = $derived(!passwordMismatch && !isCreatingUser); const valid = $derived(!passwordMismatch && !isCreatingUser);
const onClose = async () => {
await goto(AppRoute.ADMIN_USERS);
};
const onSubmit = async (event: Event) => { const onSubmit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
@@ -55,7 +55,7 @@
isCreatingUser = true; isCreatingUser = true;
const success = await handleCreateUserAdmin({ const response = await handleCreateUserAdmin({
email, email,
password, password,
shouldChangePassword, shouldChangePassword,
@@ -65,8 +65,8 @@
isAdmin, isAdmin,
}); });
if (success) { if (response) {
onClose(); await goto(`${AppRoute.ADMIN_USERS}/${response.id}`);
} }
isCreatingUser = false; isCreatingUser = false;

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, { admin: true });
const $t = await getFormatter();
return {
meta: {
title: $t('admin.user_management'),
},
};
}) satisfies PageLoad;

View File

@@ -1,4 +1,4 @@
import { AppRoute } from '$lib/constants'; import { AppRoute, UUID_REGEX } from '$lib/constants';
import { authenticate, requestServerInfo } from '$lib/utils/auth'; import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk'; import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
@@ -8,6 +8,11 @@ import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => { export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true }); await authenticate(url, { admin: true });
await requestServerInfo(); await requestServerInfo();
if (!UUID_REGEX.test(params.id)) {
redirect(302, AppRoute.ADMIN_USERS);
}
const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []); const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []);
if (!user) { if (!user) {
redirect(302, AppRoute.ADMIN_USERS); redirect(302, AppRoute.ADMIN_USERS);