1
0
mirror of https://github.com/immich-app/immich.git synced 2025-03-30 23:04:40 +02:00

feat(server,web): system config for admin ()

* feat: add admin config module for user configured config, uses it for ffmpeg

* feat: add api endpoint to retrieve admin config settings and values

* feat: add settings panel to admin page on web (wip)

* feat: add api endpoint to update the admin config

* chore: re-generate openapi spec after rebase

* refactor: move from admin config to system config naming

* chore: move away from UseGuards to new @Authenticated decorator

* style: dark mode styling for lists and fix conflicting colors

* wip: 2 column design, no edit button

* refactor: system config

* chore: generate open api

* chore: rm broken test

* chore: cleanup types

* refactor: config module names

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Zack Pollard <zack.pollard@moonpig.com>
This commit is contained in:
Jason Rasmussen 2022-11-14 23:39:32 -05:00 committed by GitHub
parent d3c35ec9c5
commit b5d75e2016
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2062 additions and 38 deletions

@ -59,6 +59,10 @@ doc/ServerStatsResponseDto.md
doc/ServerVersionReponseDto.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
doc/SystemConfigApi.md
doc/SystemConfigKey.md
doc/SystemConfigResponseDto.md
doc/SystemConfigResponseItem.md
doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
@ -79,6 +83,7 @@ lib/api/device_info_api.dart
lib/api/job_api.dart
lib/api/o_auth_api.dart
lib/api/server_info_api.dart
lib/api/system_config_api.dart
lib/api/user_api.dart
lib/api_client.dart
lib/api_exception.dart
@ -138,6 +143,9 @@ 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
lib/model/system_config_key.dart
lib/model/system_config_response_dto.dart
lib/model/system_config_response_item.dart
lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart

@ -109,6 +109,8 @@ Class | Method | HTTP request | Description
*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 |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
*UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{userId} |
@ -173,6 +175,9 @@ Class | Method | HTTP request | Description
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [SystemConfigKey](doc//SystemConfigKey.md)
- [SystemConfigResponseDto](doc//SystemConfigResponseDto.md)
- [SystemConfigResponseItem](doc//SystemConfigResponseItem.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)
- [TimeGroupEnum](doc//TimeGroupEnum.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)

@ -0,0 +1,15 @@
# openapi.model.AdminConfigResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**config** | [**Object**](.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

@ -0,0 +1,105 @@
# openapi.api.ConfigApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getSystemConfig**](ConfigApi.md#getsystemconfig) | **GET** /config/system |
[**updateSystemConfig**](ConfigApi.md#updatesystemconfig) | **PUT** /config/system |
# **getSystemConfig**
> SystemConfigResponseDto getSystemConfig()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ConfigApi();
try {
final result = api_instance.getSystemConfig();
print(result);
} catch (e) {
print('Exception when calling ConfigApi->getSystemConfig: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateSystemConfig**
> SystemConfigResponseDto updateSystemConfig(body)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ConfigApi();
final body = Object(); // Object |
try {
final result = api_instance.updateSystemConfig(body);
print(result);
} catch (e) {
print('Exception when calling ConfigApi->updateSystemConfig: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**body** | **Object**| |
### Return type
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **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)

@ -0,0 +1,105 @@
# openapi.api.SystemConfigApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config |
[**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config |
# **getConfig**
> SystemConfigResponseDto getConfig()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SystemConfigApi();
try {
final result = api_instance.getConfig();
print(result);
} catch (e) {
print('Exception when calling SystemConfigApi->getConfig: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateConfig**
> SystemConfigResponseDto updateConfig(body)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SystemConfigApi();
final body = Object(); // Object |
try {
final result = api_instance.updateConfig(body);
print(result);
} catch (e) {
print('Exception when calling SystemConfigApi->updateConfig: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**body** | **Object**| |
### Return type
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **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)

@ -0,0 +1,16 @@
# openapi.model.SystemConfigEntity
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**key** | **String** | |
**value** | [**Object**](.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

@ -0,0 +1,14 @@
# openapi.model.SystemConfigKey
## 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)

@ -0,0 +1,15 @@
# openapi.model.SystemConfigResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**config** | [**List<SystemConfigResponseItem>**](SystemConfigResponseItem.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)

@ -0,0 +1,18 @@
# openapi.model.SystemConfigResponseItem
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**name** | **String** | |
**key** | [**SystemConfigKey**](SystemConfigKey.md) | |
**value** | **String** | |
**defaultValue** | **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)

@ -34,6 +34,7 @@ part 'api/device_info_api.dart';
part 'api/job_api.dart';
part 'api/o_auth_api.dart';
part 'api/server_info_api.dart';
part 'api/system_config_api.dart';
part 'api/user_api.dart';
part 'model/add_assets_dto.dart';
@ -86,6 +87,9 @@ 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';
part 'model/system_config_key.dart';
part 'model/system_config_response_dto.dart';
part 'model/system_config_response_item.dart';
part 'model/thumbnail_format.dart';
part 'model/time_group_enum.dart';
part 'model/update_album_dto.dart';

@ -0,0 +1,106 @@
//
// 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 ConfigApi {
ConfigApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'GET /config/system' operation and returns the [Response].
Future<Response> getSystemConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/config/system';
// 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<SystemConfigResponseDto?> getSystemConfig() async {
final response = await getSystemConfigWithHttpInfo();
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), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
}
return null;
}
/// Performs an HTTP 'PUT /config/system' operation and returns the [Response].
/// Parameters:
///
/// * [Object] body (required):
Future<Response> updateSystemConfigWithHttpInfo(Object body,) async {
// ignore: prefer_const_declarations
final path = r'/config/system';
// ignore: prefer_final_locals
Object? postBody = body;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [Object] body (required):
Future<SystemConfigResponseDto?> updateSystemConfig(Object body,) async {
final response = await updateSystemConfigWithHttpInfo(body,);
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), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
}
return null;
}
}

@ -0,0 +1,106 @@
//
// 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 SystemConfigApi {
SystemConfigApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'GET /system-config' operation and returns the [Response].
Future<Response> getConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/system-config';
// 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<SystemConfigResponseDto?> getConfig() async {
final response = await getConfigWithHttpInfo();
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), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
}
return null;
}
/// Performs an HTTP 'PUT /system-config' operation and returns the [Response].
/// Parameters:
///
/// * [Object] body (required):
Future<Response> updateConfigWithHttpInfo(Object body,) async {
// ignore: prefer_const_declarations
final path = r'/system-config';
// ignore: prefer_final_locals
Object? postBody = body;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [Object] body (required):
Future<SystemConfigResponseDto?> updateConfig(Object body,) async {
final response = await updateConfigWithHttpInfo(body,);
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), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
}
return null;
}
}

@ -292,6 +292,12 @@ class ApiClient {
return SignUpDto.fromJson(value);
case 'SmartInfoResponseDto':
return SmartInfoResponseDto.fromJson(value);
case 'SystemConfigKey':
return SystemConfigKeyTypeTransformer().decode(value);
case 'SystemConfigResponseDto':
return SystemConfigResponseDto.fromJson(value);
case 'SystemConfigResponseItem':
return SystemConfigResponseItem.fromJson(value);
case 'ThumbnailFormat':
return ThumbnailFormatTypeTransformer().decode(value);
case 'TimeGroupEnum':

@ -70,6 +70,9 @@ String parameterToString(dynamic value) {
if (value is JobId) {
return JobIdTypeTransformer().encode(value).toString();
}
if (value is SystemConfigKey) {
return SystemConfigKeyTypeTransformer().encode(value).toString();
}
if (value is ThumbnailFormat) {
return ThumbnailFormatTypeTransformer().encode(value).toString();
}

@ -0,0 +1,111 @@
//
// 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 AdminConfigResponseDto {
/// Returns a new [AdminConfigResponseDto] instance.
AdminConfigResponseDto({
required this.config,
});
Object config;
@override
bool operator ==(Object other) => identical(this, other) || other is AdminConfigResponseDto &&
other.config == config;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(config.hashCode);
@override
String toString() => 'AdminConfigResponseDto[config=$config]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'config'] = config;
return _json;
}
/// Returns a new [AdminConfigResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AdminConfigResponseDto? 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 "AdminConfigResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AdminConfigResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return AdminConfigResponseDto(
config: mapValueOfType<Object>(json, r'config')!,
);
}
return null;
}
static List<AdminConfigResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <AdminConfigResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AdminConfigResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AdminConfigResponseDto> mapFromJson(dynamic json) {
final map = <String, AdminConfigResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AdminConfigResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AdminConfigResponseDto-objects as value to a dart map
static Map<String, List<AdminConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AdminConfigResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AdminConfigResponseDto.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>{
'config',
};
}

@ -0,0 +1,202 @@
//
// 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 SystemConfigEntity {
/// Returns a new [SystemConfigEntity] instance.
SystemConfigEntity({
required this.key,
required this.value,
});
SystemConfigEntityKeyEnum key;
Object value;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigEntity &&
other.key == key &&
other.value == value;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(key.hashCode) +
(value.hashCode);
@override
String toString() => 'SystemConfigEntity[key=$key, value=$value]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'key'] = key;
_json[r'value'] = value;
return _json;
}
/// Returns a new [SystemConfigEntity] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigEntity? 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 "SystemConfigEntity[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "SystemConfigEntity[$key]" has a null value in JSON.');
});
return true;
}());
return SystemConfigEntity(
key: SystemConfigEntityKeyEnum.fromJson(json[r'key'])!,
value: mapValueOfType<Object>(json, r'value')!,
);
}
return null;
}
static List<SystemConfigEntity>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigEntity>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigEntity.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigEntity> mapFromJson(dynamic json) {
final map = <String, SystemConfigEntity>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigEntity.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigEntity-objects as value to a dart map
static Map<String, List<SystemConfigEntity>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigEntity>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigEntity.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>{
'key',
'value',
};
}
class SystemConfigEntityKeyEnum {
/// Instantiate a new enum with the provided [value].
const SystemConfigEntityKeyEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const crf = SystemConfigEntityKeyEnum._(r'ffmpeg_crf');
static const preset = SystemConfigEntityKeyEnum._(r'ffmpeg_preset');
static const targetVideoCodec = SystemConfigEntityKeyEnum._(r'ffmpeg_target_video_codec');
static const targetAudioCodec = SystemConfigEntityKeyEnum._(r'ffmpeg_target_audio_codec');
static const targetScaling = SystemConfigEntityKeyEnum._(r'ffmpeg_target_scaling');
/// List of all possible values in this [enum][SystemConfigEntityKeyEnum].
static const values = <SystemConfigEntityKeyEnum>[
crf,
preset,
targetVideoCodec,
targetAudioCodec,
targetScaling,
];
static SystemConfigEntityKeyEnum? fromJson(dynamic value) => SystemConfigEntityKeyEnumTypeTransformer().decode(value);
static List<SystemConfigEntityKeyEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigEntityKeyEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigEntityKeyEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SystemConfigEntityKeyEnum] to String,
/// and [decode] dynamic data back to [SystemConfigEntityKeyEnum].
class SystemConfigEntityKeyEnumTypeTransformer {
factory SystemConfigEntityKeyEnumTypeTransformer() => _instance ??= const SystemConfigEntityKeyEnumTypeTransformer._();
const SystemConfigEntityKeyEnumTypeTransformer._();
String encode(SystemConfigEntityKeyEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SystemConfigEntityKeyEnum.
///
/// 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.
SystemConfigEntityKeyEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'ffmpeg_crf': return SystemConfigEntityKeyEnum.crf;
case r'ffmpeg_preset': return SystemConfigEntityKeyEnum.preset;
case r'ffmpeg_target_video_codec': return SystemConfigEntityKeyEnum.targetVideoCodec;
case r'ffmpeg_target_audio_codec': return SystemConfigEntityKeyEnum.targetAudioCodec;
case r'ffmpeg_target_scaling': return SystemConfigEntityKeyEnum.targetScaling;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SystemConfigEntityKeyEnumTypeTransformer] instance.
static SystemConfigEntityKeyEnumTypeTransformer? _instance;
}

@ -0,0 +1,94 @@
//
// 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 SystemConfigKey {
/// Instantiate a new enum with the provided [value].
const SystemConfigKey._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const crf = SystemConfigKey._(r'ffmpeg_crf');
static const preset = SystemConfigKey._(r'ffmpeg_preset');
static const targetVideoCodec = SystemConfigKey._(r'ffmpeg_target_video_codec');
static const targetAudioCodec = SystemConfigKey._(r'ffmpeg_target_audio_codec');
static const targetScaling = SystemConfigKey._(r'ffmpeg_target_scaling');
/// List of all possible values in this [enum][SystemConfigKey].
static const values = <SystemConfigKey>[
crf,
preset,
targetVideoCodec,
targetAudioCodec,
targetScaling,
];
static SystemConfigKey? fromJson(dynamic value) => SystemConfigKeyTypeTransformer().decode(value);
static List<SystemConfigKey>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigKey>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigKey.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SystemConfigKey] to String,
/// and [decode] dynamic data back to [SystemConfigKey].
class SystemConfigKeyTypeTransformer {
factory SystemConfigKeyTypeTransformer() => _instance ??= const SystemConfigKeyTypeTransformer._();
const SystemConfigKeyTypeTransformer._();
String encode(SystemConfigKey data) => data.value;
/// Decodes a [dynamic value][data] to a SystemConfigKey.
///
/// 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.
SystemConfigKey? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'ffmpeg_crf': return SystemConfigKey.crf;
case r'ffmpeg_preset': return SystemConfigKey.preset;
case r'ffmpeg_target_video_codec': return SystemConfigKey.targetVideoCodec;
case r'ffmpeg_target_audio_codec': return SystemConfigKey.targetAudioCodec;
case r'ffmpeg_target_scaling': return SystemConfigKey.targetScaling;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SystemConfigKeyTypeTransformer] instance.
static SystemConfigKeyTypeTransformer? _instance;
}

@ -0,0 +1,111 @@
//
// 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 SystemConfigResponseDto {
/// Returns a new [SystemConfigResponseDto] instance.
SystemConfigResponseDto({
this.config = const [],
});
List<SystemConfigResponseItem> config;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigResponseDto &&
other.config == config;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(config.hashCode);
@override
String toString() => 'SystemConfigResponseDto[config=$config]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'config'] = config;
return _json;
}
/// Returns a new [SystemConfigResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigResponseDto? 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 "SystemConfigResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "SystemConfigResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return SystemConfigResponseDto(
config: SystemConfigResponseItem.listFromJson(json[r'config'])!,
);
}
return null;
}
static List<SystemConfigResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigResponseDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigResponseDto-objects as value to a dart map
static Map<String, List<SystemConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigResponseDto.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>{
'config',
};
}

@ -0,0 +1,135 @@
//
// 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 SystemConfigResponseItem {
/// Returns a new [SystemConfigResponseItem] instance.
SystemConfigResponseItem({
required this.name,
required this.key,
required this.value,
required this.defaultValue,
});
String name;
SystemConfigKey key;
String value;
String defaultValue;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigResponseItem &&
other.name == name &&
other.key == key &&
other.value == value &&
other.defaultValue == defaultValue;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(name.hashCode) +
(key.hashCode) +
(value.hashCode) +
(defaultValue.hashCode);
@override
String toString() => 'SystemConfigResponseItem[name=$name, key=$key, value=$value, defaultValue=$defaultValue]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'name'] = name;
_json[r'key'] = key;
_json[r'value'] = value;
_json[r'defaultValue'] = defaultValue;
return _json;
}
/// Returns a new [SystemConfigResponseItem] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigResponseItem? 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 "SystemConfigResponseItem[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "SystemConfigResponseItem[$key]" has a null value in JSON.');
});
return true;
}());
return SystemConfigResponseItem(
name: mapValueOfType<String>(json, r'name')!,
key: SystemConfigKey.fromJson(json[r'key'])!,
value: mapValueOfType<String>(json, r'value')!,
defaultValue: mapValueOfType<String>(json, r'defaultValue')!,
);
}
return null;
}
static List<SystemConfigResponseItem>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigResponseItem>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigResponseItem.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigResponseItem> mapFromJson(dynamic json) {
final map = <String, SystemConfigResponseItem>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigResponseItem.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigResponseItem-objects as value to a dart map
static Map<String, List<SystemConfigResponseItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigResponseItem>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigResponseItem.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>{
'name',
'key',
'value',
'defaultValue',
};
}

@ -0,0 +1,26 @@
//
// 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 AdminConfigApi
void main() {
// final instance = AdminConfigApi();
group('tests for AdminConfigApi', () {
//Future<AdminConfigResponseDto> getAdminConfig() async
test('test getAdminConfig', () async {
// TODO
});
});
}

@ -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 AdminConfigResponseDto
void main() {
// final instance = AdminConfigResponseDto();
group('test AdminConfigResponseDto', () {
// Object config
test('to test the property `config`', () async {
// TODO
});
});
}

@ -0,0 +1,31 @@
//
// 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 ConfigApi
void main() {
// final instance = ConfigApi();
group('tests for ConfigApi', () {
//Future<SystemConfigResponseDto> getSystemConfig() async
test('test getSystemConfig', () async {
// TODO
});
//Future<SystemConfigResponseDto> putSystemConfig(Object body) async
test('test putSystemConfig', () async {
// TODO
});
});
}

@ -0,0 +1,31 @@
//
// 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 SystemConfigApi
void main() {
// final instance = SystemConfigApi();
group('tests for SystemConfigApi', () {
//Future<SystemConfigResponseDto> getConfig() async
test('test getConfig', () async {
// TODO
});
//Future<SystemConfigResponseDto> updateConfig(Object body) async
test('test updateConfig', () async {
// TODO
});
});
}

@ -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 SystemConfigEntity
void main() {
// final instance = SystemConfigEntity();
group('test SystemConfigEntity', () {
// String key
test('to test the property `key`', () async {
// TODO
});
// Object value
test('to test the property `value`', () async {
// TODO
});
});
}

@ -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 SystemConfigKey
void main() {
group('test SystemConfigKey', () {
});
}

@ -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 SystemConfigResponseDto
void main() {
// final instance = SystemConfigResponseDto();
group('test SystemConfigResponseDto', () {
// Object config
test('to test the property `config`', () async {
// TODO
});
});
}

@ -0,0 +1,37 @@
//
// 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 SystemConfigResponseItem
void main() {
// final instance = SystemConfigResponseItem();
group('test SystemConfigResponseItem', () {
// String name
test('to test the property `name`', () async {
// TODO
});
// String key
test('to test the property `key`', () async {
// TODO
});
// Object value
test('to test the property `value`', () async {
// TODO
});
});
}

@ -1 +1,6 @@
# Immich Server- NestJs
## How to run migration
1. Attached to the container shell
2. Run `npm run typeorm -- migration:generate ./libs/database/src/<migration-name> -d libs/database/src/config/database.config.ts`
3. Check if the migration file makes sense
4. Move the migration file to folder `server/libs/database/src/migrations` in your code editor.

@ -0,0 +1,20 @@
import { SystemConfigKey, SystemConfigValue } from '@app/database/entities/system-config.entity';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, ValidateNested } from 'class-validator';
export class UpdateSystemConfigDto {
@IsNotEmpty()
@ValidateNested({ each: true })
config!: SystemConfigItem[];
}
export class SystemConfigItem {
@IsNotEmpty()
@IsEnum(SystemConfigKey)
@ApiProperty({
enum: SystemConfigKey,
enumName: 'SystemConfigKey',
})
key!: SystemConfigKey;
value!: SystemConfigValue;
}

@ -0,0 +1,20 @@
import { SystemConfigKey, SystemConfigValue } from '@app/database/entities/system-config.entity';
import { ApiProperty } from '@nestjs/swagger';
export class SystemConfigResponseDto {
config!: SystemConfigResponseItem[];
}
export class SystemConfigResponseItem {
@ApiProperty({ type: 'string' })
name!: string;
@ApiProperty({ enumName: 'SystemConfigKey', enum: SystemConfigKey })
key!: SystemConfigKey;
@ApiProperty({ type: 'string' })
value!: SystemConfigValue;
@ApiProperty({ type: 'string' })
defaultValue!: SystemConfigValue;
}

@ -0,0 +1,24 @@
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { UpdateSystemConfigDto } from './dto/update-system-config';
import { SystemConfigResponseDto } from './response-dto/system-config-response.dto';
import { SystemConfigService } from './system-config.service';
@ApiTags('System Config')
@ApiBearerAuth()
@Authenticated({ admin: true })
@Controller('system-config')
export class SystemConfigController {
constructor(private readonly systemConfigService: SystemConfigService) {}
@Get()
getConfig(): Promise<SystemConfigResponseDto> {
return this.systemConfigService.getConfig();
}
@Put()
async updateConfig(@Body(ValidationPipe) dto: UpdateSystemConfigDto): Promise<SystemConfigResponseDto> {
return this.systemConfigService.updateConfig(dto);
}
}

@ -0,0 +1,14 @@
import { SystemConfigEntity } from '@app/database/entities/system-config.entity';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ImmichConfigModule } from 'libs/immich-config/src';
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { SystemConfigController } from './system-config.controller';
import { SystemConfigService } from './system-config.service';
@Module({
imports: [ImmichJwtModule, ImmichConfigModule, TypeOrmModule.forFeature([SystemConfigEntity])],
controllers: [SystemConfigController],
providers: [SystemConfigService],
})
export class SystemConfigModule {}

@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { ImmichConfigService } from 'libs/immich-config/src';
import { UpdateSystemConfigDto } from './dto/update-system-config';
import { SystemConfigResponseDto } from './response-dto/system-config-response.dto';
@Injectable()
export class SystemConfigService {
constructor(private immichConfigService: ImmichConfigService) {}
async getConfig(): Promise<SystemConfigResponseDto> {
const config = await this.immichConfigService.getSystemConfig();
return { config };
}
async updateConfig(dto: UpdateSystemConfigDto): Promise<SystemConfigResponseDto> {
await this.immichConfigService.updateSystemConfig(dto.config);
const config = await this.immichConfigService.getSystemConfig();
return { config };
}
}

@ -16,6 +16,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
import { DatabaseModule } from '@app/database';
import { JobModule } from './api-v1/job/job.module';
import { SystemConfigModule } from './api-v1/system-config/system-config.module';
import { OAuthModule } from './api-v1/oauth/oauth.module';
@Module({
@ -60,6 +61,8 @@ import { OAuthModule } from './api-v1/oauth/oauth.module';
ScheduleTasksModule,
JobModule,
SystemConfigModule,
],
controllers: [AppController],
providers: [],

@ -7,8 +7,9 @@ import { UserEntity } from '@app/database/entities/user.entity';
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ImmichConfigModule } from 'libs/immich-config/src';
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
import { MicroservicesService } from './microservices.service';
import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
@ -22,6 +23,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
imports: [
ConfigModule.forRoot(immichAppConfig),
DatabaseModule,
ImmichConfigModule,
TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity]),
BullModule.forRootAsync({
useFactory: async () => ({
@ -96,7 +98,6 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
VideoTranscodeProcessor,
GenerateChecksumProcessor,
MachineLearningProcessor,
ConfigService,
],
exports: [],
})

@ -9,6 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Job } from 'bull';
import ffmpeg from 'fluent-ffmpeg';
import { existsSync, mkdirSync } from 'fs';
import { ImmichConfigService } from 'libs/immich-config/src';
import { Repository } from 'typeorm';
@Processor(QueueNameEnum.VIDEO_CONVERSION)
@ -16,6 +17,7 @@ export class VideoTranscodeProcessor {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
private immichConfigService: ImmichConfigService,
) {}
@Process({ name: mp4ConversionProcessorName, concurrency: 1 })
@ -40,9 +42,17 @@ export class VideoTranscodeProcessor {
}
async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
const config = await this.immichConfigService.getSystemConfigMap();
return new Promise((resolve, reject) => {
ffmpeg(asset.originalPath)
.outputOptions(['-crf 23', '-preset ultrafast', '-vcodec libx264', '-acodec mp3', '-vf scale=1280:-2'])
.outputOptions([
`-crf ${config.ffmpeg_crf}`,
`-preset ${config.ffmpeg_preset}`,
`-vcodec ${config.ffmpeg_target_video_codec}`,
`-acodec ${config.ffmpeg_target_audio_codec}`,
`-vf scale=${config.ffmpeg_target_scaling}`,
])
.output(savedEncodedPath)
.on('start', () => {
Logger.log('Start Converting Video', 'mp4Conversion');

File diff suppressed because one or more lines are too long

@ -0,0 +1,27 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('system_config')
export class SystemConfigEntity {
@PrimaryColumn()
key!: SystemConfigKey;
@Column({ type: 'varchar', nullable: true })
value!: SystemConfigValue;
}
export type SystemConfig = SystemConfigEntity[];
export enum SystemConfigKey {
FFMPEG_CRF = 'ffmpeg_crf',
FFMPEG_PRESET = 'ffmpeg_preset',
FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg_target_video_codec',
FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg_target_audio_codec',
FFMPEG_TARGET_SCALING = 'ffmpeg_target_scaling',
}
export type SystemConfigValue = string | null;
export interface SystemConfigItem {
key: SystemConfigKey;
value: SystemConfigValue;
}

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSystemConfigTable1665540663419 implements MigrationInterface {
name = 'CreateSystemConfigTable1665540663419';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" character varying, CONSTRAINT "PK_aab69295b445016f56731f4d535" PRIMARY KEY ("key"))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "system_config"`);
}
}

@ -0,0 +1,11 @@
import { SystemConfigEntity } from '@app/database/entities/system-config.entity';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ImmichConfigService } from './immich-config.service';
@Module({
imports: [TypeOrmModule.forFeature([SystemConfigEntity])],
providers: [ImmichConfigService],
exports: [ImmichConfigService],
})
export class ImmichConfigModule {}

@ -0,0 +1,97 @@
import { SystemConfigEntity, SystemConfigKey, SystemConfigValue } from '@app/database/entities/system-config.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
type SystemConfigMap = Record<SystemConfigKey, SystemConfigValue>;
const configDefaults: Record<SystemConfigKey, { name: string; value: SystemConfigValue }> = {
[SystemConfigKey.FFMPEG_CRF]: {
name: 'FFmpeg Constant Rate Factor (-crf)',
value: '23',
},
[SystemConfigKey.FFMPEG_PRESET]: {
name: 'FFmpeg preset (-preset)',
value: 'ultrafast',
},
[SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC]: {
name: 'FFmpeg target video codec (-vcodec)',
value: 'libx264',
},
[SystemConfigKey.FFMPEG_TARGET_AUDIO_CODEC]: {
name: 'FFmpeg target audio codec (-acodec)',
value: 'mp3',
},
[SystemConfigKey.FFMPEG_TARGET_SCALING]: {
name: 'FFmpeg target scaling (-vf scale=)',
value: '1280:-2',
},
};
@Injectable()
export class ImmichConfigService {
constructor(
@InjectRepository(SystemConfigEntity)
private systemConfigRepository: Repository<SystemConfigEntity>,
) {}
public async getSystemConfig() {
const items = this._getDefaults();
// override default values
const overrides = await this.systemConfigRepository.find();
for (const override of overrides) {
const item = items.find((_item) => _item.key === override.key);
if (item) {
item.value = override.value;
}
}
return items;
}
public async getSystemConfigMap(): Promise<SystemConfigMap> {
const items = await this.getSystemConfig();
const map: Partial<SystemConfigMap> = {};
for (const { key, value } of items) {
map[key] = value;
}
return map as SystemConfigMap;
}
public async updateSystemConfig(items: SystemConfigEntity[]): Promise<void> {
const deletes: SystemConfigEntity[] = [];
const updates: SystemConfigEntity[] = [];
for (const item of items) {
if (item.value === null || item.value === this._getDefaultValue(item.key)) {
deletes.push(item);
continue;
}
updates.push(item);
}
if (updates.length > 0) {
await this.systemConfigRepository.save(updates);
}
if (deletes.length > 0) {
await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) });
}
}
private _getDefaults() {
return Object.values(SystemConfigKey).map((key) => ({
key,
defaultValue: configDefaults[key].value,
...configDefaults[key],
}));
}
private _getDefaultValue(key: SystemConfigKey) {
return this._getDefaults().find((item) => item.key === key)?.value || null;
}
}

@ -0,0 +1,2 @@
export * from './immich-config.module';
export * from './immich-config.service';

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/immich-config"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

@ -70,6 +70,15 @@
"compilerOptions": {
"tsConfigPath": "libs/job/tsconfig.lib.json"
}
},
"system-config": {
"type": "library",
"root": "libs/system-config",
"entryFile": "index",
"sourceRoot": "libs/system-config/src",
"compilerOptions": {
"tsConfigPath": "libs/system-config/tsconfig.lib.json"
}
}
}
}
}

@ -13,6 +13,7 @@
"build": "nest build immich && nest build microservices && nest build cli",
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
"start": "nest start",
"nest": "nest",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug 0.0.0.0:9230 --watch",
"start:prod": "node dist/main",
@ -139,7 +140,8 @@
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
"@app/database/config": "<rootDir>/libs/database/src/config",
"@app/common": "<rootDir>/libs/common/src",
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1",
"^@app/system-config(|/.*)$": "<rootDir>/libs/system-config/src/$1"
}
}
}
}

@ -16,29 +16,15 @@
"esModuleInterop": true,
"baseUrl": "./",
"paths": {
"@app/common": [
"libs/common/src"
],
"@app/common/*": [
"libs/common/src/*"
],
"@app/database": [
"libs/database/src"
],
"@app/database/*": [
"libs/database/src/*"
],
"@app/job": [
"libs/job/src"
],
"@app/job/*": [
"libs/job/src/*"
]
"@app/common": ["libs/common/src"],
"@app/common/*": ["libs/common/src/*"],
"@app/database": ["libs/database/src"],
"@app/database/*": ["libs/database/src/*"],
"@app/job": ["libs/job/src"],
"@app/job/*": ["libs/job/src/*"],
"@app/system-config": ["libs/immich-config/src"],
"@app/system-config/*": ["libs/immich-config/src/*"]
}
},
"exclude": [
"dist",
"node_modules",
"upload"
]
}
"exclude": ["dist", "node_modules", "upload"]
}

@ -8,6 +8,7 @@ import {
JobApi,
OAuthApi,
ServerInfoApi,
SystemConfigApi,
UserApi
} from './open-api';
@ -20,6 +21,7 @@ class ImmichApi {
public deviceInfoApi: DeviceInfoApi;
public serverInfoApi: ServerInfoApi;
public jobApi: JobApi;
public systemConfigApi: SystemConfigApi;
private config = new Configuration({ basePath: '/api' });
@ -32,6 +34,7 @@ class ImmichApi {
this.deviceInfoApi = new DeviceInfoApi(this.config);
this.serverInfoApi = new ServerInfoApi(this.config);
this.jobApi = new JobApi(this.config);
this.systemConfigApi = new SystemConfigApi(this.config);
}
public setAccessToken(accessToken: string) {

@ -1407,6 +1407,67 @@ export interface SmartInfoResponseDto {
* @enum {string}
*/
export const SystemConfigKey = {
Crf: 'ffmpeg_crf',
Preset: 'ffmpeg_preset',
TargetVideoCodec: 'ffmpeg_target_video_codec',
TargetAudioCodec: 'ffmpeg_target_audio_codec',
TargetScaling: 'ffmpeg_target_scaling'
} as const;
export type SystemConfigKey = typeof SystemConfigKey[keyof typeof SystemConfigKey];
/**
*
* @export
* @interface SystemConfigResponseDto
*/
export interface SystemConfigResponseDto {
/**
*
* @type {Array<SystemConfigResponseItem>}
* @memberof SystemConfigResponseDto
*/
'config': Array<SystemConfigResponseItem>;
}
/**
*
* @export
* @interface SystemConfigResponseItem
*/
export interface SystemConfigResponseItem {
/**
*
* @type {string}
* @memberof SystemConfigResponseItem
*/
'name': string;
/**
*
* @type {SystemConfigKey}
* @memberof SystemConfigResponseItem
*/
'key': SystemConfigKey;
/**
*
* @type {string}
* @memberof SystemConfigResponseItem
*/
'value': string;
/**
*
* @type {string}
* @memberof SystemConfigResponseItem
*/
'defaultValue': string;
}
/**
*
* @export
* @enum {string}
*/
export const ThumbnailFormat = {
Jpeg: 'JPEG',
Webp: 'WEBP'
@ -4946,6 +5007,173 @@ export class ServerInfoApi extends BaseAPI {
}
/**
* SystemConfigApi - axios parameter creator
* @export
*/
export const SystemConfigApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/system-config`;
// 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;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {object} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateConfig: async (body: object, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'body' is not null or undefined
assertParamExists('updateConfig', 'body', body)
const localVarPath = `/system-config`;
// 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: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* SystemConfigApi - functional programming interface
* @export
*/
export const SystemConfigApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SystemConfigApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getConfig(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {object} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateConfig(body: object, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfig(body, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* SystemConfigApi - factory interface
* @export
*/
export const SystemConfigApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = SystemConfigApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getConfig(options?: any): AxiosPromise<SystemConfigResponseDto> {
return localVarFp.getConfig(options).then((request) => request(axios, basePath));
},
/**
*
* @param {object} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateConfig(body: object, options?: any): AxiosPromise<SystemConfigResponseDto> {
return localVarFp.updateConfig(body, options).then((request) => request(axios, basePath));
},
};
};
/**
* SystemConfigApi - object-oriented interface
* @export
* @class SystemConfigApi
* @extends {BaseAPI}
*/
export class SystemConfigApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigApi
*/
public getConfig(options?: AxiosRequestConfig) {
return SystemConfigApiFp(this.configuration).getConfig(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {object} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigApi
*/
public updateConfig(body: object, options?: AxiosRequestConfig) {
return SystemConfigApiFp(this.configuration).updateConfig(body, options).then((request) => request(this.axios, this.basePath));
}
}
/**
* UserApi - axios parameter creator
* @export

@ -59,7 +59,7 @@ input:focus-visible {
@layer utilities {
.immich-form-input {
@apply bg-slate-100 p-2 rounded-md dark:text-immich-dark-bg focus:border-immich-primary text-sm;
@apply bg-slate-100 p-2 rounded-md dark:text-immich-dark-bg focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg;
}
.immich-form-label {

@ -0,0 +1,97 @@
<script lang="ts">
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigResponseItem } from '@api';
import { onMount } from 'svelte';
let isSaving = false;
let items: Array<SystemConfigResponseItem & { originalValue: string }> = [];
const refreshConfig = async () => {
const { data: systemConfig } = await api.systemConfigApi.getConfig();
items = systemConfig.config.map((item) => ({ ...item, originalValue: item.value }));
};
onMount(() => refreshConfig());
const handleSave = async () => {
try {
isSaving = true;
const updates = items
.filter((item) => item.value !== item.originalValue)
.map(({ key, value }) => ({ key, value: value || null }));
if (updates.length > 0) {
await api.systemConfigApi.updateConfig({ config: updates });
refreshConfig();
}
notificationController.show({
message: `Saved settings`,
type: NotificationType.Info
});
} catch (e) {
console.error('Error [updateSystemConfig]', e);
notificationController.show({
message: `Unable to save changes.`,
type: NotificationType.Error
});
} finally {
isSaving = false;
}
};
</script>
<section>
<table class="text-left my-4 w-full">
<thead
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
>
<tr class="flex w-full place-items-center">
<th class="text-center w-1/2 font-medium text-sm">Setting</th>
<th class="text-center w-1/2 font-medium text-sm">Value</th>
</tr>
</thead>
<tbody class="rounded-md block border dark:border-immich-dark-gray">
{#each items as item, i}
<tr
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
i % 2 == 0 ? 'bg-slate-50 dark:bg-[#181818]' : 'bg-immich-bg dark:bg-immich-dark-bg'
}`}
>
<td class="text-sm px-4 w-1/2 text-ellipsis">
{item.name}
</td>
<td class="text-sm px-4 w-1/2 text-ellipsis">
<input
style="text-align: center"
class="immich-form-input"
id={item.key}
disabled={isSaving}
name={item.key}
type="text"
bind:value={item.value}
placeholder={item.defaultValue + ''}
/>
</td>
</tr>
{/each}
</tbody>
</table>
<div class="flex justify-end">
<button
on:click={handleSave}
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
disabled={isSaving}
>
{#if isSaving}
<LoadingSpinner />
{:else}
Save
{/if}
</button>
</div>
</section>

@ -12,8 +12,6 @@ export const load: PageServerLoad = async ({ parent }) => {
}
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
return {
user: user,
allUsers: allUsers
};
return { user, allUsers };
};

@ -4,6 +4,7 @@
import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import Sync from 'svelte-material-icons/Sync.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';
@ -16,6 +17,7 @@
import type { PageData } from './$types';
import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
import SettingsPanel from '$lib/components/admin-page/settings/settings-panel.svelte';
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
import RestoreDialoge from '$lib/components/admin-page/restore-dialoge.svelte';
@ -190,11 +192,18 @@
/>
<SideBarButton
title="Jobs"
logo={Cog}
logo={Sync}
actionType={AdminSideBarSelection.JOBS}
isSelected={selectedAction === AdminSideBarSelection.JOBS}
on:selected={onButtonClicked}
/>
<SideBarButton
title="Settings"
logo={Cog}
actionType={AdminSideBarSelection.SETTINGS}
isSelected={selectedAction === AdminSideBarSelection.SETTINGS}
on:selected={onButtonClicked}
/>
<SideBarButton
title="Server Stats"
logo={Server}
@ -228,6 +237,9 @@
{#if selectedAction === AdminSideBarSelection.JOBS}
<JobsPanel />
{/if}
{#if selectedAction === AdminSideBarSelection.SETTINGS}
<SettingsPanel />
{/if}
{#if selectedAction === AdminSideBarSelection.STATS && serverStat}
<ServerStatsPanel stats={serverStat} allUsers={data.allUsers} />
{/if}