feat(server,web): update email address (#1186)
* feat: change email * test: change email
This commit is contained in:
parent
fdf51a8855
commit
380f719fd8
10 changed files with 72 additions and 9 deletions
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
|
||||
|
|
Loading…
Reference in a new issue