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> | ||||||
| 	<slot /> | 	<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 /> | ||||||
|  | 				</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