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 @@
/>