diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index 1bdb496c5..043f2e6ab 100644 Binary files a/mobile/openapi/doc/UpdateUserDto.md and b/mobile/openapi/doc/UpdateUserDto.md differ diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index d91de3aa0..bfb19c734 100644 Binary files a/mobile/openapi/lib/model/update_user_dto.dart and b/mobile/openapi/lib/model/update_user_dto.dart differ diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 3010100d8..5055f5fa9 100644 Binary files a/mobile/openapi/test/update_user_dto_test.dart and b/mobile/openapi/test/update_user_dto_test.dart differ diff --git a/server/apps/immich/src/api-v1/user/dto/update-user.dto.ts b/server/apps/immich/src/api-v1/user/dto/update-user.dto.ts index 424e08e1a..73bcdf199 100644 --- a/server/apps/immich/src/api-v1/user/dto/update-user.dto.ts +++ b/server/apps/immich/src/api-v1/user/dto/update-user.dto.ts @@ -1,9 +1,13 @@ -import { IsNotEmpty, IsOptional } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsOptional } from 'class-validator'; export class UpdateUserDto { @IsNotEmpty() id!: string; + @IsEmail() + @IsOptional() + email?: string; + @IsOptional() password?: string; diff --git a/server/apps/immich/src/api-v1/user/user.core.ts b/server/apps/immich/src/api-v1/user/user.core.ts index 788c21889..a2dec23a4 100644 --- a/server/apps/immich/src/api-v1/user/user.core.ts +++ b/server/apps/immich/src/api-v1/user/user.core.ts @@ -28,6 +28,13 @@ export class UserCore { throw new BadRequestException('Admin user exists'); } + if (dto.email) { + const duplicate = await this.userRepository.getByEmail(dto.email); + if (duplicate && duplicate.id !== id) { + throw new BadRequestException('Email already in user by another account'); + } + } + try { if (dto.password) { dto.password = await hash(dto.password, SALT_ROUNDS); diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index 0db9c9f5e..399fff209 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -102,6 +102,28 @@ describe('UserService', () => { await expect(result).rejects.toBeInstanceOf(ForbiddenException); }); + it('should let a user change their email', async () => { + const dto = { id: immichUser.id, email: 'updated@test.com' }; + + userRepositoryMock.get.mockResolvedValue(immichUser); + userRepositoryMock.update.mockResolvedValue(immichUser); + + await sut.updateUser(immichUser, dto); + + expect(userRepositoryMock.update).toHaveBeenCalledWith(immichUser.id, { email: 'updated@test.com' }); + }); + + it('should not let a user change their email to one already in use', async () => { + const dto = { id: immichUser.id, email: 'updated@test.com' }; + + userRepositoryMock.get.mockResolvedValue(immichUser); + userRepositoryMock.getByEmail.mockResolvedValue(adminUser); + + await expect(sut.updateUser(immichUser, dto)).rejects.toBeInstanceOf(BadRequestException); + + expect(userRepositoryMock.update).not.toHaveBeenCalled(); + }); + it('admin can update any user information', async () => { const update: UpdateUserDto = { id: immichUser.id, diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 866b27576..18c89a968 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2400,6 +2400,9 @@ "id": { "type": "string" }, + "email": { + "type": "string" + }, "password": { "type": "string" }, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 4fed33138..ebca7211b 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1779,6 +1779,12 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'id': string; + /** + * + * @type {string} + * @memberof UpdateUserDto + */ + 'email'?: string; /** * * @type {string} diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte index 5319d7cf5..4fde6f000 100644 --- a/web/src/lib/components/admin-page/settings/setting-input-field.svelte +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -1,5 +1,6 @@ @@ -47,10 +45,9 @@ />