forked from Cutlery/immich
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 430561d692 | |||
| e8fb529026 | |||
| 7a4ae7d142 | |||
| 9cb0a1ffbf | |||
| fa32c6660c | |||
| fe8c6b17a6 |
Generated
+1
-1
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+1
-1
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -76,6 +76,7 @@ export const signupResponseDto = {
|
||||
memoriesEnabled: true,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: 'active',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.98.0"
|
||||
version = "1.98.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 126,
|
||||
"android.injected.version.name" => "1.98.0",
|
||||
"android.injected.version.code" => 127,
|
||||
"android.injected.version.name" => "1.98.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.98.0"
|
||||
version_number: "1.98.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -49,23 +49,16 @@ class AssetService {
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// Returns `(null, null, time)` if changes are invalid -> requires full sync
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
|
||||
/// Returns `(null, null)` if changes are invalid -> requires full sync
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
|
||||
_getRemoteAssetChanges(User user, DateTime since) async {
|
||||
final deleted = await _apiService.auditApi
|
||||
.getAuditDeletes(since, EntityType.ASSET, userId: user.id);
|
||||
if (deleted == null || deleted.needsFullSync) {
|
||||
return (null, null, deleted?.requestedAt);
|
||||
}
|
||||
|
||||
if (deleted == null || deleted.needsFullSync) return (null, null);
|
||||
final assetDto = await _apiService.assetApi
|
||||
.getAllAssets(userId: user.id, updatedAfter: since);
|
||||
if (assetDto == null) return (null, null, deleted.requestedAt);
|
||||
return (
|
||||
assetDto.map(Asset.remote).toList(),
|
||||
deleted.ids,
|
||||
deleted.requestedAt
|
||||
);
|
||||
if (assetDto == null) return (null, null);
|
||||
return (assetDto.map(Asset.remote).toList(), deleted.ids);
|
||||
}
|
||||
|
||||
/// Returns the list of people of the given asset id.
|
||||
@@ -90,11 +83,10 @@ class AssetService {
|
||||
}
|
||||
|
||||
/// Returns `null` if the server state did not change, else list of assets
|
||||
|
||||
Future<List<Asset>?> _getRemoteAssets(User user, DateTime now) async {
|
||||
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
||||
const int chunkSize = 10000;
|
||||
|
||||
try {
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final List<Asset> allAssets = [];
|
||||
for (int i = 0;; i += chunkSize) {
|
||||
final List<AssetResponseDto>? assets =
|
||||
|
||||
@@ -41,20 +41,17 @@ class SyncService {
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAssetsToDb(
|
||||
User user,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
|
||||
Function(
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
User user,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
FutureOr<List<Asset>?> Function(User user, DateTime now) loadAssets,
|
||||
FutureOr<List<Asset>?> Function(User user) loadAssets,
|
||||
) =>
|
||||
_lock.run(() async {
|
||||
final (changes, serverTime) =
|
||||
await _syncRemoteAssetChanges(user, getChangedAssets);
|
||||
if (changes != null) return changes;
|
||||
final time = serverTime ?? DateTime.now().toUtc();
|
||||
return await _syncRemoteAssetsFull(user, time, loadAssets);
|
||||
});
|
||||
_lock.run(
|
||||
() async =>
|
||||
await _syncRemoteAssetChanges(user, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(user, loadAssets),
|
||||
);
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
@@ -149,22 +146,19 @@ class SyncService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Efficiently syncs assets via changes. Returns `(null, serverTime)` when a full sync is required.
|
||||
Future<(bool?, DateTime? serverTime)> _syncRemoteAssetChanges(
|
||||
/// Efficiently syncs assets via changes. Returns `null` when a full sync is required.
|
||||
Future<bool?> _syncRemoteAssetChanges(
|
||||
User user,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
|
||||
Function(
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
User user,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
) async {
|
||||
final DateTime? since = _db.eTags.getByIdSync(user.id)?.time?.toUtc();
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final (toUpsert, toDelete, serverTime) =
|
||||
await getChangedAssets(user, since ?? now);
|
||||
if (since == null || toUpsert == null || toDelete == null) {
|
||||
return (null, serverTime);
|
||||
}
|
||||
if (since == null) return null;
|
||||
final DateTime now = DateTime.now();
|
||||
final (toUpsert, toDelete) = await getChangedAssets(user, since);
|
||||
if (toUpsert == null || toDelete == null) return null;
|
||||
try {
|
||||
if (toDelete.isNotEmpty) {
|
||||
await handleRemoteAssetRemoval(toDelete);
|
||||
@@ -174,14 +168,14 @@ class SyncService {
|
||||
await upsertAssetsWithExif(updated);
|
||||
}
|
||||
if (toUpsert.isNotEmpty || toDelete.isNotEmpty) {
|
||||
await _updateUserAssetsETag(user, serverTime ?? now);
|
||||
return (true, serverTime);
|
||||
await _updateUserAssetsETag(user, now);
|
||||
return true;
|
||||
}
|
||||
return (false, serverTime);
|
||||
return false;
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
return (null, serverTime);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
@@ -208,11 +202,11 @@ class SyncService {
|
||||
|
||||
/// Syncs assets by loading and comparing all assets from the server.
|
||||
Future<bool> _syncRemoteAssetsFull(
|
||||
final User user,
|
||||
final DateTime now,
|
||||
final FutureOr<List<Asset>?> Function(User user, DateTime now) loadAssets,
|
||||
User user,
|
||||
FutureOr<List<Asset>?> Function(User user) loadAssets,
|
||||
) async {
|
||||
final List<Asset>? remote = await loadAssets(user, now);
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final List<Asset>? remote = await loadAssets(user);
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Generated
+6
@@ -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
|
||||
|
||||
Generated
+3
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.98.0
|
||||
- API version: 1.98.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -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)
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**ids** | **List<String>** | | [default to const []]
|
||||
**needsFullSync** | **bool** | |
|
||||
**requestedAt** | [**DateTime**](DateTime.md) | | [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)
|
||||
|
||||
|
||||
Generated
+15
@@ -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)
|
||||
|
||||
|
||||
Generated
+1
@@ -22,6 +22,7 @@ Name | Type | Description | Notes
|
||||
**quotaSizeInBytes** | **int** | |
|
||||
**quotaUsageInBytes** | **int** | |
|
||||
**shouldChangePassword** | **bool** | |
|
||||
**status** | [**UserStatus**](UserStatus.md) | |
|
||||
**storageLabel** | **String** | |
|
||||
**updatedAt** | [**DateTime**](DateTime.md) | |
|
||||
|
||||
|
||||
Generated
+5
-3
@@ -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)
|
||||
|
||||
Generated
+1
@@ -21,6 +21,7 @@ Name | Type | Description | Notes
|
||||
**quotaSizeInBytes** | **int** | |
|
||||
**quotaUsageInBytes** | **int** | |
|
||||
**shouldChangePassword** | **bool** | |
|
||||
**status** | [**UserStatus**](UserStatus.md) | |
|
||||
**storageLabel** | **String** | |
|
||||
**updatedAt** | [**DateTime**](DateTime.md) | |
|
||||
|
||||
|
||||
Generated
+14
@@ -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)
|
||||
|
||||
|
||||
Generated
+2
@@ -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';
|
||||
|
||||
Generated
+9
-5
@@ -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));
|
||||
}
|
||||
|
||||
Generated
+4
@@ -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':
|
||||
|
||||
Generated
+3
@@ -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();
|
||||
}
|
||||
|
||||
+13
-41
@@ -15,49 +15,30 @@ class AuditDeletesResponseDto {
|
||||
AuditDeletesResponseDto({
|
||||
this.ids = const [],
|
||||
required this.needsFullSync,
|
||||
this.requestedAt,
|
||||
});
|
||||
|
||||
List<String> ids;
|
||||
|
||||
bool needsFullSync;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
DateTime? requestedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AuditDeletesResponseDto &&
|
||||
other.ids == ids &&
|
||||
other.needsFullSync == needsFullSync &&
|
||||
other.requestedAt == requestedAt;
|
||||
bool operator ==(Object other) => identical(this, other) || other is AuditDeletesResponseDto &&
|
||||
_deepEquality.equals(other.ids, ids) &&
|
||||
other.needsFullSync == needsFullSync;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(ids.hashCode) +
|
||||
(needsFullSync.hashCode) +
|
||||
(requestedAt == null ? 0 : requestedAt!.hashCode);
|
||||
// ignore: unnecessary_parenthesis
|
||||
(ids.hashCode) +
|
||||
(needsFullSync.hashCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync, requestedAt=$requestedAt]';
|
||||
String toString() => 'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'ids'] = this.ids;
|
||||
json[r'needsFullSync'] = this.needsFullSync;
|
||||
if (this.requestedAt != null) {
|
||||
json[r'requestedAt'] = this.requestedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'requestedAt'] = null;
|
||||
}
|
||||
json[r'ids'] = this.ids;
|
||||
json[r'needsFullSync'] = this.needsFullSync;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -73,16 +54,12 @@ class AuditDeletesResponseDto {
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
needsFullSync: mapValueOfType<bool>(json, r'needsFullSync')!,
|
||||
requestedAt: mapDateTime(json, r'requestedAt', ''),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AuditDeletesResponseDto> listFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static List<AuditDeletesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AuditDeletesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
@@ -110,19 +87,13 @@ class AuditDeletesResponseDto {
|
||||
}
|
||||
|
||||
// maps a json object with a list of AuditDeletesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AuditDeletesResponseDto>> mapListFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static Map<String, List<AuditDeletesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AuditDeletesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AuditDeletesResponseDto.listFromJson(
|
||||
entry.value,
|
||||
growable: growable,
|
||||
);
|
||||
map[entry.key] = AuditDeletesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@@ -134,3 +105,4 @@ class AuditDeletesResponseDto {
|
||||
'needsFullSync',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+107
@@ -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>{
|
||||
};
|
||||
}
|
||||
|
||||
+9
-1
@@ -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',
|
||||
};
|
||||
|
||||
+9
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// DateTime requestedAt
|
||||
test('to test the property `requestedAt`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
+27
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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
@@ -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
@@ -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', () {
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.98.0+126
|
||||
version: 1.98.1+127
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
|
||||
@@ -6402,6 +6402,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteUserDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -6476,7 +6486,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -7326,10 +7336,6 @@
|
||||
},
|
||||
"needsFullSync": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requestedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7754,6 +7760,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DeleteUserDto": {
|
||||
"properties": {
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"DownloadArchiveInfo": {
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
@@ -8620,6 +8634,9 @@
|
||||
"shouldChangePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/UserStatus"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
@@ -8642,6 +8659,7 @@
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"shouldChangePassword",
|
||||
"status",
|
||||
"storageLabel",
|
||||
"updatedAt"
|
||||
],
|
||||
@@ -10565,6 +10583,9 @@
|
||||
"shouldChangePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/UserStatus"
|
||||
},
|
||||
"storageLabel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
@@ -10587,11 +10608,20 @@
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"shouldChangePassword",
|
||||
"status",
|
||||
"storageLabel",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"removing",
|
||||
"deleted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ValidateAccessTokenResponseDto": {
|
||||
"properties": {
|
||||
"authStatus": {
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@oazapfts/runtime": "^1.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.98.0
|
||||
* 1.98.1
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -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",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.11",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -29,7 +29,6 @@ export enum PathEntityType {
|
||||
export class AuditDeletesResponseDto {
|
||||
needsFullSync!: boolean;
|
||||
ids!: string[];
|
||||
requestedAt?: Date;
|
||||
}
|
||||
|
||||
export class FileReportDto {
|
||||
|
||||
@@ -61,7 +61,6 @@ describe(AuditService.name, () => {
|
||||
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||
needsFullSync: true,
|
||||
ids: [],
|
||||
requestedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||
@@ -78,7 +77,6 @@ describe(AuditService.name, () => {
|
||||
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||
needsFullSync: false,
|
||||
ids: ['asset-deleted'],
|
||||
requestedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DateTime } from 'luxon';
|
||||
import { resolve } from 'node:path';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import { AuthDto } from '../auth';
|
||||
import { AUDIT_LOG_CLEANUP_DURATION, AUDIT_LOG_MAX_DURATION } from '../domain.constant';
|
||||
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
|
||||
import { usePagination } from '../domain.util';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from '../job';
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ export class AuditService {
|
||||
}
|
||||
|
||||
async handleCleanup(): Promise<boolean> {
|
||||
await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_CLEANUP_DURATION).toJSDate());
|
||||
await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -53,16 +53,15 @@ export class AuditService {
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
|
||||
const now = DateTime.utc();
|
||||
const duration = now.diff(DateTime.fromJSDate(dto.after));
|
||||
const audits = await this.repository.getAfter(dto.after, {
|
||||
ownerId: userId,
|
||||
entityType: dto.entityType,
|
||||
action: DatabaseAction.DELETE,
|
||||
});
|
||||
|
||||
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.after));
|
||||
|
||||
return {
|
||||
requestedAt: now.toJSDate(),
|
||||
needsFullSync: duration > AUDIT_LOG_MAX_DURATION,
|
||||
ids: audits.map(({ entityId }) => entityId),
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import { readFileSync } from 'node:fs';
|
||||
import { extname, join } from 'node:path';
|
||||
|
||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||
export const AUDIT_LOG_CLEANUP_DURATION = Duration.fromObject({ days: 101 });
|
||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||
|
||||
export interface IVersion {
|
||||
|
||||
@@ -280,6 +280,11 @@ export class JobService {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.USER_DELETION: {
|
||||
this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
AssetSearchOneToOneRelationOptions,
|
||||
AssetSearchOptions,
|
||||
ReverseGeocodeResult,
|
||||
SearchExploreItem,
|
||||
} from '@app/domain';
|
||||
import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||
import { Paginated, PaginationOptions } from '../domain.util';
|
||||
@@ -140,10 +135,6 @@ export interface IAssetRepository {
|
||||
updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void>;
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getAllByFileCreationDate(
|
||||
pagination: PaginationOptions,
|
||||
options?: AssetSearchOneToOneRelationOptions,
|
||||
): Paginated<AssetEntity>;
|
||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
|
||||
save(asset: Pick<AssetEntity, 'id'> & Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AssetEntity } from '@app/infra/entities';
|
||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
|
||||
import { OptionalBetween } from '@app/infra/infra.utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In } from 'typeorm/find-options/operator/In.js';
|
||||
import { Repository } from 'typeorm/repository/Repository.js';
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
|
||||
export interface AssetCheck {
|
||||
id: string;
|
||||
checksum: Buffer;
|
||||
@@ -21,6 +22,7 @@ export interface IAssetRepositoryV1 {
|
||||
get(id: string): Promise<AssetEntity | null>;
|
||||
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
||||
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
||||
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
||||
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
||||
@@ -31,7 +33,40 @@ export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
|
||||
|
||||
@Injectable()
|
||||
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
||||
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves all assets by user ID.
|
||||
*
|
||||
* @param ownerId - The ID of the owner.
|
||||
* @param dto - The AssetSearchDto object containing search criteria.
|
||||
* @returns A Promise that resolves to an array of AssetEntity objects.
|
||||
*/
|
||||
getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
|
||||
return this.assetRepository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
isVisible: true,
|
||||
isFavorite: dto.isFavorite,
|
||||
isArchived: dto.isArchived,
|
||||
updatedAt: OptionalBetween(dto.updatedAfter, dto.updatedBefore),
|
||||
},
|
||||
relations: {
|
||||
exifInfo: true,
|
||||
tags: true,
|
||||
stack: { assets: true },
|
||||
},
|
||||
skip: dto.skip || 0,
|
||||
take: dto.take,
|
||||
order: {
|
||||
fileCreatedAt: 'DESC',
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
|
||||
return this.assetRepository
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('AssetService', () => {
|
||||
beforeEach(() => {
|
||||
assetRepositoryMockV1 = {
|
||||
get: jest.fn(),
|
||||
getAllByUserId: jest.fn(),
|
||||
getDetectedObjectsByUserId: jest.fn(),
|
||||
getLocationsByUserId: jest.fn(),
|
||||
getSearchPropertiesByUserId: jest.fn(),
|
||||
|
||||
@@ -113,19 +113,8 @@ export class AssetService {
|
||||
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||
const assets = await this.assetRepository.getAllByFileCreationDate(
|
||||
{ take: dto.take ?? 1000, skip: dto.skip },
|
||||
{
|
||||
...dto,
|
||||
userIds: [userId],
|
||||
withDeleted: true,
|
||||
orderDirection: 'DESC',
|
||||
withExif: true,
|
||||
isVisible: true,
|
||||
withStacked: true,
|
||||
},
|
||||
);
|
||||
return assets.items.map((asset) => mapAsset(asset, { withStack: true }));
|
||||
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
|
||||
return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
|
||||
}
|
||||
|
||||
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
AssetBuilderOptions,
|
||||
AssetCreate,
|
||||
AssetExploreFieldOptions,
|
||||
AssetSearchOneToOneRelationOptions,
|
||||
AssetSearchOptions,
|
||||
AssetStats,
|
||||
AssetStatsOptions,
|
||||
@@ -233,29 +232,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{ skip: 20_000, take: 10_000 },
|
||||
{
|
||||
takenBefore: DummyValue.DATE,
|
||||
userIds: [DummyValue.UUID],
|
||||
},
|
||||
],
|
||||
})
|
||||
getAllByFileCreationDate(
|
||||
pagination: PaginationOptions,
|
||||
options: AssetSearchOneToOneRelationOptions = {},
|
||||
): Paginated<AssetEntity> {
|
||||
let builder = this.repository.createQueryBuilder('asset');
|
||||
builder = searchAssetBuilder(builder, options);
|
||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||
return paginatedBuilder<AssetEntity>(builder, {
|
||||
mode: PaginationMode.LIMIT_OFFSET,
|
||||
skip: pagination.skip,
|
||||
take: pagination.take,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assets by device's Id on the database
|
||||
* @param ownerId
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -428,55 +428,6 @@ WHERE
|
||||
AND "isOffline" = $4
|
||||
)
|
||||
|
||||
-- AssetRepository.getAllByFileCreationDate
|
||||
SELECT
|
||||
"asset"."id" AS "asset_id",
|
||||
"asset"."deviceAssetId" AS "asset_deviceAssetId",
|
||||
"asset"."ownerId" AS "asset_ownerId",
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."resizePath" AS "asset_resizePath",
|
||||
"asset"."webpPath" AS "asset_webpPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
"asset"."createdAt" AS "asset_createdAt",
|
||||
"asset"."updatedAt" AS "asset_updatedAt",
|
||||
"asset"."deletedAt" AS "asset_deletedAt",
|
||||
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
|
||||
"asset"."localDateTime" AS "asset_localDateTime",
|
||||
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
|
||||
"asset"."isFavorite" AS "asset_isFavorite",
|
||||
"asset"."isArchived" AS "asset_isArchived",
|
||||
"asset"."isExternal" AS "asset_isExternal",
|
||||
"asset"."isReadOnly" AS "asset_isReadOnly",
|
||||
"asset"."isOffline" AS "asset_isOffline",
|
||||
"asset"."checksum" AS "asset_checksum",
|
||||
"asset"."duration" AS "asset_duration",
|
||||
"asset"."isVisible" AS "asset_isVisible",
|
||||
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
WHERE
|
||||
(
|
||||
"asset"."fileCreatedAt" <= $1
|
||||
AND 1 = 1
|
||||
AND "asset"."ownerId" IN ($2)
|
||||
AND 1 = 1
|
||||
AND "asset"."isArchived" = $3
|
||||
)
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"asset"."fileCreatedAt" DESC
|
||||
LIMIT
|
||||
10001
|
||||
OFFSET
|
||||
20000
|
||||
|
||||
-- AssetRepository.getAllByDeviceId
|
||||
SELECT
|
||||
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -18,7 +18,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
||||
getFirstAssetForAlbumId: jest.fn(),
|
||||
getLastUpdatedAssetForAlbumId: jest.fn(),
|
||||
getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
|
||||
getAllByFileCreationDate: jest.fn(),
|
||||
getAllByDeviceId: jest.fn(),
|
||||
updateAll: jest.fn(),
|
||||
getByLibraryId: jest.fn(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@oazapfts/runtime": "^1.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.98.0",
|
||||
"version": "1.98.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@
|
||||
>
|
||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||
The name of a CLIP model listed <a href="https://huggingface.co/immich-app"><u>here</u></a>. Note that you
|
||||
must re-run the 'Encode CLIP' job for all images upon changing a model.
|
||||
must re-run the 'Smart Search' job for all images upon changing a model.
|
||||
</p>
|
||||
</SettingInputField>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { dateFormats } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
$: startDate = formatDate(album.startDate);
|
||||
$: endDate = formatDate(album.endDate);
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined;
|
||||
};
|
||||
|
||||
const getDateRange = (start?: string, end?: string) => {
|
||||
if (start && end && start !== end) {
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
return start;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
<p>{getDateRange(startDate, endDate)}</p>
|
||||
<p>·</p>
|
||||
<p>{album.assetCount} items</p>
|
||||
</span>
|
||||
@@ -3,11 +3,9 @@
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { dateFormats } from '../../constants';
|
||||
import { createAssetInteractionStore } from '../../stores/asset-interaction.store';
|
||||
import { AssetStore } from '../../stores/assets.store';
|
||||
import { downloadArchive } from '../../utils/asset-utils';
|
||||
@@ -21,6 +19,7 @@
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import AlbumSummary from './album-summary.svelte';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
@@ -40,31 +39,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const getDateRange = () => {
|
||||
const { startDate, endDate } = album;
|
||||
|
||||
let start = '';
|
||||
let end = '';
|
||||
|
||||
if (startDate) {
|
||||
start = new Date(startDate).toLocaleDateString($locale, dateFormats.album);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
end = new Date(endDate).toLocaleDateString($locale, dateFormats.album);
|
||||
}
|
||||
|
||||
if (startDate && endDate && start !== end) {
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
return start;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||
|
||||
onMount(() => {
|
||||
@@ -148,13 +122,8 @@
|
||||
{album.albumName}
|
||||
</h1>
|
||||
|
||||
<!-- ALBUM SUMMARY -->
|
||||
{#if album.assetCount > 0}
|
||||
<span class="my-4 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
<p class="">{getDateRange()}</p>
|
||||
<p>·</p>
|
||||
<p>{album.assetCount} items</p>
|
||||
</span>
|
||||
<AlbumSummary {album} />
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM DESCRIPTION -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute, dateFormats } from '$lib/constants';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
@@ -74,6 +74,7 @@
|
||||
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
|
||||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -280,31 +281,6 @@
|
||||
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||
};
|
||||
|
||||
const getDateRange = () => {
|
||||
const { startDate, endDate } = album;
|
||||
|
||||
let start = '';
|
||||
let end = '';
|
||||
|
||||
if (startDate) {
|
||||
start = new Date(startDate).toLocaleDateString($locale, dateFormats.album);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
end = new Date(endDate).toLocaleDateString($locale, dateFormats.album);
|
||||
}
|
||||
|
||||
if (startDate && endDate && start !== end) {
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
return start;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleAddAssets = async () => {
|
||||
const assetIds = [...$timelineSelected].map((asset) => asset.id);
|
||||
|
||||
@@ -389,6 +365,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAssets = async (assetIds: string[]) => {
|
||||
assetStore.removeAssets(assetIds);
|
||||
await refreshAlbum();
|
||||
};
|
||||
|
||||
const handleUpdateThumbnail = async (assetId: string) => {
|
||||
if (viewMode !== ViewMode.SELECT_THUMBNAIL) {
|
||||
return;
|
||||
@@ -429,10 +410,10 @@
|
||||
{/if}
|
||||
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
||||
{#if isOwned || isAllUserOwned}
|
||||
<RemoveFromAlbum menuItem bind:album onRemove={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
|
||||
{/if}
|
||||
{#if isAllUserOwned}
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
{/if}
|
||||
@@ -469,9 +450,7 @@
|
||||
<CircleIconButton title="Album options" on:click={handleOpenAlbumOptions} icon={mdiDotsVertical}>
|
||||
{#if viewMode === ViewMode.ALBUM_OPTIONS}
|
||||
<ContextMenu {...contextMenuPosition}>
|
||||
{#if album.assetCount !== 0}
|
||||
<MenuOption on:click={handleStartSlideshow} text="Slideshow" />
|
||||
{/if}
|
||||
<MenuOption on:click={handleStartSlideshow} text="Slideshow" />
|
||||
<MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" />
|
||||
<MenuOption on:click={() => (viewMode = ViewMode.OPTIONS)} text="Options" />
|
||||
</ContextMenu>
|
||||
@@ -485,7 +464,7 @@
|
||||
<Button
|
||||
size="sm"
|
||||
rounded="lg"
|
||||
disabled={album.assetCount == 0}
|
||||
disabled={album.assetCount === 0}
|
||||
on:click={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
>
|
||||
Share
|
||||
@@ -557,13 +536,8 @@
|
||||
<section class="pt-24">
|
||||
<AlbumTitle id={album.id} albumName={album.albumName} {isOwned} />
|
||||
|
||||
<!-- ALBUM SUMMARY -->
|
||||
{#if album.assetCount > 0}
|
||||
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
<p class="">{getDateRange()}</p>
|
||||
<p>·</p>
|
||||
<p>{album.assetCount} items</p>
|
||||
</span>
|
||||
<AlbumSummary {album} />
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM SHARING -->
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
},
|
||||
{
|
||||
item: LibrarySettings,
|
||||
title: 'Library',
|
||||
subtitle: 'Manage library settings',
|
||||
key: 'library',
|
||||
title: 'External Library',
|
||||
subtitle: 'Manage external library settings',
|
||||
key: 'external-library',
|
||||
},
|
||||
{
|
||||
item: LoggingSettings,
|
||||
|
||||
@@ -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,22 +141,28 @@
|
||||
{#if shouldShowRestoreDialog}
|
||||
<RestoreDialogue
|
||||
user={selectedUser}
|
||||
on:success={onUserRestoreSuccess}
|
||||
on:fail={onUserRestoreFail}
|
||||
on:success={onUserRestore}
|
||||
on:fail={onUserRestore}
|
||||
on:cancel={() => (shouldShowRestoreDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowInfoPanel}
|
||||
<FullScreenModal onClose={() => (shouldShowInfoPanel = false)}>
|
||||
<div class="w-[500px] max-w-[95vw] rounded-3xl border bg-white p-8 text-sm shadow-sm">
|
||||
<h1 class="mb-4 text-lg font-medium text-immich-primary">Password reset success</h1>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl bg-immich-bg p-8 text-immich-fg shadow-sm dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<h1 class="mb-4 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Password reset success
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
The user's password has been reset to the default <code
|
||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary">password</code
|
||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
||||
>password</code
|
||||
>
|
||||
<br />
|
||||
<br />
|
||||
Please inform the user, and they will need to change the password at the next log-on.
|
||||
</p>
|
||||
|
||||
@@ -173,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'
|
||||
@@ -195,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"
|
||||
@@ -211,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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user