From b3bd5d1fd13ec7dd380f92e9cb013e0ec63a7123 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 21 Oct 2026 18:36:21 +0200 Subject: [PATCH] feat(server): remove deprecated password field from update-me (#32623) * feat(server): remove deprecated password field from update-me * fix: tests for removed password field (cherry picked from commit 3f6a071203187681b6c06b977665dc624c431336) --- e2e/src/specs/server/api/user.e2e-spec.ts | 40 ------------------- e2e/src/specs/web/auth.e2e-spec.ts | 1 + open-api/immich-openapi-specs.json | 5 --- packages/sdk/src/fetch-client.ts | 2 - server/src/dtos/user.dto.ts | 5 --- server/src/services/user.service.ts | 7 ---- .../specs/services/user.service.spec.ts | 11 ----- .../routes/auth/change-password/+page.svelte | 15 +++++-- 8 files changed, 12 insertions(+), 74 deletions(-) delete mode 100644 e2e/src/specs/server/api/user.e2e-spec.ts diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts deleted file mode 100644 index fb357b30e..000000000 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { LoginResponseDto, getMyUser, login } from '@immich/sdk'; -import { createUserDto } from 'src/fixtures.js'; -import { app, asBearerAuth, utils } from 'src/utils.js'; -import request from 'supertest'; -import { beforeAll, describe, expect, it } from 'vitest'; - -describe('/users', () => { - let admin: LoginResponseDto; - let nonAdmin: LoginResponseDto; - - beforeAll(async () => { - await utils.resetDatabase(); - admin = await utils.adminSetup({ onboarding: false }); - nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2); - }); - - describe('PUT /users/me', () => { - /** @deprecated */ - it('should allow a user to change their password (deprecated)', async () => { - const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); - - expect(user.shouldChangePassword).toBe(true); - - const { status, body } = await request(app) - .put(`/users/me`) - .send({ password: 'super-secret' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ - email: nonAdmin.userEmail, - shouldChangePassword: false, - }); - - const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } }); - - expect(token.accessToken).toBeDefined(); - }); - }); -}); diff --git a/e2e/src/specs/web/auth.e2e-spec.ts b/e2e/src/specs/web/auth.e2e-spec.ts index 37ee638a7..e49e699e7 100644 --- a/e2e/src/specs/web/auth.e2e-spec.ts +++ b/e2e/src/specs/web/auth.e2e-spec.ts @@ -71,6 +71,7 @@ test.describe('Registration', () => { // change password await expect(page.getByRole('heading')).toHaveText('Change Password'); await expect(page).toHaveURL('/auth/change-password'); + await page.getByLabel('Password', { exact: true }).fill('password'); await page.getByLabel('New Password').fill('new-password'); await page.getByLabel('Confirm Password').fill('new-password'); await page.getByRole('button', { name: 'Change password' }).click(); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a32926e84..b8fbb5483 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -34312,11 +34312,6 @@ "name": { "description": "User name", "type": "string" - }, - "password": { - "deprecated": true, - "description": "User password (deprecated, use change password endpoint)", - "type": "string" } }, "type": "object" diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 2d06a14d9..e52f746c0 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -3266,8 +3266,6 @@ export type UserUpdateMeDto = { email?: string; /** User name */ name?: string; - /** User password (deprecated, use change password endpoint) */ - password?: string; }; export type OnboardingResponseDto = { /** Is user onboarded */ diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index fa237e8eb..5e33dc2d8 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -11,11 +11,6 @@ import { isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/ export const UserUpdateMeSchema = z .object({ email: toEmail.optional().describe('User email'), - password: z - .string() - .optional() - .describe('User password (deprecated, use change password endpoint)') - .meta({ deprecated: true }), name: z.string().optional().describe('User name'), avatarColor: UserAvatarColorSchema.nullish(), }) diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 29116e8b1..33bd923c3 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -3,7 +3,6 @@ import { Updateable } from 'kysely'; import { DateTime } from 'luxon'; import type { ArgOf } from 'src/repositories/event.repository.js'; import type { JobOf, UserMetadataItem } from 'src/types.js'; -import { SALT_ROUNDS } from 'src/constants.js'; import { StorageCore } from 'src/cores/storage.core.js'; import { OnEvent, OnJob } from 'src/decorators.js'; import { AuthDto } from 'src/dtos/auth.dto.js'; @@ -68,12 +67,6 @@ export class UserService extends BaseService { avatarColor: dto.avatarColor, }; - if (dto.password) { - const hashedPassword = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS); - update.password = hashedPassword; - update.shouldChangePassword = false; - } - const updatedUser = await this.userRepository.update(user.id, update); return mapUserAdmin(updatedUser); diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index b2efa4bde..96d727f85 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -198,17 +198,6 @@ describe(UserService.name, () => { await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); await expect(sut.getMe(auth)).resolves.toMatchObject(dto); }); - - it('should clear shouldChangePassword when the password is updated', async () => { - const { sut, ctx } = setup(); - const { user } = await ctx.newUser({ shouldChangePassword: true }); - const auth = factory.auth({ user: { id: user.id } }); - - await expect(sut.updateMe(auth, { password: 'super-secret' })).resolves.toMatchObject({ - shouldChangePassword: false, - }); - await expect(sut.getMe(auth)).resolves.toMatchObject({ shouldChangePassword: false }); - }); }); describe('updateMyPreferences', () => { diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index 49578ff80..b4e43e49f 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -3,7 +3,7 @@ import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { Route } from '$lib/route'; - import { updateMyUser } from '@immich/sdk'; + import { handleChangePassword } from '$lib/services/user.service'; import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -14,9 +14,10 @@ let { data }: Props = $props(); + let currentPassword = $state(''); let password = $state(''); let passwordConfirm = $state(''); - const valid = $derived(password === passwordConfirm && passwordConfirm.length > 0); + const valid = $derived(currentPassword.length > 0 && password === passwordConfirm && passwordConfirm.length > 0); const errorMessage = $derived(passwordConfirm.length === 0 || valid ? '' : $t('password_does_not_match')); const onSubmit = async () => { @@ -24,8 +25,10 @@ return; } - await updateMyUser({ userUpdateMeDto: { password } }); - await goto(Route.logout()); + const success = await handleChangePassword({ password: currentPassword, newPassword: password }); + if (success) { + await goto(Route.logout()); + } }; @@ -38,6 +41,10 @@ + + + +