mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server,web): migrate oauth settings from env to system config (#1061)
This commit is contained in:
		
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
									
									
									
									
								
							@@ -4,6 +4,9 @@ dev:
 | 
				
			|||||||
dev-new:
 | 
					dev-new:
 | 
				
			||||||
	rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
 | 
						rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dev-new-update:
 | 
				
			||||||
 | 
						rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev-update:
 | 
					dev-update:
 | 
				
			||||||
	rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 | 
						rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,10 +28,10 @@ Before enabling OAuth in Immich, a new client application needs to be configured
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
2. Configure Redirect URIs/Origins
 | 
					2. Configure Redirect URIs/Origins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  The **Sign-in redirect URIs** should include:
 | 
					The **Sign-in redirect URIs** should include:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`)
 | 
					- All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`)
 | 
				
			||||||
  * Mobile app redirect URL `app.immich:/`
 | 
					- Mobile app redirect URL `app.immich:/`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:::caution
 | 
					:::caution
 | 
				
			||||||
You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobile app to work properly.
 | 
					You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobile app to work properly.
 | 
				
			||||||
@@ -42,17 +42,17 @@ You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Enable OAuth
 | 
					## Enable OAuth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Once you have a new OAuth client application configured, Immich can be configured using the following environment variables:
 | 
					Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Key                 | Type    | Default              | Description                                                               |
 | 
					| Setting             | Type    | Default              | Description                                                               |
 | 
				
			||||||
| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- |
 | 
					| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- |
 | 
				
			||||||
| OAUTH_ENABLED       | boolean | false                | Enable/disable OAuth2                                                     |
 | 
					| OAuth enabled       | boolean | false                | Enable/disable OAuth2                                                     |
 | 
				
			||||||
| OAUTH_ISSUER_URL    | URL     | (required)           | Required. Self-discovery URL for client (from previous step)              |
 | 
					| OAuth issuer URL    | URL     | (required)           | Required. Self-discovery URL for client (from previous step)              |
 | 
				
			||||||
| OAUTH_CLIENT_ID     | string  | (required)           | Required. Client ID (from previous step)                                  |
 | 
					| OAuth client ID     | string  | (required)           | Required. Client ID (from previous step)                                  |
 | 
				
			||||||
| OAUTH_CLIENT_SECRET | string  | (required)           | Required. Client Secret (previous step)                                    |
 | 
					| OAuth client secret | string  | (required)           | Required. Client Secret (previous step)                                   |
 | 
				
			||||||
| OAUTH_SCOPE         | string  | openid email profile | Full list of scopes to send with the request (space delimited)            |
 | 
					| OAuth scope         | string  | openid email profile | Full list of scopes to send with the request (space delimited)            |
 | 
				
			||||||
| OAUTH_AUTO_REGISTER | boolean | true                 | When true, will automatically register a user the first time they sign in |
 | 
					| OAuth button text   | string  | Login with OAuth     | Text for the OAuth button on the web                                      |
 | 
				
			||||||
| OAUTH_BUTTON_TEXT   | string  | Login with OAuth     | Text for the OAuth button on the web                                      |
 | 
					| OAuth auto register | boolean | true                 | When true, will automatically register a user the first time they sign in |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:::info
 | 
					:::info
 | 
				
			||||||
The Issuer URL should look something like the following, and return a valid json document.
 | 
					The Issuer URL should look something like the following, and return a valid json document.
 | 
				
			||||||
@@ -63,14 +63,4 @@ The Issuer URL should look something like the following, and return a valid json
 | 
				
			|||||||
The `.well-known/openid-configuration` part of the url is optional and will be automatically added during discovery.
 | 
					The `.well-known/openid-configuration` part of the url is optional and will be automatically added during discovery.
 | 
				
			||||||
:::
 | 
					:::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Here is an example of a valid configuration for setting up Immich to use OAuth with Authentik:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
OAUTH_ENABLED=true
 | 
					 | 
				
			||||||
OAUTH_ISSUER_URL=http://192.168.0.187:9000/application/o/immich
 | 
					 | 
				
			||||||
OAUTH_CLIENT_ID=f08f9c5b4f77dcfd3916b1c032336b5544a7b368
 | 
					 | 
				
			||||||
OAUTH_CLIENT_SECRET=6fe2e697644da6ff6aef73387a457d819018189086fa54b151a6067fbb884e75f7e5c90be16d3c688cf902c6974817a85eab93007d76675041eaead8c39cf5a2
 | 
					 | 
				
			||||||
OAUTH_BUTTON_TEXT=Login with Authentik
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[oidc]: https://openid.net/connect/
 | 
					[oidc]: https://openid.net/connect/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							@@ -61,9 +61,9 @@ doc/ServerVersionReponseDto.md
 | 
				
			|||||||
doc/SignUpDto.md
 | 
					doc/SignUpDto.md
 | 
				
			||||||
doc/SmartInfoResponseDto.md
 | 
					doc/SmartInfoResponseDto.md
 | 
				
			||||||
doc/SystemConfigApi.md
 | 
					doc/SystemConfigApi.md
 | 
				
			||||||
doc/SystemConfigKey.md
 | 
					doc/SystemConfigDto.md
 | 
				
			||||||
doc/SystemConfigResponseDto.md
 | 
					doc/SystemConfigFFmpegDto.md
 | 
				
			||||||
doc/SystemConfigResponseItem.md
 | 
					doc/SystemConfigOAuthDto.md
 | 
				
			||||||
doc/TagApi.md
 | 
					doc/TagApi.md
 | 
				
			||||||
doc/TagResponseDto.md
 | 
					doc/TagResponseDto.md
 | 
				
			||||||
doc/TagTypeEnum.md
 | 
					doc/TagTypeEnum.md
 | 
				
			||||||
@@ -149,9 +149,9 @@ lib/model/server_stats_response_dto.dart
 | 
				
			|||||||
lib/model/server_version_reponse_dto.dart
 | 
					lib/model/server_version_reponse_dto.dart
 | 
				
			||||||
lib/model/sign_up_dto.dart
 | 
					lib/model/sign_up_dto.dart
 | 
				
			||||||
lib/model/smart_info_response_dto.dart
 | 
					lib/model/smart_info_response_dto.dart
 | 
				
			||||||
lib/model/system_config_key.dart
 | 
					lib/model/system_config_dto.dart
 | 
				
			||||||
lib/model/system_config_response_dto.dart
 | 
					lib/model/system_config_f_fmpeg_dto.dart
 | 
				
			||||||
lib/model/system_config_response_item.dart
 | 
					lib/model/system_config_o_auth_dto.dart
 | 
				
			||||||
lib/model/tag_response_dto.dart
 | 
					lib/model/tag_response_dto.dart
 | 
				
			||||||
lib/model/tag_type_enum.dart
 | 
					lib/model/tag_type_enum.dart
 | 
				
			||||||
lib/model/thumbnail_format.dart
 | 
					lib/model/thumbnail_format.dart
 | 
				
			||||||
@@ -224,9 +224,9 @@ test/server_version_reponse_dto_test.dart
 | 
				
			|||||||
test/sign_up_dto_test.dart
 | 
					test/sign_up_dto_test.dart
 | 
				
			||||||
test/smart_info_response_dto_test.dart
 | 
					test/smart_info_response_dto_test.dart
 | 
				
			||||||
test/system_config_api_test.dart
 | 
					test/system_config_api_test.dart
 | 
				
			||||||
test/system_config_key_test.dart
 | 
					test/system_config_dto_test.dart
 | 
				
			||||||
test/system_config_response_dto_test.dart
 | 
					test/system_config_f_fmpeg_dto_test.dart
 | 
				
			||||||
test/system_config_response_item_test.dart
 | 
					test/system_config_o_auth_dto_test.dart
 | 
				
			||||||
test/tag_api_test.dart
 | 
					test/tag_api_test.dart
 | 
				
			||||||
test/tag_response_dto_test.dart
 | 
					test/tag_response_dto_test.dart
 | 
				
			||||||
test/tag_type_enum_test.dart
 | 
					test/tag_type_enum_test.dart
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@@ -112,6 +112,7 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
					*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
				
			||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
					*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | 
 | 
				
			||||||
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | 
 | 
					*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | 
 | 
				
			||||||
 | 
					*SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | 
 | 
				
			||||||
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | 
 | 
					*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | 
 | 
				
			||||||
*TagApi* | [**create**](doc//TagApi.md#create) | **POST** /tag | 
 | 
					*TagApi* | [**create**](doc//TagApi.md#create) | **POST** /tag | 
 | 
				
			||||||
*TagApi* | [**delete**](doc//TagApi.md#delete) | **DELETE** /tag/{id} | 
 | 
					*TagApi* | [**delete**](doc//TagApi.md#delete) | **DELETE** /tag/{id} | 
 | 
				
			||||||
@@ -182,9 +183,9 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
 | 
					 - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
 | 
				
			||||||
 - [SignUpDto](doc//SignUpDto.md)
 | 
					 - [SignUpDto](doc//SignUpDto.md)
 | 
				
			||||||
 - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
 | 
					 - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
 | 
				
			||||||
 - [SystemConfigKey](doc//SystemConfigKey.md)
 | 
					 - [SystemConfigDto](doc//SystemConfigDto.md)
 | 
				
			||||||
 - [SystemConfigResponseDto](doc//SystemConfigResponseDto.md)
 | 
					 - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
 | 
				
			||||||
 - [SystemConfigResponseItem](doc//SystemConfigResponseItem.md)
 | 
					 - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
 | 
				
			||||||
 - [TagResponseDto](doc//TagResponseDto.md)
 | 
					 - [TagResponseDto](doc//TagResponseDto.md)
 | 
				
			||||||
 - [TagTypeEnum](doc//TagTypeEnum.md)
 | 
					 - [TagTypeEnum](doc//TagTypeEnum.md)
 | 
				
			||||||
 - [ThumbnailFormat](doc//ThumbnailFormat.md)
 | 
					 - [ThumbnailFormat](doc//ThumbnailFormat.md)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								mobile/openapi/doc/SystemConfigApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										60
									
								
								mobile/openapi/doc/SystemConfigApi.md
									
									
									
										generated
									
									
									
								
							@@ -10,11 +10,12 @@ All URIs are relative to */api*
 | 
				
			|||||||
Method | HTTP request | Description
 | 
					Method | HTTP request | Description
 | 
				
			||||||
------------- | ------------- | -------------
 | 
					------------- | ------------- | -------------
 | 
				
			||||||
[**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config | 
 | 
					[**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config | 
 | 
				
			||||||
 | 
					[**getDefaults**](SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | 
 | 
				
			||||||
[**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config | 
 | 
					[**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **getConfig**
 | 
					# **getConfig**
 | 
				
			||||||
> SystemConfigResponseDto getConfig()
 | 
					> SystemConfigDto getConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,7 +44,7 @@ This endpoint does not need any parameter.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Return type
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
 | 
					[**SystemConfigDto**](SystemConfigDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Authorization
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,8 +57,8 @@ This endpoint does not need any parameter.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **updateConfig**
 | 
					# **getDefaults**
 | 
				
			||||||
> SystemConfigResponseDto updateConfig(body)
 | 
					> SystemConfigDto getDefaults()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,10 +73,53 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final api_instance = SystemConfigApi();
 | 
					final api_instance = SystemConfigApi();
 | 
				
			||||||
final body = Object(); // Object | 
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
    final result = api_instance.updateConfig(body);
 | 
					    final result = api_instance.getDefaults();
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling SystemConfigApi->getDefaults: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					This endpoint does not need any parameter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**SystemConfigDto**](SystemConfigDto.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**
 | 
				
			||||||
 | 
					> SystemConfigDto updateConfig(systemConfigDto)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 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 systemConfigDto = SystemConfigDto(); // SystemConfigDto | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.updateConfig(systemConfigDto);
 | 
				
			||||||
    print(result);
 | 
					    print(result);
 | 
				
			||||||
} catch (e) {
 | 
					} catch (e) {
 | 
				
			||||||
    print('Exception when calling SystemConfigApi->updateConfig: $e\n');
 | 
					    print('Exception when calling SystemConfigApi->updateConfig: $e\n');
 | 
				
			||||||
@@ -86,11 +130,11 @@ try {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Name | Type | Description  | Notes
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
------------- | ------------- | ------------- | -------------
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 **body** | **Object**|  | 
 | 
					 **systemConfigDto** | [**SystemConfigDto**](SystemConfigDto.md)|  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Return type
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[**SystemConfigResponseDto**](SystemConfigResponseDto.md)
 | 
					[**SystemConfigDto**](SystemConfigDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Authorization
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# openapi.model.SystemConfigKey
 | 
					# openapi.model.SystemConfigDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Load the model package
 | 
					## Load the model package
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
@@ -8,6 +8,8 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  | 
 | 
				
			||||||
 | 
					**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# openapi.model.SystemConfigResponseDto
 | 
					# openapi.model.SystemConfigFFmpegDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Load the model package
 | 
					## Load the model package
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
@@ -8,7 +8,11 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**config** | [**List<SystemConfigResponseItem>**](SystemConfigResponseItem.md) |  | [default to const []]
 | 
					**crf** | **String** |  | 
 | 
				
			||||||
 | 
					**preset** | **String** |  | 
 | 
				
			||||||
 | 
					**targetVideoCodec** | **String** |  | 
 | 
				
			||||||
 | 
					**targetAudioCodec** | **String** |  | 
 | 
				
			||||||
 | 
					**targetScaling** | **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)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# openapi.model.SystemConfigResponseItem
 | 
					# openapi.model.SystemConfigOAuthDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Load the model package
 | 
					## Load the model package
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
@@ -8,10 +8,13 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**name** | **String** |  | 
 | 
					**enabled** | **bool** |  | 
 | 
				
			||||||
**key** | [**SystemConfigKey**](SystemConfigKey.md) |  | 
 | 
					**issuerUrl** | **String** |  | 
 | 
				
			||||||
**value** | **String** |  | 
 | 
					**clientId** | **String** |  | 
 | 
				
			||||||
**defaultValue** | **String** |  | 
 | 
					**clientSecret** | **String** |  | 
 | 
				
			||||||
 | 
					**scope** | **String** |  | 
 | 
				
			||||||
 | 
					**buttonText** | **String** |  | 
 | 
				
			||||||
 | 
					**autoRegister** | **bool** |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							@@ -88,9 +88,9 @@ part 'model/server_stats_response_dto.dart';
 | 
				
			|||||||
part 'model/server_version_reponse_dto.dart';
 | 
					part 'model/server_version_reponse_dto.dart';
 | 
				
			||||||
part 'model/sign_up_dto.dart';
 | 
					part 'model/sign_up_dto.dart';
 | 
				
			||||||
part 'model/smart_info_response_dto.dart';
 | 
					part 'model/smart_info_response_dto.dart';
 | 
				
			||||||
part 'model/system_config_key.dart';
 | 
					part 'model/system_config_dto.dart';
 | 
				
			||||||
part 'model/system_config_response_dto.dart';
 | 
					part 'model/system_config_f_fmpeg_dto.dart';
 | 
				
			||||||
part 'model/system_config_response_item.dart';
 | 
					part 'model/system_config_o_auth_dto.dart';
 | 
				
			||||||
part 'model/tag_response_dto.dart';
 | 
					part 'model/tag_response_dto.dart';
 | 
				
			||||||
part 'model/tag_type_enum.dart';
 | 
					part 'model/tag_type_enum.dart';
 | 
				
			||||||
part 'model/thumbnail_format.dart';
 | 
					part 'model/thumbnail_format.dart';
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										59
									
								
								mobile/openapi/lib/api/system_config_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								mobile/openapi/lib/api/system_config_api.dart
									
									
									
										generated
									
									
									
								
							@@ -42,7 +42,7 @@ class SystemConfigApi {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<SystemConfigResponseDto?> getConfig() async {
 | 
					  Future<SystemConfigDto?> getConfig() async {
 | 
				
			||||||
    final response = await getConfigWithHttpInfo();
 | 
					    final response = await getConfigWithHttpInfo();
 | 
				
			||||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
					    if (response.statusCode >= HttpStatus.badRequest) {
 | 
				
			||||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
					      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
				
			||||||
@@ -51,7 +51,48 @@ class SystemConfigApi {
 | 
				
			|||||||
    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
					    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
				
			||||||
    // FormatException when trying to decode an empty string.
 | 
					    // FormatException when trying to decode an empty string.
 | 
				
			||||||
    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
					    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
				
			||||||
      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
 | 
					      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigDto',) as SystemConfigDto;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs an HTTP 'GET /system-config/defaults' operation and returns the [Response].
 | 
				
			||||||
 | 
					  Future<Response> getDefaultsWithHttpInfo() async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/system-config/defaults';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 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<SystemConfigDto?> getDefaults() async {
 | 
				
			||||||
 | 
					    final response = await getDefaultsWithHttpInfo();
 | 
				
			||||||
 | 
					    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), 'SystemConfigDto',) as SystemConfigDto;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
@@ -60,13 +101,13 @@ class SystemConfigApi {
 | 
				
			|||||||
  /// Performs an HTTP 'PUT /system-config' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'PUT /system-config' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [Object] body (required):
 | 
					  /// * [SystemConfigDto] systemConfigDto (required):
 | 
				
			||||||
  Future<Response> updateConfigWithHttpInfo(Object body,) async {
 | 
					  Future<Response> updateConfigWithHttpInfo(SystemConfigDto systemConfigDto,) async {
 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
    final path = r'/system-config';
 | 
					    final path = r'/system-config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // ignore: prefer_final_locals
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
    Object? postBody = body;
 | 
					    Object? postBody = systemConfigDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final queryParams = <QueryParam>[];
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
    final headerParams = <String, String>{};
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
@@ -88,9 +129,9 @@ class SystemConfigApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [Object] body (required):
 | 
					  /// * [SystemConfigDto] systemConfigDto (required):
 | 
				
			||||||
  Future<SystemConfigResponseDto?> updateConfig(Object body,) async {
 | 
					  Future<SystemConfigDto?> updateConfig(SystemConfigDto systemConfigDto,) async {
 | 
				
			||||||
    final response = await updateConfigWithHttpInfo(body,);
 | 
					    final response = await updateConfigWithHttpInfo(systemConfigDto,);
 | 
				
			||||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
					    if (response.statusCode >= HttpStatus.badRequest) {
 | 
				
			||||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
					      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -98,7 +139,7 @@ class SystemConfigApi {
 | 
				
			|||||||
    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
					    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
				
			||||||
    // FormatException when trying to decode an empty string.
 | 
					    // FormatException when trying to decode an empty string.
 | 
				
			||||||
    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
					    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
				
			||||||
      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigResponseDto',) as SystemConfigResponseDto;
 | 
					      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigDto',) as SystemConfigDto;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							@@ -292,12 +292,12 @@ class ApiClient {
 | 
				
			|||||||
          return SignUpDto.fromJson(value);
 | 
					          return SignUpDto.fromJson(value);
 | 
				
			||||||
        case 'SmartInfoResponseDto':
 | 
					        case 'SmartInfoResponseDto':
 | 
				
			||||||
          return SmartInfoResponseDto.fromJson(value);
 | 
					          return SmartInfoResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigKey':
 | 
					        case 'SystemConfigDto':
 | 
				
			||||||
          return SystemConfigKeyTypeTransformer().decode(value);
 | 
					          return SystemConfigDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigResponseDto':
 | 
					        case 'SystemConfigFFmpegDto':
 | 
				
			||||||
          return SystemConfigResponseDto.fromJson(value);
 | 
					          return SystemConfigFFmpegDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigResponseItem':
 | 
					        case 'SystemConfigOAuthDto':
 | 
				
			||||||
          return SystemConfigResponseItem.fromJson(value);
 | 
					          return SystemConfigOAuthDto.fromJson(value);
 | 
				
			||||||
        case 'TagResponseDto':
 | 
					        case 'TagResponseDto':
 | 
				
			||||||
          return TagResponseDto.fromJson(value);
 | 
					          return TagResponseDto.fromJson(value);
 | 
				
			||||||
        case 'TagTypeEnum':
 | 
					        case 'TagTypeEnum':
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							@@ -70,9 +70,6 @@ String parameterToString(dynamic value) {
 | 
				
			|||||||
  if (value is JobId) {
 | 
					  if (value is JobId) {
 | 
				
			||||||
    return JobIdTypeTransformer().encode(value).toString();
 | 
					    return JobIdTypeTransformer().encode(value).toString();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (value is SystemConfigKey) {
 | 
					 | 
				
			||||||
    return SystemConfigKeyTypeTransformer().encode(value).toString();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (value is TagTypeEnum) {
 | 
					  if (value is TagTypeEnum) {
 | 
				
			||||||
    return TagTypeEnumTypeTransformer().encode(value).toString();
 | 
					    return TagTypeEnumTypeTransformer().encode(value).toString();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,36 +10,42 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
part of openapi.api;
 | 
					part of openapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SystemConfigResponseDto {
 | 
					class SystemConfigDto {
 | 
				
			||||||
  /// Returns a new [SystemConfigResponseDto] instance.
 | 
					  /// Returns a new [SystemConfigDto] instance.
 | 
				
			||||||
  SystemConfigResponseDto({
 | 
					  SystemConfigDto({
 | 
				
			||||||
    this.config = const [],
 | 
					    required this.ffmpeg,
 | 
				
			||||||
 | 
					    required this.oauth,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<SystemConfigResponseItem> config;
 | 
					  SystemConfigFFmpegDto ffmpeg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SystemConfigOAuthDto oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is SystemConfigResponseDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
 | 
				
			||||||
     other.config == config;
 | 
					     other.ffmpeg == ffmpeg &&
 | 
				
			||||||
 | 
					     other.oauth == oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (config.hashCode);
 | 
					    (ffmpeg.hashCode) +
 | 
				
			||||||
 | 
					    (oauth.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'SystemConfigResponseDto[config=$config]';
 | 
					  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, oauth=$oauth]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final _json = <String, dynamic>{};
 | 
					    final _json = <String, dynamic>{};
 | 
				
			||||||
      _json[r'config'] = config;
 | 
					      _json[r'ffmpeg'] = ffmpeg;
 | 
				
			||||||
 | 
					      _json[r'oauth'] = oauth;
 | 
				
			||||||
    return _json;
 | 
					    return _json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Returns a new [SystemConfigResponseDto] instance and imports its values from
 | 
					  /// Returns a new [SystemConfigDto] instance and imports its values from
 | 
				
			||||||
  /// [value] if it's a [Map], null otherwise.
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
  // ignore: prefer_constructors_over_static_methods
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
  static SystemConfigResponseDto? fromJson(dynamic value) {
 | 
					  static SystemConfigDto? fromJson(dynamic value) {
 | 
				
			||||||
    if (value is Map) {
 | 
					    if (value is Map) {
 | 
				
			||||||
      final json = value.cast<String, dynamic>();
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,24 +54,25 @@ class SystemConfigResponseDto {
 | 
				
			|||||||
      // Note 2: this code is stripped in release mode!
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
      assert(() {
 | 
					      assert(() {
 | 
				
			||||||
        requiredKeys.forEach((key) {
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
          assert(json.containsKey(key), 'Required key "SystemConfigResponseDto[$key]" is missing from JSON.');
 | 
					          assert(json.containsKey(key), 'Required key "SystemConfigDto[$key]" is missing from JSON.');
 | 
				
			||||||
          assert(json[key] != null, 'Required key "SystemConfigResponseDto[$key]" has a null value in JSON.');
 | 
					          assert(json[key] != null, 'Required key "SystemConfigDto[$key]" has a null value in JSON.');
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      }());
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return SystemConfigResponseDto(
 | 
					      return SystemConfigDto(
 | 
				
			||||||
        config: SystemConfigResponseItem.listFromJson(json[r'config'])!,
 | 
					        ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
 | 
				
			||||||
 | 
					        oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static List<SystemConfigResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
					  static List<SystemConfigDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
    final result = <SystemConfigResponseDto>[];
 | 
					    final result = <SystemConfigDto>[];
 | 
				
			||||||
    if (json is List && json.isNotEmpty) {
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
      for (final row in json) {
 | 
					      for (final row in json) {
 | 
				
			||||||
        final value = SystemConfigResponseDto.fromJson(row);
 | 
					        final value = SystemConfigDto.fromJson(row);
 | 
				
			||||||
        if (value != null) {
 | 
					        if (value != null) {
 | 
				
			||||||
          result.add(value);
 | 
					          result.add(value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -74,12 +81,12 @@ class SystemConfigResponseDto {
 | 
				
			|||||||
    return result.toList(growable: growable);
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static Map<String, SystemConfigResponseDto> mapFromJson(dynamic json) {
 | 
					  static Map<String, SystemConfigDto> mapFromJson(dynamic json) {
 | 
				
			||||||
    final map = <String, SystemConfigResponseDto>{};
 | 
					    final map = <String, SystemConfigDto>{};
 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
        final value = SystemConfigResponseDto.fromJson(entry.value);
 | 
					        final value = SystemConfigDto.fromJson(entry.value);
 | 
				
			||||||
        if (value != null) {
 | 
					        if (value != null) {
 | 
				
			||||||
          map[entry.key] = value;
 | 
					          map[entry.key] = value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -88,13 +95,13 @@ class SystemConfigResponseDto {
 | 
				
			|||||||
    return map;
 | 
					    return map;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // maps a json object with a list of SystemConfigResponseDto-objects as value to a dart map
 | 
					  // maps a json object with a list of SystemConfigDto-objects as value to a dart map
 | 
				
			||||||
  static Map<String, List<SystemConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
					  static Map<String, List<SystemConfigDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
    final map = <String, List<SystemConfigResponseDto>>{};
 | 
					    final map = <String, List<SystemConfigDto>>{};
 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
        final value = SystemConfigResponseDto.listFromJson(entry.value, growable: growable,);
 | 
					        final value = SystemConfigDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
        if (value != null) {
 | 
					        if (value != null) {
 | 
				
			||||||
          map[entry.key] = value;
 | 
					          map[entry.key] = value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -105,7 +112,8 @@ class SystemConfigResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'config',
 | 
					    'ffmpeg',
 | 
				
			||||||
 | 
					    'oauth',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										143
									
								
								mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 SystemConfigFFmpegDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigFFmpegDto] instance.
 | 
				
			||||||
 | 
					  SystemConfigFFmpegDto({
 | 
				
			||||||
 | 
					    required this.crf,
 | 
				
			||||||
 | 
					    required this.preset,
 | 
				
			||||||
 | 
					    required this.targetVideoCodec,
 | 
				
			||||||
 | 
					    required this.targetAudioCodec,
 | 
				
			||||||
 | 
					    required this.targetScaling,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String crf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String preset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String targetVideoCodec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String targetAudioCodec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String targetScaling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto &&
 | 
				
			||||||
 | 
					     other.crf == crf &&
 | 
				
			||||||
 | 
					     other.preset == preset &&
 | 
				
			||||||
 | 
					     other.targetVideoCodec == targetVideoCodec &&
 | 
				
			||||||
 | 
					     other.targetAudioCodec == targetAudioCodec &&
 | 
				
			||||||
 | 
					     other.targetScaling == targetScaling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (crf.hashCode) +
 | 
				
			||||||
 | 
					    (preset.hashCode) +
 | 
				
			||||||
 | 
					    (targetVideoCodec.hashCode) +
 | 
				
			||||||
 | 
					    (targetAudioCodec.hashCode) +
 | 
				
			||||||
 | 
					    (targetScaling.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SystemConfigFFmpegDto[crf=$crf, preset=$preset, targetVideoCodec=$targetVideoCodec, targetAudioCodec=$targetAudioCodec, targetScaling=$targetScaling]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final _json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      _json[r'crf'] = crf;
 | 
				
			||||||
 | 
					      _json[r'preset'] = preset;
 | 
				
			||||||
 | 
					      _json[r'targetVideoCodec'] = targetVideoCodec;
 | 
				
			||||||
 | 
					      _json[r'targetAudioCodec'] = targetAudioCodec;
 | 
				
			||||||
 | 
					      _json[r'targetScaling'] = targetScaling;
 | 
				
			||||||
 | 
					    return _json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigFFmpegDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SystemConfigFFmpegDto? 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 "SystemConfigFFmpegDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SystemConfigFFmpegDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SystemConfigFFmpegDto(
 | 
				
			||||||
 | 
					        crf: mapValueOfType<String>(json, r'crf')!,
 | 
				
			||||||
 | 
					        preset: mapValueOfType<String>(json, r'preset')!,
 | 
				
			||||||
 | 
					        targetVideoCodec: mapValueOfType<String>(json, r'targetVideoCodec')!,
 | 
				
			||||||
 | 
					        targetAudioCodec: mapValueOfType<String>(json, r'targetAudioCodec')!,
 | 
				
			||||||
 | 
					        targetScaling: mapValueOfType<String>(json, r'targetScaling')!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SystemConfigFFmpegDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SystemConfigFFmpegDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SystemConfigFFmpegDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SystemConfigFFmpegDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SystemConfigFFmpegDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SystemConfigFFmpegDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SystemConfigFFmpegDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SystemConfigFFmpegDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SystemConfigFFmpegDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SystemConfigFFmpegDto.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>{
 | 
				
			||||||
 | 
					    'crf',
 | 
				
			||||||
 | 
					    'preset',
 | 
				
			||||||
 | 
					    'targetVideoCodec',
 | 
				
			||||||
 | 
					    'targetAudioCodec',
 | 
				
			||||||
 | 
					    'targetScaling',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										94
									
								
								mobile/openapi/lib/model/system_config_key.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										94
									
								
								mobile/openapi/lib/model/system_config_key.dart
									
									
									
										generated
									
									
									
								
							@@ -1,94 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// 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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										159
									
								
								mobile/openapi/lib/model/system_config_o_auth_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								mobile/openapi/lib/model/system_config_o_auth_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 SystemConfigOAuthDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigOAuthDto] instance.
 | 
				
			||||||
 | 
					  SystemConfigOAuthDto({
 | 
				
			||||||
 | 
					    required this.enabled,
 | 
				
			||||||
 | 
					    required this.issuerUrl,
 | 
				
			||||||
 | 
					    required this.clientId,
 | 
				
			||||||
 | 
					    required this.clientSecret,
 | 
				
			||||||
 | 
					    required this.scope,
 | 
				
			||||||
 | 
					    required this.buttonText,
 | 
				
			||||||
 | 
					    required this.autoRegister,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String issuerUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String clientId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String clientSecret;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String scope;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String buttonText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool autoRegister;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto &&
 | 
				
			||||||
 | 
					     other.enabled == enabled &&
 | 
				
			||||||
 | 
					     other.issuerUrl == issuerUrl &&
 | 
				
			||||||
 | 
					     other.clientId == clientId &&
 | 
				
			||||||
 | 
					     other.clientSecret == clientSecret &&
 | 
				
			||||||
 | 
					     other.scope == scope &&
 | 
				
			||||||
 | 
					     other.buttonText == buttonText &&
 | 
				
			||||||
 | 
					     other.autoRegister == autoRegister;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (enabled.hashCode) +
 | 
				
			||||||
 | 
					    (issuerUrl.hashCode) +
 | 
				
			||||||
 | 
					    (clientId.hashCode) +
 | 
				
			||||||
 | 
					    (clientSecret.hashCode) +
 | 
				
			||||||
 | 
					    (scope.hashCode) +
 | 
				
			||||||
 | 
					    (buttonText.hashCode) +
 | 
				
			||||||
 | 
					    (autoRegister.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SystemConfigOAuthDto[enabled=$enabled, issuerUrl=$issuerUrl, clientId=$clientId, clientSecret=$clientSecret, scope=$scope, buttonText=$buttonText, autoRegister=$autoRegister]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final _json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      _json[r'enabled'] = enabled;
 | 
				
			||||||
 | 
					      _json[r'issuerUrl'] = issuerUrl;
 | 
				
			||||||
 | 
					      _json[r'clientId'] = clientId;
 | 
				
			||||||
 | 
					      _json[r'clientSecret'] = clientSecret;
 | 
				
			||||||
 | 
					      _json[r'scope'] = scope;
 | 
				
			||||||
 | 
					      _json[r'buttonText'] = buttonText;
 | 
				
			||||||
 | 
					      _json[r'autoRegister'] = autoRegister;
 | 
				
			||||||
 | 
					    return _json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigOAuthDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SystemConfigOAuthDto? 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 "SystemConfigOAuthDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SystemConfigOAuthDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SystemConfigOAuthDto(
 | 
				
			||||||
 | 
					        enabled: mapValueOfType<bool>(json, r'enabled')!,
 | 
				
			||||||
 | 
					        issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
 | 
				
			||||||
 | 
					        clientId: mapValueOfType<String>(json, r'clientId')!,
 | 
				
			||||||
 | 
					        clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
 | 
				
			||||||
 | 
					        scope: mapValueOfType<String>(json, r'scope')!,
 | 
				
			||||||
 | 
					        buttonText: mapValueOfType<String>(json, r'buttonText')!,
 | 
				
			||||||
 | 
					        autoRegister: mapValueOfType<bool>(json, r'autoRegister')!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SystemConfigOAuthDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SystemConfigOAuthDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SystemConfigOAuthDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SystemConfigOAuthDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SystemConfigOAuthDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SystemConfigOAuthDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SystemConfigOAuthDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SystemConfigOAuthDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SystemConfigOAuthDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SystemConfigOAuthDto.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>{
 | 
				
			||||||
 | 
					    'enabled',
 | 
				
			||||||
 | 
					    'issuerUrl',
 | 
				
			||||||
 | 
					    'clientId',
 | 
				
			||||||
 | 
					    'clientSecret',
 | 
				
			||||||
 | 
					    'scope',
 | 
				
			||||||
 | 
					    'buttonText',
 | 
				
			||||||
 | 
					    'autoRegister',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,135 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// 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',
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										9
									
								
								mobile/openapi/test/system_config_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								mobile/openapi/test/system_config_api_test.dart
									
									
									
										generated
									
									
									
								
							@@ -17,12 +17,17 @@ void main() {
 | 
				
			|||||||
  // final instance = SystemConfigApi();
 | 
					  // final instance = SystemConfigApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('tests for SystemConfigApi', () {
 | 
					  group('tests for SystemConfigApi', () {
 | 
				
			||||||
    //Future<SystemConfigResponseDto> getConfig() async
 | 
					    //Future<SystemConfigDto> getConfig() async
 | 
				
			||||||
    test('test getConfig', () async {
 | 
					    test('test getConfig', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<SystemConfigResponseDto> updateConfig(Object body) async
 | 
					    //Future<SystemConfigDto> getDefaults() async
 | 
				
			||||||
 | 
					    test('test getDefaults', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //Future<SystemConfigDto> updateConfig(SystemConfigDto systemConfigDto) async
 | 
				
			||||||
    test('test updateConfig', () async {
 | 
					    test('test updateConfig', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,13 +11,18 @@
 | 
				
			|||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
import 'package:test/test.dart';
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tests for SystemConfigResponseDto
 | 
					// tests for SystemConfigDto
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  // final instance = SystemConfigResponseDto();
 | 
					  // final instance = SystemConfigDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('test SystemConfigResponseDto', () {
 | 
					  group('test SystemConfigDto', () {
 | 
				
			||||||
    // List<SystemConfigResponseItem> config (default value: const [])
 | 
					    // SystemConfigFFmpegDto ffmpeg
 | 
				
			||||||
    test('to test the property `config`', () async {
 | 
					    test('to test the property `ffmpeg`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // SystemConfigOAuthDto oauth
 | 
				
			||||||
 | 
					    test('to test the property `oauth`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 SystemConfigFFmpegDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SystemConfigFFmpegDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SystemConfigFFmpegDto', () {
 | 
				
			||||||
 | 
					    // String crf
 | 
				
			||||||
 | 
					    test('to test the property `crf`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String preset
 | 
				
			||||||
 | 
					    test('to test the property `preset`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String targetVideoCodec
 | 
				
			||||||
 | 
					    test('to test the property `targetVideoCodec`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String targetAudioCodec
 | 
				
			||||||
 | 
					    test('to test the property `targetAudioCodec`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String targetScaling
 | 
				
			||||||
 | 
					    test('to test the property `targetScaling`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								mobile/openapi/test/system_config_key_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/test/system_config_key_test.dart
									
									
									
										generated
									
									
									
								
							@@ -1,21 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// 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', () {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										57
									
								
								mobile/openapi/test/system_config_o_auth_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mobile/openapi/test/system_config_o_auth_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 SystemConfigOAuthDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SystemConfigOAuthDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SystemConfigOAuthDto', () {
 | 
				
			||||||
 | 
					    // bool enabled
 | 
				
			||||||
 | 
					    test('to test the property `enabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String issuerUrl
 | 
				
			||||||
 | 
					    test('to test the property `issuerUrl`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String clientId
 | 
				
			||||||
 | 
					    test('to test the property `clientId`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String clientSecret
 | 
				
			||||||
 | 
					    test('to test the property `clientSecret`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String scope
 | 
				
			||||||
 | 
					    test('to test the property `scope`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String buttonText
 | 
				
			||||||
 | 
					    test('to test the property `buttonText`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool autoRegister
 | 
				
			||||||
 | 
					    test('to test the property `autoRegister`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,42 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// 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
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // SystemConfigKey key
 | 
					 | 
				
			||||||
    test('to test the property `key`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // String value
 | 
					 | 
				
			||||||
    test('to test the property `value`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // String defaultValue
 | 
					 | 
				
			||||||
    test('to test the property `defaultValue`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { ImmichConfigModule } from '@app/immich-config';
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
 | 
					import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
 | 
				
			||||||
import { UserModule } from '../user/user.module';
 | 
					import { UserModule } from '../user/user.module';
 | 
				
			||||||
@@ -5,7 +6,7 @@ import { OAuthController } from './oauth.controller';
 | 
				
			|||||||
import { OAuthService } from './oauth.service';
 | 
					import { OAuthService } from './oauth.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [UserModule, ImmichJwtModule],
 | 
					  imports: [UserModule, ImmichJwtModule, ImmichConfigModule],
 | 
				
			||||||
  controllers: [OAuthController],
 | 
					  controllers: [OAuthController],
 | 
				
			||||||
  providers: [OAuthService],
 | 
					  providers: [OAuthService],
 | 
				
			||||||
  exports: [OAuthService],
 | 
					  exports: [OAuthService],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,13 @@
 | 
				
			|||||||
 | 
					import { SystemConfig } from '@app/database/entities/system-config.entity';
 | 
				
			||||||
import { UserEntity } from '@app/database/entities/user.entity';
 | 
					import { UserEntity } from '@app/database/entities/user.entity';
 | 
				
			||||||
 | 
					import { ImmichConfigService } from '@app/immich-config';
 | 
				
			||||||
import { BadRequestException } from '@nestjs/common';
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					 | 
				
			||||||
import { generators, Issuer } from 'openid-client';
 | 
					import { generators, Issuer } from 'openid-client';
 | 
				
			||||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 | 
					import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 | 
				
			||||||
import { LoginResponseDto } from '../auth/response-dto/login-response.dto';
 | 
					import { LoginResponseDto } from '../auth/response-dto/login-response.dto';
 | 
				
			||||||
import { OAuthService } from '../oauth/oauth.service';
 | 
					import { OAuthService } from '../oauth/oauth.service';
 | 
				
			||||||
import { IUserRepository } from '../user/user-repository';
 | 
					import { IUserRepository } from '../user/user-repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface OAuthConfig {
 | 
					 | 
				
			||||||
  OAUTH_ENABLED: boolean;
 | 
					 | 
				
			||||||
  OAUTH_AUTO_REGISTER: boolean;
 | 
					 | 
				
			||||||
  OAUTH_ISSUER_URL: string;
 | 
					 | 
				
			||||||
  OAUTH_SCOPE: string;
 | 
					 | 
				
			||||||
  OAUTH_BUTTON_TEXT: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mockConfig = (config: Partial<OAuthConfig>) => {
 | 
					 | 
				
			||||||
  return (value: keyof OAuthConfig, defaultValue: any) => config[value] ?? defaultValue ?? null;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const email = 'user@immich.com';
 | 
					const email = 'user@immich.com';
 | 
				
			||||||
const sub = 'my-auth-user-sub';
 | 
					const sub = 'my-auth-user-sub';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,7 +28,7 @@ const loginResponse = {
 | 
				
			|||||||
describe('OAuthService', () => {
 | 
					describe('OAuthService', () => {
 | 
				
			||||||
  let sut: OAuthService;
 | 
					  let sut: OAuthService;
 | 
				
			||||||
  let userRepositoryMock: jest.Mocked<IUserRepository>;
 | 
					  let userRepositoryMock: jest.Mocked<IUserRepository>;
 | 
				
			||||||
  let configServiceMock: jest.Mocked<ConfigService>;
 | 
					  let immichConfigServiceMock: jest.Mocked<ImmichConfigService>;
 | 
				
			||||||
  let immichJwtServiceMock: jest.Mocked<ImmichJwtService>;
 | 
					  let immichJwtServiceMock: jest.Mocked<ImmichJwtService>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
@@ -80,11 +69,11 @@ describe('OAuthService', () => {
 | 
				
			|||||||
      extractJwtFromCookie: jest.fn(),
 | 
					      extractJwtFromCookie: jest.fn(),
 | 
				
			||||||
    } as unknown as jest.Mocked<ImmichJwtService>;
 | 
					    } as unknown as jest.Mocked<ImmichJwtService>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    configServiceMock = {
 | 
					    immichConfigServiceMock = {
 | 
				
			||||||
      get: jest.fn(),
 | 
					      getConfig: jest.fn().mockResolvedValue({ oauth: { enabled: false } }),
 | 
				
			||||||
    } as unknown as jest.Mocked<ConfigService>;
 | 
					    } as unknown as jest.Mocked<ImmichConfigService>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					    sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should be defined', () => {
 | 
					  it('should be defined', () => {
 | 
				
			||||||
@@ -94,17 +83,17 @@ describe('OAuthService', () => {
 | 
				
			|||||||
  describe('generateConfig', () => {
 | 
					  describe('generateConfig', () => {
 | 
				
			||||||
    it('should work when oauth is not configured', async () => {
 | 
					    it('should work when oauth is not configured', async () => {
 | 
				
			||||||
      await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({ enabled: false });
 | 
					      await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({ enabled: false });
 | 
				
			||||||
      expect(configServiceMock.get).toHaveBeenCalled();
 | 
					      expect(immichConfigServiceMock.getConfig).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should generate the config', async () => {
 | 
					    it('should generate the config', async () => {
 | 
				
			||||||
      configServiceMock.get.mockImplementation(
 | 
					      immichConfigServiceMock.getConfig.mockResolvedValue({
 | 
				
			||||||
        mockConfig({
 | 
					        oauth: {
 | 
				
			||||||
          OAUTH_ENABLED: true,
 | 
					          enabled: true,
 | 
				
			||||||
          OAUTH_BUTTON_TEXT: 'OAuth',
 | 
					          buttonText: 'OAuth',
 | 
				
			||||||
        }),
 | 
					        },
 | 
				
			||||||
      );
 | 
					      } as SystemConfig);
 | 
				
			||||||
      sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					      sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
      await expect(sut.generateConfig({ redirectUri: 'http://redirect' })).resolves.toEqual({
 | 
					      await expect(sut.generateConfig({ redirectUri: 'http://redirect' })).resolves.toEqual({
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        buttonText: 'OAuth',
 | 
					        buttonText: 'OAuth',
 | 
				
			||||||
@@ -119,13 +108,13 @@ describe('OAuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not allow auto registering', async () => {
 | 
					    it('should not allow auto registering', async () => {
 | 
				
			||||||
      configServiceMock.get.mockImplementation(
 | 
					      immichConfigServiceMock.getConfig.mockResolvedValue({
 | 
				
			||||||
        mockConfig({
 | 
					        oauth: {
 | 
				
			||||||
          OAUTH_ENABLED: true,
 | 
					          enabled: true,
 | 
				
			||||||
          OAUTH_AUTO_REGISTER: false,
 | 
					          autoRegister: false,
 | 
				
			||||||
        }),
 | 
					        },
 | 
				
			||||||
      );
 | 
					      } as SystemConfig);
 | 
				
			||||||
      sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					      sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null);
 | 
				
			||||||
      userRepositoryMock.getByEmail.mockResolvedValue(null);
 | 
					      userRepositoryMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
@@ -136,13 +125,13 @@ describe('OAuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should link an existing user', async () => {
 | 
					    it('should link an existing user', async () => {
 | 
				
			||||||
      configServiceMock.get.mockImplementation(
 | 
					      immichConfigServiceMock.getConfig.mockResolvedValue({
 | 
				
			||||||
        mockConfig({
 | 
					        oauth: {
 | 
				
			||||||
          OAUTH_ENABLED: true,
 | 
					          enabled: true,
 | 
				
			||||||
          OAUTH_AUTO_REGISTER: false,
 | 
					          autoRegister: false,
 | 
				
			||||||
        }),
 | 
					        },
 | 
				
			||||||
      );
 | 
					      } as SystemConfig);
 | 
				
			||||||
      sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					      sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null);
 | 
				
			||||||
      userRepositoryMock.getByEmail.mockResolvedValue(user);
 | 
					      userRepositoryMock.getByEmail.mockResolvedValue(user);
 | 
				
			||||||
@@ -156,8 +145,13 @@ describe('OAuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should allow auto registering by default', async () => {
 | 
					    it('should allow auto registering by default', async () => {
 | 
				
			||||||
      configServiceMock.get.mockImplementation(mockConfig({ OAUTH_ENABLED: true }));
 | 
					      immichConfigServiceMock.getConfig.mockResolvedValue({
 | 
				
			||||||
      sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					        oauth: {
 | 
				
			||||||
 | 
					          enabled: true,
 | 
				
			||||||
 | 
					          autoRegister: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      } as SystemConfig);
 | 
				
			||||||
 | 
					      sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null);
 | 
				
			||||||
      jest.spyOn(sut['logger'], 'log').mockImplementation(() => null);
 | 
					      jest.spyOn(sut['logger'], 'log').mockImplementation(() => null);
 | 
				
			||||||
      userRepositoryMock.getByEmail.mockResolvedValue(null);
 | 
					      userRepositoryMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
@@ -178,13 +172,13 @@ describe('OAuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should get the session endpoint from the discovery document', async () => {
 | 
					    it('should get the session endpoint from the discovery document', async () => {
 | 
				
			||||||
      configServiceMock.get.mockImplementation(
 | 
					      immichConfigServiceMock.getConfig.mockResolvedValue({
 | 
				
			||||||
        mockConfig({
 | 
					        oauth: {
 | 
				
			||||||
          OAUTH_ENABLED: true,
 | 
					          enabled: true,
 | 
				
			||||||
          OAUTH_ISSUER_URL: 'http://issuer',
 | 
					          issuerUrl: 'http://issuer,',
 | 
				
			||||||
        }),
 | 
					        },
 | 
				
			||||||
      );
 | 
					      } as SystemConfig);
 | 
				
			||||||
      sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock);
 | 
					      sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.getLogoutEndpoint()).resolves.toBe('http://end-session-endpoint');
 | 
					      await expect(sut.getLogoutEndpoint()).resolves.toBe('http://end-session-endpoint');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
 | 
					import { ImmichConfigService } from '@app/immich-config';
 | 
				
			||||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
					import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					 | 
				
			||||||
import { ClientMetadata, generators, Issuer, UserinfoResponse } from 'openid-client';
 | 
					import { ClientMetadata, generators, Issuer, UserinfoResponse } from 'openid-client';
 | 
				
			||||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 | 
					import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 | 
				
			||||||
import { LoginResponseDto } from '../auth/response-dto/login-response.dto';
 | 
					import { LoginResponseDto } from '../auth/response-dto/login-response.dto';
 | 
				
			||||||
@@ -16,43 +16,26 @@ type OAuthProfile = UserinfoResponse & {
 | 
				
			|||||||
export class OAuthService {
 | 
					export class OAuthService {
 | 
				
			||||||
  private readonly logger = new Logger(OAuthService.name);
 | 
					  private readonly logger = new Logger(OAuthService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private readonly enabled: boolean;
 | 
					 | 
				
			||||||
  private readonly autoRegister: boolean;
 | 
					 | 
				
			||||||
  private readonly buttonText: string;
 | 
					 | 
				
			||||||
  private readonly issuerUrl: string;
 | 
					 | 
				
			||||||
  private readonly clientMetadata: ClientMetadata;
 | 
					 | 
				
			||||||
  private readonly scope: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private immichJwtService: ImmichJwtService,
 | 
					    private immichJwtService: ImmichJwtService,
 | 
				
			||||||
    configService: ConfigService,
 | 
					    private immichConfigService: ImmichConfigService,
 | 
				
			||||||
    @Inject(USER_REPOSITORY) private userRepository: IUserRepository,
 | 
					    @Inject(USER_REPOSITORY) private userRepository: IUserRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {}
 | 
				
			||||||
    this.enabled = configService.get('OAUTH_ENABLED', false);
 | 
					 | 
				
			||||||
    this.autoRegister = configService.get('OAUTH_AUTO_REGISTER', true);
 | 
					 | 
				
			||||||
    this.issuerUrl = configService.get<string>('OAUTH_ISSUER_URL', '');
 | 
					 | 
				
			||||||
    this.scope = configService.get<string>('OAUTH_SCOPE', '');
 | 
					 | 
				
			||||||
    this.buttonText = configService.get<string>('OAUTH_BUTTON_TEXT', '');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.clientMetadata = {
 | 
					 | 
				
			||||||
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
      client_id: configService.get('OAUTH_CLIENT_ID')!,
 | 
					 | 
				
			||||||
      client_secret: configService.get('OAUTH_CLIENT_SECRET'),
 | 
					 | 
				
			||||||
      response_types: ['code'],
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
 | 
					  public async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
 | 
				
			||||||
    if (!this.enabled) {
 | 
					    const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
 | 
					    const { enabled, scope, buttonText } = config.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!enabled) {
 | 
				
			||||||
      return { enabled: false };
 | 
					      return { enabled: false };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const url = (await this.getClient()).authorizationUrl({
 | 
					    const url = (await this.getClient()).authorizationUrl({
 | 
				
			||||||
      redirect_uri: dto.redirectUri,
 | 
					      redirect_uri: dto.redirectUri,
 | 
				
			||||||
      scope: this.scope,
 | 
					      scope,
 | 
				
			||||||
      state: generators.state(),
 | 
					      state: generators.state(),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return { enabled: true, buttonText: this.buttonText, url };
 | 
					    return { enabled: true, buttonText, url };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async callback(dto: OAuthCallbackDto): Promise<LoginResponseDto> {
 | 
					  public async callback(dto: OAuthCallbackDto): Promise<LoginResponseDto> {
 | 
				
			||||||
@@ -75,9 +58,11 @@ export class OAuthService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // register new user
 | 
					    // register new user
 | 
				
			||||||
    if (!user) {
 | 
					    if (!user) {
 | 
				
			||||||
      if (!this.autoRegister) {
 | 
					      const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
 | 
					      const { autoRegister } = config.oauth;
 | 
				
			||||||
 | 
					      if (!autoRegister) {
 | 
				
			||||||
        this.logger.warn(
 | 
					        this.logger.warn(
 | 
				
			||||||
          `Unable to register ${profile.email}. To enable auto registering, set OAUTH_AUTO_REGISTER=true.`,
 | 
					          `Unable to register ${profile.email}. To enable set OAuth Auto Register to true in admin settings.`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        throw new BadRequestException(`User does not exist and auto registering is disabled.`);
 | 
					        throw new BadRequestException(`User does not exist and auto registering is disabled.`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -95,20 +80,31 @@ export class OAuthService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async getLogoutEndpoint(): Promise<string | null> {
 | 
					  public async getLogoutEndpoint(): Promise<string | null> {
 | 
				
			||||||
    if (!this.enabled) {
 | 
					    const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
 | 
					    const { enabled } = config.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!enabled) {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return (await this.getClient()).issuer.metadata.end_session_endpoint || null;
 | 
					    return (await this.getClient()).issuer.metadata.end_session_endpoint || null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async getClient() {
 | 
					  private async getClient() {
 | 
				
			||||||
    if (!this.enabled) {
 | 
					    const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
 | 
					    const { enabled, clientId, clientSecret, issuerUrl } = config.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!enabled) {
 | 
				
			||||||
      throw new BadRequestException('OAuth2 is not enabled');
 | 
					      throw new BadRequestException('OAuth2 is not enabled');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const issuer = await Issuer.discover(this.issuerUrl);
 | 
					    const metadata: ClientMetadata = {
 | 
				
			||||||
 | 
					      client_id: clientId,
 | 
				
			||||||
 | 
					      client_secret: clientSecret,
 | 
				
			||||||
 | 
					      response_types: ['code'],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const issuer = await Issuer.discover(issuerUrl);
 | 
				
			||||||
    const algorithms = (issuer.id_token_signing_alg_values_supported || []) as string[];
 | 
					    const algorithms = (issuer.id_token_signing_alg_values_supported || []) as string[];
 | 
				
			||||||
    const metadata = { ...this.clientMetadata };
 | 
					 | 
				
			||||||
    if (algorithms[0] === 'HS256') {
 | 
					    if (algorithms[0] === 'HS256') {
 | 
				
			||||||
      metadata.id_token_signed_response_alg = algorithms[0];
 | 
					      metadata.id_token_signed_response_alg = algorithms[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { IsString } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SystemConfigFFmpegDto {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  crf!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  preset!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  targetVideoCodec!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  targetAudioCodec!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  targetScaling!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { IsBoolean, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SystemConfigOAuthDto {
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  enabled!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateIf(isEnabled)
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  issuerUrl!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateIf(isEnabled)
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  clientId!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateIf(isEnabled)
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  clientSecret!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  scope!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  buttonText!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  autoRegister!: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { SystemConfig } from '@app/database/entities/system-config.entity';
 | 
				
			||||||
 | 
					import { ValidateNested } from 'class-validator';
 | 
				
			||||||
 | 
					import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
 | 
				
			||||||
 | 
					import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SystemConfigDto {
 | 
				
			||||||
 | 
					  @ValidateNested()
 | 
				
			||||||
 | 
					  ffmpeg!: SystemConfigFFmpegDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateNested()
 | 
				
			||||||
 | 
					  oauth!: SystemConfigOAuthDto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function mapConfig(config: SystemConfig): SystemConfigDto {
 | 
				
			||||||
 | 
					  return config;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
 | 
					import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
 | 
				
			||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
 | 
					import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
 | 
					import { Authenticated } from '../../decorators/authenticated.decorator';
 | 
				
			||||||
import { UpdateSystemConfigDto } from './dto/update-system-config';
 | 
					import { SystemConfigDto } from './dto/system-config.dto';
 | 
				
			||||||
import { SystemConfigResponseDto } from './response-dto/system-config-response.dto';
 | 
					 | 
				
			||||||
import { SystemConfigService } from './system-config.service';
 | 
					import { SystemConfigService } from './system-config.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiTags('System Config')
 | 
					@ApiTags('System Config')
 | 
				
			||||||
@@ -13,12 +12,17 @@ export class SystemConfigController {
 | 
				
			|||||||
  constructor(private readonly systemConfigService: SystemConfigService) {}
 | 
					  constructor(private readonly systemConfigService: SystemConfigService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get()
 | 
					  @Get()
 | 
				
			||||||
  getConfig(): Promise<SystemConfigResponseDto> {
 | 
					  public getConfig(): Promise<SystemConfigDto> {
 | 
				
			||||||
    return this.systemConfigService.getConfig();
 | 
					    return this.systemConfigService.getConfig();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('defaults')
 | 
				
			||||||
 | 
					  public getDefaults(): SystemConfigDto {
 | 
				
			||||||
 | 
					    return this.systemConfigService.getDefaults();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Put()
 | 
					  @Put()
 | 
				
			||||||
  async updateConfig(@Body(ValidationPipe) dto: UpdateSystemConfigDto): Promise<SystemConfigResponseDto> {
 | 
					  public updateConfig(@Body(ValidationPipe) dto: SystemConfigDto): Promise<SystemConfigDto> {
 | 
				
			||||||
    return this.systemConfigService.updateConfig(dto);
 | 
					    return this.systemConfigService.updateConfig(dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,23 @@
 | 
				
			|||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { ImmichConfigService } from 'libs/immich-config/src';
 | 
					import { ImmichConfigService } from 'libs/immich-config/src';
 | 
				
			||||||
import { UpdateSystemConfigDto } from './dto/update-system-config';
 | 
					import { mapConfig, SystemConfigDto } from './dto/system-config.dto';
 | 
				
			||||||
import { SystemConfigResponseDto } from './response-dto/system-config-response.dto';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SystemConfigService {
 | 
					export class SystemConfigService {
 | 
				
			||||||
  constructor(private immichConfigService: ImmichConfigService) {}
 | 
					  constructor(private immichConfigService: ImmichConfigService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getConfig(): Promise<SystemConfigResponseDto> {
 | 
					  public async getConfig(): Promise<SystemConfigDto> {
 | 
				
			||||||
    const config = await this.immichConfigService.getSystemConfig();
 | 
					    const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
    return { config };
 | 
					    return mapConfig(config);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateConfig(dto: UpdateSystemConfigDto): Promise<SystemConfigResponseDto> {
 | 
					  public getDefaults(): SystemConfigDto {
 | 
				
			||||||
    await this.immichConfigService.updateSystemConfig(dto.config);
 | 
					    const config = this.immichConfigService.getDefaults();
 | 
				
			||||||
    const config = await this.immichConfigService.getSystemConfig();
 | 
					    return mapConfig(config);
 | 
				
			||||||
    return { config };
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
 | 
				
			||||||
 | 
					    await this.immichConfigService.updateConfig(dto);
 | 
				
			||||||
 | 
					    return this.getConfig();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,16 +42,16 @@ export class VideoTranscodeProcessor {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
 | 
					  async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> {
 | 
				
			||||||
    const config = await this.immichConfigService.getSystemConfigMap();
 | 
					    const config = await this.immichConfigService.getConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
      ffmpeg(asset.originalPath)
 | 
					      ffmpeg(asset.originalPath)
 | 
				
			||||||
        .outputOptions([
 | 
					        .outputOptions([
 | 
				
			||||||
          `-crf ${config.ffmpeg_crf}`,
 | 
					          `-crf ${config.ffmpeg.crf}`,
 | 
				
			||||||
          `-preset ${config.ffmpeg_preset}`,
 | 
					          `-preset ${config.ffmpeg.preset}`,
 | 
				
			||||||
          `-vcodec ${config.ffmpeg_target_video_codec}`,
 | 
					          `-vcodec ${config.ffmpeg.targetVideoCodec}`,
 | 
				
			||||||
          `-acodec ${config.ffmpeg_target_audio_codec}`,
 | 
					          `-acodec ${config.ffmpeg.targetAudioCodec}`,
 | 
				
			||||||
          `-vf scale=${config.ffmpeg_target_scaling}`,
 | 
					          `-vf scale=${config.ffmpeg.targetScaling}`,
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
        .output(savedEncodedPath)
 | 
					        .output(savedEncodedPath)
 | 
				
			||||||
        .on('start', () => {
 | 
					        .on('start', () => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2086,7 +2086,7 @@
 | 
				
			|||||||
            "content": {
 | 
					            "content": {
 | 
				
			||||||
              "application/json": {
 | 
					              "application/json": {
 | 
				
			||||||
                "schema": {
 | 
					                "schema": {
 | 
				
			||||||
                  "$ref": "#/components/schemas/SystemConfigResponseDto"
 | 
					                  "$ref": "#/components/schemas/SystemConfigDto"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -2109,7 +2109,7 @@
 | 
				
			|||||||
          "content": {
 | 
					          "content": {
 | 
				
			||||||
            "application/json": {
 | 
					            "application/json": {
 | 
				
			||||||
              "schema": {
 | 
					              "schema": {
 | 
				
			||||||
                "$ref": "#/components/schemas/UpdateSystemConfigDto"
 | 
					                "$ref": "#/components/schemas/SystemConfigDto"
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -2120,7 +2120,33 @@
 | 
				
			|||||||
            "content": {
 | 
					            "content": {
 | 
				
			||||||
              "application/json": {
 | 
					              "application/json": {
 | 
				
			||||||
                "schema": {
 | 
					                "schema": {
 | 
				
			||||||
                  "$ref": "#/components/schemas/SystemConfigResponseDto"
 | 
					                  "$ref": "#/components/schemas/SystemConfigDto"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "System Config"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "security": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "bearer": []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "/system-config/defaults": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "operationId": "getDefaults",
 | 
				
			||||||
 | 
					        "parameters": [],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "description": "",
 | 
				
			||||||
 | 
					            "content": {
 | 
				
			||||||
 | 
					              "application/json": {
 | 
				
			||||||
 | 
					                "schema": {
 | 
				
			||||||
 | 
					                  "$ref": "#/components/schemas/SystemConfigDto"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -3568,56 +3594,82 @@
 | 
				
			|||||||
          "command"
 | 
					          "command"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "SystemConfigKey": {
 | 
					      "SystemConfigFFmpegDto": {
 | 
				
			||||||
        "type": "string",
 | 
					 | 
				
			||||||
        "enum": [
 | 
					 | 
				
			||||||
          "ffmpeg_crf",
 | 
					 | 
				
			||||||
          "ffmpeg_preset",
 | 
					 | 
				
			||||||
          "ffmpeg_target_video_codec",
 | 
					 | 
				
			||||||
          "ffmpeg_target_audio_codec",
 | 
					 | 
				
			||||||
          "ffmpeg_target_scaling"
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "SystemConfigResponseItem": {
 | 
					 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "name": {
 | 
					          "crf": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "key": {
 | 
					          "preset": {
 | 
				
			||||||
            "$ref": "#/components/schemas/SystemConfigKey"
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "value": {
 | 
					 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "defaultValue": {
 | 
					          "targetVideoCodec": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "targetAudioCodec": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "targetScaling": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "name",
 | 
					          "crf",
 | 
				
			||||||
          "key",
 | 
					          "preset",
 | 
				
			||||||
          "value",
 | 
					          "targetVideoCodec",
 | 
				
			||||||
          "defaultValue"
 | 
					          "targetAudioCodec",
 | 
				
			||||||
 | 
					          "targetScaling"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "SystemConfigResponseDto": {
 | 
					      "SystemConfigOAuthDto": {
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "config": {
 | 
					          "enabled": {
 | 
				
			||||||
            "type": "array",
 | 
					            "type": "boolean"
 | 
				
			||||||
            "items": {
 | 
					          },
 | 
				
			||||||
              "$ref": "#/components/schemas/SystemConfigResponseItem"
 | 
					          "issuerUrl": {
 | 
				
			||||||
            }
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "clientId": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "clientSecret": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "scope": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "buttonText": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "autoRegister": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "config"
 | 
					          "enabled",
 | 
				
			||||||
 | 
					          "issuerUrl",
 | 
				
			||||||
 | 
					          "clientId",
 | 
				
			||||||
 | 
					          "clientSecret",
 | 
				
			||||||
 | 
					          "scope",
 | 
				
			||||||
 | 
					          "buttonText",
 | 
				
			||||||
 | 
					          "autoRegister"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "UpdateSystemConfigDto": {
 | 
					      "SystemConfigDto": {
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {}
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "ffmpeg": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/SystemConfigFFmpegDto"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "oauth": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/SystemConfigOAuthDto"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "ffmpeg",
 | 
				
			||||||
 | 
					          "oauth"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,12 +16,6 @@ const jwtSecretValidator: Joi.CustomValidator<string> = (value) => {
 | 
				
			|||||||
  return value;
 | 
					  return value;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WHEN_OAUTH_ENABLED = Joi.when('OAUTH_ENABLED', {
 | 
					 | 
				
			||||||
  is: true,
 | 
					 | 
				
			||||||
  then: Joi.string().required(),
 | 
					 | 
				
			||||||
  otherwise: Joi.string().optional(),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const immichAppConfig: ConfigModuleOptions = {
 | 
					export const immichAppConfig: ConfigModuleOptions = {
 | 
				
			||||||
  envFilePath: '.env',
 | 
					  envFilePath: '.env',
 | 
				
			||||||
  isGlobal: true,
 | 
					  isGlobal: true,
 | 
				
			||||||
@@ -34,12 +28,5 @@ export const immichAppConfig: ConfigModuleOptions = {
 | 
				
			|||||||
    DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
 | 
					    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'),
 | 
					    LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
 | 
				
			||||||
    OAUTH_ENABLED: Joi.bool().valid(true, false).default(false),
 | 
					 | 
				
			||||||
    OAUTH_BUTTON_TEXT: Joi.string().optional().default('Login with OAuth'),
 | 
					 | 
				
			||||||
    OAUTH_AUTO_REGISTER: Joi.bool().valid(true, false).default(true),
 | 
					 | 
				
			||||||
    OAUTH_ISSUER_URL: WHEN_OAUTH_ENABLED,
 | 
					 | 
				
			||||||
    OAUTH_SCOPE: Joi.string().optional().default('openid email profile'),
 | 
					 | 
				
			||||||
    OAUTH_CLIENT_ID: WHEN_OAUTH_ENABLED,
 | 
					 | 
				
			||||||
    OAUTH_CLIENT_SECRET: WHEN_OAUTH_ENABLED,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,27 +1,47 @@
 | 
				
			|||||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
 | 
					import { Column, Entity, PrimaryColumn } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('system_config')
 | 
					@Entity('system_config')
 | 
				
			||||||
export class SystemConfigEntity {
 | 
					export class SystemConfigEntity<T = string> {
 | 
				
			||||||
  @PrimaryColumn()
 | 
					  @PrimaryColumn()
 | 
				
			||||||
  key!: SystemConfigKey;
 | 
					  key!: SystemConfigKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ type: 'varchar', nullable: true })
 | 
					  @Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
 | 
				
			||||||
  value!: SystemConfigValue;
 | 
					  value!: T;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SystemConfig = SystemConfigEntity[];
 | 
					export type SystemConfigValue = any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dot notation matches path in `SystemConfig`
 | 
				
			||||||
export enum SystemConfigKey {
 | 
					export enum SystemConfigKey {
 | 
				
			||||||
  FFMPEG_CRF = 'ffmpeg_crf',
 | 
					  FFMPEG_CRF = 'ffmpeg.crf',
 | 
				
			||||||
  FFMPEG_PRESET = 'ffmpeg_preset',
 | 
					  FFMPEG_PRESET = 'ffmpeg.preset',
 | 
				
			||||||
  FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg_target_video_codec',
 | 
					  FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
 | 
				
			||||||
  FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg_target_audio_codec',
 | 
					  FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
 | 
				
			||||||
  FFMPEG_TARGET_SCALING = 'ffmpeg_target_scaling',
 | 
					  FFMPEG_TARGET_SCALING = 'ffmpeg.targetScaling',
 | 
				
			||||||
 | 
					  OAUTH_ENABLED = 'oauth.enabled',
 | 
				
			||||||
 | 
					  OAUTH_ISSUER_URL = 'oauth.issuerUrl',
 | 
				
			||||||
 | 
					  OAUTH_CLIENT_ID = 'oauth.clientId',
 | 
				
			||||||
 | 
					  OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
 | 
				
			||||||
 | 
					  OAUTH_SCOPE = 'oauth.scope',
 | 
				
			||||||
 | 
					  OAUTH_BUTTON_TEXT = 'oauth.buttonText',
 | 
				
			||||||
 | 
					  OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SystemConfigValue = string | null;
 | 
					export interface SystemConfig {
 | 
				
			||||||
 | 
					  ffmpeg: {
 | 
				
			||||||
export interface SystemConfigItem {
 | 
					    crf: string;
 | 
				
			||||||
  key: SystemConfigKey;
 | 
					    preset: string;
 | 
				
			||||||
  value: SystemConfigValue;
 | 
					    targetVideoCodec: string;
 | 
				
			||||||
 | 
					    targetAudioCodec: string;
 | 
				
			||||||
 | 
					    targetScaling: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  oauth: {
 | 
				
			||||||
 | 
					    enabled: boolean;
 | 
				
			||||||
 | 
					    issuerUrl: string;
 | 
				
			||||||
 | 
					    clientId: string;
 | 
				
			||||||
 | 
					    clientSecret: string;
 | 
				
			||||||
 | 
					    scope: string;
 | 
				
			||||||
 | 
					    buttonText: string;
 | 
				
			||||||
 | 
					    autoRegister: boolean;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TruncateOldConfigItems1670607437008 implements MigrationInterface {
 | 
				
			||||||
 | 
					  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    await queryRunner.query(`TRUNCATE TABLE "system_config"`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async down(): Promise<void> {
 | 
				
			||||||
 | 
					    // noop
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,32 +1,27 @@
 | 
				
			|||||||
import { SystemConfigEntity, SystemConfigKey, SystemConfigValue } from '@app/database/entities/system-config.entity';
 | 
					import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/database/entities/system-config.entity';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { In, Repository } from 'typeorm';
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
 | 
					import { DeepPartial, In, Repository } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SystemConfigMap = Record<SystemConfigKey, SystemConfigValue>;
 | 
					const defaults: SystemConfig = Object.freeze({
 | 
				
			||||||
 | 
					  ffmpeg: {
 | 
				
			||||||
const configDefaults: Record<SystemConfigKey, { name: string; value: SystemConfigValue }> = {
 | 
					    crf: '23',
 | 
				
			||||||
  [SystemConfigKey.FFMPEG_CRF]: {
 | 
					    preset: 'ultrafast',
 | 
				
			||||||
    name: 'FFmpeg Constant Rate Factor (-crf)',
 | 
					    targetVideoCodec: 'libx264',
 | 
				
			||||||
    value: '23',
 | 
					    targetAudioCodec: 'mp3',
 | 
				
			||||||
 | 
					    targetScaling: '1280:-2',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [SystemConfigKey.FFMPEG_PRESET]: {
 | 
					  oauth: {
 | 
				
			||||||
    name: 'FFmpeg preset (-preset)',
 | 
					    enabled: false,
 | 
				
			||||||
    value: 'ultrafast',
 | 
					    issuerUrl: '',
 | 
				
			||||||
 | 
					    clientId: '',
 | 
				
			||||||
 | 
					    clientSecret: '',
 | 
				
			||||||
 | 
					    scope: 'openid email profile',
 | 
				
			||||||
 | 
					    buttonText: 'Login with OAuth',
 | 
				
			||||||
 | 
					    autoRegister: true,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [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()
 | 
					@Injectable()
 | 
				
			||||||
export class ImmichConfigService {
 | 
					export class ImmichConfigService {
 | 
				
			||||||
@@ -35,38 +30,32 @@ export class ImmichConfigService {
 | 
				
			|||||||
    private systemConfigRepository: Repository<SystemConfigEntity>,
 | 
					    private systemConfigRepository: Repository<SystemConfigEntity>,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async getSystemConfig() {
 | 
					  public getDefaults(): SystemConfig {
 | 
				
			||||||
    const items = this._getDefaults();
 | 
					    return defaults;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // override default values
 | 
					  public async getConfig() {
 | 
				
			||||||
    const overrides = await this.systemConfigRepository.find();
 | 
					    const overrides = await this.systemConfigRepository.find();
 | 
				
			||||||
    for (const override of overrides) {
 | 
					    const config: DeepPartial<SystemConfig> = {};
 | 
				
			||||||
      const item = items.find((_item) => _item.key === override.key);
 | 
					    for (const { key, value } of overrides) {
 | 
				
			||||||
      if (item) {
 | 
					      // set via dot notation
 | 
				
			||||||
        item.value = override.value;
 | 
					      _.set(config, key, value);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return items;
 | 
					    return _.defaultsDeep(config, defaults) as SystemConfig;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async getSystemConfigMap(): Promise<SystemConfigMap> {
 | 
					  public async updateConfig(config: DeepPartial<SystemConfig> | null): Promise<void> {
 | 
				
			||||||
    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[] = [];
 | 
					    const updates: SystemConfigEntity[] = [];
 | 
				
			||||||
 | 
					    const deletes: SystemConfigEntity[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const item of items) {
 | 
					    for (const key of Object.values(SystemConfigKey)) {
 | 
				
			||||||
      if (item.value === null || item.value === this._getDefaultValue(item.key)) {
 | 
					      // get via dot notation
 | 
				
			||||||
 | 
					      const item = { key, value: _.get(config, key) };
 | 
				
			||||||
 | 
					      const defaultValue = _.get(defaults, key);
 | 
				
			||||||
 | 
					      const isMissing = !_.has(config, key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isMissing || item.value === null || item.value === '' || item.value === defaultValue) {
 | 
				
			||||||
        deletes.push(item);
 | 
					        deletes.push(item);
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -82,16 +71,4 @@ export class ImmichConfigService {
 | 
				
			|||||||
      await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) });
 | 
					      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;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,13 +71,13 @@
 | 
				
			|||||||
        "tsConfigPath": "libs/job/tsconfig.lib.json"
 | 
					        "tsConfigPath": "libs/job/tsconfig.lib.json"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "system-config": {
 | 
					    "immich-config": {
 | 
				
			||||||
      "type": "library",
 | 
					      "type": "library",
 | 
				
			||||||
      "root": "libs/system-config",
 | 
					      "root": "libs/immich-config",
 | 
				
			||||||
      "entryFile": "index",
 | 
					      "entryFile": "index",
 | 
				
			||||||
      "sourceRoot": "libs/system-config/src",
 | 
					      "sourceRoot": "libs/immich-config/src",
 | 
				
			||||||
      "compilerOptions": {
 | 
					      "compilerOptions": {
 | 
				
			||||||
        "tsConfigPath": "libs/system-config/tsconfig.lib.json"
 | 
					        "tsConfigPath": "libs/immich-config/tsconfig.lib.json"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -142,7 +142,7 @@
 | 
				
			|||||||
      "@app/database/config": "<rootDir>/libs/database/src/config",
 | 
					      "@app/database/config": "<rootDir>/libs/database/src/config",
 | 
				
			||||||
      "@app/common": "<rootDir>/libs/common/src",
 | 
					      "@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"
 | 
					      "^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,8 +22,8 @@
 | 
				
			|||||||
      "@app/database/*": ["libs/database/src/*"],
 | 
					      "@app/database/*": ["libs/database/src/*"],
 | 
				
			||||||
      "@app/job": ["libs/job/src"],
 | 
					      "@app/job": ["libs/job/src"],
 | 
				
			||||||
      "@app/job/*": ["libs/job/src/*"],
 | 
					      "@app/job/*": ["libs/job/src/*"],
 | 
				
			||||||
      "@app/system-config": ["libs/immich-config/src"],
 | 
					      "@app/immich-config": ["libs/immich-config/src"],
 | 
				
			||||||
      "@app/system-config/*": ["libs/immich-config/src/*"]
 | 
					      "@app/immich-config/*": ["libs/immich-config/src/*"]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "exclude": ["dist", "node_modules", "upload"]
 | 
					  "exclude": ["dist", "node_modules", "upload"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										208
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										208
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@@ -1428,63 +1428,107 @@ export interface SmartInfoResponseDto {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
 * @enum {string}
 | 
					 * @interface SystemConfigDto
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					export interface SystemConfigDto {
 | 
				
			||||||
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>}
 | 
					     * @type {SystemConfigFFmpegDto}
 | 
				
			||||||
     * @memberof SystemConfigResponseDto
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'config': Array<SystemConfigResponseItem>;
 | 
					    'ffmpeg': SystemConfigFFmpegDto;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {SystemConfigOAuthDto}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'oauth': SystemConfigOAuthDto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
 * @interface SystemConfigResponseItem
 | 
					 * @interface SystemConfigFFmpegDto
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export interface SystemConfigResponseItem {
 | 
					export interface SystemConfigFFmpegDto {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     * @memberof SystemConfigResponseItem
 | 
					     * @memberof SystemConfigFFmpegDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'name': string;
 | 
					    'crf': string;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {SystemConfigKey}
 | 
					 | 
				
			||||||
     * @memberof SystemConfigResponseItem
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'key': SystemConfigKey;
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     * @memberof SystemConfigResponseItem
 | 
					     * @memberof SystemConfigFFmpegDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'value': string;
 | 
					    'preset': string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     * @memberof SystemConfigResponseItem
 | 
					     * @memberof SystemConfigFFmpegDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'defaultValue': string;
 | 
					    'targetVideoCodec': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigFFmpegDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'targetAudioCodec': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigFFmpegDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'targetScaling': string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SystemConfigOAuthDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SystemConfigOAuthDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'enabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'issuerUrl': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'clientId': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'clientSecret': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'scope': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'buttonText': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigOAuthDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'autoRegister': boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@@ -5254,13 +5298,46 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {object} body 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        updateConfig: async (body: object, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					        getDefaults: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
            // verify required parameter 'body' is not null or undefined
 | 
					            const localVarPath = `/system-config/defaults`;
 | 
				
			||||||
            assertParamExists('updateConfig', 'body', body)
 | 
					            // 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 {SystemConfigDto} systemConfigDto 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        updateConfig: async (systemConfigDto: SystemConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            // verify required parameter 'systemConfigDto' is not null or undefined
 | 
				
			||||||
 | 
					            assertParamExists('updateConfig', 'systemConfigDto', systemConfigDto)
 | 
				
			||||||
            const localVarPath = `/system-config`;
 | 
					            const localVarPath = `/system-config`;
 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
@@ -5284,7 +5361,7 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
 | 
				
			|||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
 | 
					            localVarRequestOptions.data = serializeDataIfNeeded(systemConfigDto, localVarRequestOptions, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
@@ -5306,18 +5383,27 @@ export const SystemConfigApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        async getConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigResponseDto>> {
 | 
					        async getConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigDto>> {
 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getConfig(options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getConfig(options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {object} body 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        async updateConfig(body: object, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigResponseDto>> {
 | 
					        async getDefaults(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigDto>> {
 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfig(body, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getDefaults(options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {SystemConfigDto} systemConfigDto 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async updateConfig(systemConfigDto: SystemConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfig(systemConfigDto, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -5335,17 +5421,25 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
 | 
				
			|||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        getConfig(options?: any): AxiosPromise<SystemConfigResponseDto> {
 | 
					        getConfig(options?: any): AxiosPromise<SystemConfigDto> {
 | 
				
			||||||
            return localVarFp.getConfig(options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getConfig(options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {object} body 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        updateConfig(body: object, options?: any): AxiosPromise<SystemConfigResponseDto> {
 | 
					        getDefaults(options?: any): AxiosPromise<SystemConfigDto> {
 | 
				
			||||||
            return localVarFp.updateConfig(body, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getDefaults(options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {SystemConfigDto} systemConfigDto 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        updateConfig(systemConfigDto: SystemConfigDto, options?: any): AxiosPromise<SystemConfigDto> {
 | 
				
			||||||
 | 
					            return localVarFp.updateConfig(systemConfigDto, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -5369,13 +5463,23 @@ export class SystemConfigApi extends BaseAPI {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {object} body 
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
     * @throws {RequiredError}
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
     * @memberof SystemConfigApi
 | 
					     * @memberof SystemConfigApi
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public updateConfig(body: object, options?: AxiosRequestConfig) {
 | 
					    public getDefaults(options?: AxiosRequestConfig) {
 | 
				
			||||||
        return SystemConfigApiFp(this.configuration).updateConfig(body, options).then((request) => request(this.axios, this.basePath));
 | 
					        return SystemConfigApiFp(this.configuration).getDefaults(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {SystemConfigDto} systemConfigDto 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public updateConfig(systemConfigDto: SystemConfigDto, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return SystemConfigApiFp(this.configuration).updateConfig(systemConfigDto, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ input:focus-visible {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@layer utilities {
 | 
					@layer utilities {
 | 
				
			||||||
	.immich-form-input {
 | 
						.immich-form-input {
 | 
				
			||||||
		@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;
 | 
							@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-500 dark:disabled:bg-gray-900 disabled:cursor-not-allowed;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.immich-form-label {
 | 
						.immich-form-label {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import {
 | 
				
			||||||
 | 
							notificationController,
 | 
				
			||||||
 | 
							NotificationType
 | 
				
			||||||
 | 
						} from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
 | 
						import { api, SystemConfigFFmpegDto } from '@api';
 | 
				
			||||||
 | 
						import SettingButtonsRow from '../setting-buttons-row.svelte';
 | 
				
			||||||
 | 
						import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
 | 
				
			||||||
 | 
						import _ from 'lodash';
 | 
				
			||||||
 | 
						export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
 | 
				
			||||||
 | 
						import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let savedConfig: SystemConfigFFmpegDto;
 | 
				
			||||||
 | 
						let defaultConfig: SystemConfigFFmpegDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function getConfigs() {
 | 
				
			||||||
 | 
							[savedConfig, defaultConfig] = await Promise.all([
 | 
				
			||||||
 | 
								api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
 | 
				
			||||||
 | 
								api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg)
 | 
				
			||||||
 | 
							]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function saveSetting() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const { data: configs } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const result = await api.systemConfigApi.updateConfig({
 | 
				
			||||||
 | 
									ffmpeg: ffmpegConfig,
 | 
				
			||||||
 | 
									oauth: configs.oauth
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ffmpegConfig = result.data.ffmpeg;
 | 
				
			||||||
 | 
								savedConfig = result.data.ffmpeg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								notificationController.show({
 | 
				
			||||||
 | 
									message: 'FFmpeg settings saved',
 | 
				
			||||||
 | 
									type: NotificationType.Info
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								console.error('Error [ffmpeg-settings] [saveSetting]', e);
 | 
				
			||||||
 | 
								notificationController.show({
 | 
				
			||||||
 | 
									message: 'Unable to save settings',
 | 
				
			||||||
 | 
									type: NotificationType.Error
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function reset() {
 | 
				
			||||||
 | 
							const { data: resetConfig } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ffmpegConfig = resetConfig.ffmpeg;
 | 
				
			||||||
 | 
							savedConfig = resetConfig.ffmpeg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notificationController.show({
 | 
				
			||||||
 | 
								message: 'Reset FFmpeg settings to the recent saved settings',
 | 
				
			||||||
 | 
								type: NotificationType.Info
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function resetToDefault() {
 | 
				
			||||||
 | 
							const { data: configs } = await api.systemConfigApi.getDefaults();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ffmpegConfig = configs.ffmpeg;
 | 
				
			||||||
 | 
							defaultConfig = configs.ffmpeg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notificationController.show({
 | 
				
			||||||
 | 
								message: 'Reset FFmpeg settings to default',
 | 
				
			||||||
 | 
								type: NotificationType.Info
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						{#await getConfigs() then}
 | 
				
			||||||
 | 
							<div in:fade={{ duration: 500 }}>
 | 
				
			||||||
 | 
								<form autocomplete="off" on:submit|preventDefault>
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.NUMBER}
 | 
				
			||||||
 | 
										label="CRF"
 | 
				
			||||||
 | 
										bind:value={ffmpegConfig.crf}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="PRESET"
 | 
				
			||||||
 | 
										bind:value={ffmpegConfig.preset}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="AUDIO CODEC"
 | 
				
			||||||
 | 
										bind:value={ffmpegConfig.targetAudioCodec}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="VIDEO CODEC"
 | 
				
			||||||
 | 
										bind:value={ffmpegConfig.targetVideoCodec}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="SCALING"
 | 
				
			||||||
 | 
										bind:value={ffmpegConfig.targetScaling}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingButtonsRow
 | 
				
			||||||
 | 
										on:reset={reset}
 | 
				
			||||||
 | 
										on:save={saveSetting}
 | 
				
			||||||
 | 
										on:reset-to-default={resetToDefault}
 | 
				
			||||||
 | 
										showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/await}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,147 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import {
 | 
				
			||||||
 | 
							notificationController,
 | 
				
			||||||
 | 
							NotificationType
 | 
				
			||||||
 | 
						} from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
 | 
						import { api, SystemConfigOAuthDto } from '@api';
 | 
				
			||||||
 | 
						import SettingButtonsRow from '../setting-buttons-row.svelte';
 | 
				
			||||||
 | 
						import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
 | 
				
			||||||
 | 
						import SettingSwitch from '../setting-switch.svelte';
 | 
				
			||||||
 | 
						import _ from 'lodash';
 | 
				
			||||||
 | 
						import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let oauthConfig: SystemConfigOAuthDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let savedConfig: SystemConfigOAuthDto;
 | 
				
			||||||
 | 
						let defaultConfig: SystemConfigOAuthDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function getConfigs() {
 | 
				
			||||||
 | 
							[savedConfig, defaultConfig] = await Promise.all([
 | 
				
			||||||
 | 
								api.systemConfigApi.getConfig().then((res) => res.data.oauth),
 | 
				
			||||||
 | 
								api.systemConfigApi.getDefaults().then((res) => res.data.oauth)
 | 
				
			||||||
 | 
							]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function reset() {
 | 
				
			||||||
 | 
							const { data: resetConfig } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							oauthConfig = resetConfig.oauth;
 | 
				
			||||||
 | 
							savedConfig = resetConfig.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notificationController.show({
 | 
				
			||||||
 | 
								message: 'Reset OAuth settings to the recent saved settings',
 | 
				
			||||||
 | 
								type: NotificationType.Info
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function saveSetting() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const { data: currentConfig } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const result = await api.systemConfigApi.updateConfig({
 | 
				
			||||||
 | 
									ffmpeg: currentConfig.ffmpeg,
 | 
				
			||||||
 | 
									oauth: oauthConfig
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								oauthConfig = result.data.oauth;
 | 
				
			||||||
 | 
								savedConfig = result.data.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								notificationController.show({
 | 
				
			||||||
 | 
									message: 'OAuth settings saved',
 | 
				
			||||||
 | 
									type: NotificationType.Info
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								console.error('Error [oauth-settings] [saveSetting]', e);
 | 
				
			||||||
 | 
								notificationController.show({
 | 
				
			||||||
 | 
									message: 'Unable to save settings',
 | 
				
			||||||
 | 
									type: NotificationType.Error
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function resetToDefault() {
 | 
				
			||||||
 | 
							const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							oauthConfig = defaultConfig.oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notificationController.show({
 | 
				
			||||||
 | 
								message: 'Reset OAuth settings to default',
 | 
				
			||||||
 | 
								type: NotificationType.Info
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="mt-2">
 | 
				
			||||||
 | 
						{#await getConfigs() then}
 | 
				
			||||||
 | 
							<div in:fade={{ duration: 500 }}>
 | 
				
			||||||
 | 
								<form autocomplete="off" on:submit|preventDefault>
 | 
				
			||||||
 | 
									<div class="mt-4">
 | 
				
			||||||
 | 
										<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<hr class="m-4" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="ISSUER URL"
 | 
				
			||||||
 | 
										bind:value={oauthConfig.issuerUrl}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="CLIENT ID"
 | 
				
			||||||
 | 
										bind:value={oauthConfig.clientId}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="CLIENT SECRET"
 | 
				
			||||||
 | 
										bind:value={oauthConfig.clientSecret}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="SCOPE"
 | 
				
			||||||
 | 
										bind:value={oauthConfig.scope}
 | 
				
			||||||
 | 
										required={true}
 | 
				
			||||||
 | 
										disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										isEdited={!(oauthConfig.scope == savedConfig.scope)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingInputField
 | 
				
			||||||
 | 
										inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
										label="BUTTON TEXT"
 | 
				
			||||||
 | 
										bind:value={oauthConfig.buttonText}
 | 
				
			||||||
 | 
										required={false}
 | 
				
			||||||
 | 
										disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="mt-4">
 | 
				
			||||||
 | 
										<SettingSwitch
 | 
				
			||||||
 | 
											title="AUTO REGISTER"
 | 
				
			||||||
 | 
											subtitle="Automatically register new users after singning in with OAuth"
 | 
				
			||||||
 | 
											bind:checked={oauthConfig.autoRegister}
 | 
				
			||||||
 | 
											disabled={!oauthConfig.enabled}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<SettingButtonsRow
 | 
				
			||||||
 | 
										on:reset={reset}
 | 
				
			||||||
 | 
										on:save={saveSetting}
 | 
				
			||||||
 | 
										on:reset-to-default={resetToDefault}
 | 
				
			||||||
 | 
										showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/await}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { slide } from 'svelte/transition';
 | 
				
			||||||
 | 
						export let title: string;
 | 
				
			||||||
 | 
						export let subtitle = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let isOpen = false;
 | 
				
			||||||
 | 
						const toggle = () => (isOpen = !isOpen);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="border-b-[1px] border-gray-200 dark:border-gray-700 py-4">
 | 
				
			||||||
 | 
						<div class="flex justify-between place-items-center">
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
 | 
				
			||||||
 | 
									{title}
 | 
				
			||||||
 | 
								</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<button
 | 
				
			||||||
 | 
								on:click={toggle}
 | 
				
			||||||
 | 
								aria-expanded={isOpen}
 | 
				
			||||||
 | 
								class="immich-circle-icon-button hover:bg-immich-primary/10 dark:text-immich-dark-fg hover:dark:bg-immich-dark-primary/20  rounded-full p-3 flex place-items-center place-content-center transition-all"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								<svg
 | 
				
			||||||
 | 
									style="tran"
 | 
				
			||||||
 | 
									width="20"
 | 
				
			||||||
 | 
									height="20"
 | 
				
			||||||
 | 
									fill="none"
 | 
				
			||||||
 | 
									stroke-linecap="round"
 | 
				
			||||||
 | 
									stroke-linejoin="round"
 | 
				
			||||||
 | 
									stroke-width="2"
 | 
				
			||||||
 | 
									viewBox="0 0 24 24"
 | 
				
			||||||
 | 
									stroke="currentColor"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<path d="M19 9l-7 7-7-7" />
 | 
				
			||||||
 | 
								</svg>
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{#if isOpen}
 | 
				
			||||||
 | 
							<ul transition:slide={{ duration: 250 }} class="mb-2 ml-4">
 | 
				
			||||||
 | 
								<slot />
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						svg {
 | 
				
			||||||
 | 
							transition: transform 0.2s ease-in;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[aria-expanded='true'] svg {
 | 
				
			||||||
 | 
							transform: rotate(0.5turn);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let showResetToDefault = true;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex justify-between gap-2 mx-4 mt-8">
 | 
				
			||||||
 | 
						<div class="left">
 | 
				
			||||||
 | 
							{#if showResetToDefault}
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									on:click|preventDefault={() => dispatch('reset-to-default')}
 | 
				
			||||||
 | 
									class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									Reset to default
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="right">
 | 
				
			||||||
 | 
							<button
 | 
				
			||||||
 | 
								on:click|preventDefault={() => dispatch('reset')}
 | 
				
			||||||
 | 
								class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
 | 
				
			||||||
 | 
								>Reset
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<button
 | 
				
			||||||
 | 
								type="submit"
 | 
				
			||||||
 | 
								on:click={() => dispatch('save')}
 | 
				
			||||||
 | 
								class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
 | 
				
			||||||
 | 
								>Save
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					<script lang="ts" context="module">
 | 
				
			||||||
 | 
						export enum SettingInputFieldType {
 | 
				
			||||||
 | 
							TEXT = 'text',
 | 
				
			||||||
 | 
							NUMBER = 'number',
 | 
				
			||||||
 | 
							PASSWORD = 'password'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { quintOut } from 'svelte/easing';
 | 
				
			||||||
 | 
						import { fly } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let inputType: SettingInputFieldType;
 | 
				
			||||||
 | 
						export let value: string;
 | 
				
			||||||
 | 
						export let label: string;
 | 
				
			||||||
 | 
						export let required = false;
 | 
				
			||||||
 | 
						export let disabled = false;
 | 
				
			||||||
 | 
						export let isEdited: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleInput = (e: Event) => {
 | 
				
			||||||
 | 
							value = (e.target as HTMLInputElement).value;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
 | 
						<div class="flex place-items-center gap-1">
 | 
				
			||||||
 | 
							<label class="immich-form-label" for={label}>{label.toUpperCase()} </label>
 | 
				
			||||||
 | 
							{#if required}
 | 
				
			||||||
 | 
								<div class="text-red-400">*</div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{#if isEdited}
 | 
				
			||||||
 | 
								<div
 | 
				
			||||||
 | 
									transition:fly={{ x: 10, duration: 200, easing: quintOut }}
 | 
				
			||||||
 | 
									class="text-gray-500 text-xs italic"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									Unsaved change
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<input
 | 
				
			||||||
 | 
							class="immich-form-input"
 | 
				
			||||||
 | 
							id={label}
 | 
				
			||||||
 | 
							name={label}
 | 
				
			||||||
 | 
							type={inputType}
 | 
				
			||||||
 | 
							{required}
 | 
				
			||||||
 | 
							{value}
 | 
				
			||||||
 | 
							on:input={handleInput}
 | 
				
			||||||
 | 
							{disabled}
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						export let title: string;
 | 
				
			||||||
 | 
						export let subtitle = '';
 | 
				
			||||||
 | 
						export let checked = false;
 | 
				
			||||||
 | 
						export let disabled = false;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex justify-between mx-4 place-items-center">
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							<h2 class="immich-form-label">
 | 
				
			||||||
 | 
								{title.toUpperCase()}
 | 
				
			||||||
 | 
							</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<label class="relative inline-block w-[36px] h-[10px]" {disabled}>
 | 
				
			||||||
 | 
							<input
 | 
				
			||||||
 | 
								class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
 | 
				
			||||||
 | 
								type="checkbox"
 | 
				
			||||||
 | 
								bind:checked
 | 
				
			||||||
 | 
								{disabled}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{#if disabled}
 | 
				
			||||||
 | 
								<span class="slider-disable" />
 | 
				
			||||||
 | 
							{:else}
 | 
				
			||||||
 | 
								<span class="slider" />
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.slider,
 | 
				
			||||||
 | 
						.slider-disable {
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							cursor: pointer;
 | 
				
			||||||
 | 
							top: 0;
 | 
				
			||||||
 | 
							left: 0;
 | 
				
			||||||
 | 
							right: 0;
 | 
				
			||||||
 | 
							bottom: 0;
 | 
				
			||||||
 | 
							background-color: #ccc;
 | 
				
			||||||
 | 
							-webkit-transition: 0.4s;
 | 
				
			||||||
 | 
							transition: 0.4s;
 | 
				
			||||||
 | 
							border-radius: 34px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input:disabled {
 | 
				
			||||||
 | 
							cursor: not-allowed;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.slider:before,
 | 
				
			||||||
 | 
						.slider-disable:before {
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							content: '';
 | 
				
			||||||
 | 
							height: 20px;
 | 
				
			||||||
 | 
							width: 20px;
 | 
				
			||||||
 | 
							left: 0px;
 | 
				
			||||||
 | 
							right: 0px;
 | 
				
			||||||
 | 
							bottom: -4px;
 | 
				
			||||||
 | 
							background-color: gray;
 | 
				
			||||||
 | 
							-webkit-transition: 0.4s;
 | 
				
			||||||
 | 
							transition: 0.4s;
 | 
				
			||||||
 | 
							border-radius: 50%;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input:checked + .slider-disable {
 | 
				
			||||||
 | 
							background-color: gray;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input:checked + .slider {
 | 
				
			||||||
 | 
							background-color: #adcbfa;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input:checked + .slider:before {
 | 
				
			||||||
 | 
							-webkit-transform: translateX(18px);
 | 
				
			||||||
 | 
							-ms-transform: translateX(18px);
 | 
				
			||||||
 | 
							transform: translateX(18px);
 | 
				
			||||||
 | 
							background-color: #4250af;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -1,97 +0,0 @@
 | 
				
			|||||||
<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>
 | 
					 | 
				
			||||||
@@ -1,91 +0,0 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import { UserResponseDto } from '@api';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	import { createEventDispatcher } from 'svelte';
 | 
					 | 
				
			||||||
	import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
 | 
					 | 
				
			||||||
	import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
 | 
					 | 
				
			||||||
	import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export let allUsers: Array<UserResponseDto>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const dispatch = createEventDispatcher();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const isDeleted = (user: UserResponseDto): boolean => {
 | 
					 | 
				
			||||||
		return user.deletedAt != null;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const locale = navigator.language;
 | 
					 | 
				
			||||||
	const deleteDateFormat: Intl.DateTimeFormatOptions = {
 | 
					 | 
				
			||||||
		month: 'long',
 | 
					 | 
				
			||||||
		day: 'numeric',
 | 
					 | 
				
			||||||
		year: 'numeric'
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const getDeleteDate = (user: UserResponseDto): string => {
 | 
					 | 
				
			||||||
		let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
 | 
					 | 
				
			||||||
		deletedAt.setDate(deletedAt.getDate() + 7);
 | 
					 | 
				
			||||||
		return deletedAt.toLocaleString(locale, deleteDateFormat);
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<table class="text-left w-full my-5">
 | 
					 | 
				
			||||||
	<thead
 | 
					 | 
				
			||||||
		class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full 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/4 font-medium text-sm">Email</th>
 | 
					 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">First name</th>
 | 
					 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">Last name</th>
 | 
					 | 
				
			||||||
			<th class="text-center w-1/4 font-medium text-sm">Action</th>
 | 
					 | 
				
			||||||
		</tr>
 | 
					 | 
				
			||||||
	</thead>
 | 
					 | 
				
			||||||
	<tbody
 | 
					 | 
				
			||||||
		class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
 | 
					 | 
				
			||||||
	>
 | 
					 | 
				
			||||||
		{#each allUsers as user, i}
 | 
					 | 
				
			||||||
			<tr
 | 
					 | 
				
			||||||
				class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
 | 
					 | 
				
			||||||
					isDeleted(user)
 | 
					 | 
				
			||||||
						? 'bg-red-300 dark:bg-red-900'
 | 
					 | 
				
			||||||
						: i % 2 == 0
 | 
					 | 
				
			||||||
						? 'bg-immich-gray dark:bg-immich-dark-gray/75'
 | 
					 | 
				
			||||||
						: 'bg-immich-bg dark:bg-immich-dark-gray/50'
 | 
					 | 
				
			||||||
				}`}
 | 
					 | 
				
			||||||
			>
 | 
					 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
 | 
					 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
 | 
					 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
 | 
					 | 
				
			||||||
				<td class="text-sm px-4 w-1/4 text-ellipsis">
 | 
					 | 
				
			||||||
					{#if !isDeleted(user)}
 | 
					 | 
				
			||||||
						<button
 | 
					 | 
				
			||||||
							on:click={() => {
 | 
					 | 
				
			||||||
								dispatch('edit-user', { user });
 | 
					 | 
				
			||||||
							}}
 | 
					 | 
				
			||||||
							class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
					 | 
				
			||||||
							><PencilOutline size="16" /></button
 | 
					 | 
				
			||||||
						>
 | 
					 | 
				
			||||||
						<button
 | 
					 | 
				
			||||||
							on:click={() => {
 | 
					 | 
				
			||||||
								dispatch('delete-user', { user });
 | 
					 | 
				
			||||||
							}}
 | 
					 | 
				
			||||||
							class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
					 | 
				
			||||||
							><TrashCanOutline size="16" /></button
 | 
					 | 
				
			||||||
						>
 | 
					 | 
				
			||||||
					{/if}
 | 
					 | 
				
			||||||
					{#if isDeleted(user)}
 | 
					 | 
				
			||||||
						<button
 | 
					 | 
				
			||||||
							on:click={() => {
 | 
					 | 
				
			||||||
								dispatch('restore-user', { user });
 | 
					 | 
				
			||||||
							}}
 | 
					 | 
				
			||||||
							class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
					 | 
				
			||||||
							title={`scheduled removal on ${getDeleteDate(user)}`}
 | 
					 | 
				
			||||||
							><DeleteRestore size="16" /></button
 | 
					 | 
				
			||||||
						>
 | 
					 | 
				
			||||||
					{/if}
 | 
					 | 
				
			||||||
				</td>
 | 
					 | 
				
			||||||
			</tr>
 | 
					 | 
				
			||||||
		{/each}
 | 
					 | 
				
			||||||
	</tbody>
 | 
					 | 
				
			||||||
</table>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>
 | 
					 | 
				
			||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
<section
 | 
					<section
 | 
				
			||||||
	in:fade={{ duration: 100 }}
 | 
						in:fade={{ duration: 100 }}
 | 
				
			||||||
	out:fade={{ duration: 100 }}
 | 
						out:fade={{ duration: 100 }}
 | 
				
			||||||
	class="absolute w-full h-full bg-black/40 z-[100] flex place-items-center place-content-center "
 | 
						class="absolute left-0 top-0 w-full h-full bg-black/40 z-[100] flex place-items-center place-content-center"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<div class="z-[9999]" use:clickOutside on:out-click={() => dispatch('clickOutside')}>
 | 
						<div class="z-[9999]" use:clickOutside on:out-click={() => dispatch('clickOutside')}>
 | 
				
			||||||
		<slot />
 | 
							<slot />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@
 | 
				
			|||||||
	import { clickOutside } from '../../utils/click-outside';
 | 
						import { clickOutside } from '../../utils/click-outside';
 | 
				
			||||||
	import { api, UserResponseDto } from '@api';
 | 
						import { api, UserResponseDto } from '@api';
 | 
				
			||||||
	import ThemeButton from './theme-button.svelte';
 | 
						import ThemeButton from './theme-button.svelte';
 | 
				
			||||||
 | 
						import { AppRoute } from '../../constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let user: UserResponseDto;
 | 
						export let user: UserResponseDto;
 | 
				
			||||||
	export let shouldShowUploadButton = true;
 | 
						export let shouldShowUploadButton = true;
 | 
				
			||||||
@@ -70,7 +71,7 @@
 | 
				
			|||||||
		<section class="flex gap-4 place-items-center">
 | 
							<section class="flex gap-4 place-items-center">
 | 
				
			||||||
			<ThemeButton />
 | 
								<ThemeButton />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			{#if $page.url.pathname !== '/admin' && shouldShowUploadButton}
 | 
								{#if !$page.url.pathname.includes('/admin') && shouldShowUploadButton}
 | 
				
			||||||
				<button
 | 
									<button
 | 
				
			||||||
					in:fly={{ x: 50, duration: 250 }}
 | 
										in:fly={{ x: 50, duration: 250 }}
 | 
				
			||||||
					on:click={() => dispatch('uploadClicked')}
 | 
										on:click={() => dispatch('uploadClicked')}
 | 
				
			||||||
@@ -82,10 +83,10 @@
 | 
				
			|||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			{#if user.isAdmin}
 | 
								{#if user.isAdmin}
 | 
				
			||||||
				<a data-sveltekit-preload-data="hover" href={`admin`}>
 | 
									<a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_USER_MANAGEMENT}>
 | 
				
			||||||
					<button
 | 
										<button
 | 
				
			||||||
						class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5  dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg p-2 rounded-lg font-medium ${
 | 
											class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5  dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg p-2 rounded-lg font-medium ${
 | 
				
			||||||
							$page.url.pathname == '/admin' &&
 | 
												$page.url.pathname.includes('/admin') &&
 | 
				
			||||||
							'text-immich-primary dark:immich-dark-primary underline'
 | 
												'text-immich-primary dark:immich-dark-primary underline'
 | 
				
			||||||
						}`}>Administration</button
 | 
											}`}>Administration</button
 | 
				
			||||||
					>
 | 
										>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,22 +3,12 @@
 | 
				
			|||||||
	// TODO: why `any` here? There should be a expected type for this
 | 
						// TODO: why `any` here? There should be a expected type for this
 | 
				
			||||||
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
						// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
	export let logo: any;
 | 
						export let logo: any;
 | 
				
			||||||
	export let actionType: AdminSideBarSelection | AppSideBarSelection;
 | 
					 | 
				
			||||||
	export let isSelected: boolean;
 | 
						export let isSelected: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	import { createEventDispatcher } from 'svelte';
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
	import type {
 | 
					 | 
				
			||||||
		AdminSideBarSelection,
 | 
					 | 
				
			||||||
		AppSideBarSelection
 | 
					 | 
				
			||||||
	} from '../../../models/admin-sidebar-selection';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const dispatch = createEventDispatcher();
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
						const onButtonClicked = () => dispatch('selected');
 | 
				
			||||||
	const onButtonClicked = () => {
 | 
					 | 
				
			||||||
		dispatch('selected', {
 | 
					 | 
				
			||||||
			actionType
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
 | 
					 | 
				
			||||||
	import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
	import { page } from '$app/stores';
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
	import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 | 
						import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 | 
				
			||||||
	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
						import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
				
			||||||
@@ -11,28 +9,12 @@
 | 
				
			|||||||
	import { api } from '@api';
 | 
						import { api } from '@api';
 | 
				
			||||||
	import { fade } from 'svelte/transition';
 | 
						import { fade } from 'svelte/transition';
 | 
				
			||||||
	import LoadingSpinner from '../loading-spinner.svelte';
 | 
						import LoadingSpinner from '../loading-spinner.svelte';
 | 
				
			||||||
 | 
						import { AppRoute } from '../../../constants';
 | 
				
			||||||
	let selectedAction: AppSideBarSelection;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let showAssetCount = false;
 | 
						let showAssetCount = false;
 | 
				
			||||||
	let showSharingCount = false;
 | 
						let showSharingCount = false;
 | 
				
			||||||
	let showAlbumsCount = false;
 | 
						let showAlbumsCount = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// let domCount = 0;
 | 
					 | 
				
			||||||
	onMount(async () => {
 | 
					 | 
				
			||||||
		if ($page.route.id == 'albums') {
 | 
					 | 
				
			||||||
			selectedAction = AppSideBarSelection.ALBUMS;
 | 
					 | 
				
			||||||
		} else if ($page.route.id == 'photos') {
 | 
					 | 
				
			||||||
			selectedAction = AppSideBarSelection.PHOTOS;
 | 
					 | 
				
			||||||
		} else if ($page.route.id == 'sharing') {
 | 
					 | 
				
			||||||
			selectedAction = AppSideBarSelection.SHARING;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// setInterval(() => {
 | 
					 | 
				
			||||||
		// 	domCount = document.getElementsByTagName('*').length;
 | 
					 | 
				
			||||||
		// }, 500);
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const getAssetCount = async () => {
 | 
						const getAssetCount = async () => {
 | 
				
			||||||
		const { data: assetCount } = await api.assetApi.getAssetCountByUserId();
 | 
							const { data: assetCount } = await api.assetApi.getAssetCountByUserId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,14 +38,13 @@
 | 
				
			|||||||
	<a
 | 
						<a
 | 
				
			||||||
		data-sveltekit-preload-data="hover"
 | 
							data-sveltekit-preload-data="hover"
 | 
				
			||||||
		data-sveltekit-noscroll
 | 
							data-sveltekit-noscroll
 | 
				
			||||||
		href={$page.route.id !== 'photos' ? `/photos` : null}
 | 
							href={AppRoute.PHOTOS}
 | 
				
			||||||
		class="relative"
 | 
							class="relative"
 | 
				
			||||||
	>
 | 
						>
 | 
				
			||||||
		<SideBarButton
 | 
							<SideBarButton
 | 
				
			||||||
			title={`Photos`}
 | 
								title={`Photos`}
 | 
				
			||||||
			logo={ImageOutline}
 | 
								logo={ImageOutline}
 | 
				
			||||||
			actionType={AppSideBarSelection.PHOTOS}
 | 
								isSelected={$page.route.id === AppRoute.PHOTOS}
 | 
				
			||||||
			isSelected={selectedAction === AppSideBarSelection.PHOTOS}
 | 
					 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
			id="asset-count-info"
 | 
								id="asset-count-info"
 | 
				
			||||||
@@ -75,7 +56,6 @@
 | 
				
			|||||||
			{#if showAssetCount}
 | 
								{#if showAssetCount}
 | 
				
			||||||
				<div
 | 
									<div
 | 
				
			||||||
					transition:fade={{ duration: 200 }}
 | 
										transition:fade={{ duration: 200 }}
 | 
				
			||||||
					id="asset-count-info-detail"
 | 
					 | 
				
			||||||
					class="w-32 rounded-lg p-4 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
 | 
										class="w-32 rounded-lg p-4 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
 | 
				
			||||||
				>
 | 
									>
 | 
				
			||||||
					{#await getAssetCount()}
 | 
										{#await getAssetCount()}
 | 
				
			||||||
@@ -91,16 +71,11 @@
 | 
				
			|||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</a>
 | 
						</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<a
 | 
						<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} class="relative">
 | 
				
			||||||
		data-sveltekit-preload-data="hover"
 | 
					 | 
				
			||||||
		href={$page.route.id !== 'sharing' ? `/sharing` : null}
 | 
					 | 
				
			||||||
		class="relative"
 | 
					 | 
				
			||||||
	>
 | 
					 | 
				
			||||||
		<SideBarButton
 | 
							<SideBarButton
 | 
				
			||||||
			title="Sharing"
 | 
								title="Sharing"
 | 
				
			||||||
			logo={AccountMultipleOutline}
 | 
								logo={AccountMultipleOutline}
 | 
				
			||||||
			actionType={AppSideBarSelection.SHARING}
 | 
								isSelected={$page.route.id === AppRoute.SHARING}
 | 
				
			||||||
			isSelected={selectedAction === AppSideBarSelection.SHARING}
 | 
					 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
			id="sharing-count-info"
 | 
								id="sharing-count-info"
 | 
				
			||||||
@@ -112,7 +87,6 @@
 | 
				
			|||||||
			{#if showSharingCount}
 | 
								{#if showSharingCount}
 | 
				
			||||||
				<div
 | 
									<div
 | 
				
			||||||
					transition:fade={{ duration: 200 }}
 | 
										transition:fade={{ duration: 200 }}
 | 
				
			||||||
					id="asset-count-info-detail"
 | 
					 | 
				
			||||||
					class="w-24 rounded-lg p-4 shadow-lg bg-white absolute -right-[105px] top-0 z-[9999] flex place-items-center place-content-center"
 | 
										class="w-24 rounded-lg p-4 shadow-lg bg-white absolute -right-[105px] top-0 z-[9999] flex place-items-center place-content-center"
 | 
				
			||||||
				>
 | 
									>
 | 
				
			||||||
					{#await getAlbumCount()}
 | 
										{#await getAlbumCount()}
 | 
				
			||||||
@@ -129,16 +103,11 @@
 | 
				
			|||||||
	<div class="text-xs ml-5 my-4 dark:text-immich-dark-fg">
 | 
						<div class="text-xs ml-5 my-4 dark:text-immich-dark-fg">
 | 
				
			||||||
		<p>LIBRARY</p>
 | 
							<p>LIBRARY</p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<a
 | 
						<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} class="relative">
 | 
				
			||||||
		data-sveltekit-preload-data="hover"
 | 
					 | 
				
			||||||
		href={$page.route.id !== 'albums' ? `/albums` : null}
 | 
					 | 
				
			||||||
		class="relative"
 | 
					 | 
				
			||||||
	>
 | 
					 | 
				
			||||||
		<SideBarButton
 | 
							<SideBarButton
 | 
				
			||||||
			title="Albums"
 | 
								title="Albums"
 | 
				
			||||||
			logo={ImageAlbum}
 | 
								logo={ImageAlbum}
 | 
				
			||||||
			actionType={AppSideBarSelection.ALBUMS}
 | 
								isSelected={$page.route.id === AppRoute.ALBUMS}
 | 
				
			||||||
			isSelected={selectedAction === AppSideBarSelection.ALBUMS}
 | 
					 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,2 +1,13 @@
 | 
				
			|||||||
import { env } from '$env/dynamic/public';
 | 
					import { env } from '$env/dynamic/public';
 | 
				
			||||||
export const loginPageMessage: string | undefined = env.PUBLIC_LOGIN_PAGE_MESSAGE;
 | 
					export const loginPageMessage: string | undefined = env.PUBLIC_LOGIN_PAGE_MESSAGE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum AppRoute {
 | 
				
			||||||
 | 
						ADMIN_USER_MANAGEMENT = '/admin/user-management',
 | 
				
			||||||
 | 
						ADMIN_SETTINGS = '/admin/settings',
 | 
				
			||||||
 | 
						ADMIN_STATS = '/admin/server-status',
 | 
				
			||||||
 | 
						ADMIN_JOBS = '/admin/jobs-status',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ALBUMS = '/albums',
 | 
				
			||||||
 | 
						PHOTOS = '/photos',
 | 
				
			||||||
 | 
						SHARING = '/sharing'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
export enum AdminSideBarSelection {
 | 
					 | 
				
			||||||
	USER_MANAGEMENT = 'User management',
 | 
					 | 
				
			||||||
	JOBS = 'Jobs',
 | 
					 | 
				
			||||||
	SETTINGS = 'Settings',
 | 
					 | 
				
			||||||
	STATS = 'Server Stats'
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum AppSideBarSelection {
 | 
					 | 
				
			||||||
	PHOTOS = 'Photos',
 | 
					 | 
				
			||||||
	EXPLORE = 'Explore',
 | 
					 | 
				
			||||||
	ALBUMS = 'Albums',
 | 
					 | 
				
			||||||
	SHARING = 'Sharing'
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,3 +1,82 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
 | 
				
			||||||
 | 
						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 StatusBox from '$lib/components/shared-components/status-box.svelte';
 | 
				
			||||||
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
						import { AppRoute } from '../../lib/constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const getPageTitle = (routeId: string | null) => {
 | 
				
			||||||
 | 
							switch (routeId) {
 | 
				
			||||||
 | 
								case AppRoute.ADMIN_USER_MANAGEMENT:
 | 
				
			||||||
 | 
									return 'User Management';
 | 
				
			||||||
 | 
								case AppRoute.ADMIN_SETTINGS:
 | 
				
			||||||
 | 
									return 'Settings';
 | 
				
			||||||
 | 
								case AppRoute.ADMIN_JOBS:
 | 
				
			||||||
 | 
									return 'Jobs';
 | 
				
			||||||
 | 
								case AppRoute.ADMIN_STATS:
 | 
				
			||||||
 | 
									return 'Server Stats';
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									return '';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>Administration - Immich</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<NavigationBar user={$page.data.user} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<main>
 | 
					<main>
 | 
				
			||||||
 | 
						<section class="grid grid-cols-[250px_auto] pt-[72px] h-screen">
 | 
				
			||||||
 | 
							<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col gap-1">
 | 
				
			||||||
 | 
								<SideBarButton
 | 
				
			||||||
 | 
									title="Users"
 | 
				
			||||||
 | 
									logo={AccountMultipleOutline}
 | 
				
			||||||
 | 
									isSelected={$page.route.id === AppRoute.ADMIN_USER_MANAGEMENT}
 | 
				
			||||||
 | 
									on:selected={() => goto(AppRoute.ADMIN_USER_MANAGEMENT)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
								<SideBarButton
 | 
				
			||||||
 | 
									title="Jobs"
 | 
				
			||||||
 | 
									logo={Sync}
 | 
				
			||||||
 | 
									isSelected={$page.route.id === AppRoute.ADMIN_JOBS}
 | 
				
			||||||
 | 
									on:selected={() => goto(AppRoute.ADMIN_JOBS)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
								<SideBarButton
 | 
				
			||||||
 | 
									title="Settings"
 | 
				
			||||||
 | 
									logo={Cog}
 | 
				
			||||||
 | 
									isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS}
 | 
				
			||||||
 | 
									on:selected={() => goto(AppRoute.ADMIN_SETTINGS)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
								<SideBarButton
 | 
				
			||||||
 | 
									title="Server Stats"
 | 
				
			||||||
 | 
									logo={Server}
 | 
				
			||||||
 | 
									isSelected={$page.route.id === AppRoute.ADMIN_STATS}
 | 
				
			||||||
 | 
									on:selected={() => goto(AppRoute.ADMIN_STATS)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
								<div class="mb-6 mt-auto">
 | 
				
			||||||
 | 
									<StatusBox />
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<section class="overflow-y-auto ">
 | 
				
			||||||
 | 
								<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg dark:bg-immich-dark-bg">
 | 
				
			||||||
 | 
									<h1 class="text-lg ml-8 mb-4 text-immich-primary dark:text-immich-dark-primary font-medium">
 | 
				
			||||||
 | 
										{getPageTitle($page.route.id)}
 | 
				
			||||||
 | 
									</h1>
 | 
				
			||||||
 | 
									<hr class="dark:border-immich-dark-gray" />
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<section id="setting-content" class="pt-[85px] flex place-content-center">
 | 
				
			||||||
 | 
									<section class="w-[800px] pt-5">
 | 
				
			||||||
					<slot />
 | 
										<slot />
 | 
				
			||||||
 | 
									</section>
 | 
				
			||||||
 | 
								</section>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
</main>
 | 
					</main>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import { redirect } from '@sveltejs/kit';
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
import { serverApi } from '@api';
 | 
					 | 
				
			||||||
import type { PageServerLoad } from './$types';
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load: PageServerLoad = async ({ parent }) => {
 | 
					export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			||||||
@@ -11,7 +10,5 @@ export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			|||||||
		throw redirect(302, '/photos');
 | 
							throw redirect(302, '/photos');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
 | 
						throw redirect(302, '/admin/user-management');
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return { user, allUsers };
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,249 +0,0 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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';
 | 
					 | 
				
			||||||
	import UserManagement from '$lib/components/admin-page/user-management.svelte';
 | 
					 | 
				
			||||||
	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
					 | 
				
			||||||
	import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
 | 
					 | 
				
			||||||
	import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
 | 
					 | 
				
			||||||
	import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
 | 
					 | 
				
			||||||
	import StatusBox from '$lib/components/shared-components/status-box.svelte';
 | 
					 | 
				
			||||||
	import 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';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export let data: PageData;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let selectedUser: UserResponseDto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let shouldShowEditUserForm = false;
 | 
					 | 
				
			||||||
	let shouldShowCreateUserForm = false;
 | 
					 | 
				
			||||||
	let shouldShowInfoPanel = false;
 | 
					 | 
				
			||||||
	let shouldShowDeleteConfirmDialog = false;
 | 
					 | 
				
			||||||
	let shouldShowRestoreDialog = false;
 | 
					 | 
				
			||||||
	let serverStat: ServerStatsResponseDto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onButtonClicked = (buttonType: CustomEvent) => {
 | 
					 | 
				
			||||||
		selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	onMount(() => {
 | 
					 | 
				
			||||||
		selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
 | 
					 | 
				
			||||||
		getServerStats();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onUserCreated = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowCreateUserForm = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const editUserHandler = async (event: CustomEvent) => {
 | 
					 | 
				
			||||||
		const { user } = event.detail;
 | 
					 | 
				
			||||||
		selectedUser = user;
 | 
					 | 
				
			||||||
		shouldShowEditUserForm = true;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onEditUserSuccess = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowEditUserForm = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onEditPasswordSuccess = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowEditUserForm = false;
 | 
					 | 
				
			||||||
		shouldShowInfoPanel = true;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const deleteUserHandler = async (event: CustomEvent) => {
 | 
					 | 
				
			||||||
		const { user } = event.detail;
 | 
					 | 
				
			||||||
		selectedUser = user;
 | 
					 | 
				
			||||||
		shouldShowDeleteConfirmDialog = true;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onUserDeleteSuccess = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowDeleteConfirmDialog = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onUserDeleteFail = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowDeleteConfirmDialog = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const restoreUserHandler = async (event: CustomEvent) => {
 | 
					 | 
				
			||||||
		const { user } = event.detail;
 | 
					 | 
				
			||||||
		selectedUser = user;
 | 
					 | 
				
			||||||
		shouldShowRestoreDialog = true;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onUserRestoreSuccess = async () => {
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowRestoreDialog = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const onUserRestoreFail = async () => {
 | 
					 | 
				
			||||||
		// show fail dialog
 | 
					 | 
				
			||||||
		const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
					 | 
				
			||||||
		data.allUsers = getAllUsersRes.data;
 | 
					 | 
				
			||||||
		shouldShowRestoreDialog = false;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const getServerStats = async () => {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const res = await api.serverInfoApi.getStats();
 | 
					 | 
				
			||||||
			serverStat = res.data;
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			console.log(e);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<svelte:head>
 | 
					 | 
				
			||||||
	<title>Administration - Immich</title>
 | 
					 | 
				
			||||||
</svelte:head>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<NavigationBar user={data.user} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if shouldShowCreateUserForm}
 | 
					 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
 | 
					 | 
				
			||||||
		<CreateUserForm on:user-created={onUserCreated} />
 | 
					 | 
				
			||||||
	</FullScreenModal>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if shouldShowEditUserForm}
 | 
					 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
 | 
					 | 
				
			||||||
		<EditUserForm
 | 
					 | 
				
			||||||
			user={selectedUser}
 | 
					 | 
				
			||||||
			on:edit-success={onEditUserSuccess}
 | 
					 | 
				
			||||||
			on:reset-password-success={onEditPasswordSuccess}
 | 
					 | 
				
			||||||
		/>
 | 
					 | 
				
			||||||
	</FullScreenModal>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if shouldShowDeleteConfirmDialog}
 | 
					 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowDeleteConfirmDialog = false)}>
 | 
					 | 
				
			||||||
		<DeleteConfirmDialog
 | 
					 | 
				
			||||||
			user={selectedUser}
 | 
					 | 
				
			||||||
			on:user-delete-success={onUserDeleteSuccess}
 | 
					 | 
				
			||||||
			on:user-delete-fail={onUserDeleteFail}
 | 
					 | 
				
			||||||
		/>
 | 
					 | 
				
			||||||
	</FullScreenModal>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if shouldShowRestoreDialog}
 | 
					 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowRestoreDialog = false)}>
 | 
					 | 
				
			||||||
		<RestoreDialoge
 | 
					 | 
				
			||||||
			user={selectedUser}
 | 
					 | 
				
			||||||
			on:user-restore-success={onUserRestoreSuccess}
 | 
					 | 
				
			||||||
			on:user-restore-fail={onUserRestoreFail}
 | 
					 | 
				
			||||||
		/>
 | 
					 | 
				
			||||||
	</FullScreenModal>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if shouldShowInfoPanel}
 | 
					 | 
				
			||||||
	<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
 | 
					 | 
				
			||||||
		<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
 | 
					 | 
				
			||||||
			<h1 class="font-medium text-immich-primary text-lg mb-4">Password reset success</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<p>
 | 
					 | 
				
			||||||
				The user's password has been reset to the default <code
 | 
					 | 
				
			||||||
					class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code
 | 
					 | 
				
			||||||
				>
 | 
					 | 
				
			||||||
				<br />
 | 
					 | 
				
			||||||
				Please inform the user, and they will need to change the password at the next log-on.
 | 
					 | 
				
			||||||
			</p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<div class="flex w-full">
 | 
					 | 
				
			||||||
				<button
 | 
					 | 
				
			||||||
					on:click={() => (shouldShowInfoPanel = false)}
 | 
					 | 
				
			||||||
					class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
 | 
					 | 
				
			||||||
					>Done
 | 
					 | 
				
			||||||
				</button>
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	</FullScreenModal>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
 | 
					 | 
				
			||||||
	<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col gap-1">
 | 
					 | 
				
			||||||
		<SideBarButton
 | 
					 | 
				
			||||||
			title="Users"
 | 
					 | 
				
			||||||
			logo={AccountMultipleOutline}
 | 
					 | 
				
			||||||
			actionType={AdminSideBarSelection.USER_MANAGEMENT}
 | 
					 | 
				
			||||||
			isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
 | 
					 | 
				
			||||||
			on:selected={onButtonClicked}
 | 
					 | 
				
			||||||
		/>
 | 
					 | 
				
			||||||
		<SideBarButton
 | 
					 | 
				
			||||||
			title="Jobs"
 | 
					 | 
				
			||||||
			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}
 | 
					 | 
				
			||||||
			actionType={AdminSideBarSelection.STATS}
 | 
					 | 
				
			||||||
			isSelected={selectedAction === AdminSideBarSelection.STATS}
 | 
					 | 
				
			||||||
			on:selected={onButtonClicked}
 | 
					 | 
				
			||||||
		/>
 | 
					 | 
				
			||||||
		<div class="mb-6 mt-auto">
 | 
					 | 
				
			||||||
			<StatusBox />
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	</section>
 | 
					 | 
				
			||||||
	<section class="overflow-y-auto relative">
 | 
					 | 
				
			||||||
		<div id="setting-title" class="pt-10 fixed w-full z-50">
 | 
					 | 
				
			||||||
			<h1 class="text-lg ml-8 mb-4 text-immich-primary dark:text-immich-dark-primary font-medium">
 | 
					 | 
				
			||||||
				{selectedAction}
 | 
					 | 
				
			||||||
			</h1>
 | 
					 | 
				
			||||||
			<hr class="dark:border-immich-dark-gray" />
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<section id="setting-content" class="relative pt-[85px] flex place-content-center">
 | 
					 | 
				
			||||||
			<section class="w-[800px] pt-5">
 | 
					 | 
				
			||||||
				{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
 | 
					 | 
				
			||||||
					<UserManagement
 | 
					 | 
				
			||||||
						allUsers={data.allUsers}
 | 
					 | 
				
			||||||
						on:create-user={() => (shouldShowCreateUserForm = true)}
 | 
					 | 
				
			||||||
						on:edit-user={editUserHandler}
 | 
					 | 
				
			||||||
						on:delete-user={deleteUserHandler}
 | 
					 | 
				
			||||||
						on:restore-user={restoreUserHandler}
 | 
					 | 
				
			||||||
					/>
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
				{#if selectedAction === AdminSideBarSelection.JOBS}
 | 
					 | 
				
			||||||
					<JobsPanel />
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
				{#if selectedAction === AdminSideBarSelection.SETTINGS}
 | 
					 | 
				
			||||||
					<SettingsPanel />
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
				{#if selectedAction === AdminSideBarSelection.STATS && serverStat}
 | 
					 | 
				
			||||||
					<ServerStatsPanel stats={serverStat} allUsers={data.allUsers} />
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
			</section>
 | 
					 | 
				
			||||||
		</section>
 | 
					 | 
				
			||||||
	</section>
 | 
					 | 
				
			||||||
</section>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								web/src/routes/admin/jobs-status/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/src/routes/admin/jobs-status/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						} else if (!user.isAdmin) {
 | 
				
			||||||
 | 
							throw redirect(302, '/photos');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								web/src/routes/admin/jobs-status/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/src/routes/admin/jobs-status/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
						import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>Jobs Status - Immich</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section>
 | 
				
			||||||
 | 
						<JobsPanel />
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
							
								
								
									
										17
									
								
								web/src/routes/admin/server-status/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/src/routes/admin/server-status/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import { serverApi } from '@api';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						} else if (!user.isAdmin) {
 | 
				
			||||||
 | 
							throw redirect(302, '/photos');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return { user, allUsers };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										29
									
								
								web/src/routes/admin/server-status/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/src/routes/admin/server-status/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
 | 
				
			||||||
 | 
						import { api, ServerStatsResponseDto } from '@api';
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let serverStat: ServerStatsResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							getServerStats();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const getServerStats = async () => {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const res = await api.serverInfoApi.getStats();
 | 
				
			||||||
 | 
								serverStat = res.data;
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								console.log(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>Jobs Status - Immich</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if $page.data.allUsers && serverStat}
 | 
				
			||||||
 | 
						<ServerStatsPanel stats={serverStat} allUsers={$page.data.allUsers} />
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
							
								
								
									
										14
									
								
								web/src/routes/admin/settings/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/src/routes/admin/settings/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						} else if (!user.isAdmin) {
 | 
				
			||||||
 | 
							throw redirect(302, '/photos');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return { user };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										33
									
								
								web/src/routes/admin/settings/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/routes/admin/settings/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
 | 
				
			||||||
 | 
						import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
 | 
				
			||||||
 | 
						import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
 | 
				
			||||||
 | 
						import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
				
			||||||
 | 
						import { api, SystemConfigDto } from '@api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let systemConfig: SystemConfigDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const getConfig = async () => {
 | 
				
			||||||
 | 
							const { data } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
							systemConfig = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return data;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section class="">
 | 
				
			||||||
 | 
						{#await getConfig()}
 | 
				
			||||||
 | 
							<LoadingSpinner />
 | 
				
			||||||
 | 
						{:then configs}
 | 
				
			||||||
 | 
							<SettingAccordion
 | 
				
			||||||
 | 
								title="FFmpeg Settings"
 | 
				
			||||||
 | 
								subtitle="Manage the resolution and encoding information of the video files"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								<FFmpegSettings ffmpegConfig={configs.ffmpeg} />
 | 
				
			||||||
 | 
							</SettingAccordion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<SettingAccordion title="OAuth Settings" subtitle="Manage the OAuth integration to Immich app">
 | 
				
			||||||
 | 
								<OAuthSettings oauthConfig={configs.oauth} />
 | 
				
			||||||
 | 
							</SettingAccordion>
 | 
				
			||||||
 | 
						{/await}
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
							
								
								
									
										17
									
								
								web/src/routes/admin/user-management/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/src/routes/admin/user-management/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import { serverApi } from '@api';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ parent }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						} else if (!user.isAdmin) {
 | 
				
			||||||
 | 
							throw redirect(302, '/photos');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return { user, allUsers };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										232
									
								
								web/src/routes/admin/user-management/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								web/src/routes/admin/user-management/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { api, UserResponseDto } from '@api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
 | 
				
			||||||
 | 
						import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
 | 
				
			||||||
 | 
						import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
 | 
				
			||||||
 | 
						import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
				
			||||||
 | 
						import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
 | 
				
			||||||
 | 
						import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
 | 
				
			||||||
 | 
						import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
 | 
				
			||||||
 | 
						import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let allUsers: UserResponseDto[] = [];
 | 
				
			||||||
 | 
						let shouldShowEditUserForm = false;
 | 
				
			||||||
 | 
						let shouldShowCreateUserForm = false;
 | 
				
			||||||
 | 
						let shouldShowInfoPanel = false;
 | 
				
			||||||
 | 
						let shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						let shouldShowRestoreDialog = false;
 | 
				
			||||||
 | 
						let selectedUser: UserResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							allUsers = $page.data.allUsers;
 | 
				
			||||||
 | 
							console.log('getting all users', allUsers);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const isDeleted = (user: UserResponseDto): boolean => {
 | 
				
			||||||
 | 
							return user.deletedAt != null;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const locale = navigator.language;
 | 
				
			||||||
 | 
						const deleteDateFormat: Intl.DateTimeFormatOptions = {
 | 
				
			||||||
 | 
							month: 'long',
 | 
				
			||||||
 | 
							day: 'numeric',
 | 
				
			||||||
 | 
							year: 'numeric'
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const getDeleteDate = (user: UserResponseDto): string => {
 | 
				
			||||||
 | 
							let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
 | 
				
			||||||
 | 
							deletedAt.setDate(deletedAt.getDate() + 7);
 | 
				
			||||||
 | 
							return deletedAt.toLocaleString(locale, deleteDateFormat);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserCreated = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowCreateUserForm = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const editUserHandler = async (user: UserResponseDto) => {
 | 
				
			||||||
 | 
							selectedUser = user;
 | 
				
			||||||
 | 
							shouldShowEditUserForm = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onEditUserSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowEditUserForm = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onEditPasswordSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowEditUserForm = false;
 | 
				
			||||||
 | 
							shouldShowInfoPanel = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const deleteUserHandler = async (user: UserResponseDto) => {
 | 
				
			||||||
 | 
							selectedUser = user;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserDeleteSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserDeleteFail = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowDeleteConfirmDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const restoreUserHandler = async (user: UserResponseDto) => {
 | 
				
			||||||
 | 
							selectedUser = user;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = true;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserRestoreSuccess = async () => {
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onUserRestoreFail = async () => {
 | 
				
			||||||
 | 
							// show fail dialog
 | 
				
			||||||
 | 
							const getAllUsersRes = await api.userApi.getAllUsers(false);
 | 
				
			||||||
 | 
							allUsers = getAllUsersRes.data;
 | 
				
			||||||
 | 
							shouldShowRestoreDialog = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>User Management - Immich</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section>
 | 
				
			||||||
 | 
						{#if shouldShowCreateUserForm}
 | 
				
			||||||
 | 
							<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
 | 
				
			||||||
 | 
								<CreateUserForm on:user-created={onUserCreated} />
 | 
				
			||||||
 | 
							</FullScreenModal>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{#if shouldShowEditUserForm}
 | 
				
			||||||
 | 
							<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
 | 
				
			||||||
 | 
								<EditUserForm
 | 
				
			||||||
 | 
									user={selectedUser}
 | 
				
			||||||
 | 
									on:edit-success={onEditUserSuccess}
 | 
				
			||||||
 | 
									on:reset-password-success={onEditPasswordSuccess}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</FullScreenModal>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{#if shouldShowDeleteConfirmDialog}
 | 
				
			||||||
 | 
							<FullScreenModal on:clickOutside={() => (shouldShowDeleteConfirmDialog = false)}>
 | 
				
			||||||
 | 
								<DeleteConfirmDialog
 | 
				
			||||||
 | 
									user={selectedUser}
 | 
				
			||||||
 | 
									on:user-delete-success={onUserDeleteSuccess}
 | 
				
			||||||
 | 
									on:user-delete-fail={onUserDeleteFail}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</FullScreenModal>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{#if shouldShowRestoreDialog}
 | 
				
			||||||
 | 
							<FullScreenModal on:clickOutside={() => (shouldShowRestoreDialog = false)}>
 | 
				
			||||||
 | 
								<RestoreDialogue
 | 
				
			||||||
 | 
									user={selectedUser}
 | 
				
			||||||
 | 
									on:user-restore-success={onUserRestoreSuccess}
 | 
				
			||||||
 | 
									on:user-restore-fail={onUserRestoreFail}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</FullScreenModal>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{#if shouldShowInfoPanel}
 | 
				
			||||||
 | 
							<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
 | 
				
			||||||
 | 
								<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
 | 
				
			||||||
 | 
									<h1 class="font-medium text-immich-primary text-lg mb-4">Password reset success</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<p>
 | 
				
			||||||
 | 
										The user's password has been reset to the default <code
 | 
				
			||||||
 | 
											class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
										<br />
 | 
				
			||||||
 | 
										Please inform the user, and they will need to change the password at the next log-on.
 | 
				
			||||||
 | 
									</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="flex w-full">
 | 
				
			||||||
 | 
										<button
 | 
				
			||||||
 | 
											on:click={() => (shouldShowInfoPanel = false)}
 | 
				
			||||||
 | 
											class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
 | 
				
			||||||
 | 
											>Done
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</FullScreenModal>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<table class="text-left w-full my-5">
 | 
				
			||||||
 | 
							<thead
 | 
				
			||||||
 | 
								class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full 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/4 font-medium text-sm">Email</th>
 | 
				
			||||||
 | 
									<th class="text-center w-1/4 font-medium text-sm">First name</th>
 | 
				
			||||||
 | 
									<th class="text-center w-1/4 font-medium text-sm">Last name</th>
 | 
				
			||||||
 | 
									<th class="text-center w-1/4 font-medium text-sm">Action</th>
 | 
				
			||||||
 | 
								</tr>
 | 
				
			||||||
 | 
							</thead>
 | 
				
			||||||
 | 
							<tbody
 | 
				
			||||||
 | 
								class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								{#if allUsers}
 | 
				
			||||||
 | 
									{#each allUsers as user, i}
 | 
				
			||||||
 | 
										<tr
 | 
				
			||||||
 | 
											class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
 | 
				
			||||||
 | 
												isDeleted(user)
 | 
				
			||||||
 | 
													? 'bg-red-300 dark:bg-red-900'
 | 
				
			||||||
 | 
													: i % 2 == 0
 | 
				
			||||||
 | 
													? 'bg-immich-gray dark:bg-immich-dark-gray/75'
 | 
				
			||||||
 | 
													: 'bg-immich-bg dark:bg-immich-dark-gray/50'
 | 
				
			||||||
 | 
											}`}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
 | 
				
			||||||
 | 
											<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
 | 
				
			||||||
 | 
											<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
 | 
				
			||||||
 | 
											<td class="text-sm px-4 w-1/4 text-ellipsis">
 | 
				
			||||||
 | 
												{#if !isDeleted(user)}
 | 
				
			||||||
 | 
													<button
 | 
				
			||||||
 | 
														on:click={() => editUserHandler(user)}
 | 
				
			||||||
 | 
														class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														<PencilOutline size="16" />
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
													<button
 | 
				
			||||||
 | 
														on:click={() => deleteUserHandler(user)}
 | 
				
			||||||
 | 
														class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														<TrashCanOutline size="16" />
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
												{/if}
 | 
				
			||||||
 | 
												{#if isDeleted(user)}
 | 
				
			||||||
 | 
													<button
 | 
				
			||||||
 | 
														on:click={() => restoreUserHandler(user)}
 | 
				
			||||||
 | 
														class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700  rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 | 
				
			||||||
 | 
														title={`scheduled removal on ${getDeleteDate(user)}`}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														<DeleteRestore size="16" />
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
												{/if}
 | 
				
			||||||
 | 
											</td>
 | 
				
			||||||
 | 
										</tr>
 | 
				
			||||||
 | 
									{/each}
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</tbody>
 | 
				
			||||||
 | 
						</table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<button on:click={() => (shouldShowCreateUserForm = true)} class="immich-btn-primary"
 | 
				
			||||||
 | 
							>Create user</button
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user