You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(server,web): update email address (#1186)
* feat: change email * test: change email
This commit is contained in:
		
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							| @@ -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]  | ||||
|   | ||||
							
								
								
									
										19
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _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<String>(json, r'id')!, | ||||
|         email: mapValueOfType<String>(json, r'email'), | ||||
|         password: mapValueOfType<String>(json, r'password'), | ||||
|         firstName: mapValueOfType<String>(json, r'firstName'), | ||||
|         lastName: mapValueOfType<String>(json, r'lastName'), | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -2400,6 +2400,9 @@ | ||||
|           "id": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "email": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "password": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|   | ||||
							
								
								
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1779,6 +1779,12 @@ export interface UpdateUserDto { | ||||
|      * @memberof UpdateUserDto | ||||
|      */ | ||||
|     'id': string; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof UpdateUserDto | ||||
|      */ | ||||
|     'email'?: string; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <script lang="ts" context="module"> | ||||
| 	export enum SettingInputFieldType { | ||||
| 		EMAIL = 'email', | ||||
| 		TEXT = 'text', | ||||
| 		NUMBER = 'number', | ||||
| 		PASSWORD = 'password' | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| 	} from '$lib/components/shared-components/notification/notification'; | ||||
| 	import { api, UserResponseDto } from '@api'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { handleError } from '../../utils/handle-error'; | ||||
| 	import SettingInputField, { | ||||
| 		SettingInputFieldType | ||||
| 	} from '../admin-page/settings/setting-input-field.svelte'; | ||||
| @@ -15,6 +16,7 @@ | ||||
| 		try { | ||||
| 			const { data } = await api.userApi.updateUser({ | ||||
| 				id: user.id, | ||||
| 				email: user.email, | ||||
| 				firstName: user.firstName, | ||||
| 				lastName: user.lastName | ||||
| 			}); | ||||
| @@ -26,11 +28,7 @@ | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
| 		} catch (error) { | ||||
| 			console.error('Error [user-profile] [updateProfile]', error); | ||||
| 			notificationController.show({ | ||||
| 				message: 'Unable to save profile', | ||||
| 				type: NotificationType.Error | ||||
| 			}); | ||||
| 			handleError(error, 'Unable to save profile'); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| @@ -47,10 +45,9 @@ | ||||
| 				/> | ||||
|  | ||||
| 				<SettingInputField | ||||
| 					inputType={SettingInputFieldType.TEXT} | ||||
| 					inputType={SettingInputFieldType.EMAIL} | ||||
| 					label="Email" | ||||
| 					bind:value={user.email} | ||||
| 					disabled={true} | ||||
| 				/> | ||||
|  | ||||
| 				<SettingInputField | ||||
|   | ||||
		Reference in New Issue
	
	Block a user