mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web) add asset count stats on admin page (#843)
This commit is contained in:
		@@ -48,6 +48,7 @@ doc/SearchAssetDto.md
 | 
			
		||||
doc/ServerInfoApi.md
 | 
			
		||||
doc/ServerInfoResponseDto.md
 | 
			
		||||
doc/ServerPingResponse.md
 | 
			
		||||
doc/ServerStatsResponseDto.md
 | 
			
		||||
doc/ServerVersionReponseDto.md
 | 
			
		||||
doc/SignUpDto.md
 | 
			
		||||
doc/SmartInfoResponseDto.md
 | 
			
		||||
@@ -56,6 +57,7 @@ doc/TimeGroupEnum.md
 | 
			
		||||
doc/UpdateAlbumDto.md
 | 
			
		||||
doc/UpdateDeviceInfoDto.md
 | 
			
		||||
doc/UpdateUserDto.md
 | 
			
		||||
doc/UsageByUserDto.md
 | 
			
		||||
doc/UserApi.md
 | 
			
		||||
doc/UserCountResponseDto.md
 | 
			
		||||
doc/UserResponseDto.md
 | 
			
		||||
@@ -117,6 +119,7 @@ lib/model/remove_assets_dto.dart
 | 
			
		||||
lib/model/search_asset_dto.dart
 | 
			
		||||
lib/model/server_info_response_dto.dart
 | 
			
		||||
lib/model/server_ping_response.dart
 | 
			
		||||
lib/model/server_stats_response_dto.dart
 | 
			
		||||
lib/model/server_version_reponse_dto.dart
 | 
			
		||||
lib/model/sign_up_dto.dart
 | 
			
		||||
lib/model/smart_info_response_dto.dart
 | 
			
		||||
@@ -125,6 +128,7 @@ lib/model/time_group_enum.dart
 | 
			
		||||
lib/model/update_album_dto.dart
 | 
			
		||||
lib/model/update_device_info_dto.dart
 | 
			
		||||
lib/model/update_user_dto.dart
 | 
			
		||||
lib/model/usage_by_user_dto.dart
 | 
			
		||||
lib/model/user_count_response_dto.dart
 | 
			
		||||
lib/model/user_response_dto.dart
 | 
			
		||||
lib/model/validate_access_token_response_dto.dart
 | 
			
		||||
 
 | 
			
		||||
@@ -102,6 +102,7 @@ Class | Method | HTTP request | Description
 | 
			
		||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} | 
 | 
			
		||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
			
		||||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 | 
			
		||||
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
			
		||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
			
		||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | 
 | 
			
		||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | 
 | 
			
		||||
@@ -155,6 +156,7 @@ Class | Method | HTTP request | Description
 | 
			
		||||
 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
			
		||||
 - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
 | 
			
		||||
 - [ServerPingResponse](doc//ServerPingResponse.md)
 | 
			
		||||
 - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
 | 
			
		||||
 - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
 | 
			
		||||
 - [SignUpDto](doc//SignUpDto.md)
 | 
			
		||||
 - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
 | 
			
		||||
@@ -163,6 +165,7 @@ Class | Method | HTTP request | Description
 | 
			
		||||
 - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
 | 
			
		||||
 - [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
 | 
			
		||||
 - [UpdateUserDto](doc//UpdateUserDto.md)
 | 
			
		||||
 - [UsageByUserDto](doc//UsageByUserDto.md)
 | 
			
		||||
 - [UserCountResponseDto](doc//UserCountResponseDto.md)
 | 
			
		||||
 - [UserResponseDto](doc//UserResponseDto.md)
 | 
			
		||||
 - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/AssetCountResponseDto.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/AssetCountResponseDto.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# openapi.model.AssetCountResponseDto
 | 
			
		||||
 | 
			
		||||
## Load the model package
 | 
			
		||||
```dart
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Properties
 | 
			
		||||
Name | Type | Description | Notes
 | 
			
		||||
------------ | ------------- | ------------- | -------------
 | 
			
		||||
**photos** | **int** |  | 
 | 
			
		||||
**videos** | **int** |  | 
 | 
			
		||||
 | 
			
		||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -11,6 +11,7 @@ Method | HTTP request | Description
 | 
			
		||||
------------- | ------------- | -------------
 | 
			
		||||
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
			
		||||
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 | 
			
		||||
[**getStats**](ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
			
		||||
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -88,6 +89,43 @@ No authorization required
 | 
			
		||||
 | 
			
		||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
# **getStats**
 | 
			
		||||
> ServerStatsResponseDto getStats()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Example
 | 
			
		||||
```dart
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
final api_instance = ServerInfoApi();
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    final result = api_instance.getStats();
 | 
			
		||||
    print(result);
 | 
			
		||||
} catch (e) {
 | 
			
		||||
    print('Exception when calling ServerInfoApi->getStats: $e\n');
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Parameters
 | 
			
		||||
This endpoint does not need any parameter.
 | 
			
		||||
 | 
			
		||||
### Return type
 | 
			
		||||
 | 
			
		||||
[**ServerStatsResponseDto**](ServerStatsResponseDto.md)
 | 
			
		||||
 | 
			
		||||
### Authorization
 | 
			
		||||
 | 
			
		||||
No authorization required
 | 
			
		||||
 | 
			
		||||
### HTTP request headers
 | 
			
		||||
 | 
			
		||||
 - **Content-Type**: Not defined
 | 
			
		||||
 - **Accept**: application/json
 | 
			
		||||
 | 
			
		||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
# **pingServer**
 | 
			
		||||
> ServerPingResponse pingServer()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								mobile/openapi/doc/ServerStatsResponseDto.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mobile/openapi/doc/ServerStatsResponseDto.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# openapi.model.ServerStatsResponseDto
 | 
			
		||||
 | 
			
		||||
## Load the model package
 | 
			
		||||
```dart
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Properties
 | 
			
		||||
Name | Type | Description | Notes
 | 
			
		||||
------------ | ------------- | ------------- | -------------
 | 
			
		||||
**photos** | **int** |  | 
 | 
			
		||||
**videos** | **int** |  | 
 | 
			
		||||
**objects** | **int** |  | 
 | 
			
		||||
**usageRaw** | **int** |  | 
 | 
			
		||||
**usage** | **String** |  | 
 | 
			
		||||
**usageByUser** | [**List<UsageByUserDto>**](UsageByUserDto.md) |  | [default to const []]
 | 
			
		||||
 | 
			
		||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								mobile/openapi/doc/UsageByUserDto.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mobile/openapi/doc/UsageByUserDto.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# openapi.model.UsageByUserDto
 | 
			
		||||
 | 
			
		||||
## Load the model package
 | 
			
		||||
```dart
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Properties
 | 
			
		||||
Name | Type | Description | Notes
 | 
			
		||||
------------ | ------------- | ------------- | -------------
 | 
			
		||||
**userId** | **String** |  | 
 | 
			
		||||
**objects** | **int** |  | 
 | 
			
		||||
**videos** | **int** |  | 
 | 
			
		||||
**photos** | **int** |  | 
 | 
			
		||||
**usageRaw** | **int** |  | 
 | 
			
		||||
**usage** | **String** |  | 
 | 
			
		||||
 | 
			
		||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +75,7 @@ part 'model/remove_assets_dto.dart';
 | 
			
		||||
part 'model/search_asset_dto.dart';
 | 
			
		||||
part 'model/server_info_response_dto.dart';
 | 
			
		||||
part 'model/server_ping_response.dart';
 | 
			
		||||
part 'model/server_stats_response_dto.dart';
 | 
			
		||||
part 'model/server_version_reponse_dto.dart';
 | 
			
		||||
part 'model/sign_up_dto.dart';
 | 
			
		||||
part 'model/smart_info_response_dto.dart';
 | 
			
		||||
@@ -83,6 +84,7 @@ part 'model/time_group_enum.dart';
 | 
			
		||||
part 'model/update_album_dto.dart';
 | 
			
		||||
part 'model/update_device_info_dto.dart';
 | 
			
		||||
part 'model/update_user_dto.dart';
 | 
			
		||||
part 'model/usage_by_user_dto.dart';
 | 
			
		||||
part 'model/user_count_response_dto.dart';
 | 
			
		||||
part 'model/user_response_dto.dart';
 | 
			
		||||
part 'model/validate_access_token_response_dto.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,47 @@ class ServerInfoApi {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Performs an HTTP 'GET /server-info/stats' operation and returns the [Response].
 | 
			
		||||
  Future<Response> getStatsWithHttpInfo() async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
    final path = r'/server-info/stats';
 | 
			
		||||
 | 
			
		||||
    // ignore: prefer_final_locals
 | 
			
		||||
    Object? postBody;
 | 
			
		||||
 | 
			
		||||
    final queryParams = <QueryParam>[];
 | 
			
		||||
    final headerParams = <String, String>{};
 | 
			
		||||
    final formParams = <String, String>{};
 | 
			
		||||
 | 
			
		||||
    const contentTypes = <String>[];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return apiClient.invokeAPI(
 | 
			
		||||
      path,
 | 
			
		||||
      'GET',
 | 
			
		||||
      queryParams,
 | 
			
		||||
      postBody,
 | 
			
		||||
      headerParams,
 | 
			
		||||
      formParams,
 | 
			
		||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<ServerStatsResponseDto?> getStats() async {
 | 
			
		||||
    final response = await getStatsWithHttpInfo();
 | 
			
		||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
			
		||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
			
		||||
    }
 | 
			
		||||
    // When a remote server returns no body with a status of 204, we shall not decode it.
 | 
			
		||||
    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
			
		||||
    // FormatException when trying to decode an empty string.
 | 
			
		||||
    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
			
		||||
      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
 | 
			
		||||
    
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Performs an HTTP 'GET /server-info/ping' operation and returns the [Response].
 | 
			
		||||
  Future<Response> pingServerWithHttpInfo() async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
 
 | 
			
		||||
@@ -272,6 +272,8 @@ class ApiClient {
 | 
			
		||||
          return ServerInfoResponseDto.fromJson(value);
 | 
			
		||||
        case 'ServerPingResponse':
 | 
			
		||||
          return ServerPingResponse.fromJson(value);
 | 
			
		||||
        case 'ServerStatsResponseDto':
 | 
			
		||||
          return ServerStatsResponseDto.fromJson(value);
 | 
			
		||||
        case 'ServerVersionReponseDto':
 | 
			
		||||
          return ServerVersionReponseDto.fromJson(value);
 | 
			
		||||
        case 'SignUpDto':
 | 
			
		||||
@@ -288,6 +290,8 @@ class ApiClient {
 | 
			
		||||
          return UpdateDeviceInfoDto.fromJson(value);
 | 
			
		||||
        case 'UpdateUserDto':
 | 
			
		||||
          return UpdateUserDto.fromJson(value);
 | 
			
		||||
        case 'UsageByUserDto':
 | 
			
		||||
          return UsageByUserDto.fromJson(value);
 | 
			
		||||
        case 'UserCountResponseDto':
 | 
			
		||||
          return UserCountResponseDto.fromJson(value);
 | 
			
		||||
        case 'UserResponseDto':
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/asset_count_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/asset_count_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
//
 | 
			
		||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
			
		||||
//
 | 
			
		||||
// @dart=2.12
 | 
			
		||||
 | 
			
		||||
// ignore_for_file: unused_element, unused_import
 | 
			
		||||
// ignore_for_file: always_put_required_named_parameters_first
 | 
			
		||||
// ignore_for_file: constant_identifier_names
 | 
			
		||||
// ignore_for_file: lines_longer_than_80_chars
 | 
			
		||||
 | 
			
		||||
part of openapi.api;
 | 
			
		||||
 | 
			
		||||
class AssetCountResponseDto {
 | 
			
		||||
  /// Returns a new [AssetCountResponseDto] instance.
 | 
			
		||||
  AssetCountResponseDto({
 | 
			
		||||
    required this.photos,
 | 
			
		||||
    required this.videos,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  int photos;
 | 
			
		||||
 | 
			
		||||
  int videos;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) => identical(this, other) || other is AssetCountResponseDto &&
 | 
			
		||||
     other.photos == photos &&
 | 
			
		||||
     other.videos == videos;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
    // ignore: unnecessary_parenthesis
 | 
			
		||||
    (photos.hashCode) +
 | 
			
		||||
    (videos.hashCode);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'AssetCountResponseDto[photos=$photos, videos=$videos]';
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    final _json = <String, dynamic>{};
 | 
			
		||||
      _json[r'photos'] = photos;
 | 
			
		||||
      _json[r'videos'] = videos;
 | 
			
		||||
    return _json;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Returns a new [AssetCountResponseDto] instance and imports its values from
 | 
			
		||||
  /// [value] if it's a [Map], null otherwise.
 | 
			
		||||
  // ignore: prefer_constructors_over_static_methods
 | 
			
		||||
  static AssetCountResponseDto? fromJson(dynamic value) {
 | 
			
		||||
    if (value is Map) {
 | 
			
		||||
      final json = value.cast<String, dynamic>();
 | 
			
		||||
 | 
			
		||||
      // Ensure that the map contains the required keys.
 | 
			
		||||
      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
			
		||||
      // Note 2: this code is stripped in release mode!
 | 
			
		||||
      assert(() {
 | 
			
		||||
        requiredKeys.forEach((key) {
 | 
			
		||||
          assert(json.containsKey(key), 'Required key "AssetCountResponseDto[$key]" is missing from JSON.');
 | 
			
		||||
          assert(json[key] != null, 'Required key "AssetCountResponseDto[$key]" has a null value in JSON.');
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
      }());
 | 
			
		||||
 | 
			
		||||
      return AssetCountResponseDto(
 | 
			
		||||
        photos: mapValueOfType<int>(json, r'photos')!,
 | 
			
		||||
        videos: mapValueOfType<int>(json, r'videos')!,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static List<AssetCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final result = <AssetCountResponseDto>[];
 | 
			
		||||
    if (json is List && json.isNotEmpty) {
 | 
			
		||||
      for (final row in json) {
 | 
			
		||||
        final value = AssetCountResponseDto.fromJson(row);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          result.add(value);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result.toList(growable: growable);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Map<String, AssetCountResponseDto> mapFromJson(dynamic json) {
 | 
			
		||||
    final map = <String, AssetCountResponseDto>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = AssetCountResponseDto.fromJson(entry.value);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // maps a json object with a list of AssetCountResponseDto-objects as value to a dart map
 | 
			
		||||
  static Map<String, List<AssetCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final map = <String, List<AssetCountResponseDto>>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = AssetCountResponseDto.listFromJson(entry.value, growable: growable,);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// The list of required keys that must be present in a JSON.
 | 
			
		||||
  static const requiredKeys = <String>{
 | 
			
		||||
    'photos',
 | 
			
		||||
    'videos',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151
									
								
								mobile/openapi/lib/model/server_stats_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								mobile/openapi/lib/model/server_stats_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
//
 | 
			
		||||
// 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 ServerStatsResponseDto {
 | 
			
		||||
  /// Returns a new [ServerStatsResponseDto] instance.
 | 
			
		||||
  ServerStatsResponseDto({
 | 
			
		||||
    required this.photos,
 | 
			
		||||
    required this.videos,
 | 
			
		||||
    required this.objects,
 | 
			
		||||
    required this.usageRaw,
 | 
			
		||||
    required this.usage,
 | 
			
		||||
    this.usageByUser = const [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  int photos;
 | 
			
		||||
 | 
			
		||||
  int videos;
 | 
			
		||||
 | 
			
		||||
  int objects;
 | 
			
		||||
 | 
			
		||||
  int usageRaw;
 | 
			
		||||
 | 
			
		||||
  String usage;
 | 
			
		||||
 | 
			
		||||
  List<UsageByUserDto> usageByUser;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) => identical(this, other) || other is ServerStatsResponseDto &&
 | 
			
		||||
     other.photos == photos &&
 | 
			
		||||
     other.videos == videos &&
 | 
			
		||||
     other.objects == objects &&
 | 
			
		||||
     other.usageRaw == usageRaw &&
 | 
			
		||||
     other.usage == usage &&
 | 
			
		||||
     other.usageByUser == usageByUser;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
    // ignore: unnecessary_parenthesis
 | 
			
		||||
    (photos.hashCode) +
 | 
			
		||||
    (videos.hashCode) +
 | 
			
		||||
    (objects.hashCode) +
 | 
			
		||||
    (usageRaw.hashCode) +
 | 
			
		||||
    (usage.hashCode) +
 | 
			
		||||
    (usageByUser.hashCode);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'ServerStatsResponseDto[photos=$photos, videos=$videos, objects=$objects, usageRaw=$usageRaw, usage=$usage, usageByUser=$usageByUser]';
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    final _json = <String, dynamic>{};
 | 
			
		||||
      _json[r'photos'] = photos;
 | 
			
		||||
      _json[r'videos'] = videos;
 | 
			
		||||
      _json[r'objects'] = objects;
 | 
			
		||||
      _json[r'usageRaw'] = usageRaw;
 | 
			
		||||
      _json[r'usage'] = usage;
 | 
			
		||||
      _json[r'usageByUser'] = usageByUser;
 | 
			
		||||
    return _json;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Returns a new [ServerStatsResponseDto] instance and imports its values from
 | 
			
		||||
  /// [value] if it's a [Map], null otherwise.
 | 
			
		||||
  // ignore: prefer_constructors_over_static_methods
 | 
			
		||||
  static ServerStatsResponseDto? fromJson(dynamic value) {
 | 
			
		||||
    if (value is Map) {
 | 
			
		||||
      final json = value.cast<String, dynamic>();
 | 
			
		||||
 | 
			
		||||
      // Ensure that the map contains the required keys.
 | 
			
		||||
      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
			
		||||
      // Note 2: this code is stripped in release mode!
 | 
			
		||||
      assert(() {
 | 
			
		||||
        requiredKeys.forEach((key) {
 | 
			
		||||
          assert(json.containsKey(key), 'Required key "ServerStatsResponseDto[$key]" is missing from JSON.');
 | 
			
		||||
          assert(json[key] != null, 'Required key "ServerStatsResponseDto[$key]" has a null value in JSON.');
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
      }());
 | 
			
		||||
 | 
			
		||||
      return ServerStatsResponseDto(
 | 
			
		||||
        photos: mapValueOfType<int>(json, r'photos')!,
 | 
			
		||||
        videos: mapValueOfType<int>(json, r'videos')!,
 | 
			
		||||
        objects: mapValueOfType<int>(json, r'objects')!,
 | 
			
		||||
        usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
 | 
			
		||||
        usage: mapValueOfType<String>(json, r'usage')!,
 | 
			
		||||
        usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser'])!,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static List<ServerStatsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final result = <ServerStatsResponseDto>[];
 | 
			
		||||
    if (json is List && json.isNotEmpty) {
 | 
			
		||||
      for (final row in json) {
 | 
			
		||||
        final value = ServerStatsResponseDto.fromJson(row);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          result.add(value);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result.toList(growable: growable);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Map<String, ServerStatsResponseDto> mapFromJson(dynamic json) {
 | 
			
		||||
    final map = <String, ServerStatsResponseDto>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = ServerStatsResponseDto.fromJson(entry.value);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // maps a json object with a list of ServerStatsResponseDto-objects as value to a dart map
 | 
			
		||||
  static Map<String, List<ServerStatsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final map = <String, List<ServerStatsResponseDto>>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = ServerStatsResponseDto.listFromJson(entry.value, growable: growable,);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// The list of required keys that must be present in a JSON.
 | 
			
		||||
  static const requiredKeys = <String>{
 | 
			
		||||
    'photos',
 | 
			
		||||
    'videos',
 | 
			
		||||
    'objects',
 | 
			
		||||
    'usageRaw',
 | 
			
		||||
    'usage',
 | 
			
		||||
    'usageByUser',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151
									
								
								mobile/openapi/lib/model/usage_by_user_dto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								mobile/openapi/lib/model/usage_by_user_dto.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
//
 | 
			
		||||
// 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 UsageByUserDto {
 | 
			
		||||
  /// Returns a new [UsageByUserDto] instance.
 | 
			
		||||
  UsageByUserDto({
 | 
			
		||||
    required this.userId,
 | 
			
		||||
    required this.objects,
 | 
			
		||||
    required this.videos,
 | 
			
		||||
    required this.photos,
 | 
			
		||||
    required this.usageRaw,
 | 
			
		||||
    required this.usage,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  String userId;
 | 
			
		||||
 | 
			
		||||
  int objects;
 | 
			
		||||
 | 
			
		||||
  int videos;
 | 
			
		||||
 | 
			
		||||
  int photos;
 | 
			
		||||
 | 
			
		||||
  int usageRaw;
 | 
			
		||||
 | 
			
		||||
  String usage;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto &&
 | 
			
		||||
     other.userId == userId &&
 | 
			
		||||
     other.objects == objects &&
 | 
			
		||||
     other.videos == videos &&
 | 
			
		||||
     other.photos == photos &&
 | 
			
		||||
     other.usageRaw == usageRaw &&
 | 
			
		||||
     other.usage == usage;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
    // ignore: unnecessary_parenthesis
 | 
			
		||||
    (userId.hashCode) +
 | 
			
		||||
    (objects.hashCode) +
 | 
			
		||||
    (videos.hashCode) +
 | 
			
		||||
    (photos.hashCode) +
 | 
			
		||||
    (usageRaw.hashCode) +
 | 
			
		||||
    (usage.hashCode);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'UsageByUserDto[userId=$userId, objects=$objects, videos=$videos, photos=$photos, usageRaw=$usageRaw, usage=$usage]';
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    final _json = <String, dynamic>{};
 | 
			
		||||
      _json[r'userId'] = userId;
 | 
			
		||||
      _json[r'objects'] = objects;
 | 
			
		||||
      _json[r'videos'] = videos;
 | 
			
		||||
      _json[r'photos'] = photos;
 | 
			
		||||
      _json[r'usageRaw'] = usageRaw;
 | 
			
		||||
      _json[r'usage'] = usage;
 | 
			
		||||
    return _json;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Returns a new [UsageByUserDto] instance and imports its values from
 | 
			
		||||
  /// [value] if it's a [Map], null otherwise.
 | 
			
		||||
  // ignore: prefer_constructors_over_static_methods
 | 
			
		||||
  static UsageByUserDto? fromJson(dynamic value) {
 | 
			
		||||
    if (value is Map) {
 | 
			
		||||
      final json = value.cast<String, dynamic>();
 | 
			
		||||
 | 
			
		||||
      // Ensure that the map contains the required keys.
 | 
			
		||||
      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
			
		||||
      // Note 2: this code is stripped in release mode!
 | 
			
		||||
      assert(() {
 | 
			
		||||
        requiredKeys.forEach((key) {
 | 
			
		||||
          assert(json.containsKey(key), 'Required key "UsageByUserDto[$key]" is missing from JSON.');
 | 
			
		||||
          assert(json[key] != null, 'Required key "UsageByUserDto[$key]" has a null value in JSON.');
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
      }());
 | 
			
		||||
 | 
			
		||||
      return UsageByUserDto(
 | 
			
		||||
        userId: mapValueOfType<String>(json, r'userId')!,
 | 
			
		||||
        objects: mapValueOfType<int>(json, r'objects')!,
 | 
			
		||||
        videos: mapValueOfType<int>(json, r'videos')!,
 | 
			
		||||
        photos: mapValueOfType<int>(json, r'photos')!,
 | 
			
		||||
        usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
 | 
			
		||||
        usage: mapValueOfType<String>(json, r'usage')!,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static List<UsageByUserDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final result = <UsageByUserDto>[];
 | 
			
		||||
    if (json is List && json.isNotEmpty) {
 | 
			
		||||
      for (final row in json) {
 | 
			
		||||
        final value = UsageByUserDto.fromJson(row);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          result.add(value);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result.toList(growable: growable);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Map<String, UsageByUserDto> mapFromJson(dynamic json) {
 | 
			
		||||
    final map = <String, UsageByUserDto>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = UsageByUserDto.fromJson(entry.value);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // maps a json object with a list of UsageByUserDto-objects as value to a dart map
 | 
			
		||||
  static Map<String, List<UsageByUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
			
		||||
    final map = <String, List<UsageByUserDto>>{};
 | 
			
		||||
    if (json is Map && json.isNotEmpty) {
 | 
			
		||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
			
		||||
      for (final entry in json.entries) {
 | 
			
		||||
        final value = UsageByUserDto.listFromJson(entry.value, growable: growable,);
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
          map[entry.key] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// The list of required keys that must be present in a JSON.
 | 
			
		||||
  static const requiredKeys = <String>{
 | 
			
		||||
    'userId',
 | 
			
		||||
    'objects',
 | 
			
		||||
    'videos',
 | 
			
		||||
    'photos',
 | 
			
		||||
    'usageRaw',
 | 
			
		||||
    'usage',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								mobile/openapi/test/asset_count_response_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/asset_count_response_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
//
 | 
			
		||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
			
		||||
//
 | 
			
		||||
// @dart=2.12
 | 
			
		||||
 | 
			
		||||
// ignore_for_file: unused_element, unused_import
 | 
			
		||||
// ignore_for_file: always_put_required_named_parameters_first
 | 
			
		||||
// ignore_for_file: constant_identifier_names
 | 
			
		||||
// ignore_for_file: lines_longer_than_80_chars
 | 
			
		||||
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:test/test.dart';
 | 
			
		||||
 | 
			
		||||
// tests for AssetCountResponseDto
 | 
			
		||||
void main() {
 | 
			
		||||
  // final instance = AssetCountResponseDto();
 | 
			
		||||
 | 
			
		||||
  group('test AssetCountResponseDto', () {
 | 
			
		||||
    // int photos
 | 
			
		||||
    test('to test the property `photos`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // int videos
 | 
			
		||||
    test('to test the property `videos`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								mobile/openapi/test/server_stats_response_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								mobile/openapi/test/server_stats_response_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
//
 | 
			
		||||
// 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 ServerStatsResponseDto
 | 
			
		||||
void main() {
 | 
			
		||||
  // final instance = ServerStatsResponseDto();
 | 
			
		||||
 | 
			
		||||
  group('test ServerStatsResponseDto', () {
 | 
			
		||||
    // int photos
 | 
			
		||||
    test('to test the property `photos`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // int videos
 | 
			
		||||
    test('to test the property `videos`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // int objects
 | 
			
		||||
    test('to test the property `objects`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // UsagePerUser diskUsagesByUser
 | 
			
		||||
    test('to test the property `diskUsagesByUser`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								mobile/openapi/test/usage_by_user_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								mobile/openapi/test/usage_by_user_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
//
 | 
			
		||||
// 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 UsageByUserDto
 | 
			
		||||
void main() {
 | 
			
		||||
  // final instance = UsageByUserDto();
 | 
			
		||||
 | 
			
		||||
  group('test UsageByUserDto', () {
 | 
			
		||||
    // int usageRaw
 | 
			
		||||
    test('to test the property `usageRaw`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // num objects
 | 
			
		||||
    test('to test the property `objects`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // num videos
 | 
			
		||||
    test('to test the property `videos`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // num images
 | 
			
		||||
    test('to test the property `images`', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -182,6 +182,7 @@ export class AssetController {
 | 
			
		||||
  async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
 | 
			
		||||
    return this.assetService.getAssetCountByUserId(authUser);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all AssetEntity belong to the user
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ export class AuthService {
 | 
			
		||||
    const validatedUser = await this.validateUser(loginCredential);
 | 
			
		||||
 | 
			
		||||
    if (!validatedUser) {
 | 
			
		||||
      Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`)
 | 
			
		||||
      Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
 | 
			
		||||
      throw new BadRequestException('Incorrect email or password');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { ApiResponseProperty } from '@nestjs/swagger';
 | 
			
		||||
 | 
			
		||||
export class LogoutResponseDto {
 | 
			
		||||
    constructor (successful: boolean) {
 | 
			
		||||
        this.successful = successful;
 | 
			
		||||
    }
 | 
			
		||||
  constructor(successful: boolean) {
 | 
			
		||||
    this.successful = successful;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    @ApiResponseProperty()
 | 
			
		||||
    successful!: boolean;
 | 
			
		||||
};
 | 
			
		||||
  @ApiResponseProperty()
 | 
			
		||||
  successful!: boolean;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { UsageByUserDto } from './usage-by-user-response.dto';
 | 
			
		||||
 | 
			
		||||
export class ServerStatsResponseDto {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.photos = 0;
 | 
			
		||||
    this.videos = 0;
 | 
			
		||||
    this.objects = 0;
 | 
			
		||||
    this.usageByUser = [];
 | 
			
		||||
    this.usageRaw = 0;
 | 
			
		||||
    this.usage = '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  photos!: number;
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  videos!: number;
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  objects!: number;
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
			
		||||
  usageRaw!: number;
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'string' })
 | 
			
		||||
  usage!: string;
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({
 | 
			
		||||
    isArray: true,
 | 
			
		||||
    type: UsageByUserDto,
 | 
			
		||||
    title: 'Array of usage for each user',
 | 
			
		||||
    example: [
 | 
			
		||||
      {
 | 
			
		||||
        photos: 1,
 | 
			
		||||
        videos: 1,
 | 
			
		||||
        objects: 1,
 | 
			
		||||
        diskUsageRaw: 1,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  })
 | 
			
		||||
  usageByUser!: UsageByUserDto[];
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
 | 
			
		||||
export class UsageByUserDto {
 | 
			
		||||
  constructor(userId: string) {
 | 
			
		||||
    this.userId = userId;
 | 
			
		||||
    this.objects = 0;
 | 
			
		||||
    this.videos = 0;
 | 
			
		||||
    this.photos = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @ApiProperty({ type: 'string' })
 | 
			
		||||
  userId: string;
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  objects: number;
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  videos: number;
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  photos: number;
 | 
			
		||||
  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
			
		||||
  usageRaw!: number;
 | 
			
		||||
  @ApiProperty({ type: 'string' })
 | 
			
		||||
  usage!: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@ import { ApiTags } from '@nestjs/swagger';
 | 
			
		||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
 | 
			
		||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
 | 
			
		||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
 | 
			
		||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Server Info')
 | 
			
		||||
@Controller('server-info')
 | 
			
		||||
@@ -25,4 +26,9 @@ export class ServerInfoController {
 | 
			
		||||
  async getServerVersion(): Promise<ServerVersionReponseDto> {
 | 
			
		||||
    return serverVersion;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Get('/stats')
 | 
			
		||||
  async getStats(): Promise<ServerStatsResponseDto> {
 | 
			
		||||
    return await this.serverInfoService.getStats();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { ServerInfoService } from './server-info.service';
 | 
			
		||||
import { ServerInfoController } from './server-info.controller';
 | 
			
		||||
import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
			
		||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [TypeOrmModule.forFeature([AssetEntity])],
 | 
			
		||||
  controllers: [ServerInfoController],
 | 
			
		||||
  providers: [ServerInfoService],
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,21 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
 | 
			
		||||
import diskusage from 'diskusage';
 | 
			
		||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
 | 
			
		||||
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
 | 
			
		||||
import { AssetEntity } from '@app/database/entities/asset.entity';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { readdirSync, statSync } from 'fs';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ServerInfoService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(AssetEntity)
 | 
			
		||||
    private assetRepository: Repository<AssetEntity>,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async getServerInfo(): Promise<ServerInfoResponseDto> {
 | 
			
		||||
    const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +30,6 @@ export class ServerInfoService {
 | 
			
		||||
    serverInfo.diskSizeRaw = diskInfo.total;
 | 
			
		||||
    serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
 | 
			
		||||
    serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
 | 
			
		||||
 | 
			
		||||
    return serverInfo;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -48,4 +59,61 @@ export class ServerInfoService {
 | 
			
		||||
      return `${sizeInByte}B`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getStats(): Promise<ServerStatsResponseDto> {
 | 
			
		||||
    const res = await this.assetRepository
 | 
			
		||||
      .createQueryBuilder('asset')
 | 
			
		||||
      .select(`COUNT(asset.id)`, 'count')
 | 
			
		||||
      .addSelect(`asset.type`, 'type')
 | 
			
		||||
      .addSelect(`asset.userId`, 'userId')
 | 
			
		||||
      .groupBy('asset.type, asset.userId')
 | 
			
		||||
      .addGroupBy('asset.type')
 | 
			
		||||
      .getRawMany();
 | 
			
		||||
 | 
			
		||||
    const serverStats = new ServerStatsResponseDto();
 | 
			
		||||
    const tmpMap = new Map<string, UsageByUserDto>();
 | 
			
		||||
    const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
 | 
			
		||||
    res.map((item) => {
 | 
			
		||||
      const usage: UsageByUserDto = getUsageByUser(item.userId);
 | 
			
		||||
      if (item.type === 'IMAGE') {
 | 
			
		||||
        usage.photos = parseInt(item.count);
 | 
			
		||||
        serverStats.photos += usage.photos;
 | 
			
		||||
      } else if (item.type === 'VIDEO') {
 | 
			
		||||
        usage.videos = parseInt(item.count);
 | 
			
		||||
        serverStats.videos += usage.videos;
 | 
			
		||||
      }
 | 
			
		||||
      tmpMap.set(item.userId, usage);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const userId of tmpMap.keys()) {
 | 
			
		||||
      const usage = getUsageByUser(userId);
 | 
			
		||||
      const userDiskUsage = await ServerInfoService.getDirectoryStats(path.join(APP_UPLOAD_LOCATION, userId));
 | 
			
		||||
      usage.usageRaw = userDiskUsage.size;
 | 
			
		||||
      usage.objects = userDiskUsage.fileCount;
 | 
			
		||||
      usage.usage = ServerInfoService.getHumanReadableString(usage.usageRaw);
 | 
			
		||||
      serverStats.usageRaw += usage.usageRaw;
 | 
			
		||||
      serverStats.objects += usage.objects;
 | 
			
		||||
    }
 | 
			
		||||
    serverStats.usage = ServerInfoService.getHumanReadableString(serverStats.usageRaw);
 | 
			
		||||
    serverStats.usageByUser = Array.from(tmpMap.values());
 | 
			
		||||
    return serverStats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static async getDirectoryStats(dirPath: string) {
 | 
			
		||||
    let size = 0;
 | 
			
		||||
    let fileCount = 0;
 | 
			
		||||
    for (const filename of readdirSync(dirPath)) {
 | 
			
		||||
      const absFilename = path.join(dirPath, filename);
 | 
			
		||||
      const fileStat = statSync(absFilename);
 | 
			
		||||
      if (fileStat.isFile()) {
 | 
			
		||||
        size += fileStat.size;
 | 
			
		||||
        fileCount += 1;
 | 
			
		||||
      } else if (fileStat.isDirectory()) {
 | 
			
		||||
        const subDirStat = await ServerInfoService.getDirectoryStats(absFilename);
 | 
			
		||||
        size += subDirStat.size;
 | 
			
		||||
        fileCount += subDirStat.fileCount;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return { size, fileCount };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,13 @@ import { validate } from 'class-validator';
 | 
			
		||||
import { CreateUserDto } from './create-user.dto';
 | 
			
		||||
 | 
			
		||||
describe('create user DTO', () => {
 | 
			
		||||
  it('validates the email', async() => {
 | 
			
		||||
  it('validates the email', async () => {
 | 
			
		||||
    const params: Partial<CreateUserDto> = {
 | 
			
		||||
      email: undefined,
 | 
			
		||||
      password: 'password',
 | 
			
		||||
      firstName: 'first name',
 | 
			
		||||
      lastName: 'last name',
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
    let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
 | 
			
		||||
    let errors = await validate(dto);
 | 
			
		||||
    expect(errors).toHaveLength(1);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { Not, Repository } from 'typeorm';
 | 
			
		||||
import { CreateUserDto } from './dto/create-user.dto';
 | 
			
		||||
import * as bcrypt from 'bcrypt';
 | 
			
		||||
import { UpdateUserDto } from './dto/update-user.dto'
 | 
			
		||||
import { UpdateUserDto } from './dto/update-user.dto';
 | 
			
		||||
 | 
			
		||||
export interface IUserRepository {
 | 
			
		||||
  get(userId: string): Promise<UserEntity | null>;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,8 @@ import { UserRepository, USER_REPOSITORY } from './user-repository';
 | 
			
		||||
    ImmichJwtService,
 | 
			
		||||
    {
 | 
			
		||||
      provide: USER_REPOSITORY,
 | 
			
		||||
      useClass: UserRepository
 | 
			
		||||
    }
 | 
			
		||||
      useClass: UserRepository,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class UserModule {}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,20 +1,20 @@
 | 
			
		||||
import { Logger } from '@nestjs/common';
 | 
			
		||||
import { ConfigModuleOptions } from '@nestjs/config';
 | 
			
		||||
import Joi from 'joi';
 | 
			
		||||
import { createSecretKey, generateKeySync } from 'node:crypto'
 | 
			
		||||
import { createSecretKey, generateKeySync } from 'node:crypto';
 | 
			
		||||
 | 
			
		||||
const jwtSecretValidator: Joi.CustomValidator<string> = (value, ) => {
 | 
			
		||||
  const key = createSecretKey(value, "base64")
 | 
			
		||||
  const keySizeBits = (key.symmetricKeySize ?? 0) * 8
 | 
			
		||||
const jwtSecretValidator: Joi.CustomValidator<string> = (value) => {
 | 
			
		||||
  const key = createSecretKey(value, 'base64');
 | 
			
		||||
  const keySizeBits = (key.symmetricKeySize ?? 0) * 8;
 | 
			
		||||
 | 
			
		||||
  if (keySizeBits < 128) {
 | 
			
		||||
    const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64')
 | 
			
		||||
    Logger.warn("The current JWT_SECRET key is insecure. It should be at least 128 bits long!")
 | 
			
		||||
    Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`)
 | 
			
		||||
    const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64');
 | 
			
		||||
    Logger.warn('The current JWT_SECRET key is insecure. It should be at least 128 bits long!');
 | 
			
		||||
    Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const immichAppConfig: ConfigModuleOptions = {
 | 
			
		||||
  envFilePath: '.env',
 | 
			
		||||
@@ -26,7 +26,7 @@ export const immichAppConfig: ConfigModuleOptions = {
 | 
			
		||||
    DB_DATABASE_NAME: Joi.string().required(),
 | 
			
		||||
    JWT_SECRET: Joi.string().required().custom(jwtSecretValidator),
 | 
			
		||||
    DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
 | 
			
		||||
    REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0,1,2,3).default(3),
 | 
			
		||||
    REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
 | 
			
		||||
    LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export class AlbumEntity {
 | 
			
		||||
  @CreateDateColumn({ type: 'timestamptz' })
 | 
			
		||||
  createdAt!: string;
 | 
			
		||||
 | 
			
		||||
  @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true})
 | 
			
		||||
  @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
 | 
			
		||||
  albumThumbnailAssetId!: string | null;
 | 
			
		||||
 | 
			
		||||
  @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique} from 'typeorm';
 | 
			
		||||
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
			
		||||
import { AlbumEntity } from './album.entity';
 | 
			
		||||
import { AssetEntity } from './asset.entity';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,17 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from "typeorm"
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
 | 
			
		||||
        await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
 | 
			
		||||
        await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,12 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from "typeorm";
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`
 | 
			
		||||
      ALTER TABLE exif 
 | 
			
		||||
      DROP COLUMN IF EXISTS exif_text_searchable_column;
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +28,5 @@ export class DropExifTextSearchableColumns1656888918620 implements MigrationInte
 | 
			
		||||
        ON exif 
 | 
			
		||||
        USING GIN (exif_text_searchable_column);
 | 
			
		||||
    `);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from "typeorm";
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
 | 
			
		||||
 | 
			
		||||
    public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
 | 
			
		||||
                         COALESCE(make, '') || ' ' ||
 | 
			
		||||
                         COALESCE(model, '') || ' ' ||
 | 
			
		||||
                         COALESCE(orientation, '') || ' ' ||
 | 
			
		||||
@@ -11,36 +10,63 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
 | 
			
		||||
                         COALESCE("city", '') || ' ' ||
 | 
			
		||||
                         COALESCE("state", '') || ' ' ||
 | 
			
		||||
                         COALESCE("country", ''))) STORED`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","postgres","public","exif"]);
 | 
			
		||||
        await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["postgres","public","exif","GENERATED_COLUMN","exifTextSearchableColumn","TO_TSVECTOR('english',\n                         COALESCE(make, '') || ' ' ||\n                         COALESCE(model, '') || ' ' ||\n                         COALESCE(orientation, '') || ' ' ||\n                         COALESCE(\"lensModel\", '') || ' ' ||\n                         COALESCE(\"city\", '') || ' ' ||\n                         COALESCE(\"state\", '') || ' ' ||\n                         COALESCE(\"country\", ''))"]);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
 | 
			
		||||
        await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","immich","public","exif"]);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
 | 
			
		||||
      ['GENERATED_COLUMN', 'exifTextSearchableColumn', 'postgres', 'public', 'exif'],
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
 | 
			
		||||
      [
 | 
			
		||||
        'postgres',
 | 
			
		||||
        'public',
 | 
			
		||||
        'exif',
 | 
			
		||||
        'GENERATED_COLUMN',
 | 
			
		||||
        'exifTextSearchableColumn',
 | 
			
		||||
        "TO_TSVECTOR('english',\n                         COALESCE(make, '') || ' ' ||\n                         COALESCE(model, '') || ' ' ||\n                         COALESCE(orientation, '') || ' ' ||\n                         COALESCE(\"lensModel\", '') || ' ' ||\n                         COALESCE(\"city\", '') || ' ' ||\n                         COALESCE(\"state\", '') || ' ' ||\n                         COALESCE(\"country\", ''))",
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`,
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
 | 
			
		||||
      ['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`,
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
 | 
			
		||||
    );
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,17 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class AddAssetChecksum1661881837496 implements MigrationInterface {
 | 
			
		||||
  name = 'AddAssetChecksum1661881837496'
 | 
			
		||||
  name = 'AddAssetChecksum1661881837496';
 | 
			
		||||
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
 | 
			
		||||
    await queryRunner.query(`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
 | 
			
		||||
  name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662'
 | 
			
		||||
  name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662';
 | 
			
		||||
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
 | 
			
		||||
@@ -10,7 +10,8 @@ export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements Mig
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`);
 | 
			
		||||
    await queryRunner.query(
 | 
			
		||||
      `ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1157,6 +1157,49 @@ export interface ServerPingResponse {
 | 
			
		||||
     */
 | 
			
		||||
    'res': string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface ServerStatsResponseDto
 | 
			
		||||
 */
 | 
			
		||||
export interface ServerStatsResponseDto {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'photos': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'videos': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'objects': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'usageRaw': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'usage': string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {Array<UsageByUserDto>}
 | 
			
		||||
     * @memberof ServerStatsResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'usageByUser': Array<UsageByUserDto>;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
@@ -1365,6 +1408,49 @@ export interface UpdateUserDto {
 | 
			
		||||
     */
 | 
			
		||||
    'profileImagePath'?: string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface UsageByUserDto
 | 
			
		||||
 */
 | 
			
		||||
export interface UsageByUserDto {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'userId': string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'objects': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'videos': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'photos': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'usageRaw': number;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof UsageByUserDto
 | 
			
		||||
     */
 | 
			
		||||
    'usage': string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
@@ -4132,6 +4218,35 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getStats: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/server-info/stats`;
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
@@ -4198,6 +4313,15 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getStats(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerStatsResponseDto>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getStats(options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@@ -4233,6 +4357,14 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
 | 
			
		||||
        getServerVersion(options?: any): AxiosPromise<ServerVersionReponseDto> {
 | 
			
		||||
            return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getStats(options?: any): AxiosPromise<ServerStatsResponseDto> {
 | 
			
		||||
            return localVarFp.getStats(options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@@ -4271,6 +4403,16 @@ export class ServerInfoApi extends BaseAPI {
 | 
			
		||||
        return ServerInfoApiFp(this.configuration).getServerVersion(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof ServerInfoApi
 | 
			
		||||
     */
 | 
			
		||||
    public getStats(options?: AxiosRequestConfig) {
 | 
			
		||||
        return ServerInfoApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								web/src/lib/components/admin-page/server-stats.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web/src/lib/components/admin-page/server-stats.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { ServerStatsResponseDto, UserResponseDto } from '@api';
 | 
			
		||||
	export let stats: ServerStatsResponseDto;
 | 
			
		||||
	export let allUsers: Array<UserResponseDto>;
 | 
			
		||||
 | 
			
		||||
	const getFullName = (userId: string) => {
 | 
			
		||||
		let name = 'Admin'; // since we do not have admin user in allUsers
 | 
			
		||||
		allUsers.forEach((user) => {
 | 
			
		||||
			if (user.id === userId) name = `${user.firstName} ${user.lastName}`;
 | 
			
		||||
		});
 | 
			
		||||
		return name;
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-6">
 | 
			
		||||
	<div class="border p-6 rounded-2xl bg-white text-center">
 | 
			
		||||
		<h1 class="font-medium text-immich-primary">Server Usage</h1>
 | 
			
		||||
		<div class="flex flex-row gap-6 mt-4 font-medium">
 | 
			
		||||
			<p class="grow">Photos: {stats.photos}</p>
 | 
			
		||||
			<p class="grow">Videos: {stats.videos}</p>
 | 
			
		||||
			<p class="grow">Objects: {stats.objects}</p>
 | 
			
		||||
			<p class="grow">Size: {stats.usage}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="border p-6 rounded-2xl bg-white">
 | 
			
		||||
		<h1 class="font-medium text-immich-primary">Usage by User</h1>
 | 
			
		||||
		<table class="text-left w-full mt-4">
 | 
			
		||||
			<!-- table header -->
 | 
			
		||||
			<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12">
 | 
			
		||||
				<tr class="flex w-full place-items-center">
 | 
			
		||||
					<th class="text-center w-1/5 font-medium text-sm">User</th>
 | 
			
		||||
					<th class="text-center w-1/5 font-medium text-sm">Photos</th>
 | 
			
		||||
					<th class="text-center w-1/5 font-medium text-sm">Videos</th>
 | 
			
		||||
					<th class="text-center w-1/5 font-medium text-sm">Objects</th>
 | 
			
		||||
					<th class="text-center w-1/5 font-medium text-sm">Size</th>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</thead>
 | 
			
		||||
			<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border">
 | 
			
		||||
				{#each stats.usageByUser as user}
 | 
			
		||||
					<tr class="text-center flex place-items-center w-full h-[40px]">
 | 
			
		||||
						<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td>
 | 
			
		||||
						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td>
 | 
			
		||||
						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td>
 | 
			
		||||
						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.objects}</td>
 | 
			
		||||
						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.usage}</td>
 | 
			
		||||
					</tr>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</tbody>
 | 
			
		||||
		</table>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
export enum AdminSideBarSelection {
 | 
			
		||||
	USER_MANAGEMENT = 'User management',
 | 
			
		||||
	JOBS = 'Jobs',
 | 
			
		||||
	SETTINGS = 'Settings'
 | 
			
		||||
	SETTINGS = 'Settings',
 | 
			
		||||
	STATS = 'Server Stats'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum AppSideBarSelection {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,10 @@ export const load: PageServerLoad = async ({ parent }) => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
 | 
			
		||||
	const { data: stats } = await serverApi.serverInfoApi.getStats();
 | 
			
		||||
	return {
 | 
			
		||||
		user: user,
 | 
			
		||||
		allUsers: allUsers
 | 
			
		||||
		allUsers: allUsers,
 | 
			
		||||
		stats: stats
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
	import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
 | 
			
		||||
	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 | 
			
		||||
	import Cog from 'svelte-material-icons/Cog.svelte';
 | 
			
		||||
	import Server from 'svelte-material-icons/Server.svelte';
 | 
			
		||||
	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
 | 
			
		||||
	import UserManagement from '$lib/components/admin-page/user-management.svelte';
 | 
			
		||||
	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
			
		||||
@@ -14,6 +15,7 @@
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
	import { api, UserResponseDto } from '@api';
 | 
			
		||||
	import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
 | 
			
		||||
	import ServerStats from '$lib/components/admin-page/server-stats.svelte';
 | 
			
		||||
 | 
			
		||||
	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
 | 
			
		||||
 | 
			
		||||
@@ -121,6 +123,13 @@
 | 
			
		||||
			isSelected={selectedAction === AdminSideBarSelection.JOBS}
 | 
			
		||||
			on:selected={onButtonClicked}
 | 
			
		||||
		/>
 | 
			
		||||
		<SideBarButton
 | 
			
		||||
			title="Server Stats"
 | 
			
		||||
			logo={Server}
 | 
			
		||||
			actionType={AdminSideBarSelection.STATS}
 | 
			
		||||
			isSelected={selectedAction === AdminSideBarSelection.STATS}
 | 
			
		||||
			on:selected={onButtonClicked}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
		<div class="mb-6 mt-auto">
 | 
			
		||||
			<StatusBox />
 | 
			
		||||
@@ -144,6 +153,9 @@
 | 
			
		||||
				{#if selectedAction === AdminSideBarSelection.JOBS}
 | 
			
		||||
					<JobsPanel />
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if selectedAction === AdminSideBarSelection.STATS}
 | 
			
		||||
					<ServerStats stats={data.stats} allUsers={data.allUsers} />
 | 
			
		||||
				{/if}
 | 
			
		||||
			</section>
 | 
			
		||||
		</section>
 | 
			
		||||
	</section>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user