You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(web): user profile (#1148)
* fix: allow updateUser for admin account * feat: update user first/last name * feat(web): change password
This commit is contained in:
		
							
								
								
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -19,6 +19,7 @@ doc/AssetFileUploadResponseDto.md | ||||
| doc/AssetResponseDto.md | ||||
| doc/AssetTypeEnum.md | ||||
| doc/AuthenticationApi.md | ||||
| doc/ChangePasswordDto.md | ||||
| doc/CheckDuplicateAssetDto.md | ||||
| doc/CheckDuplicateAssetResponseDto.md | ||||
| doc/CheckExistingAssetsDto.md | ||||
| @@ -114,6 +115,7 @@ lib/model/asset_count_by_user_id_response_dto.dart | ||||
| lib/model/asset_file_upload_response_dto.dart | ||||
| lib/model/asset_response_dto.dart | ||||
| lib/model/asset_type_enum.dart | ||||
| lib/model/change_password_dto.dart | ||||
| lib/model/check_duplicate_asset_dto.dart | ||||
| lib/model/check_duplicate_asset_response_dto.dart | ||||
| lib/model/check_existing_assets_dto.dart | ||||
| @@ -186,6 +188,7 @@ test/asset_file_upload_response_dto_test.dart | ||||
| test/asset_response_dto_test.dart | ||||
| test/asset_type_enum_test.dart | ||||
| test/authentication_api_test.dart | ||||
| test/change_password_dto_test.dart | ||||
| test/check_duplicate_asset_dto_test.dart | ||||
| test/check_duplicate_asset_response_dto_test.dart | ||||
| test/check_existing_assets_dto_test.dart | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -3,7 +3,7 @@ Immich API | ||||
| 
 | ||||
| This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: | ||||
| 
 | ||||
| - API version: 1.38.2 | ||||
| - API version: 1.39.0 | ||||
| - Build package: org.openapitools.codegen.languages.DartClientCodegen | ||||
| 
 | ||||
| ## Requirements | ||||
| @@ -96,6 +96,7 @@ Class | Method | HTTP request | Description | ||||
| *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} |  | ||||
| *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |  | ||||
| *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |  | ||||
| *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |  | ||||
| *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |  | ||||
| *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |  | ||||
| *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |  | ||||
| @@ -147,6 +148,7 @@ Class | Method | HTTP request | Description | ||||
|  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md) | ||||
|  - [AssetResponseDto](doc//AssetResponseDto.md) | ||||
|  - [AssetTypeEnum](doc//AssetTypeEnum.md) | ||||
|  - [ChangePasswordDto](doc//ChangePasswordDto.md) | ||||
|  - [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md) | ||||
|  - [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md) | ||||
|  - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) | ||||
|   | ||||
							
								
								
									
										48
									
								
								mobile/openapi/doc/AuthenticationApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										48
									
								
								mobile/openapi/doc/AuthenticationApi.md
									
									
									
										generated
									
									
									
								
							| @@ -10,6 +10,7 @@ All URIs are relative to */api* | ||||
| Method | HTTP request | Description | ||||
| ------------- | ------------- | ------------- | ||||
| [**adminSignUp**](AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |  | ||||
| [**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |  | ||||
| [**login**](AuthenticationApi.md#login) | **POST** /auth/login |  | ||||
| [**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |  | ||||
| [**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |  | ||||
| @@ -56,6 +57,53 @@ No authorization required | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **changePassword** | ||||
| > UserResponseDto changePassword(changePasswordDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AuthenticationApi(); | ||||
| final changePasswordDto = ChangePasswordDto(); // ChangePasswordDto |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.changePassword(changePasswordDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AuthenticationApi->changePassword: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **changePasswordDto** | [**ChangePasswordDto**](ChangePasswordDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**UserResponseDto**](UserResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: application/json | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **login** | ||||
| > LoginResponseDto login(loginCredentialDto) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/ChangePasswordDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/ChangePasswordDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| # openapi.model.ChangePasswordDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **password** | **String** |  |  | ||||
| **newPassword** | **String** |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -51,6 +51,7 @@ part 'model/asset_count_by_user_id_response_dto.dart'; | ||||
| part 'model/asset_file_upload_response_dto.dart'; | ||||
| part 'model/asset_response_dto.dart'; | ||||
| part 'model/asset_type_enum.dart'; | ||||
| part 'model/change_password_dto.dart'; | ||||
| part 'model/check_duplicate_asset_dto.dart'; | ||||
| part 'model/check_duplicate_asset_response_dto.dart'; | ||||
| part 'model/check_existing_assets_dto.dart'; | ||||
|   | ||||
							
								
								
									
										47
									
								
								mobile/openapi/lib/api/authentication_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										47
									
								
								mobile/openapi/lib/api/authentication_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -63,6 +63,53 @@ class AuthenticationApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'POST /auth/change-password' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [ChangePasswordDto] changePasswordDto (required): | ||||
|   Future<Response> changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/auth/change-password'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = changePasswordDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'POST', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [ChangePasswordDto] changePasswordDto (required): | ||||
|   Future<UserResponseDto?> changePassword(ChangePasswordDto changePasswordDto,) async { | ||||
|     final response = await changePasswordWithHttpInfo(changePasswordDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'POST /auth/login' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -218,6 +218,8 @@ class ApiClient { | ||||
|           return AssetResponseDto.fromJson(value); | ||||
|         case 'AssetTypeEnum': | ||||
|           return AssetTypeEnumTypeTransformer().decode(value); | ||||
|         case 'ChangePasswordDto': | ||||
|           return ChangePasswordDto.fromJson(value); | ||||
|         case 'CheckDuplicateAssetDto': | ||||
|           return CheckDuplicateAssetDto.fromJson(value); | ||||
|         case 'CheckDuplicateAssetResponseDto': | ||||
|   | ||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/change_password_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/change_password_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class ChangePasswordDto { | ||||
|   /// Returns a new [ChangePasswordDto] instance. | ||||
|   ChangePasswordDto({ | ||||
|     required this.password, | ||||
|     required this.newPassword, | ||||
|   }); | ||||
| 
 | ||||
|   String password; | ||||
| 
 | ||||
|   String newPassword; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is ChangePasswordDto && | ||||
|      other.password == password && | ||||
|      other.newPassword == newPassword; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (password.hashCode) + | ||||
|     (newPassword.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'ChangePasswordDto[password=$password, newPassword=$newPassword]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'password'] = password; | ||||
|       _json[r'newPassword'] = newPassword; | ||||
|     return _json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [ChangePasswordDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static ChangePasswordDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       // Ensure that the map contains the required keys. | ||||
|       // Note 1: the values aren't checked for validity beyond being non-null. | ||||
|       // Note 2: this code is stripped in release mode! | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "ChangePasswordDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "ChangePasswordDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
| 
 | ||||
|       return ChangePasswordDto( | ||||
|         password: mapValueOfType<String>(json, r'password')!, | ||||
|         newPassword: mapValueOfType<String>(json, r'newPassword')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<ChangePasswordDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <ChangePasswordDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = ChangePasswordDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, ChangePasswordDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, ChangePasswordDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = ChangePasswordDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of ChangePasswordDto-objects as value to a dart map | ||||
|   static Map<String, List<ChangePasswordDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<ChangePasswordDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = ChangePasswordDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'password', | ||||
|     'newPassword', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/authentication_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/authentication_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -22,6 +22,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<UserResponseDto> changePassword(ChangePasswordDto changePasswordDto) async | ||||
|     test('test changePassword', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async | ||||
|     test('test login', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										32
									
								
								mobile/openapi/test/change_password_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/change_password_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for ChangePasswordDto | ||||
| void main() { | ||||
|   // final instance = ChangePasswordDto(); | ||||
| 
 | ||||
|   group('test ChangePasswordDto', () { | ||||
|     // String password | ||||
|     test('to test the property `password`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // String newPassword | ||||
|     test('to test the property `newPassword`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -5,7 +5,9 @@ import { AuthType, IMMICH_AUTH_TYPE_COOKIE } from '../../constants/jwt.constant' | ||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||
| import { Authenticated } from '../../decorators/authenticated.decorator'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { UserResponseDto } from '../user/response-dto/user-response.dto'; | ||||
| import { AuthService } from './auth.service'; | ||||
| import { ChangePasswordDto } from './dto/change-password.dto'; | ||||
| import { LoginCredentialDto } from './dto/login-credential.dto'; | ||||
| import { SignUpDto } from './dto/sign-up.dto'; | ||||
| import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto'; | ||||
| @@ -45,6 +47,13 @@ export class AuthController { | ||||
|     return new ValidateAccessTokenResponseDto(true); | ||||
|   } | ||||
|  | ||||
|   @Authenticated() | ||||
|   @ApiBearerAuth() | ||||
|   @Post('change-password') | ||||
|   async changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> { | ||||
|     return this.authService.changePassword(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Post('/logout') | ||||
|   async logout(@Req() req: Request, @Res({ passthrough: true }) response: Response): Promise<LogoutResponseDto> { | ||||
|     const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE]; | ||||
|   | ||||
| @@ -1,9 +1,18 @@ | ||||
| import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; | ||||
| import { | ||||
|   BadRequestException, | ||||
|   Inject, | ||||
|   Injectable, | ||||
|   InternalServerErrorException, | ||||
|   Logger, | ||||
|   UnauthorizedException, | ||||
| } from '@nestjs/common'; | ||||
| import * as bcrypt from 'bcrypt'; | ||||
| import { UserEntity } from '../../../../../libs/database/src/entities/user.entity'; | ||||
| import { AuthType } from '../../constants/jwt.constant'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { IUserRepository, USER_REPOSITORY } from '../user/user-repository'; | ||||
| import { ChangePasswordDto } from './dto/change-password.dto'; | ||||
| import { LoginCredentialDto } from './dto/login-credential.dto'; | ||||
| import { SignUpDto } from './dto/sign-up.dto'; | ||||
| import { AdminSignupResponseDto, mapAdminSignupResponse } from './response-dto/admin-signup-response.dto'; | ||||
| @@ -48,6 +57,23 @@ export class AuthService { | ||||
|     return { successful: true, redirectUri: '/auth/login' }; | ||||
|   } | ||||
|  | ||||
|   public async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) { | ||||
|     const { password, newPassword } = dto; | ||||
|     const user = await this.userRepository.getByEmail(authUser.email, true); | ||||
|     if (!user) { | ||||
|       throw new UnauthorizedException(); | ||||
|     } | ||||
|  | ||||
|     const valid = await this.validatePassword(password, user); | ||||
|     if (!valid) { | ||||
|       throw new BadRequestException('Wrong password'); | ||||
|     } | ||||
|  | ||||
|     user.password = newPassword; | ||||
|  | ||||
|     return this.userRepository.update(user.id, user); | ||||
|   } | ||||
|  | ||||
|   public async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> { | ||||
|     const adminUser = await this.userRepository.getAdmin(); | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsNotEmpty, IsString, MinLength } from 'class-validator'; | ||||
|  | ||||
| export class ChangePasswordDto { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @ApiProperty({ example: 'password' }) | ||||
|   password!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @MinLength(8) | ||||
|   @ApiProperty({ example: 'password' }) | ||||
|   newPassword!: string; | ||||
| } | ||||
| @@ -86,7 +86,7 @@ export class UserRepository implements IUserRepository { | ||||
|     if (user.isAdmin) { | ||||
|       const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } }); | ||||
|  | ||||
|       if (adminUser) { | ||||
|       if (adminUser && adminUser.id !== id) { | ||||
|         throw new BadRequestException('Admin user exists'); | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -1707,6 +1707,42 @@ | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/auth/change-password": { | ||||
|       "post": { | ||||
|         "operationId": "changePassword", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/ChangePasswordDto" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "201": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/UserResponseDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "Authentication" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/auth/logout": { | ||||
|       "post": { | ||||
|         "operationId": "logout", | ||||
| @@ -3258,6 +3294,23 @@ | ||||
|           "authStatus" | ||||
|         ] | ||||
|       }, | ||||
|       "ChangePasswordDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "password": { | ||||
|             "type": "string", | ||||
|             "example": "password" | ||||
|           }, | ||||
|           "newPassword": { | ||||
|             "type": "string", | ||||
|             "example": "password" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "password", | ||||
|           "newPassword" | ||||
|         ] | ||||
|       }, | ||||
|       "LogoutResponseDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|   | ||||
							
								
								
									
										90
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										90
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  * The version of the OpenAPI document: 1.39.0 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
| @@ -481,6 +481,25 @@ export const AssetTypeEnum = { | ||||
| export type AssetTypeEnum = typeof AssetTypeEnum[keyof typeof AssetTypeEnum]; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface ChangePasswordDto | ||||
|  */ | ||||
| export interface ChangePasswordDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof ChangePasswordDto | ||||
|      */ | ||||
|     'password': string; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof ChangePasswordDto | ||||
|      */ | ||||
|     'newPassword': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -4171,6 +4190,45 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {ChangePasswordDto} changePasswordDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         changePassword: async (changePasswordDto: ChangePasswordDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'changePasswordDto' is not null or undefined
 | ||||
|             assertParamExists('changePassword', 'changePasswordDto', changePasswordDto) | ||||
|             const localVarPath = `/auth/change-password`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(changePasswordDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {LoginCredentialDto} loginCredentialDto  | ||||
| @@ -4288,6 +4346,16 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.adminSignUp(signUpDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {ChangePasswordDto} changePasswordDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async changePassword(changePasswordDto: ChangePasswordDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.changePassword(changePasswordDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {LoginCredentialDto} loginCredentialDto  | ||||
| @@ -4335,6 +4403,15 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, | ||||
|         adminSignUp(signUpDto: SignUpDto, options?: any): AxiosPromise<AdminSignupResponseDto> { | ||||
|             return localVarFp.adminSignUp(signUpDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {ChangePasswordDto} changePasswordDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         changePassword(changePasswordDto: ChangePasswordDto, options?: any): AxiosPromise<UserResponseDto> { | ||||
|             return localVarFp.changePassword(changePasswordDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {LoginCredentialDto} loginCredentialDto  | ||||
| @@ -4381,6 +4458,17 @@ export class AuthenticationApi extends BaseAPI { | ||||
|         return AuthenticationApiFp(this.configuration).adminSignUp(signUpDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {ChangePasswordDto} changePasswordDto  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AuthenticationApi | ||||
|      */ | ||||
|     public changePassword(changePasswordDto: ChangePasswordDto, options?: AxiosRequestConfig) { | ||||
|         return AuthenticationApiFp(this.configuration).changePassword(changePasswordDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {LoginCredentialDto} loginCredentialDto  | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  * The version of the OpenAPI document: 1.39.0 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  * The version of the OpenAPI document: 1.39.0 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  * The version of the OpenAPI document: 1.39.0 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|  * Immich | ||||
|  * Immich API | ||||
|  * | ||||
|  * The version of the OpenAPI document: 1.38.2 | ||||
|  * The version of the OpenAPI document: 1.39.0 | ||||
|  *  | ||||
|  * | ||||
|  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | ||||
|   | ||||
| @@ -1,27 +1,154 @@ | ||||
| <script lang="ts"> | ||||
| 	import { UserResponseDto } from '@api'; | ||||
| 	import { | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '$lib/components/shared-components/notification/notification'; | ||||
| 	import { api, UserResponseDto } from '@api'; | ||||
| 	import { AxiosError } from 'axios'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import SettingAccordion from '../admin-page/settings/setting-accordion.svelte'; | ||||
| 	import SettingInputField, { | ||||
| 		SettingInputFieldType | ||||
| 	} from '../admin-page/settings/setting-input-field.svelte'; | ||||
|  | ||||
| 	export let user: UserResponseDto; | ||||
|  | ||||
| 	const handleSaveProfile = async () => { | ||||
| 		try { | ||||
| 			const { data } = await api.userApi.updateUser({ | ||||
| 				id: user.id, | ||||
| 				firstName: user.firstName, | ||||
| 				lastName: user.lastName | ||||
| 			}); | ||||
|  | ||||
| 			Object.assign(user, data); | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'Saved profile', | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
| 		} catch (error) { | ||||
| 			console.error('Error [user-profile] [updateProfile]', error); | ||||
| 			notificationController.show({ | ||||
| 				message: 'Unable to save profile', | ||||
| 				type: NotificationType.Error | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	let password = ''; | ||||
| 	let newPassword = ''; | ||||
| 	let confirmPassword = ''; | ||||
|  | ||||
| 	const handleChangePassword = async () => { | ||||
| 		try { | ||||
| 			await api.authenticationApi.changePassword({ | ||||
| 				password, | ||||
| 				newPassword | ||||
| 			}); | ||||
|  | ||||
| 			notificationController.show({ | ||||
| 				message: 'Updated password', | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
|  | ||||
| 			password = ''; | ||||
| 			newPassword = ''; | ||||
| 			confirmPassword = ''; | ||||
| 		} catch (error: AxiosError | any) { | ||||
| 			console.error('Error [user-profile] [changePassword]', error); | ||||
| 			notificationController.show({ | ||||
| 				message: error?.response?.data?.message || 'Unable to change password', | ||||
| 				type: NotificationType.Error | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <SettingAccordion title="User profile" subtitle="Manage the user information"> | ||||
| <SettingAccordion title="User Profile" subtitle="View and manage your profile"> | ||||
| 	<section class="my-4"> | ||||
| 		<SettingInputField | ||||
| 			inputType={SettingInputFieldType.TEXT} | ||||
| 			label="First name" | ||||
| 			bind:value={user.firstName} | ||||
| 			required={true} | ||||
| 		/> | ||||
| 		<div in:fade={{ duration: 500 }}> | ||||
| 			<form autocomplete="off" on:submit|preventDefault> | ||||
| 				<div class="flex flex-col gap-4 ml-4 mt-4"> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="User ID" | ||||
| 						bind:value={user.id} | ||||
| 						disabled={true} | ||||
| 					/> | ||||
|  | ||||
| 		<SettingInputField | ||||
| 			inputType={SettingInputFieldType.TEXT} | ||||
| 			label="Last name" | ||||
| 			bind:value={user.lastName} | ||||
| 			required={true} | ||||
| 		/> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="Email" | ||||
| 						bind:value={user.email} | ||||
| 						disabled={true} | ||||
| 					/> | ||||
|  | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="First name" | ||||
| 						bind:value={user.firstName} | ||||
| 						required={true} | ||||
| 					/> | ||||
|  | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.TEXT} | ||||
| 						label="Last name" | ||||
| 						bind:value={user.lastName} | ||||
| 						required={true} | ||||
| 					/> | ||||
|  | ||||
| 					<div class="flex justify-end"> | ||||
| 						<button | ||||
| 							type="submit" | ||||
| 							on:click={() => handleSaveProfile()} | ||||
| 							class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed" | ||||
| 							>Save | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </SettingAccordion> | ||||
|  | ||||
| <SettingAccordion title="Password" subtitle="Change your password"> | ||||
| 	<section class="my-4"> | ||||
| 		<div in:fade={{ duration: 500 }}> | ||||
| 			<form autocomplete="off" on:submit|preventDefault> | ||||
| 				<div class="flex flex-col gap-4 ml-4 mt-4"> | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.PASSWORD} | ||||
| 						label="Password" | ||||
| 						bind:value={password} | ||||
| 						required={true} | ||||
| 					/> | ||||
|  | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.PASSWORD} | ||||
| 						label="New password" | ||||
| 						bind:value={newPassword} | ||||
| 						required={true} | ||||
| 					/> | ||||
|  | ||||
| 					<SettingInputField | ||||
| 						inputType={SettingInputFieldType.PASSWORD} | ||||
| 						label="Confirm password" | ||||
| 						bind:value={confirmPassword} | ||||
| 						required={true} | ||||
| 					/> | ||||
|  | ||||
| 					<div class="flex justify-end"> | ||||
| 						<button | ||||
| 							type="submit" | ||||
| 							disabled={!(password && newPassword && newPassword === confirmPassword)} | ||||
| 							on:click={() => handleChangePassword()} | ||||
| 							class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed" | ||||
| 							>Save | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </SettingAccordion> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user