diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index 1bdb496c5..043f2e6ab 100644 --- a/mobile/openapi/doc/UpdateUserDto.md +++ b/mobile/openapi/doc/UpdateUserDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **id** | **String** | | +**email** | **String** | | [optional] **password** | **String** | | [optional] **firstName** | **String** | | [optional] **lastName** | **String** | | [optional] diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index d91de3aa0..bfb19c734 100644 --- a/mobile/openapi/lib/model/update_user_dto.dart +++ b/mobile/openapi/lib/model/update_user_dto.dart @@ -14,6 +14,7 @@ class UpdateUserDto { /// Returns a new [UpdateUserDto] instance. UpdateUserDto({ required this.id, + this.email, this.password, this.firstName, this.lastName, @@ -24,6 +25,14 @@ class UpdateUserDto { String id; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? email; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -75,6 +84,7 @@ class UpdateUserDto { @override bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto && other.id == id && + other.email == email && other.password == password && other.firstName == firstName && other.lastName == lastName && @@ -86,6 +96,7 @@ class UpdateUserDto { int get hashCode => // ignore: unnecessary_parenthesis (id.hashCode) + + (email == null ? 0 : email!.hashCode) + (password == null ? 0 : password!.hashCode) + (firstName == null ? 0 : firstName!.hashCode) + (lastName == null ? 0 : lastName!.hashCode) + @@ -94,11 +105,16 @@ class UpdateUserDto { (profileImagePath == null ? 0 : profileImagePath!.hashCode); @override - String toString() => 'UpdateUserDto[id=$id, password=$password, firstName=$firstName, lastName=$lastName, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword, profileImagePath=$profileImagePath]'; + String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword, profileImagePath=$profileImagePath]'; Map toJson() { final _json = {}; _json[r'id'] = id; + if (email != null) { + _json[r'email'] = email; + } else { + _json[r'email'] = null; + } if (password != null) { _json[r'password'] = password; } else { @@ -152,6 +168,7 @@ class UpdateUserDto { return UpdateUserDto( id: mapValueOfType(json, r'id')!, + email: mapValueOfType(json, r'email'), password: mapValueOfType(json, r'password'), firstName: mapValueOfType(json, r'firstName'), lastName: mapValueOfType(json, r'lastName'), diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 3010100d8..5055f5fa9 100644 --- a/mobile/openapi/test/update_user_dto_test.dart +++ b/mobile/openapi/test/update_user_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // String email + test('to test the property `email`', () async { + // TODO + }); + // String password test('to test the property `password`', () async { // TODO 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 @@ />