mirror of
https://github.com/immich-app/immich.git
synced 2025-03-17 21:18:12 +02:00
feat(server,web): add force delete to immediately remove user (#7681)
* feat(server,web): add force delete to immediately remove user * update wording on force delete confirmation * fix force delete css * PR feedback * cleanup user service delete for force * adding user status column * some cleanup and tests * more test fixes * run npm run sql:generate * chore: cleanup and websocket * chore: linting * userRepository.restore * removed bad color class from delete-confirm-dialoge * additional confirmation for user force delete * shorten confirmation message --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
9cb0a1ffbf
commit
7a4ae7d142
@ -76,6 +76,7 @@ export const signupResponseDto = {
|
||||
memoriesEnabled: true,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: 'active',
|
||||
},
|
||||
};
|
||||
|
||||
|
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
@ -58,6 +58,7 @@ doc/CreateTagDto.md
|
||||
doc/CreateUserDto.md
|
||||
doc/CuratedLocationsResponseDto.md
|
||||
doc/CuratedObjectsResponseDto.md
|
||||
doc/DeleteUserDto.md
|
||||
doc/DownloadApi.md
|
||||
doc/DownloadArchiveInfo.md
|
||||
doc/DownloadInfoDto.md
|
||||
@ -184,6 +185,7 @@ doc/UserApi.md
|
||||
doc/UserAvatarColor.md
|
||||
doc/UserDto.md
|
||||
doc/UserResponseDto.md
|
||||
doc/UserStatus.md
|
||||
doc/ValidateAccessTokenResponseDto.md
|
||||
doc/ValidateLibraryDto.md
|
||||
doc/ValidateLibraryImportPathResponseDto.md
|
||||
@ -268,6 +270,7 @@ lib/model/create_tag_dto.dart
|
||||
lib/model/create_user_dto.dart
|
||||
lib/model/curated_locations_response_dto.dart
|
||||
lib/model/curated_objects_response_dto.dart
|
||||
lib/model/delete_user_dto.dart
|
||||
lib/model/download_archive_info.dart
|
||||
lib/model/download_info_dto.dart
|
||||
lib/model/download_response_dto.dart
|
||||
@ -380,6 +383,7 @@ lib/model/usage_by_user_dto.dart
|
||||
lib/model/user_avatar_color.dart
|
||||
lib/model/user_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/user_status.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
lib/model/validate_library_dto.dart
|
||||
lib/model/validate_library_import_path_response_dto.dart
|
||||
@ -441,6 +445,7 @@ test/create_tag_dto_test.dart
|
||||
test/create_user_dto_test.dart
|
||||
test/curated_locations_response_dto_test.dart
|
||||
test/curated_objects_response_dto_test.dart
|
||||
test/delete_user_dto_test.dart
|
||||
test/download_api_test.dart
|
||||
test/download_archive_info_test.dart
|
||||
test/download_info_dto_test.dart
|
||||
@ -567,6 +572,7 @@ test/user_api_test.dart
|
||||
test/user_avatar_color_test.dart
|
||||
test/user_dto_test.dart
|
||||
test/user_response_dto_test.dart
|
||||
test/user_status_test.dart
|
||||
test/validate_access_token_response_dto_test.dart
|
||||
test/validate_library_dto_test.dart
|
||||
test/validate_library_import_path_response_dto_test.dart
|
||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -264,6 +264,7 @@ Class | Method | HTTP request | Description
|
||||
- [CreateUserDto](doc//CreateUserDto.md)
|
||||
- [CuratedLocationsResponseDto](doc//CuratedLocationsResponseDto.md)
|
||||
- [CuratedObjectsResponseDto](doc//CuratedObjectsResponseDto.md)
|
||||
- [DeleteUserDto](doc//DeleteUserDto.md)
|
||||
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
|
||||
- [DownloadInfoDto](doc//DownloadInfoDto.md)
|
||||
- [DownloadResponseDto](doc//DownloadResponseDto.md)
|
||||
@ -376,6 +377,7 @@ Class | Method | HTTP request | Description
|
||||
- [UserAvatarColor](doc//UserAvatarColor.md)
|
||||
- [UserDto](doc//UserDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [UserStatus](doc//UserStatus.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
- [ValidateLibraryDto](doc//ValidateLibraryDto.md)
|
||||
- [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md)
|
||||
|
15
mobile/openapi/doc/DeleteUserDto.md
generated
Normal file
15
mobile/openapi/doc/DeleteUserDto.md
generated
Normal file
@ -0,0 +1,15 @@
|
||||
# openapi.model.DeleteUserDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**force** | **bool** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
1
mobile/openapi/doc/PartnerResponseDto.md
generated
1
mobile/openapi/doc/PartnerResponseDto.md
generated
@ -22,6 +22,7 @@ Name | Type | Description | Notes
|
||||
**quotaSizeInBytes** | **int** | |
|
||||
**quotaUsageInBytes** | **int** | |
|
||||
**shouldChangePassword** | **bool** | |
|
||||
**status** | [**UserStatus**](UserStatus.md) | |
|
||||
**storageLabel** | **String** | |
|
||||
**updatedAt** | [**DateTime**](DateTime.md) | |
|
||||
|
||||
|
8
mobile/openapi/doc/UserApi.md
generated
8
mobile/openapi/doc/UserApi.md
generated
@ -182,7 +182,7 @@ void (empty response body)
|
||||
[[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(id)
|
||||
> UserResponseDto deleteUser(id, deleteUserDto)
|
||||
|
||||
|
||||
|
||||
@ -206,9 +206,10 @@ import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = UserApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final deleteUserDto = DeleteUserDto(); // DeleteUserDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.deleteUser(id);
|
||||
final result = api_instance.deleteUser(id, deleteUserDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling UserApi->deleteUser: $e\n');
|
||||
@ -220,6 +221,7 @@ try {
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**deleteUserDto** | [**DeleteUserDto**](DeleteUserDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
@ -231,7 +233,7 @@ Name | Type | Description | Notes
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
@ -21,6 +21,7 @@ Name | Type | Description | Notes
|
||||
**quotaSizeInBytes** | **int** | |
|
||||
**quotaUsageInBytes** | **int** | |
|
||||
**shouldChangePassword** | **bool** | |
|
||||
**status** | [**UserStatus**](UserStatus.md) | |
|
||||
**storageLabel** | **String** | |
|
||||
**updatedAt** | [**DateTime**](DateTime.md) | |
|
||||
|
||||
|
14
mobile/openapi/doc/UserStatus.md
generated
Normal file
14
mobile/openapi/doc/UserStatus.md
generated
Normal file
@ -0,0 +1,14 @@
|
||||
# openapi.model.UserStatus
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@ -99,6 +99,7 @@ part 'model/create_tag_dto.dart';
|
||||
part 'model/create_user_dto.dart';
|
||||
part 'model/curated_locations_response_dto.dart';
|
||||
part 'model/curated_objects_response_dto.dart';
|
||||
part 'model/delete_user_dto.dart';
|
||||
part 'model/download_archive_info.dart';
|
||||
part 'model/download_info_dto.dart';
|
||||
part 'model/download_response_dto.dart';
|
||||
@ -211,6 +212,7 @@ part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_avatar_color.dart';
|
||||
part 'model/user_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
part 'model/user_status.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
part 'model/validate_library_dto.dart';
|
||||
part 'model/validate_library_import_path_response_dto.dart';
|
||||
|
14
mobile/openapi/lib/api/user_api.dart
generated
14
mobile/openapi/lib/api/user_api.dart
generated
@ -157,19 +157,21 @@ class UserApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteUserWithHttpInfo(String id,) async {
|
||||
///
|
||||
/// * [DeleteUserDto] deleteUserDto (required):
|
||||
Future<Response> deleteUserWithHttpInfo(String id, DeleteUserDto deleteUserDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/user/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
Object? postBody = deleteUserDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
@ -186,8 +188,10 @@ class UserApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<UserResponseDto?> deleteUser(String id,) async {
|
||||
final response = await deleteUserWithHttpInfo(id,);
|
||||
///
|
||||
/// * [DeleteUserDto] deleteUserDto (required):
|
||||
Future<UserResponseDto?> deleteUser(String id, DeleteUserDto deleteUserDto,) async {
|
||||
final response = await deleteUserWithHttpInfo(id, deleteUserDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@ -280,6 +280,8 @@ class ApiClient {
|
||||
return CuratedLocationsResponseDto.fromJson(value);
|
||||
case 'CuratedObjectsResponseDto':
|
||||
return CuratedObjectsResponseDto.fromJson(value);
|
||||
case 'DeleteUserDto':
|
||||
return DeleteUserDto.fromJson(value);
|
||||
case 'DownloadArchiveInfo':
|
||||
return DownloadArchiveInfo.fromJson(value);
|
||||
case 'DownloadInfoDto':
|
||||
@ -504,6 +506,8 @@ class ApiClient {
|
||||
return UserDto.fromJson(value);
|
||||
case 'UserResponseDto':
|
||||
return UserResponseDto.fromJson(value);
|
||||
case 'UserStatus':
|
||||
return UserStatusTypeTransformer().decode(value);
|
||||
case 'ValidateAccessTokenResponseDto':
|
||||
return ValidateAccessTokenResponseDto.fromJson(value);
|
||||
case 'ValidateLibraryDto':
|
||||
|
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@ -136,6 +136,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is UserAvatarColor) {
|
||||
return UserAvatarColorTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is UserStatus) {
|
||||
return UserStatusTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is VideoCodec) {
|
||||
return VideoCodecTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
107
mobile/openapi/lib/model/delete_user_dto.dart
generated
Normal file
107
mobile/openapi/lib/model/delete_user_dto.dart
generated
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class DeleteUserDto {
|
||||
/// Returns a new [DeleteUserDto] instance.
|
||||
DeleteUserDto({
|
||||
this.force,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? force;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DeleteUserDto &&
|
||||
other.force == force;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(force == null ? 0 : force!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'DeleteUserDto[force=$force]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.force != null) {
|
||||
json[r'force'] = this.force;
|
||||
} else {
|
||||
// json[r'force'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [DeleteUserDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DeleteUserDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return DeleteUserDto(
|
||||
force: mapValueOfType<bool>(json, r'force'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<DeleteUserDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <DeleteUserDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = DeleteUserDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, DeleteUserDto> mapFromJson(dynamic json) {
|
||||
final map = <String, DeleteUserDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = DeleteUserDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of DeleteUserDto-objects as value to a dart map
|
||||
static Map<String, List<DeleteUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<DeleteUserDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = DeleteUserDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
@ -27,6 +27,7 @@ class PartnerResponseDto {
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
required this.shouldChangePassword,
|
||||
required this.status,
|
||||
required this.storageLabel,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@ -71,6 +72,8 @@ class PartnerResponseDto {
|
||||
|
||||
bool shouldChangePassword;
|
||||
|
||||
UserStatus status;
|
||||
|
||||
String? storageLabel;
|
||||
|
||||
DateTime updatedAt;
|
||||
@ -91,6 +94,7 @@ class PartnerResponseDto {
|
||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||
other.quotaUsageInBytes == quotaUsageInBytes &&
|
||||
other.shouldChangePassword == shouldChangePassword &&
|
||||
other.status == status &&
|
||||
other.storageLabel == storageLabel &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@ -111,11 +115,12 @@ class PartnerResponseDto {
|
||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
|
||||
(shouldChangePassword.hashCode) +
|
||||
(status.hashCode) +
|
||||
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -153,6 +158,7 @@ class PartnerResponseDto {
|
||||
// json[r'quotaUsageInBytes'] = null;
|
||||
}
|
||||
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
||||
json[r'status'] = this.status;
|
||||
if (this.storageLabel != null) {
|
||||
json[r'storageLabel'] = this.storageLabel;
|
||||
} else {
|
||||
@ -184,6 +190,7 @@ class PartnerResponseDto {
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||
status: UserStatus.fromJson(json[r'status'])!,
|
||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
);
|
||||
@ -245,6 +252,7 @@ class PartnerResponseDto {
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'shouldChangePassword',
|
||||
'status',
|
||||
'storageLabel',
|
||||
'updatedAt',
|
||||
};
|
||||
|
10
mobile/openapi/lib/model/user_response_dto.dart
generated
10
mobile/openapi/lib/model/user_response_dto.dart
generated
@ -26,6 +26,7 @@ class UserResponseDto {
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
required this.shouldChangePassword,
|
||||
required this.status,
|
||||
required this.storageLabel,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@ -62,6 +63,8 @@ class UserResponseDto {
|
||||
|
||||
bool shouldChangePassword;
|
||||
|
||||
UserStatus status;
|
||||
|
||||
String? storageLabel;
|
||||
|
||||
DateTime updatedAt;
|
||||
@ -81,6 +84,7 @@ class UserResponseDto {
|
||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||
other.quotaUsageInBytes == quotaUsageInBytes &&
|
||||
other.shouldChangePassword == shouldChangePassword &&
|
||||
other.status == status &&
|
||||
other.storageLabel == storageLabel &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@ -100,11 +104,12 @@ class UserResponseDto {
|
||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
|
||||
(shouldChangePassword.hashCode) +
|
||||
(status.hashCode) +
|
||||
(storageLabel == null ? 0 : storageLabel!.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -137,6 +142,7 @@ class UserResponseDto {
|
||||
// json[r'quotaUsageInBytes'] = null;
|
||||
}
|
||||
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
||||
json[r'status'] = this.status;
|
||||
if (this.storageLabel != null) {
|
||||
json[r'storageLabel'] = this.storageLabel;
|
||||
} else {
|
||||
@ -167,6 +173,7 @@ class UserResponseDto {
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||
status: UserStatus.fromJson(json[r'status'])!,
|
||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
);
|
||||
@ -228,6 +235,7 @@ class UserResponseDto {
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'shouldChangePassword',
|
||||
'status',
|
||||
'storageLabel',
|
||||
'updatedAt',
|
||||
};
|
||||
|
88
mobile/openapi/lib/model/user_status.dart
generated
Normal file
88
mobile/openapi/lib/model/user_status.dart
generated
Normal file
@ -0,0 +1,88 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class UserStatus {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const UserStatus._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const active = UserStatus._(r'active');
|
||||
static const removing = UserStatus._(r'removing');
|
||||
static const deleted = UserStatus._(r'deleted');
|
||||
|
||||
/// List of all possible values in this [enum][UserStatus].
|
||||
static const values = <UserStatus>[
|
||||
active,
|
||||
removing,
|
||||
deleted,
|
||||
];
|
||||
|
||||
static UserStatus? fromJson(dynamic value) => UserStatusTypeTransformer().decode(value);
|
||||
|
||||
static List<UserStatus> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UserStatus>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UserStatus.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [UserStatus] to String,
|
||||
/// and [decode] dynamic data back to [UserStatus].
|
||||
class UserStatusTypeTransformer {
|
||||
factory UserStatusTypeTransformer() => _instance ??= const UserStatusTypeTransformer._();
|
||||
|
||||
const UserStatusTypeTransformer._();
|
||||
|
||||
String encode(UserStatus data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a UserStatus.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
UserStatus? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'active': return UserStatus.active;
|
||||
case r'removing': return UserStatus.removing;
|
||||
case r'deleted': return UserStatus.deleted;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [UserStatusTypeTransformer] instance.
|
||||
static UserStatusTypeTransformer? _instance;
|
||||
}
|
||||
|
27
mobile/openapi/test/delete_user_dto_test.dart
generated
Normal file
27
mobile/openapi/test/delete_user_dto_test.dart
generated
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for DeleteUserDto
|
||||
void main() {
|
||||
// final instance = DeleteUserDto();
|
||||
|
||||
group('test DeleteUserDto', () {
|
||||
// bool force
|
||||
test('to test the property `force`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
@ -86,6 +86,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// UserStatus status
|
||||
test('to test the property `status`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String storageLabel
|
||||
test('to test the property `storageLabel`', () async {
|
||||
// TODO
|
||||
|
2
mobile/openapi/test/user_api_test.dart
generated
2
mobile/openapi/test/user_api_test.dart
generated
@ -32,7 +32,7 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<UserResponseDto> deleteUser(String id) async
|
||||
//Future<UserResponseDto> deleteUser(String id, DeleteUserDto deleteUserDto) async
|
||||
test('test deleteUser', () async {
|
||||
// TODO
|
||||
});
|
||||
|
5
mobile/openapi/test/user_response_dto_test.dart
generated
5
mobile/openapi/test/user_response_dto_test.dart
generated
@ -81,6 +81,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// UserStatus status
|
||||
test('to test the property `status`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String storageLabel
|
||||
test('to test the property `storageLabel`', () async {
|
||||
// TODO
|
||||
|
21
mobile/openapi/test/user_status_test.dart
generated
Normal file
21
mobile/openapi/test/user_status_test.dart
generated
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for UserStatus
|
||||
void main() {
|
||||
|
||||
group('test UserStatus', () {
|
||||
|
||||
});
|
||||
|
||||
}
|
@ -6402,6 +6402,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteUserDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@ -7750,6 +7760,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DeleteUserDto": {
|
||||
"properties": {
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"DownloadArchiveInfo": {
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
@ -8616,6 +8634,9 @@
|
||||
"shouldChangePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/UserStatus"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
@ -8638,6 +8659,7 @@
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"shouldChangePassword",
|
||||
"status",
|
||||
"storageLabel",
|
||||
"updatedAt"
|
||||
],
|
||||
@ -10561,6 +10583,9 @@
|
||||
"shouldChangePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/UserStatus"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
@ -10583,11 +10608,20 @@
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"shouldChangePassword",
|
||||
"status",
|
||||
"storageLabel",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"removing",
|
||||
"deleted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ValidateAccessTokenResponseDto": {
|
||||
"properties": {
|
||||
"authStatus": {
|
||||
|
@ -75,6 +75,7 @@ export type UserResponseDto = {
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number | null;
|
||||
shouldChangePassword: boolean;
|
||||
status: UserStatus;
|
||||
storageLabel: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
@ -518,6 +519,7 @@ export type PartnerResponseDto = {
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number | null;
|
||||
shouldChangePassword: boolean;
|
||||
status: UserStatus;
|
||||
storageLabel: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
@ -994,6 +996,9 @@ export type CreateProfileImageResponseDto = {
|
||||
profileImagePath: string;
|
||||
userId: string;
|
||||
};
|
||||
export type DeleteUserDto = {
|
||||
force?: boolean;
|
||||
};
|
||||
export function getActivities({ albumId, assetId, level, $type, userId }: {
|
||||
albumId: string;
|
||||
assetId?: string;
|
||||
@ -2678,16 +2683,18 @@ export function getProfileImage({ id }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function deleteUser({ id }: {
|
||||
export function deleteUser({ id, deleteUserDto }: {
|
||||
id: string;
|
||||
deleteUserDto: DeleteUserDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: UserResponseDto;
|
||||
}>(`/user/${encodeURIComponent(id)}`, {
|
||||
}>(`/user/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
method: "DELETE",
|
||||
body: deleteUserDto
|
||||
})));
|
||||
}
|
||||
export function restoreUser({ id }: {
|
||||
id: string;
|
||||
@ -2724,6 +2731,11 @@ export enum UserAvatarColor {
|
||||
Gray = "gray",
|
||||
Amber = "amber"
|
||||
}
|
||||
export enum UserStatus {
|
||||
Active = "active",
|
||||
Removing = "removing",
|
||||
Deleted = "deleted"
|
||||
}
|
||||
export enum TagTypeEnum {
|
||||
Object = "OBJECT",
|
||||
Face = "FACE",
|
||||
|
@ -280,6 +280,11 @@ export class JobService {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.USER_DELETION: {
|
||||
this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ export const ICommunicationRepository = 'ICommunicationRepository';
|
||||
|
||||
export enum ClientEvent {
|
||||
UPLOAD_SUCCESS = 'on_upload_success',
|
||||
USER_DELETE = 'on_user_delete',
|
||||
ASSET_DELETE = 'on_asset_delete',
|
||||
ASSET_TRASH = 'on_asset_trash',
|
||||
ASSET_UPDATE = 'on_asset_update',
|
||||
@ -22,6 +23,7 @@ export enum ServerEvent {
|
||||
|
||||
export interface ClientEventMap {
|
||||
[ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto;
|
||||
[ClientEvent.USER_DELETE]: string;
|
||||
[ClientEvent.ASSET_DELETE]: string;
|
||||
[ClientEvent.ASSET_TRASH]: string[];
|
||||
[ClientEvent.ASSET_UPDATE]: AssetResponseDto;
|
||||
|
@ -32,7 +32,6 @@ export interface IUserRepository {
|
||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||
restore(user: UserEntity): Promise<UserEntity>;
|
||||
updateUsage(id: string, delta: number): Promise<void>;
|
||||
syncUsage(id?: string): Promise<void>;
|
||||
}
|
||||
|
6
server/src/domain/user/dto/delete-user.dto.ts
Normal file
6
server/src/domain/user/dto/delete-user.dto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ValidateBoolean } from '../../domain.util';
|
||||
|
||||
export class DeleteUserDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
force?: boolean;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './create-profile-image.dto';
|
||||
export * from './create-user.dto';
|
||||
export * from './delete-user.dto';
|
||||
export * from './update-user.dto';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
|
||||
import { UserAvatarColor, UserEntity, UserStatus } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
|
||||
@ -33,6 +33,8 @@ export class UserResponseDto extends UserDto {
|
||||
quotaSizeInBytes!: number | null;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaUsageInBytes!: number | null;
|
||||
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
|
||||
status!: string;
|
||||
}
|
||||
|
||||
export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
||||
@ -58,5 +60,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
memoriesEnabled: entity.memoriesEnabled,
|
||||
quotaSizeInBytes: entity.quotaSizeInBytes,
|
||||
quotaUsageInBytes: entity.quotaUsageInBytes,
|
||||
status: entity.status,
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserEntity } from '@app/infra/entities';
|
||||
import { UserEntity, UserStatus } from '@app/infra/entities';
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
@ -243,16 +243,14 @@ describe(UserService.name, () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(null);
|
||||
await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
|
||||
expect(userMock.restore).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restore an user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.restore.mockResolvedValue(userStub.user1);
|
||||
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: true });
|
||||
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1);
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
|
||||
});
|
||||
});
|
||||
|
||||
@ -260,27 +258,47 @@ describe(UserService.name, () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
userMock.get.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
|
||||
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException);
|
||||
expect(userMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cannot delete admin user', async () => {
|
||||
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('should require the auth user be an admin', async () => {
|
||||
await expect(sut.delete(authStub.user1, authStub.admin.user.id)).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(sut.delete(authStub.user1, authStub.admin.user.id, {})).rejects.toBeInstanceOf(ForbiddenException);
|
||||
|
||||
expect(userMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.delete.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {});
|
||||
expect(userMock.delete).toHaveBeenCalledWith(userStub.user1);
|
||||
await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUser(userStub.user1));
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
status: UserStatus.DELETED,
|
||||
deletedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('should force delete user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.user1.id, { force: true })).resolves.toEqual(
|
||||
mapUser(userStub.user1),
|
||||
);
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
status: UserStatus.REMOVING,
|
||||
deletedAt: expect.any(Date),
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.USER_DELETION,
|
||||
data: { id: userStub.user1.id, force: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserEntity } from '@app/infra/entities';
|
||||
import { UserEntity, UserStatus } from '@app/infra/entities';
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
@ -18,7 +18,7 @@ import {
|
||||
} from '../repositories';
|
||||
import { StorageCore, StorageFolder } from '../storage';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { CreateUserDto, UpdateUserDto } from './dto';
|
||||
import { CreateUserDto, DeleteUserDto, UpdateUserDto } from './dto';
|
||||
import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto';
|
||||
import { UserCore } from './user.core';
|
||||
|
||||
@ -73,22 +73,29 @@ export class UserService {
|
||||
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<UserResponseDto> {
|
||||
const user = await this.findOrFail(id, {});
|
||||
if (user.isAdmin) {
|
||||
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
|
||||
const { force } = dto;
|
||||
const { isAdmin } = await this.findOrFail(id, {});
|
||||
if (isAdmin) {
|
||||
throw new ForbiddenException('Cannot delete admin user');
|
||||
}
|
||||
|
||||
await this.albumRepository.softDeleteAll(id);
|
||||
|
||||
return this.userRepository.delete(user).then(mapUser);
|
||||
const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
|
||||
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
||||
|
||||
if (force) {
|
||||
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
|
||||
}
|
||||
|
||||
return mapUser(user);
|
||||
}
|
||||
|
||||
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
|
||||
let user = await this.findOrFail(id, { withDeleted: true });
|
||||
user = await this.userRepository.restore(user);
|
||||
await this.findOrFail(id, { withDeleted: true });
|
||||
await this.albumRepository.restoreAll(id);
|
||||
return mapUser(user);
|
||||
return this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }).then(mapUser);
|
||||
}
|
||||
|
||||
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
|
||||
@ -154,7 +161,7 @@ export class UserService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleUserDelete({ id }: IEntityJob) {
|
||||
async handleUserDelete({ id, force }: IEntityJob) {
|
||||
const config = await this.configCore.getConfig();
|
||||
const user = await this.userRepository.get(id, { withDeleted: true });
|
||||
if (!user) {
|
||||
@ -162,7 +169,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
// just for extra protection here
|
||||
if (!this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
||||
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
||||
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
|
||||
return false;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
CreateUserDto as CreateDto,
|
||||
CreateProfileImageDto,
|
||||
CreateProfileImageResponseDto,
|
||||
DeleteUserDto,
|
||||
UpdateUserDto as UpdateDto,
|
||||
UserResponseDto,
|
||||
UserService,
|
||||
@ -66,8 +67,12 @@ export class UserController {
|
||||
|
||||
@AdminRoute()
|
||||
@Delete(':id')
|
||||
deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
return this.service.delete(auth, id);
|
||||
deleteUser(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: DeleteUserDto,
|
||||
): Promise<UserResponseDto> {
|
||||
return this.service.delete(auth, id, dto);
|
||||
}
|
||||
|
||||
@AdminRoute()
|
||||
|
@ -23,6 +23,12 @@ export enum UserAvatarColor {
|
||||
AMBER = 'amber',
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active',
|
||||
REMOVING = 'removing',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
@Entity('users')
|
||||
export class UserEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -61,6 +67,9 @@ export class UserEntity {
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'varchar', default: UserStatus.ACTIVE })
|
||||
status!: UserStatus;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
|
14
server/src/infra/migrations/1709870213078-AddUserStatus.ts
Normal file
14
server/src/infra/migrations/1709870213078-AddUserStatus.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddUserStatus1709870213078 implements MigrationInterface {
|
||||
name = 'AddUserStatus1709870213078'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "status" character varying NOT NULL DEFAULT 'active'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "status"`);
|
||||
}
|
||||
|
||||
}
|
@ -77,10 +77,6 @@ export class UserRepository implements IUserRepository {
|
||||
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
|
||||
}
|
||||
|
||||
async restore(user: UserEntity): Promise<UserEntity> {
|
||||
return this.userRepository.recover(user);
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async getUserStats(): Promise<UserStatsQueryResponse[]> {
|
||||
const stats = await this.userRepository
|
||||
@ -135,6 +131,6 @@ export class UserRepository implements IUserRepository {
|
||||
|
||||
private async save(user: Partial<UserEntity>) {
|
||||
const { id } = await this.userRepository.save(user);
|
||||
return this.userRepository.findOneByOrFail({ id });
|
||||
return this.userRepository.findOneOrFail({ where: { id }, withDeleted: true });
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -41,6 +42,7 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -100,6 +102,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -115,6 +118,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -156,6 +160,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -171,6 +176,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -284,6 +290,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -311,6 +318,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -355,6 +363,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -382,6 +391,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -463,6 +473,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
|
||||
@ -490,6 +501,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
@ -552,6 +564,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
|
@ -20,6 +20,7 @@ FROM
|
||||
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
|
||||
"APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status",
|
||||
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled",
|
||||
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
|
||||
|
@ -28,6 +28,7 @@ FROM
|
||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
@ -143,6 +144,7 @@ SELECT
|
||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
@ -188,6 +190,7 @@ SELECT
|
||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
@ -227,6 +230,7 @@ SELECT
|
||||
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
|
||||
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
|
@ -155,6 +155,7 @@ FROM
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
|
||||
@ -258,6 +259,7 @@ SELECT
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
|
||||
@ -311,6 +313,7 @@ FROM
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",
|
||||
|
@ -13,6 +13,7 @@ SELECT
|
||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||
"UserEntity"."createdAt" AS "UserEntity_createdAt",
|
||||
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
@ -59,6 +60,7 @@ SELECT
|
||||
"user"."shouldChangePassword" AS "user_shouldChangePassword",
|
||||
"user"."createdAt" AS "user_createdAt",
|
||||
"user"."deletedAt" AS "user_deletedAt",
|
||||
"user"."status" AS "user_status",
|
||||
"user"."updatedAt" AS "user_updatedAt",
|
||||
"user"."memoriesEnabled" AS "user_memoriesEnabled",
|
||||
"user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
|
||||
@ -82,6 +84,7 @@ SELECT
|
||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||
"UserEntity"."createdAt" AS "UserEntity_createdAt",
|
||||
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
@ -107,6 +110,7 @@ SELECT
|
||||
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
|
||||
"UserEntity"."createdAt" AS "UserEntity_createdAt",
|
||||
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
|
@ -23,6 +23,7 @@ FROM
|
||||
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
|
||||
"UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
|
||||
"UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
|
||||
"UserTokenEntity__UserTokenEntity_user"."status" AS "UserTokenEntity__UserTokenEntity_user_status",
|
||||
"UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
|
||||
"UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled",
|
||||
"UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes",
|
||||
|
@ -17,7 +17,6 @@ export const newUserRepositoryMock = (reset = true): jest.Mocked<IUserRepository
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getDeletedUsers: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
hasAdmin: jest.fn(),
|
||||
updateUsage: jest.fn(),
|
||||
syncUsage: jest.fn(),
|
||||
|
@ -7,6 +7,10 @@
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let forceDelete = false;
|
||||
let deleteButtonDisabled = false;
|
||||
let userIdInput: string = '';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
success: void;
|
||||
fail: void;
|
||||
@ -15,7 +19,11 @@
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
try {
|
||||
const { deletedAt } = await deleteUser({ id: user.id });
|
||||
const { deletedAt } = await deleteUser({
|
||||
id: user.id,
|
||||
deleteUserDto: { force: forceDelete },
|
||||
});
|
||||
|
||||
if (deletedAt == undefined) {
|
||||
dispatch('fail');
|
||||
} else {
|
||||
@ -26,20 +34,68 @@
|
||||
dispatch('fail');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = (e: Event) => {
|
||||
userIdInput = (e.target as HTMLInputElement).value;
|
||||
deleteButtonDisabled = userIdInput != user.email;
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue
|
||||
title="Delete User"
|
||||
confirmText="Delete"
|
||||
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
|
||||
onConfirm={handleDeleteUser}
|
||||
onClose={() => dispatch('cancel')}
|
||||
disabled={deleteButtonDisabled}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>
|
||||
<b>{user.name}</b>'s account and assets will be permanently deleted after {$serverConfig.userDeleteDelay} days.
|
||||
</p>
|
||||
<p>Are you sure you want to continue?</p>
|
||||
{#if forceDelete}
|
||||
<p>
|
||||
<b>{user.name}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
<b>{user.name}</b>'s account and assets will be scheduled for permanent deletion in {$serverConfig.userDeleteDelay}
|
||||
days.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center m-4 gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="forceDelete">
|
||||
Queue user and assets for immediate deletion
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="forceDelete"
|
||||
type="checkbox"
|
||||
class="form-checkbox h-5 w-5"
|
||||
bind:checked={forceDelete}
|
||||
on:change={() => {
|
||||
deleteButtonDisabled = forceDelete;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if forceDelete}
|
||||
<p class="text-immich-error">
|
||||
WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be
|
||||
recovered.
|
||||
</p>
|
||||
|
||||
<p class="immich-form-label text-sm" id="confirm-user-desc">
|
||||
To confirm, type "{user.email}" below
|
||||
</p>
|
||||
|
||||
<input
|
||||
class="immich-form-input w-full pb-2"
|
||||
id="confirm-user-id"
|
||||
aria-describedby="confirm-user-desc"
|
||||
name="confirm-user-id"
|
||||
type="text"
|
||||
on:input={handleConfirm}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
@ -12,10 +13,15 @@
|
||||
}>();
|
||||
|
||||
const handleRestoreUser = async () => {
|
||||
const { deletedAt } = await restoreUser({ id: user.id });
|
||||
if (deletedAt == undefined) {
|
||||
dispatch('success');
|
||||
} else {
|
||||
try {
|
||||
const { deletedAt } = await restoreUser({ id: user.id });
|
||||
if (deletedAt == undefined) {
|
||||
dispatch('success');
|
||||
} else {
|
||||
dispatch('fail');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to restore user');
|
||||
dispatch('fail');
|
||||
}
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ export interface ReleaseEvent {
|
||||
}
|
||||
export interface Events {
|
||||
on_upload_success: (asset: AssetResponseDto) => void;
|
||||
on_user_delete: (id: string) => void;
|
||||
on_asset_delete: (assetId: string) => void;
|
||||
on_asset_trash: (assetIds: string[]) => void;
|
||||
on_asset_update: (asset: AssetResponseDto) => void;
|
||||
|
@ -8,11 +8,18 @@
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||
import { getAllUsers, type UserResponseDto } from '@immich/sdk';
|
||||
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
|
||||
import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@ -26,13 +33,26 @@
|
||||
let shouldShowRestoreDialog = false;
|
||||
let selectedUser: UserResponseDto;
|
||||
|
||||
const refresh = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
};
|
||||
|
||||
const onDeleteSuccess = (userId: string) => {
|
||||
const user = allUsers.find(({ id }) => id === userId);
|
||||
if (user) {
|
||||
allUsers = allUsers.filter((user) => user.id !== userId);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `User ${user.email} has been successfully removed.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
allUsers = $page.data.allUsers;
|
||||
});
|
||||
|
||||
const isDeleted = (user: UserResponseDto): boolean => {
|
||||
return user.deletedAt != undefined;
|
||||
};
|
||||
return websocketEvents.on('on_user_delete', onDeleteSuccess);
|
||||
});
|
||||
|
||||
const deleteDateFormat: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
@ -40,14 +60,14 @@
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const getDeleteDate = (user: UserResponseDto): string => {
|
||||
let deletedAt = new Date(user.deletedAt ?? Date.now());
|
||||
deletedAt.setDate(deletedAt.getDate() + 7);
|
||||
return deletedAt.toLocaleString($locale, deleteDateFormat);
|
||||
const getDeleteDate = (deletedAt: string): string => {
|
||||
return DateTime.fromISO(deletedAt)
|
||||
.plus({ days: $serverConfig.userDeleteDelay })
|
||||
.toLocaleString(deleteDateFormat, { locale: $locale });
|
||||
};
|
||||
|
||||
const onUserCreated = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
await refresh();
|
||||
shouldShowCreateUserForm = false;
|
||||
};
|
||||
|
||||
@ -57,12 +77,12 @@
|
||||
};
|
||||
|
||||
const onEditUserSuccess = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
await refresh();
|
||||
shouldShowEditUserForm = false;
|
||||
};
|
||||
|
||||
const onEditPasswordSuccess = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
await refresh();
|
||||
shouldShowEditUserForm = false;
|
||||
shouldShowInfoPanel = true;
|
||||
};
|
||||
@ -72,13 +92,8 @@
|
||||
shouldShowDeleteConfirmDialog = true;
|
||||
};
|
||||
|
||||
const onUserDeleteSuccess = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
|
||||
const onUserDeleteFail = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
const onUserDelete = async () => {
|
||||
await refresh();
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
|
||||
@ -87,14 +102,8 @@
|
||||
shouldShowRestoreDialog = true;
|
||||
};
|
||||
|
||||
const onUserRestoreSuccess = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
|
||||
const onUserRestoreFail = async () => {
|
||||
// show fail dialog
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
const onUserRestore = async () => {
|
||||
await refresh();
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
</script>
|
||||
@ -123,8 +132,8 @@
|
||||
{#if shouldShowDeleteConfirmDialog}
|
||||
<DeleteConfirmDialog
|
||||
user={selectedUser}
|
||||
on:success={onUserDeleteSuccess}
|
||||
on:fail={onUserDeleteFail}
|
||||
on:success={onUserDelete}
|
||||
on:fail={onUserDelete}
|
||||
on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
@ -132,8 +141,8 @@
|
||||
{#if shouldShowRestoreDialog}
|
||||
<RestoreDialogue
|
||||
user={selectedUser}
|
||||
on:success={onUserRestoreSuccess}
|
||||
on:fail={onUserRestoreFail}
|
||||
on:success={onUserRestore}
|
||||
on:fail={onUserRestore}
|
||||
on:cancel={() => (shouldShowRestoreDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
@ -179,9 +188,7 @@
|
||||
{#if allUsers}
|
||||
{#each allUsers as immichUser, index}
|
||||
<tr
|
||||
class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {isDeleted(
|
||||
immichUser,
|
||||
)
|
||||
class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {immichUser.deletedAt
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: index % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
@ -201,7 +208,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm">
|
||||
{#if !isDeleted(immichUser)}
|
||||
{#if !immichUser.deletedAt}
|
||||
<button
|
||||
on:click={() => editUserHandler(immichUser)}
|
||||
class="rounded-full bg-immich-primary p-2 sm:p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1"
|
||||
@ -217,11 +224,11 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if isDeleted(immichUser)}
|
||||
{#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
|
||||
<button
|
||||
on:click={() => restoreUserHandler(immichUser)}
|
||||
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
|
||||
title="scheduled removal on {getDeleteDate(immichUser)}"
|
||||
title="scheduled removal on {getDeleteDate(immichUser.deletedAt)}"
|
||||
>
|
||||
<Icon path={mdiDeleteRestore} size="16" />
|
||||
</button>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
|
||||
import { UserAvatarColor, UserStatus, type UserResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
|
||||
export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||
@ -18,4 +18,5 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||
avatarColor: UserAvatarColor.Primary,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: UserStatus.Active,
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user