mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
feat(server,web): update email address (#1186)
* feat: change email * test: change email
This commit is contained in:
parent
fdf51a8855
commit
380f719fd8
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
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
|
**email** | **String** | | [optional]
|
||||||
**password** | **String** | | [optional]
|
**password** | **String** | | [optional]
|
||||||
**firstName** | **String** | | [optional]
|
**firstName** | **String** | | [optional]
|
||||||
**lastName** | **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.
|
/// Returns a new [UpdateUserDto] instance.
|
||||||
UpdateUserDto({
|
UpdateUserDto({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.email,
|
||||||
this.password,
|
this.password,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
this.lastName,
|
this.lastName,
|
||||||
@ -24,6 +25,14 @@ class UpdateUserDto {
|
|||||||
|
|
||||||
String id;
|
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
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -75,6 +84,7 @@ class UpdateUserDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.email == email &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
@ -86,6 +96,7 @@ class UpdateUserDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
(firstName == null ? 0 : firstName!.hashCode) +
|
(firstName == null ? 0 : firstName!.hashCode) +
|
||||||
(lastName == null ? 0 : lastName!.hashCode) +
|
(lastName == null ? 0 : lastName!.hashCode) +
|
||||||
@ -94,11 +105,16 @@ class UpdateUserDto {
|
|||||||
(profileImagePath == null ? 0 : profileImagePath!.hashCode);
|
(profileImagePath == null ? 0 : profileImagePath!.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
_json[r'id'] = id;
|
_json[r'id'] = id;
|
||||||
|
if (email != null) {
|
||||||
|
_json[r'email'] = email;
|
||||||
|
} else {
|
||||||
|
_json[r'email'] = null;
|
||||||
|
}
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
_json[r'password'] = password;
|
_json[r'password'] = password;
|
||||||
} else {
|
} else {
|
||||||
@ -152,6 +168,7 @@ class UpdateUserDto {
|
|||||||
|
|
||||||
return UpdateUserDto(
|
return UpdateUserDto(
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
firstName: mapValueOfType<String>(json, r'firstName'),
|
firstName: mapValueOfType<String>(json, r'firstName'),
|
||||||
lastName: mapValueOfType<String>(json, r'lastName'),
|
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
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String email
|
||||||
|
test('to test the property `email`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String password
|
// String password
|
||||||
test('to test the property `password`', () async {
|
test('to test the property `password`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserDto {
|
export class UpdateUserDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
||||||
|
@ -28,6 +28,13 @@ export class UserCore {
|
|||||||
throw new BadRequestException('Admin user exists');
|
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 {
|
try {
|
||||||
if (dto.password) {
|
if (dto.password) {
|
||||||
dto.password = await hash(dto.password, SALT_ROUNDS);
|
dto.password = await hash(dto.password, SALT_ROUNDS);
|
||||||
|
@ -102,6 +102,28 @@ describe('UserService', () => {
|
|||||||
await expect(result).rejects.toBeInstanceOf(ForbiddenException);
|
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 () => {
|
it('admin can update any user information', async () => {
|
||||||
const update: UpdateUserDto = {
|
const update: UpdateUserDto = {
|
||||||
id: immichUser.id,
|
id: immichUser.id,
|
||||||
|
@ -2400,6 +2400,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"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
|
* @memberof UpdateUserDto
|
||||||
*/
|
*/
|
||||||
'id': string;
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateUserDto
|
||||||
|
*/
|
||||||
|
'email'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export enum SettingInputFieldType {
|
export enum SettingInputFieldType {
|
||||||
|
EMAIL = 'email',
|
||||||
TEXT = 'text',
|
TEXT = 'text',
|
||||||
NUMBER = 'number',
|
NUMBER = 'number',
|
||||||
PASSWORD = 'password'
|
PASSWORD = 'password'
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { api, UserResponseDto } from '@api';
|
import { api, UserResponseDto } from '@api';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { handleError } from '../../utils/handle-error';
|
||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType
|
SettingInputFieldType
|
||||||
} from '../admin-page/settings/setting-input-field.svelte';
|
} from '../admin-page/settings/setting-input-field.svelte';
|
||||||
@ -15,6 +16,7 @@
|
|||||||
try {
|
try {
|
||||||
const { data } = await api.userApi.updateUser({
|
const { data } = await api.userApi.updateUser({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName
|
lastName: user.lastName
|
||||||
});
|
});
|
||||||
@ -26,11 +28,7 @@
|
|||||||
type: NotificationType.Info
|
type: NotificationType.Info
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error [user-profile] [updateProfile]', error);
|
handleError(error, 'Unable to save profile');
|
||||||
notificationController.show({
|
|
||||||
message: 'Unable to save profile',
|
|
||||||
type: NotificationType.Error
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -47,10 +45,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.EMAIL}
|
||||||
label="Email"
|
label="Email"
|
||||||
bind:value={user.email}
|
bind:value={user.email}
|
||||||
disabled={true}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
|
Loading…
x
Reference in New Issue
Block a user