mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(server,web): Delete and restore user from the admin portal (#935)
* delete and restore user from admin UI * addressed review comments and fix e2e test * added cron job to delete user, and some formatting changes * addressed review comments * adding missing queue registration
This commit is contained in:
		
							parent
							
								
									948ff5530c
								
							
						
					
					
						commit
						fe4b307fe6
					
				@ -108,11 +108,13 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
					*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
				
			||||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 | 
					*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 | 
				
			||||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | 
 | 
					*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | 
 | 
				
			||||||
 | 
					*UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{userId} | 
 | 
				
			||||||
*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | 
 | 
					*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | 
 | 
				
			||||||
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | 
 | 
					*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | 
 | 
				
			||||||
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
 | 
					*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
 | 
				
			||||||
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{userId} | 
 | 
					*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{userId} | 
 | 
				
			||||||
*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count | 
 | 
					*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count | 
 | 
				
			||||||
 | 
					*UserApi* | [**restoreUser**](doc//UserApi.md#restoreuser) | **POST** /user/{userId}/restore | 
 | 
				
			||||||
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user | 
 | 
					*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,11 +11,13 @@ Method | HTTP request | Description
 | 
				
			|||||||
------------- | ------------- | -------------
 | 
					------------- | ------------- | -------------
 | 
				
			||||||
[**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 | 
					[**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 | 
				
			||||||
[**createUser**](UserApi.md#createuser) | **POST** /user | 
 | 
					[**createUser**](UserApi.md#createuser) | **POST** /user | 
 | 
				
			||||||
 | 
					[**deleteUser**](UserApi.md#deleteuser) | **DELETE** /user/{userId} | 
 | 
				
			||||||
[**getAllUsers**](UserApi.md#getallusers) | **GET** /user | 
 | 
					[**getAllUsers**](UserApi.md#getallusers) | **GET** /user | 
 | 
				
			||||||
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | 
 | 
					[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | 
 | 
				
			||||||
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
 | 
					[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
 | 
				
			||||||
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{userId} | 
 | 
					[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{userId} | 
 | 
				
			||||||
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count | 
 | 
					[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count | 
 | 
				
			||||||
 | 
					[**restoreUser**](UserApi.md#restoreuser) | **POST** /user/{userId}/restore | 
 | 
				
			||||||
[**updateUser**](UserApi.md#updateuser) | **PUT** /user | 
 | 
					[**updateUser**](UserApi.md#updateuser) | **PUT** /user | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -113,6 +115,53 @@ Name | Type | Description  | Notes
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[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)
 | 
					[[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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **deleteUser**
 | 
				
			||||||
 | 
					> UserResponseDto deleteUser(userId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 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 = UserApi();
 | 
				
			||||||
 | 
					final userId = userId_example; // String | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.deleteUser(userId);
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling UserApi->deleteUser: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					 **userId** | **String**|  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**UserResponseDto**](UserResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bearer](../README.md#bearer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **getAllUsers**
 | 
					# **getAllUsers**
 | 
				
			||||||
> List<UserResponseDto> getAllUsers(isAll)
 | 
					> List<UserResponseDto> getAllUsers(isAll)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -322,6 +371,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)
 | 
					[[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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **restoreUser**
 | 
				
			||||||
 | 
					> UserResponseDto restoreUser(userId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 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 = UserApi();
 | 
				
			||||||
 | 
					final userId = userId_example; // String | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.restoreUser(userId);
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling UserApi->restoreUser: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					 **userId** | **String**|  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**UserResponseDto**](UserResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bearer](../README.md#bearer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **updateUser**
 | 
					# **updateUser**
 | 
				
			||||||
> UserResponseDto updateUser(updateUserDto)
 | 
					> UserResponseDto updateUser(updateUserDto)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**profileImagePath** | **String** |  | 
 | 
					**profileImagePath** | **String** |  | 
 | 
				
			||||||
**shouldChangePassword** | **bool** |  | 
 | 
					**shouldChangePassword** | **bool** |  | 
 | 
				
			||||||
**isAdmin** | **bool** |  | 
 | 
					**isAdmin** | **bool** |  | 
 | 
				
			||||||
 | 
					**deletedAt** | [**DateTime**](DateTime.md) |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -120,6 +120,54 @@ class UserApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs an HTTP 'DELETE /user/{userId}' operation and returns the [Response].
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId (required):
 | 
				
			||||||
 | 
					  Future<Response> deleteUserWithHttpInfo(String userId,) async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/user/{userId}'
 | 
				
			||||||
 | 
					      .replaceAll('{userId}', userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'DELETE',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId (required):
 | 
				
			||||||
 | 
					  Future<UserResponseDto?> deleteUser(String userId,) async {
 | 
				
			||||||
 | 
					    final response = await deleteUserWithHttpInfo(userId,);
 | 
				
			||||||
 | 
					    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 'GET /user' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'GET /user' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -350,6 +398,54 @@ class UserApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs an HTTP 'POST /user/{userId}/restore' operation and returns the [Response].
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId (required):
 | 
				
			||||||
 | 
					  Future<Response> restoreUserWithHttpInfo(String userId,) async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/user/{userId}/restore'
 | 
				
			||||||
 | 
					      .replaceAll('{userId}', userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'POST',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] userId (required):
 | 
				
			||||||
 | 
					  Future<UserResponseDto?> restoreUser(String userId,) async {
 | 
				
			||||||
 | 
					    final response = await restoreUserWithHttpInfo(userId,);
 | 
				
			||||||
 | 
					    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 'PUT /user' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'PUT /user' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,7 @@ class UserResponseDto {
 | 
				
			|||||||
    required this.profileImagePath,
 | 
					    required this.profileImagePath,
 | 
				
			||||||
    required this.shouldChangePassword,
 | 
					    required this.shouldChangePassword,
 | 
				
			||||||
    required this.isAdmin,
 | 
					    required this.isAdmin,
 | 
				
			||||||
 | 
					    required this.deletedAt,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String id;
 | 
					  String id;
 | 
				
			||||||
@ -39,6 +40,8 @@ class UserResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  bool isAdmin;
 | 
					  bool isAdmin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  DateTime? deletedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
 | 
				
			||||||
     other.id == id &&
 | 
					     other.id == id &&
 | 
				
			||||||
@ -48,7 +51,8 @@ class UserResponseDto {
 | 
				
			|||||||
     other.createdAt == createdAt &&
 | 
					     other.createdAt == createdAt &&
 | 
				
			||||||
     other.profileImagePath == profileImagePath &&
 | 
					     other.profileImagePath == profileImagePath &&
 | 
				
			||||||
     other.shouldChangePassword == shouldChangePassword &&
 | 
					     other.shouldChangePassword == shouldChangePassword &&
 | 
				
			||||||
     other.isAdmin == isAdmin;
 | 
					     other.isAdmin == isAdmin &&
 | 
				
			||||||
 | 
					     other.deletedAt == deletedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
@ -60,10 +64,11 @@ class UserResponseDto {
 | 
				
			|||||||
    (createdAt.hashCode) +
 | 
					    (createdAt.hashCode) +
 | 
				
			||||||
    (profileImagePath.hashCode) +
 | 
					    (profileImagePath.hashCode) +
 | 
				
			||||||
    (shouldChangePassword.hashCode) +
 | 
					    (shouldChangePassword.hashCode) +
 | 
				
			||||||
    (isAdmin.hashCode);
 | 
					    (isAdmin.hashCode) +
 | 
				
			||||||
 | 
					    (deletedAt == null ? 0 : deletedAt!.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin]';
 | 
					  String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final _json = <String, dynamic>{};
 | 
					    final _json = <String, dynamic>{};
 | 
				
			||||||
@ -75,6 +80,11 @@ class UserResponseDto {
 | 
				
			|||||||
      _json[r'profileImagePath'] = profileImagePath;
 | 
					      _json[r'profileImagePath'] = profileImagePath;
 | 
				
			||||||
      _json[r'shouldChangePassword'] = shouldChangePassword;
 | 
					      _json[r'shouldChangePassword'] = shouldChangePassword;
 | 
				
			||||||
      _json[r'isAdmin'] = isAdmin;
 | 
					      _json[r'isAdmin'] = isAdmin;
 | 
				
			||||||
 | 
					    if (deletedAt != null) {
 | 
				
			||||||
 | 
					      _json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _json[r'deletedAt'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return _json;
 | 
					    return _json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -105,6 +115,7 @@ class UserResponseDto {
 | 
				
			|||||||
        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
					        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
				
			||||||
        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
					        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
				
			||||||
        isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
 | 
					        isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
 | 
				
			||||||
 | 
					        deletedAt: mapDateTime(json, r'deletedAt', ''),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
@ -162,6 +173,7 @@ class UserResponseDto {
 | 
				
			|||||||
    'profileImagePath',
 | 
					    'profileImagePath',
 | 
				
			||||||
    'shouldChangePassword',
 | 
					    'shouldChangePassword',
 | 
				
			||||||
    'isAdmin',
 | 
					    'isAdmin',
 | 
				
			||||||
 | 
					    'deletedAt',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ export class UserResponseDto {
 | 
				
			|||||||
  profileImagePath!: string;
 | 
					  profileImagePath!: string;
 | 
				
			||||||
  shouldChangePassword!: boolean;
 | 
					  shouldChangePassword!: boolean;
 | 
				
			||||||
  isAdmin!: boolean;
 | 
					  isAdmin!: boolean;
 | 
				
			||||||
 | 
					  deletedAt!: Date | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function mapUser(entity: UserEntity): UserResponseDto {
 | 
					export function mapUser(entity: UserEntity): UserResponseDto {
 | 
				
			||||||
@ -21,5 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
 | 
				
			|||||||
    profileImagePath: entity.profileImagePath,
 | 
					    profileImagePath: entity.profileImagePath,
 | 
				
			||||||
    shouldChangePassword: entity.shouldChangePassword,
 | 
					    shouldChangePassword: entity.shouldChangePassword,
 | 
				
			||||||
    isAdmin: entity.isAdmin,
 | 
					    isAdmin: entity.isAdmin,
 | 
				
			||||||
 | 
					    deletedAt: entity.deletedAt || null,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,12 +7,14 @@ import * as bcrypt from 'bcrypt';
 | 
				
			|||||||
import { UpdateUserDto } from './dto/update-user.dto';
 | 
					import { UpdateUserDto } from './dto/update-user.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IUserRepository {
 | 
					export interface IUserRepository {
 | 
				
			||||||
  get(userId: string): Promise<UserEntity | null>;
 | 
					  get(userId: string, withDeleted?: boolean): Promise<UserEntity | null>;
 | 
				
			||||||
  getByEmail(email: string): Promise<UserEntity | null>;
 | 
					  getByEmail(email: string): Promise<UserEntity | null>;
 | 
				
			||||||
  getList(filter?: { excludeId?: string }): Promise<UserEntity[]>;
 | 
					  getList(filter?: { excludeId?: string }): Promise<UserEntity[]>;
 | 
				
			||||||
  create(createUserDto: CreateUserDto): Promise<UserEntity>;
 | 
					  create(createUserDto: CreateUserDto): Promise<UserEntity>;
 | 
				
			||||||
  update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity>;
 | 
					  update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity>;
 | 
				
			||||||
  createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity>;
 | 
					  createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity>;
 | 
				
			||||||
 | 
					  delete(user: UserEntity): Promise<UserEntity>;
 | 
				
			||||||
 | 
					  restore(user: UserEntity): Promise<UserEntity>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const USER_REPOSITORY = 'USER_REPOSITORY';
 | 
					export const USER_REPOSITORY = 'USER_REPOSITORY';
 | 
				
			||||||
@ -27,8 +29,8 @@ export class UserRepository implements IUserRepository {
 | 
				
			|||||||
    return bcrypt.hash(password, salt);
 | 
					    return bcrypt.hash(password, salt);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async get(userId: string): Promise<UserEntity | null> {
 | 
					  async get(userId: string, withDeleted?: boolean): Promise<UserEntity | null> {
 | 
				
			||||||
    return this.userRepository.findOne({ where: { id: userId } });
 | 
					    return this.userRepository.findOne({ where: { id: userId }, withDeleted: withDeleted });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getByEmail(email: string): Promise<UserEntity | null> {
 | 
					  async getByEmail(email: string): Promise<UserEntity | null> {
 | 
				
			||||||
@ -40,9 +42,10 @@ export class UserRepository implements IUserRepository {
 | 
				
			|||||||
    if (!excludeId) {
 | 
					    if (!excludeId) {
 | 
				
			||||||
      return this.userRepository.find(); // TODO: this should also be ordered the same as below
 | 
					      return this.userRepository.find(); // TODO: this should also be ordered the same as below
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return this.userRepository
 | 
				
			||||||
    return this.userRepository.find({
 | 
					    .find({
 | 
				
			||||||
      where: { id: Not(excludeId) },
 | 
					      where: { id: Not(excludeId) },
 | 
				
			||||||
 | 
					      withDeleted: true,
 | 
				
			||||||
      order: {
 | 
					      order: {
 | 
				
			||||||
        createdAt: 'DESC',
 | 
					        createdAt: 'DESC',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -88,6 +91,17 @@ export class UserRepository implements IUserRepository {
 | 
				
			|||||||
    return this.userRepository.save(user);
 | 
					    return this.userRepository.save(user);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async delete(user: UserEntity): Promise<UserEntity> {
 | 
				
			||||||
 | 
					    if (user.isAdmin) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Cannot delete admin user! stay sane!');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this.userRepository.softRemove(user);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async restore(user: UserEntity): Promise<UserEntity> {
 | 
				
			||||||
 | 
					    return this.userRepository.recover(user);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity> {
 | 
					  async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity> {
 | 
				
			||||||
    user.profileImagePath = fileInfo.path;
 | 
					    user.profileImagePath = fileInfo.path;
 | 
				
			||||||
    return this.userRepository.save(user);
 | 
					    return this.userRepository.save(user);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import {
 | 
				
			|||||||
  Controller,
 | 
					  Controller,
 | 
				
			||||||
  Get,
 | 
					  Get,
 | 
				
			||||||
  Post,
 | 
					  Post,
 | 
				
			||||||
 | 
					  Delete,
 | 
				
			||||||
  Body,
 | 
					  Body,
 | 
				
			||||||
  Param,
 | 
					  Param,
 | 
				
			||||||
  ValidationPipe,
 | 
					  ValidationPipe,
 | 
				
			||||||
@ -67,6 +68,20 @@ export class UserController {
 | 
				
			|||||||
    return await this.userService.getUserCount();
 | 
					    return await this.userService.getUserCount();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Authenticated({ admin: true })
 | 
				
			||||||
 | 
					  @ApiBearerAuth()
 | 
				
			||||||
 | 
					  @Delete('/:userId')
 | 
				
			||||||
 | 
					  async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
 | 
				
			||||||
 | 
					    return await this.userService.deleteUser(authUser, userId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Authenticated({ admin: true })
 | 
				
			||||||
 | 
					  @ApiBearerAuth()
 | 
				
			||||||
 | 
					  @Post('/:userId/restore')
 | 
				
			||||||
 | 
					  async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
 | 
				
			||||||
 | 
					    return await this.userService.restoreUser(authUser, userId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Authenticated()
 | 
					  @Authenticated()
 | 
				
			||||||
  @ApiBearerAuth()
 | 
					  @ApiBearerAuth()
 | 
				
			||||||
  @Put()
 | 
					  @Put()
 | 
				
			||||||
 | 
				
			|||||||
@ -65,6 +65,8 @@ describe('UserService', () => {
 | 
				
			|||||||
      getByEmail: jest.fn(),
 | 
					      getByEmail: jest.fn(),
 | 
				
			||||||
      getList: jest.fn(),
 | 
					      getList: jest.fn(),
 | 
				
			||||||
      update: jest.fn(),
 | 
					      update: jest.fn(),
 | 
				
			||||||
 | 
					      delete: jest.fn(),
 | 
				
			||||||
 | 
					      restore: jest.fn(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sui = new UserService(userRepositoryMock);
 | 
					    sui = new UserService(userRepositoryMock);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,13 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  BadRequestException,
 | 
					  BadRequestException,
 | 
				
			||||||
 | 
					  ForbiddenException,
 | 
				
			||||||
  Inject,
 | 
					  Inject,
 | 
				
			||||||
  Injectable,
 | 
					  Injectable,
 | 
				
			||||||
  InternalServerErrorException,
 | 
					  InternalServerErrorException,
 | 
				
			||||||
  Logger,
 | 
					  Logger,
 | 
				
			||||||
  NotFoundException,
 | 
					  NotFoundException,
 | 
				
			||||||
  StreamableFile,
 | 
					  StreamableFile,
 | 
				
			||||||
 | 
					  UnauthorizedException,
 | 
				
			||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
import { CreateUserDto } from './dto/create-user.dto';
 | 
					import { CreateUserDto } from './dto/create-user.dto';
 | 
				
			||||||
@ -38,8 +40,8 @@ export class UserService {
 | 
				
			|||||||
    return allUserExceptRequestedUser.map(mapUser);
 | 
					    return allUserExceptRequestedUser.map(mapUser);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getUserById(userId: string): Promise<UserResponseDto> {
 | 
					  async getUserById(userId: string, withDeleted = false): Promise<UserResponseDto> {
 | 
				
			||||||
    const user = await this.userRepository.get(userId);
 | 
					    const user = await this.userRepository.get(userId, withDeleted);
 | 
				
			||||||
    if (!user) {
 | 
					    if (!user) {
 | 
				
			||||||
      throw new NotFoundException('User not found');
 | 
					      throw new NotFoundException('User not found');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -105,6 +107,48 @@ export class UserService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteUser(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> {
 | 
				
			||||||
 | 
					    const requestor = await this.userRepository.get(authUser.id);
 | 
				
			||||||
 | 
					    if (!requestor) {
 | 
				
			||||||
 | 
					      throw new UnauthorizedException('Requestor not found');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!requestor.isAdmin) {
 | 
				
			||||||
 | 
					      throw new ForbiddenException('Unauthorized');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const user = await this.userRepository.get(userId);
 | 
				
			||||||
 | 
					    if (!user) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('User not found');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const deletedUser = await this.userRepository.delete(user);
 | 
				
			||||||
 | 
					      return mapUser(deletedUser);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      Logger.error(e, 'Failed to delete user');
 | 
				
			||||||
 | 
					      throw new InternalServerErrorException('Failed to delete user');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async restoreUser(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> {
 | 
				
			||||||
 | 
					    const requestor = await this.userRepository.get(authUser.id);
 | 
				
			||||||
 | 
					    if (!requestor) {
 | 
				
			||||||
 | 
					      throw new UnauthorizedException('Requestor not found');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!requestor.isAdmin) {
 | 
				
			||||||
 | 
					      throw new ForbiddenException('Unauthorized');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const user = await this.userRepository.get(userId, true);
 | 
				
			||||||
 | 
					    if (!user) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('User not found');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const restoredUser = await this.userRepository.restore(user);
 | 
				
			||||||
 | 
					      return mapUser(restoredUser);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      Logger.error(e, 'Failed to restore deleted user');
 | 
				
			||||||
 | 
					      throw new InternalServerErrorException('Failed to restore deleted user');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async createProfileImage(
 | 
					  async createProfileImage(
 | 
				
			||||||
    authUser: AuthUserDto,
 | 
					    authUser: AuthUserDto,
 | 
				
			||||||
    fileInfo: Express.Multer.File,
 | 
					    fileInfo: Express.Multer.File,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,10 +2,10 @@ import { Process, Processor } from '@nestjs/bull';
 | 
				
			|||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Repository } from 'typeorm';
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
					import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
				
			||||||
import fs from 'fs';
 | 
					 | 
				
			||||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
 | 
					import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
 | 
				
			||||||
import { Job } from 'bull';
 | 
					import { Job } from 'bull';
 | 
				
			||||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
 | 
					import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
 | 
				
			||||||
 | 
					import { assetUtils } from '@app/common/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Processor('background-task')
 | 
					@Processor('background-task')
 | 
				
			||||||
export class BackgroundTaskProcessor {
 | 
					export class BackgroundTaskProcessor {
 | 
				
			||||||
@ -23,37 +23,7 @@ export class BackgroundTaskProcessor {
 | 
				
			|||||||
    const { assets } = job.data;
 | 
					    const { assets } = job.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const asset of assets) {
 | 
					    for (const asset of assets) {
 | 
				
			||||||
      fs.unlink(asset.originalPath, (err) => {
 | 
					      assetUtils.deleteFiles(asset);
 | 
				
			||||||
        if (err) {
 | 
					 | 
				
			||||||
          console.log('error deleting ', asset.originalPath);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // TODO: what if there is no asset.resizePath. Should fail the Job?
 | 
					 | 
				
			||||||
      // => panoti report: Job not fail
 | 
					 | 
				
			||||||
      if (asset.resizePath) {
 | 
					 | 
				
			||||||
        fs.unlink(asset.resizePath, (err) => {
 | 
					 | 
				
			||||||
          if (err) {
 | 
					 | 
				
			||||||
            console.log('error deleting ', asset.resizePath);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (asset.webpPath) {
 | 
					 | 
				
			||||||
        fs.unlink(asset.webpPath, (err) => {
 | 
					 | 
				
			||||||
          if (err) {
 | 
					 | 
				
			||||||
            console.log('error deleting ', asset.webpPath);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (asset.encodedVideoPath) {
 | 
					 | 
				
			||||||
        fs.unlink(asset.encodedVideoPath, (err) => {
 | 
					 | 
				
			||||||
          if (err) {
 | 
					 | 
				
			||||||
            console.log('error deleting ', asset.encodedVideoPath);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,19 @@ import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
				
			|||||||
import { ScheduleTasksService } from './schedule-tasks.service';
 | 
					import { ScheduleTasksService } from './schedule-tasks.service';
 | 
				
			||||||
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
 | 
					import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
 | 
				
			||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
 | 
					import { ExifEntity } from '@app/database/entities/exif.entity';
 | 
				
			||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
					    TypeOrmModule.forFeature([AssetEntity, ExifEntity, UserEntity]),
 | 
				
			||||||
 | 
					    BullModule.registerQueue({
 | 
				
			||||||
 | 
					      name: QueueNameEnum.USER_DELETION,
 | 
				
			||||||
 | 
					      defaultJobOptions: {
 | 
				
			||||||
 | 
					        attempts: 3,
 | 
				
			||||||
 | 
					        removeOnComplete: true,
 | 
				
			||||||
 | 
					        removeOnFail: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
    BullModule.registerQueue({
 | 
					    BullModule.registerQueue({
 | 
				
			||||||
      name: QueueNameEnum.VIDEO_CONVERSION,
 | 
					      name: QueueNameEnum.VIDEO_CONVERSION,
 | 
				
			||||||
      defaultJobOptions: {
 | 
					      defaultJobOptions: {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import { Queue } from 'bull';
 | 
				
			|||||||
import { randomUUID } from 'crypto';
 | 
					import { randomUUID } from 'crypto';
 | 
				
			||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
 | 
					import { ExifEntity } from '@app/database/entities/exif.entity';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  userDeletionProcessorName,
 | 
				
			||||||
  exifExtractionProcessorName,
 | 
					  exifExtractionProcessorName,
 | 
				
			||||||
  generateWEBPThumbnailProcessorName,
 | 
					  generateWEBPThumbnailProcessorName,
 | 
				
			||||||
  IMetadataExtractionJob,
 | 
					  IMetadataExtractionJob,
 | 
				
			||||||
@ -18,10 +19,16 @@ import {
 | 
				
			|||||||
  videoMetadataExtractionProcessorName,
 | 
					  videoMetadataExtractionProcessorName,
 | 
				
			||||||
} from '@app/job';
 | 
					} from '@app/job';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface';
 | 
				
			||||||
 | 
					import { userUtils } from '@app/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class ScheduleTasksService {
 | 
					export class ScheduleTasksService {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(UserEntity)
 | 
				
			||||||
 | 
					    private userRepository: Repository<UserEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @InjectRepository(AssetEntity)
 | 
					    @InjectRepository(AssetEntity)
 | 
				
			||||||
    private assetRepository: Repository<AssetEntity>,
 | 
					    private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,6 +44,9 @@ export class ScheduleTasksService {
 | 
				
			|||||||
    @InjectQueue(QueueNameEnum.METADATA_EXTRACTION)
 | 
					    @InjectQueue(QueueNameEnum.METADATA_EXTRACTION)
 | 
				
			||||||
    private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
 | 
					    private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @InjectQueue(QueueNameEnum.USER_DELETION)
 | 
				
			||||||
 | 
					    private userDeletionQueue: Queue<IUserDeletionJob>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private configService: ConfigService,
 | 
					    private configService: ConfigService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -128,4 +138,14 @@ export class ScheduleTasksService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Cron(CronExpression.EVERY_DAY_AT_11PM)
 | 
				
			||||||
 | 
					  async deleteUserAndRelatedAssets() {
 | 
				
			||||||
 | 
					    const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });
 | 
				
			||||||
 | 
					    for (const user of usersToDelete) {
 | 
				
			||||||
 | 
					      if (userUtils.isReadyForDeletion(user)) {
 | 
				
			||||||
 | 
					        await this.userDeletionQueue.add(userDeletionProcessorName, { user: user }, { jobId: randomUUID() });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -104,6 +104,7 @@ describe('User', () => {
 | 
				
			|||||||
              isAdmin: false,
 | 
					              isAdmin: false,
 | 
				
			||||||
              shouldChangePassword: true,
 | 
					              shouldChangePassword: true,
 | 
				
			||||||
              profileImagePath: '',
 | 
					              profileImagePath: '',
 | 
				
			||||||
 | 
					              deletedAt: null,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              email: userTwoEmail,
 | 
					              email: userTwoEmail,
 | 
				
			||||||
@ -114,6 +115,7 @@ describe('User', () => {
 | 
				
			|||||||
              isAdmin: false,
 | 
					              isAdmin: false,
 | 
				
			||||||
              shouldChangePassword: true,
 | 
					              shouldChangePassword: true,
 | 
				
			||||||
              profileImagePath: '',
 | 
					              profileImagePath: '',
 | 
				
			||||||
 | 
					              deletedAt: null,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ]),
 | 
					          ]),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import { APP_UPLOAD_LOCATION, userUtils } from '@app/common';
 | 
				
			||||||
 | 
					import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
				
			||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					import { QueueNameEnum, userDeletionProcessorName } from '@app/job';
 | 
				
			||||||
 | 
					import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface';
 | 
				
			||||||
 | 
					import { Process, Processor } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Job } from 'bull';
 | 
				
			||||||
 | 
					import { join } from 'path';
 | 
				
			||||||
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Processor(QueueNameEnum.USER_DELETION)
 | 
				
			||||||
 | 
					export class UserDeletionProcessor {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(UserEntity)
 | 
				
			||||||
 | 
					    private userRepository: Repository<UserEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity)
 | 
				
			||||||
 | 
					    private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(userDeletionProcessorName)
 | 
				
			||||||
 | 
					  async processUserDeletion(job: Job<IUserDeletionJob>) {
 | 
				
			||||||
 | 
					    const { user } = job.data;
 | 
				
			||||||
 | 
					    // just for extra protection here
 | 
				
			||||||
 | 
					    if (userUtils.isReadyForDeletion(user)) {
 | 
				
			||||||
 | 
					      const basePath = APP_UPLOAD_LOCATION;
 | 
				
			||||||
 | 
					      const userAssetDir = join(basePath, user.id)
 | 
				
			||||||
 | 
					      fs.rmSync(userAssetDir, { recursive: true, force: true })
 | 
				
			||||||
 | 
					      await this.assetRepository.delete({ userId: user.id })
 | 
				
			||||||
 | 
					      await this.userRepository.remove(user);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										39
									
								
								server/libs/common/src/utils/asset-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								server/libs/common/src/utils/asset-utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
				
			||||||
 | 
					import { AssetResponseDto } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
 | 
				
			||||||
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteFiles = (asset: AssetEntity | AssetResponseDto) => {
 | 
				
			||||||
 | 
					  fs.unlink(asset.originalPath, (err) => {
 | 
				
			||||||
 | 
					    if (err) {
 | 
				
			||||||
 | 
					      console.log('error deleting ', asset.originalPath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO: what if there is no asset.resizePath. Should fail the Job?
 | 
				
			||||||
 | 
					  // => panoti report: Job not fail
 | 
				
			||||||
 | 
					  if (asset.resizePath) {
 | 
				
			||||||
 | 
					    fs.unlink(asset.resizePath, (err) => {
 | 
				
			||||||
 | 
					      if (err) {
 | 
				
			||||||
 | 
					        console.log('error deleting ', asset.resizePath);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (asset.webpPath) {
 | 
				
			||||||
 | 
					    fs.unlink(asset.webpPath, (err) => {
 | 
				
			||||||
 | 
					      if (err) {
 | 
				
			||||||
 | 
					        console.log('error deleting ', asset.webpPath);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (asset.encodedVideoPath) {
 | 
				
			||||||
 | 
					    fs.unlink(asset.encodedVideoPath, (err) => {
 | 
				
			||||||
 | 
					      if (err) {
 | 
				
			||||||
 | 
					        console.log('error deleting ', asset.encodedVideoPath);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const assetUtils = { deleteFiles };
 | 
				
			||||||
@ -1 +1,3 @@
 | 
				
			|||||||
export * from './time-utils';
 | 
					export * from './time-utils';
 | 
				
			||||||
 | 
					export * from './asset-utils';
 | 
				
			||||||
 | 
					export * from './user-utils';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								server/libs/common/src/utils/user-utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								server/libs/common/src/utils/user-utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					// create unit test for user utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					import { userUtils } from './user-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('User Utilities', () => {
 | 
				
			||||||
 | 
					  describe('checkIsReadyForDeletion', () => {
 | 
				
			||||||
 | 
					    it('check that user is not ready to be deleted', () => {
 | 
				
			||||||
 | 
					      const result = userUtils.isReadyForDeletion({ deletedAt: new Date() } as UserEntity);
 | 
				
			||||||
 | 
					      expect(result).toBeFalsy();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('check that user is ready to be deleted', () => {
 | 
				
			||||||
 | 
					      const aWeekAgo = new Date(new Date().getTime() - 8 * 86400000);
 | 
				
			||||||
 | 
					      const result = userUtils.isReadyForDeletion({ deletedAt: aWeekAgo } as UserEntity);
 | 
				
			||||||
 | 
					      expect(result).toBeTruthy();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										16
									
								
								server/libs/common/src/utils/user-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/libs/common/src/utils/user-utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createUserUtils() {
 | 
				
			||||||
 | 
					  const isReadyForDeletion = (user: UserEntity): boolean => {
 | 
				
			||||||
 | 
					    if (user.deletedAt == null) return false;
 | 
				
			||||||
 | 
					    const millisecondsInDay = 86400000;
 | 
				
			||||||
 | 
					    // get this number (7 days) from some configuration perhaps ?
 | 
				
			||||||
 | 
					    const millisecondsDeleteWait = millisecondsInDay * 7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const millisecondsSinceDelete = new Date().getTime() - (user.deletedAt?.getTime() ?? 0);
 | 
				
			||||||
 | 
					    return millisecondsSinceDelete >= millisecondsDeleteWait;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return { isReadyForDeletion };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const userUtils = createUserUtils();
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
 | 
					import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('users')
 | 
					@Entity('users')
 | 
				
			||||||
export class UserEntity {
 | 
					export class UserEntity {
 | 
				
			||||||
@ -31,4 +31,7 @@ export class UserEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @CreateDateColumn()
 | 
					  @CreateDateColumn()
 | 
				
			||||||
  createdAt!: string;
 | 
					  createdAt!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @DeleteDateColumn()
 | 
				
			||||||
 | 
					  deletedAt?: Date;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AddingDeletedAtColumnInUserEntity1667762360744 implements MigrationInterface {
 | 
				
			||||||
 | 
					  name = 'AddingDeletedAtColumnInUserEntity1667762360744';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "users" ADD "deletedAt" TIMESTAMP`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deletedAt"`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -29,3 +29,8 @@ export enum MachineLearningJobNameEnum {
 | 
				
			|||||||
  OBJECT_DETECTION = 'detect-object',
 | 
					  OBJECT_DETECTION = 'detect-object',
 | 
				
			||||||
  IMAGE_TAGGING = 'tag-image',
 | 
					  IMAGE_TAGGING = 'tag-image',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * User deletion Queue Jobs
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const userDeletionProcessorName = 'user-deletion';
 | 
				
			||||||
 | 
				
			|||||||
@ -5,4 +5,5 @@ export enum QueueNameEnum {
 | 
				
			|||||||
  CHECKSUM_GENERATION = 'generate-checksum-queue',
 | 
					  CHECKSUM_GENERATION = 'generate-checksum-queue',
 | 
				
			||||||
  ASSET_UPLOADED = 'asset-uploaded-queue',
 | 
					  ASSET_UPLOADED = 'asset-uploaded-queue',
 | 
				
			||||||
  MACHINE_LEARNING = 'machine-learning-queue',
 | 
					  MACHINE_LEARNING = 'machine-learning-queue',
 | 
				
			||||||
 | 
					  USER_DELETION = 'user-deletion-queue',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IUserDeletionJob {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The user entity that was saved in the database
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  user: UserEntity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1575,6 +1575,12 @@ export interface UserResponseDto {
 | 
				
			|||||||
     * @memberof UserResponseDto
 | 
					     * @memberof UserResponseDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'isAdmin': boolean;
 | 
					    'isAdmin': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof UserResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'deletedAt': string | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@ -4711,6 +4717,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
 | 
				
			|||||||
                options: localVarRequestOptions,
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        deleteUser: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            // verify required parameter 'userId' is not null or undefined
 | 
				
			||||||
 | 
					            assertParamExists('deleteUser', 'userId', userId)
 | 
				
			||||||
 | 
					            const localVarPath = `/user/{userId}`
 | 
				
			||||||
 | 
					                .replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
 | 
				
			||||||
 | 
					            // 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: 'DELETE', ...baseOptions, ...options};
 | 
				
			||||||
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication bearer required
 | 
				
			||||||
 | 
					            // http bearer authentication required
 | 
				
			||||||
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {boolean} isAll 
 | 
					         * @param {boolean} isAll 
 | 
				
			||||||
@ -4870,6 +4913,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        restoreUser: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            // verify required parameter 'userId' is not null or undefined
 | 
				
			||||||
 | 
					            assertParamExists('restoreUser', 'userId', userId)
 | 
				
			||||||
 | 
					            const localVarPath = `/user/{userId}/restore`
 | 
				
			||||||
 | 
					                .replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
 | 
				
			||||||
 | 
					            // 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
@ -4948,6 +5028,16 @@ export const UserApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async deleteUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(userId, options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {boolean} isAll 
 | 
					         * @param {boolean} isAll 
 | 
				
			||||||
@ -4996,6 +5086,16 @@ export const UserApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async restoreUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.restoreUser(userId, options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {UpdateUserDto} updateUserDto 
 | 
					         * @param {UpdateUserDto} updateUserDto 
 | 
				
			||||||
@ -5034,6 +5134,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
 | 
				
			|||||||
        createUser(createUserDto: CreateUserDto, options?: any): AxiosPromise<UserResponseDto> {
 | 
					        createUser(createUserDto: CreateUserDto, options?: any): AxiosPromise<UserResponseDto> {
 | 
				
			||||||
            return localVarFp.createUser(createUserDto, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.createUser(createUserDto, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        deleteUser(userId: string, options?: any): AxiosPromise<UserResponseDto> {
 | 
				
			||||||
 | 
					            return localVarFp.deleteUser(userId, options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {boolean} isAll 
 | 
					         * @param {boolean} isAll 
 | 
				
			||||||
@ -5077,6 +5186,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
 | 
				
			|||||||
        getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
 | 
					        getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
 | 
				
			||||||
            return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} userId 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        restoreUser(userId: string, options?: any): AxiosPromise<UserResponseDto> {
 | 
				
			||||||
 | 
					            return localVarFp.restoreUser(userId, options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {UpdateUserDto} updateUserDto 
 | 
					         * @param {UpdateUserDto} updateUserDto 
 | 
				
			||||||
@ -5118,6 +5236,17 @@ export class UserApi extends BaseAPI {
 | 
				
			|||||||
        return UserApiFp(this.configuration).createUser(createUserDto, options).then((request) => request(this.axios, this.basePath));
 | 
					        return UserApiFp(this.configuration).createUser(createUserDto, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {string} userId 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof UserApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public deleteUser(userId: string, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return UserApiFp(this.configuration).deleteUser(userId, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {boolean} isAll 
 | 
					     * @param {boolean} isAll 
 | 
				
			||||||
@ -5171,6 +5300,17 @@ export class UserApi extends BaseAPI {
 | 
				
			|||||||
        return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
 | 
					        return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {string} userId 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof UserApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public restoreUser(userId: string, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return UserApiFp(this.configuration).restoreUser(userId, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {UpdateUserDto} updateUserDto 
 | 
					     * @param {UpdateUserDto} updateUserDto 
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { api, UserResponseDto } from '@api';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let user: UserResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const deleteUser = async () => {
 | 
				
			||||||
 | 
							const deletedUser = await api.userApi.deleteUser(user.id);
 | 
				
			||||||
 | 
							if (deletedUser.data.deletedAt != null) dispatch('user-delete-success');
 | 
				
			||||||
 | 
							else dispatch('user-delete-fail');
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
						class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<div
 | 
				
			||||||
 | 
							class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
 | 
				
			||||||
 | 
								Confirm User Deletion
 | 
				
			||||||
 | 
							</h1>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							<p class="ml-4 text-md py-5 text-center">
 | 
				
			||||||
 | 
								{user.firstName}
 | 
				
			||||||
 | 
								{user.lastName} account and assets along will be marked to delete completely after 7 days. are
 | 
				
			||||||
 | 
								you sure you want to proceed ?
 | 
				
			||||||
 | 
							</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex w-full px-4 gap-4 mt-8">
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									on:click={deleteUser}
 | 
				
			||||||
 | 
									class="flex-1 transition-colors bg-red-500 hover:bg-red-400 px-6 py-3 text-white rounded-full w-full font-medium"
 | 
				
			||||||
 | 
									>Confirm
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										40
									
								
								web/src/lib/components/admin-page/restore-dialoge.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/lib/components/admin-page/restore-dialoge.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { api, UserResponseDto } from '@api';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let user: UserResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const restoreUser = async () => {
 | 
				
			||||||
 | 
							const restoredUser = await api.userApi.restoreUser(user.id);
 | 
				
			||||||
 | 
							if (restoredUser.data.deletedAt == null) dispatch('user-restore-success');
 | 
				
			||||||
 | 
							else dispatch('user-restore-fail');
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
						class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<div
 | 
				
			||||||
 | 
							class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
 | 
				
			||||||
 | 
								Restore User
 | 
				
			||||||
 | 
							</h1>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							<p class="ml-4 text-md py-5 text-center">
 | 
				
			||||||
 | 
								{user.firstName}
 | 
				
			||||||
 | 
								{user.lastName} account will restored
 | 
				
			||||||
 | 
							</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex w-full px-4 gap-4 mt-8">
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									on:click={restoreUser}
 | 
				
			||||||
 | 
									class="flex-1 transition-colors bg-lime-600 hover:bg-lime-500 px-6 py-3 text-white rounded-full w-full font-medium"
 | 
				
			||||||
 | 
									>Confirm
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -3,9 +3,21 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	import { createEventDispatcher } from 'svelte';
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
	import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
 | 
						import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
 | 
				
			||||||
 | 
						import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
 | 
				
			||||||
 | 
						import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
 | 
				
			||||||
 | 
						import moment from 'moment';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let allUsers: Array<UserResponseDto>;
 | 
						export let allUsers: Array<UserResponseDto>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const dispatch = createEventDispatcher();
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const isDeleted = (user: UserResponseDto): boolean => {
 | 
				
			||||||
 | 
							return user.deletedAt != null;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const getDeleteDate = (user: UserResponseDto): string => {
 | 
				
			||||||
 | 
							return moment(user.deletedAt).add(7, 'days').format('LL');
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<table class="text-left w-full my-5">
 | 
					<table class="text-left w-full my-5">
 | 
				
			||||||
@ -16,7 +28,7 @@
 | 
				
			|||||||
			<th class="text-center w-1/4 font-medium text-sm">Email</th>
 | 
								<th class="text-center w-1/4 font-medium text-sm">Email</th>
 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">First name</th>
 | 
								<th class="text-center w-1/4 font-medium text-sm">First name</th>
 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">Last name</th>
 | 
								<th class="text-center w-1/4 font-medium text-sm">Last name</th>
 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">Edit</th>
 | 
								<th class="text-center w-1/4 font-medium text-sm">Action</th>
 | 
				
			||||||
		</tr>
 | 
							</tr>
 | 
				
			||||||
	</thead>
 | 
						</thead>
 | 
				
			||||||
	<tbody
 | 
						<tbody
 | 
				
			||||||
@ -25,21 +37,44 @@
 | 
				
			|||||||
		{#each allUsers as user, i}
 | 
							{#each allUsers as user, i}
 | 
				
			||||||
			<tr
 | 
								<tr
 | 
				
			||||||
				class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-bg ${
 | 
									class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-bg ${
 | 
				
			||||||
					i % 2 == 0 ? 'bg-immich-gray dark:bg-[#e5e5e5]' : 'bg-immich-bg dark:bg-[#eeeeee]'
 | 
										isDeleted(user)
 | 
				
			||||||
 | 
											? 'bg-red-50'
 | 
				
			||||||
 | 
											: i % 2 == 0
 | 
				
			||||||
 | 
											? 'bg-immich-gray dark:bg-[#e5e5e5]'
 | 
				
			||||||
 | 
											: 'bg-immich-bg dark:bg-[#eeeeee]'
 | 
				
			||||||
				}`}
 | 
									}`}
 | 
				
			||||||
			>
 | 
								>
 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
 | 
									<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
 | 
									<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
 | 
									<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis"
 | 
									<td class="text-sm px-4 w-1/4 text-ellipsis">
 | 
				
			||||||
					><button
 | 
										{#if !isDeleted(user)}
 | 
				
			||||||
 | 
											<button
 | 
				
			||||||
							on:click={() => {
 | 
												on:click={() => {
 | 
				
			||||||
								dispatch('edit-user', { user });
 | 
													dispatch('edit-user', { user });
 | 
				
			||||||
							}}
 | 
												}}
 | 
				
			||||||
							class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
												class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
						><PencilOutline size="20" /></button
 | 
												><PencilOutline size="16" /></button
 | 
				
			||||||
					></td
 | 
					 | 
				
			||||||
						>
 | 
											>
 | 
				
			||||||
 | 
											<button
 | 
				
			||||||
 | 
												on:click={() => {
 | 
				
			||||||
 | 
													dispatch('delete-user', { user });
 | 
				
			||||||
 | 
												}}
 | 
				
			||||||
 | 
												class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
 | 
												><TrashCanOutline size="16" /></button
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
										{/if}
 | 
				
			||||||
 | 
										{#if isDeleted(user)}
 | 
				
			||||||
 | 
											<button
 | 
				
			||||||
 | 
												on:click={() => {
 | 
				
			||||||
 | 
													dispatch('restore-user', { user });
 | 
				
			||||||
 | 
												}}
 | 
				
			||||||
 | 
												class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
 | 
												title={`scheduled removal on ${getDeleteDate(user)}`}
 | 
				
			||||||
 | 
												><DeleteRestore size="16" /></button
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
										{/if}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
			</tr>
 | 
								</tr>
 | 
				
			||||||
		{/each}
 | 
							{/each}
 | 
				
			||||||
	</tbody>
 | 
						</tbody>
 | 
				
			||||||
 | 
				
			|||||||
@ -11,21 +11,25 @@
 | 
				
			|||||||
	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
						import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
				
			||||||
	import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
 | 
						import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
 | 
				
			||||||
	import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
 | 
						import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
 | 
				
			||||||
 | 
						import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
 | 
				
			||||||
	import StatusBox from '$lib/components/shared-components/status-box.svelte';
 | 
						import StatusBox from '$lib/components/shared-components/status-box.svelte';
 | 
				
			||||||
	import type { PageData } from './$types';
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
	import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
 | 
						import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
 | 
				
			||||||
	import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
 | 
						import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
 | 
				
			||||||
	import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
 | 
						import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
 | 
				
			||||||
 | 
						import RestoreDialoge from '$lib/components/admin-page/restore-dialoge.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
 | 
						let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let data: PageData;
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let editUser: UserResponseDto;
 | 
						let selectedUser: UserResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let shouldShowEditUserForm = false;
 | 
						let shouldShowEditUserForm = false;
 | 
				
			||||||
	let shouldShowCreateUserForm = false;
 | 
						let shouldShowCreateUserForm = false;
 | 
				
			||||||
	let shouldShowInfoPanel = false;
 | 
						let shouldShowInfoPanel = false;
 | 
				
			||||||
 | 
						let shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						let shouldShowRestoreDialog = false;
 | 
				
			||||||
	let serverStat: ServerStatsResponseDto;
 | 
						let serverStat: ServerStatsResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const onButtonClicked = (buttonType: CustomEvent) => {
 | 
						const onButtonClicked = (buttonType: CustomEvent) => {
 | 
				
			||||||
@ -45,7 +49,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const editUserHandler = async (event: CustomEvent) => {
 | 
						const editUserHandler = async (event: CustomEvent) => {
 | 
				
			||||||
		const { user } = event.detail;
 | 
							const { user } = event.detail;
 | 
				
			||||||
		editUser = user;
 | 
							selectedUser = user;
 | 
				
			||||||
		shouldShowEditUserForm = true;
 | 
							shouldShowEditUserForm = true;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -62,6 +66,43 @@
 | 
				
			|||||||
		shouldShowInfoPanel = true;
 | 
							shouldShowInfoPanel = true;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const deleteUserHandler = async (event: CustomEvent) => {
 | 
				
			||||||
 | 
							const { user } = event.detail;
 | 
				
			||||||
 | 
							selectedUser = user;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserDeleteSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							data.allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserDeleteFail = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							data.allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const restoreUserHandler = async (event: CustomEvent) => {
 | 
				
			||||||
 | 
							const { user } = event.detail;
 | 
				
			||||||
 | 
							selectedUser = user;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserRestoreSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							data.allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserRestoreFail = async () => {
 | 
				
			||||||
 | 
							// show fail dialog
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							data.allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const getServerStats = async () => {
 | 
						const getServerStats = async () => {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			const res = await api.serverInfoApi.getStats();
 | 
								const res = await api.serverInfoApi.getStats();
 | 
				
			||||||
@ -87,13 +128,33 @@
 | 
				
			|||||||
{#if shouldShowEditUserForm}
 | 
					{#if shouldShowEditUserForm}
 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
 | 
						<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
 | 
				
			||||||
		<EditUserForm
 | 
							<EditUserForm
 | 
				
			||||||
			user={editUser}
 | 
								user={selectedUser}
 | 
				
			||||||
			on:edit-success={onEditUserSuccess}
 | 
								on:edit-success={onEditUserSuccess}
 | 
				
			||||||
			on:reset-password-success={onEditPasswordSuccess}
 | 
								on:reset-password-success={onEditPasswordSuccess}
 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
	</FullScreenModal>
 | 
						</FullScreenModal>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if shouldShowDeleteConfirmDialog}
 | 
				
			||||||
 | 
						<FullScreenModal on:clickOutside={() => (shouldShowDeleteConfirmDialog = false)}>
 | 
				
			||||||
 | 
							<DeleteConfirmDialog
 | 
				
			||||||
 | 
								user={selectedUser}
 | 
				
			||||||
 | 
								on:user-delete-success={onUserDeleteSuccess}
 | 
				
			||||||
 | 
								on:user-delete-fail={onUserDeleteFail}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</FullScreenModal>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if shouldShowRestoreDialog}
 | 
				
			||||||
 | 
						<FullScreenModal on:clickOutside={() => (shouldShowRestoreDialog = false)}>
 | 
				
			||||||
 | 
							<RestoreDialoge
 | 
				
			||||||
 | 
								user={selectedUser}
 | 
				
			||||||
 | 
								on:user-restore-success={onUserRestoreSuccess}
 | 
				
			||||||
 | 
								on:user-restore-fail={onUserRestoreFail}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</FullScreenModal>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if shouldShowInfoPanel}
 | 
					{#if shouldShowInfoPanel}
 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
 | 
						<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
 | 
				
			||||||
		<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
 | 
							<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
 | 
				
			||||||
@ -160,6 +221,8 @@
 | 
				
			|||||||
						allUsers={data.allUsers}
 | 
											allUsers={data.allUsers}
 | 
				
			||||||
						on:create-user={() => (shouldShowCreateUserForm = true)}
 | 
											on:create-user={() => (shouldShowCreateUserForm = true)}
 | 
				
			||||||
						on:edit-user={editUserHandler}
 | 
											on:edit-user={editUserHandler}
 | 
				
			||||||
 | 
											on:delete-user={deleteUserHandler}
 | 
				
			||||||
 | 
											on:restore-user={restoreUserHandler}
 | 
				
			||||||
					/>
 | 
										/>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				{#if selectedAction === AdminSideBarSelection.JOBS}
 | 
									{#if selectedAction === AdminSideBarSelection.JOBS}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user