mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04: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)}
|
||||||
on:click={() => {
|
<button
|
||||||
dispatch('edit-user', { user });
|
on:click={() => {
|
||||||
}}
|
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"
|
}}
|
||||||
><PencilOutline size="20" /></button
|
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"
|
||||||
></td
|
><PencilOutline size="16" /></button
|
||||||
>
|
>
|
||||||
|
<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