From 5e680551b9795b8651ce6913da50556c6838ab56 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 9 Dec 2022 15:51:42 -0500 Subject: [PATCH] feat(server,web): migrate oauth settings from env to system config (#1061) --- Makefile | 3 + docs/docs/usage/oauth.md | 38 +-- mobile/openapi/.openapi-generator/FILES | 18 +- mobile/openapi/README.md | 7 +- mobile/openapi/doc/SystemConfigApi.md | 60 ++++- ...{SystemConfigKey.md => SystemConfigDto.md} | 4 +- ...esponseDto.md => SystemConfigFFmpegDto.md} | 8 +- ...esponseItem.md => SystemConfigOAuthDto.md} | 13 +- mobile/openapi/lib/api.dart | 6 +- mobile/openapi/lib/api/system_config_api.dart | 59 ++++- mobile/openapi/lib/api_client.dart | 12 +- mobile/openapi/lib/api_helper.dart | 3 - ...sponse_dto.dart => system_config_dto.dart} | 62 +++-- .../lib/model/system_config_f_fmpeg_dto.dart | 143 ++++++++++ .../openapi/lib/model/system_config_key.dart | 94 ------- .../lib/model/system_config_o_auth_dto.dart | 159 +++++++++++ .../model/system_config_response_item.dart | 135 ---------- .../openapi/test/system_config_api_test.dart | 9 +- ..._test.dart => system_config_dto_test.dart} | 15 +- .../test/system_config_f_fmpeg_dto_test.dart | 47 ++++ .../openapi/test/system_config_key_test.dart | 21 -- .../test/system_config_o_auth_dto_test.dart | 57 ++++ .../system_config_response_item_test.dart | 42 --- .../immich/src/api-v1/oauth/oauth.module.ts | 3 +- .../src/api-v1/oauth/oauth.service.spec.ts | 92 +++---- .../immich/src/api-v1/oauth/oauth.service.ts | 60 ++--- .../dto/system-config-ffmpeg.dto.ts | 18 ++ .../dto/system-config-oauth.dto.ts | 32 +++ .../system-config/dto/system-config.dto.ts | 16 ++ .../system-config/dto/update-system-config.ts | 20 -- .../system-config-response.dto.ts | 20 -- .../system-config/system-config.controller.ts | 12 +- .../system-config/system-config.service.ts | 21 +- .../processors/video-transcode.processor.ts | 12 +- server/immich-openapi-specs.json | 118 ++++++--- server/libs/common/src/config/app.config.ts | 13 - .../src/entities/system-config.entity.ts | 48 +++- .../1670607437008-TruncateOldConfigItems.ts | 11 + .../src/immich-config.service.ts | 97 +++---- server/nest-cli.json | 10 +- server/package.json | 2 +- server/tsconfig.json | 4 +- web/src/api/open-api/api.ts | 208 +++++++++++---- web/src/app.css | 2 +- .../settings/ffmpeg/ffmpeg-settings.svelte | 126 +++++++++ .../settings/oauth/oauth-settings.svelte | 147 +++++++++++ .../settings/setting-accordion.svelte | 56 ++++ .../settings/setting-buttons-row.svelte | 35 +++ .../settings/setting-input-field.svelte | 51 ++++ .../admin-page/settings/setting-switch.svelte | 81 ++++++ .../admin-page/settings/settings-panel.svelte | 97 ------- .../admin-page/user-management.svelte | 91 ------- .../full-screen-modal.svelte | 2 +- .../shared-components/navigation-bar.svelte | 7 +- .../side-bar/side-bar-button.svelte | 12 +- .../side-bar/side-bar.svelte | 45 +--- web/src/lib/constants.ts | 11 + web/src/lib/models/admin-sidebar-selection.ts | 13 - web/src/routes/admin/+layout.svelte | 81 +++++- web/src/routes/admin/+page.server.ts | 5 +- web/src/routes/admin/+page.svelte | 249 ------------------ .../routes/admin/jobs-status/+page.server.ts | 12 + web/src/routes/admin/jobs-status/+page.svelte | 11 + .../admin/server-status/+page.server.ts | 17 ++ .../routes/admin/server-status/+page.svelte | 29 ++ web/src/routes/admin/settings/+page.server.ts | 14 + web/src/routes/admin/settings/+page.svelte | 33 +++ .../admin/user-management/+page.server.ts | 17 ++ .../routes/admin/user-management/+page.svelte | 232 ++++++++++++++++ 69 files changed, 2079 insertions(+), 1229 deletions(-) rename mobile/openapi/doc/{SystemConfigKey.md => SystemConfigDto.md} (66%) rename mobile/openapi/doc/{SystemConfigResponseDto.md => SystemConfigFFmpegDto.md} (62%) rename mobile/openapi/doc/{SystemConfigResponseItem.md => SystemConfigOAuthDto.md} (56%) rename mobile/openapi/lib/model/{system_config_response_dto.dart => system_config_dto.dart} (55%) create mode 100644 mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart delete mode 100644 mobile/openapi/lib/model/system_config_key.dart create mode 100644 mobile/openapi/lib/model/system_config_o_auth_dto.dart delete mode 100644 mobile/openapi/lib/model/system_config_response_item.dart rename mobile/openapi/test/{system_config_response_dto_test.dart => system_config_dto_test.dart} (55%) create mode 100644 mobile/openapi/test/system_config_f_fmpeg_dto_test.dart delete mode 100644 mobile/openapi/test/system_config_key_test.dart create mode 100644 mobile/openapi/test/system_config_o_auth_dto_test.dart delete mode 100644 mobile/openapi/test/system_config_response_item_test.dart create mode 100644 server/apps/immich/src/api-v1/system-config/dto/system-config-ffmpeg.dto.ts create mode 100644 server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts create mode 100644 server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts delete mode 100644 server/apps/immich/src/api-v1/system-config/dto/update-system-config.ts delete mode 100644 server/apps/immich/src/api-v1/system-config/response-dto/system-config-response.dto.ts create mode 100644 server/libs/database/src/migrations/1670607437008-TruncateOldConfigItems.ts create mode 100644 web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte create mode 100644 web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte create mode 100644 web/src/lib/components/admin-page/settings/setting-accordion.svelte create mode 100644 web/src/lib/components/admin-page/settings/setting-buttons-row.svelte create mode 100644 web/src/lib/components/admin-page/settings/setting-input-field.svelte create mode 100644 web/src/lib/components/admin-page/settings/setting-switch.svelte delete mode 100644 web/src/lib/components/admin-page/settings/settings-panel.svelte delete mode 100644 web/src/lib/components/admin-page/user-management.svelte delete mode 100644 web/src/lib/models/admin-sidebar-selection.ts create mode 100644 web/src/routes/admin/jobs-status/+page.server.ts create mode 100644 web/src/routes/admin/jobs-status/+page.svelte create mode 100644 web/src/routes/admin/server-status/+page.server.ts create mode 100644 web/src/routes/admin/server-status/+page.svelte create mode 100644 web/src/routes/admin/settings/+page.server.ts create mode 100644 web/src/routes/admin/settings/+page.svelte create mode 100644 web/src/routes/admin/user-management/+page.server.ts create mode 100644 web/src/routes/admin/user-management/+page.svelte diff --git a/Makefile b/Makefile index 34c9619c51..90e048a9b4 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ dev: dev-new: 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: rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans diff --git a/docs/docs/usage/oauth.md b/docs/docs/usage/oauth.md index 78eb47aed5..85eba70d60 100644 --- a/docs/docs/usage/oauth.md +++ b/docs/docs/usage/oauth.md @@ -28,13 +28,13 @@ Before enabling OAuth in Immich, a new client application needs to be configured 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`) +- Mobile app redirect URL `app.immich:/` - * 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:/` - :::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. **Authentik example** @@ -42,17 +42,17 @@ You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobi ## 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_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_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_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 enabled | boolean | false | Enable/disable OAuth2 | +| 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 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 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 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. ::: -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/ diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 15fce354b1..608df54b10 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -61,9 +61,9 @@ doc/ServerVersionReponseDto.md doc/SignUpDto.md doc/SmartInfoResponseDto.md doc/SystemConfigApi.md -doc/SystemConfigKey.md -doc/SystemConfigResponseDto.md -doc/SystemConfigResponseItem.md +doc/SystemConfigDto.md +doc/SystemConfigFFmpegDto.md +doc/SystemConfigOAuthDto.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -149,9 +149,9 @@ lib/model/server_stats_response_dto.dart lib/model/server_version_reponse_dto.dart lib/model/sign_up_dto.dart lib/model/smart_info_response_dto.dart -lib/model/system_config_key.dart -lib/model/system_config_response_dto.dart -lib/model/system_config_response_item.dart +lib/model/system_config_dto.dart +lib/model/system_config_f_fmpeg_dto.dart +lib/model/system_config_o_auth_dto.dart lib/model/tag_response_dto.dart lib/model/tag_type_enum.dart lib/model/thumbnail_format.dart @@ -224,9 +224,9 @@ test/server_version_reponse_dto_test.dart test/sign_up_dto_test.dart test/smart_info_response_dto_test.dart test/system_config_api_test.dart -test/system_config_key_test.dart -test/system_config_response_dto_test.dart -test/system_config_response_item_test.dart +test/system_config_dto_test.dart +test/system_config_f_fmpeg_dto_test.dart +test/system_config_o_auth_dto_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a0482b3e03..76b13b2329 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -112,6 +112,7 @@ Class | Method | HTTP request | Description *ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | +*SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | *TagApi* | [**create**](doc//TagApi.md#create) | **POST** /tag | *TagApi* | [**delete**](doc//TagApi.md#delete) | **DELETE** /tag/{id} | @@ -182,9 +183,9 @@ Class | Method | HTTP request | Description - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md) - [SignUpDto](doc//SignUpDto.md) - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md) - - [SystemConfigKey](doc//SystemConfigKey.md) - - [SystemConfigResponseDto](doc//SystemConfigResponseDto.md) - - [SystemConfigResponseItem](doc//SystemConfigResponseItem.md) + - [SystemConfigDto](doc//SystemConfigDto.md) + - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) - [TagResponseDto](doc//TagResponseDto.md) - [TagTypeEnum](doc//TagTypeEnum.md) - [ThumbnailFormat](doc//ThumbnailFormat.md) diff --git a/mobile/openapi/doc/SystemConfigApi.md b/mobile/openapi/doc/SystemConfigApi.md index 9e40b4e81d..365ac3f764 100644 --- a/mobile/openapi/doc/SystemConfigApi.md +++ b/mobile/openapi/doc/SystemConfigApi.md @@ -10,11 +10,12 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config | +[**getDefaults**](SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults | [**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config | # **getConfig** -> SystemConfigResponseDto getConfig() +> SystemConfigDto getConfig() @@ -43,7 +44,7 @@ This endpoint does not need any parameter. ### Return type -[**SystemConfigResponseDto**](SystemConfigResponseDto.md) +[**SystemConfigDto**](SystemConfigDto.md) ### 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) -# **updateConfig** -> SystemConfigResponseDto updateConfig(body) +# **getDefaults** +> SystemConfigDto getDefaults() @@ -72,10 +73,53 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = SystemConfigApi(); -final body = Object(); // Object | 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('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemConfigApi(); +final systemConfigDto = SystemConfigDto(); // SystemConfigDto | + +try { + final result = api_instance.updateConfig(systemConfigDto); print(result); } catch (e) { print('Exception when calling SystemConfigApi->updateConfig: $e\n'); @@ -86,11 +130,11 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **body** | **Object**| | + **systemConfigDto** | [**SystemConfigDto**](SystemConfigDto.md)| | ### Return type -[**SystemConfigResponseDto**](SystemConfigResponseDto.md) +[**SystemConfigDto**](SystemConfigDto.md) ### Authorization diff --git a/mobile/openapi/doc/SystemConfigKey.md b/mobile/openapi/doc/SystemConfigDto.md similarity index 66% rename from mobile/openapi/doc/SystemConfigKey.md rename to mobile/openapi/doc/SystemConfigDto.md index b142ab5595..af283c4fde 100644 --- a/mobile/openapi/doc/SystemConfigKey.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigKey +# openapi.model.SystemConfigDto ## Load the model package ```dart @@ -8,6 +8,8 @@ import 'package:openapi/api.dart'; ## Properties 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) diff --git a/mobile/openapi/doc/SystemConfigResponseDto.md b/mobile/openapi/doc/SystemConfigFFmpegDto.md similarity index 62% rename from mobile/openapi/doc/SystemConfigResponseDto.md rename to mobile/openapi/doc/SystemConfigFFmpegDto.md index 506d531b96..b208d7b9ff 100644 --- a/mobile/openapi/doc/SystemConfigResponseDto.md +++ b/mobile/openapi/doc/SystemConfigFFmpegDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigResponseDto +# openapi.model.SystemConfigFFmpegDto ## Load the model package ```dart @@ -8,7 +8,11 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**config** | [**List**](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) diff --git a/mobile/openapi/doc/SystemConfigResponseItem.md b/mobile/openapi/doc/SystemConfigOAuthDto.md similarity index 56% rename from mobile/openapi/doc/SystemConfigResponseItem.md rename to mobile/openapi/doc/SystemConfigOAuthDto.md index 03b753cb6b..e91850e504 100644 --- a/mobile/openapi/doc/SystemConfigResponseItem.md +++ b/mobile/openapi/doc/SystemConfigOAuthDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigResponseItem +# openapi.model.SystemConfigOAuthDto ## Load the model package ```dart @@ -8,10 +8,13 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**name** | **String** | | -**key** | [**SystemConfigKey**](SystemConfigKey.md) | | -**value** | **String** | | -**defaultValue** | **String** | | +**enabled** | **bool** | | +**issuerUrl** | **String** | | +**clientId** | **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) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 69efb7a304..b30e4f6814 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -88,9 +88,9 @@ part 'model/server_stats_response_dto.dart'; part 'model/server_version_reponse_dto.dart'; part 'model/sign_up_dto.dart'; part 'model/smart_info_response_dto.dart'; -part 'model/system_config_key.dart'; -part 'model/system_config_response_dto.dart'; -part 'model/system_config_response_item.dart'; +part 'model/system_config_dto.dart'; +part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_o_auth_dto.dart'; part 'model/tag_response_dto.dart'; part 'model/tag_type_enum.dart'; part 'model/thumbnail_format.dart'; diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index e228c7199f..7bd66c6700 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -42,7 +42,7 @@ class SystemConfigApi { ); } - Future getConfig() async { + Future getConfig() async { final response = await getConfigWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { 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" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigResponseDto',) as SystemConfigResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigDto',) as SystemConfigDto; + + } + return null; + } + + /// Performs an HTTP 'GET /system-config/defaults' operation and returns the [Response]. + Future getDefaultsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-config/defaults'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future 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; @@ -60,13 +101,13 @@ class SystemConfigApi { /// Performs an HTTP 'PUT /system-config' operation and returns the [Response]. /// Parameters: /// - /// * [Object] body (required): - Future updateConfigWithHttpInfo(Object body,) async { + /// * [SystemConfigDto] systemConfigDto (required): + Future updateConfigWithHttpInfo(SystemConfigDto systemConfigDto,) async { // ignore: prefer_const_declarations final path = r'/system-config'; // ignore: prefer_final_locals - Object? postBody = body; + Object? postBody = systemConfigDto; final queryParams = []; final headerParams = {}; @@ -88,9 +129,9 @@ class SystemConfigApi { /// Parameters: /// - /// * [Object] body (required): - Future updateConfig(Object body,) async { - final response = await updateConfigWithHttpInfo(body,); + /// * [SystemConfigDto] systemConfigDto (required): + Future updateConfig(SystemConfigDto systemConfigDto,) async { + final response = await updateConfigWithHttpInfo(systemConfigDto,); if (response.statusCode >= HttpStatus.badRequest) { 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" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigResponseDto',) as SystemConfigResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SystemConfigDto',) as SystemConfigDto; } return null; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 5057f6950e..628b533e7e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -292,12 +292,12 @@ class ApiClient { return SignUpDto.fromJson(value); case 'SmartInfoResponseDto': return SmartInfoResponseDto.fromJson(value); - case 'SystemConfigKey': - return SystemConfigKeyTypeTransformer().decode(value); - case 'SystemConfigResponseDto': - return SystemConfigResponseDto.fromJson(value); - case 'SystemConfigResponseItem': - return SystemConfigResponseItem.fromJson(value); + case 'SystemConfigDto': + return SystemConfigDto.fromJson(value); + case 'SystemConfigFFmpegDto': + return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigOAuthDto': + return SystemConfigOAuthDto.fromJson(value); case 'TagResponseDto': return TagResponseDto.fromJson(value); case 'TagTypeEnum': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 828ff84fcb..c59dc0f913 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -70,9 +70,6 @@ String parameterToString(dynamic value) { if (value is JobId) { return JobIdTypeTransformer().encode(value).toString(); } - if (value is SystemConfigKey) { - return SystemConfigKeyTypeTransformer().encode(value).toString(); - } if (value is TagTypeEnum) { return TagTypeEnumTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/system_config_response_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart similarity index 55% rename from mobile/openapi/lib/model/system_config_response_dto.dart rename to mobile/openapi/lib/model/system_config_dto.dart index 23020930a9..a667236e74 100644 --- a/mobile/openapi/lib/model/system_config_response_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -10,36 +10,42 @@ part of openapi.api; -class SystemConfigResponseDto { - /// Returns a new [SystemConfigResponseDto] instance. - SystemConfigResponseDto({ - this.config = const [], +class SystemConfigDto { + /// Returns a new [SystemConfigDto] instance. + SystemConfigDto({ + required this.ffmpeg, + required this.oauth, }); - List config; + SystemConfigFFmpegDto ffmpeg; + + SystemConfigOAuthDto oauth; @override - bool operator ==(Object other) => identical(this, other) || other is SystemConfigResponseDto && - other.config == config; + bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && + other.ffmpeg == ffmpeg && + other.oauth == oauth; @override int get hashCode => // ignore: unnecessary_parenthesis - (config.hashCode); + (ffmpeg.hashCode) + + (oauth.hashCode); @override - String toString() => 'SystemConfigResponseDto[config=$config]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, oauth=$oauth]'; Map toJson() { final _json = {}; - _json[r'config'] = config; + _json[r'ffmpeg'] = ffmpeg; + _json[r'oauth'] = oauth; 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. // ignore: prefer_constructors_over_static_methods - static SystemConfigResponseDto? fromJson(dynamic value) { + static SystemConfigDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); @@ -48,24 +54,25 @@ class SystemConfigResponseDto { // Note 2: this code is stripped in release mode! assert(() { requiredKeys.forEach((key) { - assert(json.containsKey(key), 'Required key "SystemConfigResponseDto[$key]" is missing from JSON.'); - assert(json[key] != null, 'Required key "SystemConfigResponseDto[$key]" has a null value in JSON.'); + assert(json.containsKey(key), 'Required key "SystemConfigDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "SystemConfigDto[$key]" has a null value in JSON.'); }); return true; }()); - return SystemConfigResponseDto( - config: SystemConfigResponseItem.listFromJson(json[r'config'])!, + return SystemConfigDto( + ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, + oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, ); } return null; } - static List? listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = SystemConfigResponseDto.fromJson(row); + final value = SystemConfigDto.fromJson(row); if (value != null) { result.add(value); } @@ -74,12 +81,12 @@ class SystemConfigResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = SystemConfigResponseDto.fromJson(entry.value); + final value = SystemConfigDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -88,13 +95,13 @@ class SystemConfigResponseDto { return map; } - // maps a json object with a list of SystemConfigResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of SystemConfigDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments 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) { map[entry.key] = value; } @@ -105,7 +112,8 @@ class SystemConfigResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'config', + 'ffmpeg', + 'oauth', }; } diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart new file mode 100644 index 0000000000..73692bdcea --- /dev/null +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -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 toJson() { + final _json = {}; + _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(); + + // 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(json, r'crf')!, + preset: mapValueOfType(json, r'preset')!, + targetVideoCodec: mapValueOfType(json, r'targetVideoCodec')!, + targetAudioCodec: mapValueOfType(json, r'targetAudioCodec')!, + targetScaling: mapValueOfType(json, r'targetScaling')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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 = { + 'crf', + 'preset', + 'targetVideoCodec', + 'targetAudioCodec', + 'targetScaling', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_key.dart b/mobile/openapi/lib/model/system_config_key.dart deleted file mode 100644 index 1154864bfa..0000000000 --- a/mobile/openapi/lib/model/system_config_key.dart +++ /dev/null @@ -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 = [ - crf, - preset, - targetVideoCodec, - targetAudioCodec, - targetScaling, - ]; - - static SystemConfigKey? fromJson(dynamic value) => SystemConfigKeyTypeTransformer().decode(value); - - static List? listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - 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; -} - diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart new file mode 100644 index 0000000000..9ca02ce414 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -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 toJson() { + final _json = {}; + _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(); + + // 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(json, r'enabled')!, + issuerUrl: mapValueOfType(json, r'issuerUrl')!, + clientId: mapValueOfType(json, r'clientId')!, + clientSecret: mapValueOfType(json, r'clientSecret')!, + scope: mapValueOfType(json, r'scope')!, + buttonText: mapValueOfType(json, r'buttonText')!, + autoRegister: mapValueOfType(json, r'autoRegister')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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 = { + 'enabled', + 'issuerUrl', + 'clientId', + 'clientSecret', + 'scope', + 'buttonText', + 'autoRegister', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_response_item.dart b/mobile/openapi/lib/model/system_config_response_item.dart deleted file mode 100644 index f52ea6eaf0..0000000000 --- a/mobile/openapi/lib/model/system_config_response_item.dart +++ /dev/null @@ -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 toJson() { - final _json = {}; - _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(); - - // 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(json, r'name')!, - key: SystemConfigKey.fromJson(json[r'key'])!, - value: mapValueOfType(json, r'value')!, - defaultValue: mapValueOfType(json, r'defaultValue')!, - ); - } - return null; - } - - static List? listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - 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 mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // 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 = { - 'name', - 'key', - 'value', - 'defaultValue', - }; -} - diff --git a/mobile/openapi/test/system_config_api_test.dart b/mobile/openapi/test/system_config_api_test.dart index db9e8c0023..84d1ceeb90 100644 --- a/mobile/openapi/test/system_config_api_test.dart +++ b/mobile/openapi/test/system_config_api_test.dart @@ -17,12 +17,17 @@ void main() { // final instance = SystemConfigApi(); group('tests for SystemConfigApi', () { - //Future getConfig() async + //Future getConfig() async test('test getConfig', () async { // TODO }); - //Future updateConfig(Object body) async + //Future getDefaults() async + test('test getDefaults', () async { + // TODO + }); + + //Future updateConfig(SystemConfigDto systemConfigDto) async test('test updateConfig', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_response_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart similarity index 55% rename from mobile/openapi/test/system_config_response_dto_test.dart rename to mobile/openapi/test/system_config_dto_test.dart index e9f39ea7ea..1cf6c7f23c 100644 --- a/mobile/openapi/test/system_config_response_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -11,13 +11,18 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for SystemConfigResponseDto +// tests for SystemConfigDto void main() { - // final instance = SystemConfigResponseDto(); + // final instance = SystemConfigDto(); - group('test SystemConfigResponseDto', () { - // List config (default value: const []) - test('to test the property `config`', () async { + group('test SystemConfigDto', () { + // SystemConfigFFmpegDto ffmpeg + test('to test the property `ffmpeg`', () async { + // TODO + }); + + // SystemConfigOAuthDto oauth + test('to test the property `oauth`', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart new file mode 100644 index 0000000000..a088dc6110 --- /dev/null +++ b/mobile/openapi/test/system_config_f_fmpeg_dto_test.dart @@ -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 + }); + + + }); + +} diff --git a/mobile/openapi/test/system_config_key_test.dart b/mobile/openapi/test/system_config_key_test.dart deleted file mode 100644 index 47271c7a78..0000000000 --- a/mobile/openapi/test/system_config_key_test.dart +++ /dev/null @@ -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', () { - - }); - -} diff --git a/mobile/openapi/test/system_config_o_auth_dto_test.dart b/mobile/openapi/test/system_config_o_auth_dto_test.dart new file mode 100644 index 0000000000..d3bfed19ef --- /dev/null +++ b/mobile/openapi/test/system_config_o_auth_dto_test.dart @@ -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 + }); + + + }); + +} diff --git a/mobile/openapi/test/system_config_response_item_test.dart b/mobile/openapi/test/system_config_response_item_test.dart deleted file mode 100644 index 1fa5fd3d6b..0000000000 --- a/mobile/openapi/test/system_config_response_item_test.dart +++ /dev/null @@ -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 - }); - - - }); - -} diff --git a/server/apps/immich/src/api-v1/oauth/oauth.module.ts b/server/apps/immich/src/api-v1/oauth/oauth.module.ts index 1036458812..8d43799c27 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.module.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.module.ts @@ -1,3 +1,4 @@ +import { ImmichConfigModule } from '@app/immich-config'; import { Module } from '@nestjs/common'; import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; import { UserModule } from '../user/user.module'; @@ -5,7 +6,7 @@ import { OAuthController } from './oauth.controller'; import { OAuthService } from './oauth.service'; @Module({ - imports: [UserModule, ImmichJwtModule], + imports: [UserModule, ImmichJwtModule, ImmichConfigModule], controllers: [OAuthController], providers: [OAuthService], exports: [OAuthService], diff --git a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts index d62d442084..8d7ac78d1d 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts @@ -1,24 +1,13 @@ +import { SystemConfig } from '@app/database/entities/system-config.entity'; import { UserEntity } from '@app/database/entities/user.entity'; +import { ImmichConfigService } from '@app/immich-config'; import { BadRequestException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { generators, Issuer } from 'openid-client'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { LoginResponseDto } from '../auth/response-dto/login-response.dto'; import { OAuthService } from '../oauth/oauth.service'; 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) => { - return (value: keyof OAuthConfig, defaultValue: any) => config[value] ?? defaultValue ?? null; -}; - const email = 'user@immich.com'; const sub = 'my-auth-user-sub'; @@ -39,7 +28,7 @@ const loginResponse = { describe('OAuthService', () => { let sut: OAuthService; let userRepositoryMock: jest.Mocked; - let configServiceMock: jest.Mocked; + let immichConfigServiceMock: jest.Mocked; let immichJwtServiceMock: jest.Mocked; beforeEach(async () => { @@ -80,11 +69,11 @@ describe('OAuthService', () => { extractJwtFromCookie: jest.fn(), } as unknown as jest.Mocked; - configServiceMock = { - get: jest.fn(), - } as unknown as jest.Mocked; + immichConfigServiceMock = { + getConfig: jest.fn().mockResolvedValue({ oauth: { enabled: false } }), + } as unknown as jest.Mocked; - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); }); it('should be defined', () => { @@ -94,17 +83,17 @@ describe('OAuthService', () => { describe('generateConfig', () => { it('should work when oauth is not configured', async () => { 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 () => { - configServiceMock.get.mockImplementation( - mockConfig({ - OAUTH_ENABLED: true, - OAUTH_BUTTON_TEXT: 'OAuth', - }), - ); - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + immichConfigServiceMock.getConfig.mockResolvedValue({ + oauth: { + enabled: true, + buttonText: 'OAuth', + }, + } as SystemConfig); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); await expect(sut.generateConfig({ redirectUri: 'http://redirect' })).resolves.toEqual({ enabled: true, buttonText: 'OAuth', @@ -119,13 +108,13 @@ describe('OAuthService', () => { }); it('should not allow auto registering', async () => { - configServiceMock.get.mockImplementation( - mockConfig({ - OAUTH_ENABLED: true, - OAUTH_AUTO_REGISTER: false, - }), - ); - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + immichConfigServiceMock.getConfig.mockResolvedValue({ + oauth: { + enabled: true, + autoRegister: false, + }, + } as SystemConfig); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null); jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null); userRepositoryMock.getByEmail.mockResolvedValue(null); @@ -136,13 +125,13 @@ describe('OAuthService', () => { }); it('should link an existing user', async () => { - configServiceMock.get.mockImplementation( - mockConfig({ - OAUTH_ENABLED: true, - OAUTH_AUTO_REGISTER: false, - }), - ); - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + immichConfigServiceMock.getConfig.mockResolvedValue({ + oauth: { + enabled: true, + autoRegister: false, + }, + } as SystemConfig); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null); jest.spyOn(sut['logger'], 'warn').mockImplementation(() => null); userRepositoryMock.getByEmail.mockResolvedValue(user); @@ -156,8 +145,13 @@ describe('OAuthService', () => { }); it('should allow auto registering by default', async () => { - configServiceMock.get.mockImplementation(mockConfig({ OAUTH_ENABLED: true })); - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + immichConfigServiceMock.getConfig.mockResolvedValue({ + oauth: { + enabled: true, + autoRegister: true, + }, + } as SystemConfig); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); jest.spyOn(sut['logger'], 'debug').mockImplementation(() => null); jest.spyOn(sut['logger'], 'log').mockImplementation(() => null); userRepositoryMock.getByEmail.mockResolvedValue(null); @@ -178,13 +172,13 @@ describe('OAuthService', () => { }); it('should get the session endpoint from the discovery document', async () => { - configServiceMock.get.mockImplementation( - mockConfig({ - OAUTH_ENABLED: true, - OAUTH_ISSUER_URL: 'http://issuer', - }), - ); - sut = new OAuthService(immichJwtServiceMock, configServiceMock, userRepositoryMock); + immichConfigServiceMock.getConfig.mockResolvedValue({ + oauth: { + enabled: true, + issuerUrl: 'http://issuer,', + }, + } as SystemConfig); + sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock); await expect(sut.getLogoutEndpoint()).resolves.toBe('http://end-session-endpoint'); }); diff --git a/server/apps/immich/src/api-v1/oauth/oauth.service.ts b/server/apps/immich/src/api-v1/oauth/oauth.service.ts index 349b7d3cf4..cea624f8ef 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.service.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.service.ts @@ -1,5 +1,5 @@ +import { ImmichConfigService } from '@app/immich-config'; import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { ClientMetadata, generators, Issuer, UserinfoResponse } from 'openid-client'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { LoginResponseDto } from '../auth/response-dto/login-response.dto'; @@ -16,43 +16,26 @@ type OAuthProfile = UserinfoResponse & { export class OAuthService { 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( private immichJwtService: ImmichJwtService, - configService: ConfigService, + private immichConfigService: ImmichConfigService, @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('OAUTH_ISSUER_URL', ''); - this.scope = configService.get('OAUTH_SCOPE', ''); - this.buttonText = configService.get('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 { - if (!this.enabled) { + const config = await this.immichConfigService.getConfig(); + const { enabled, scope, buttonText } = config.oauth; + + if (!enabled) { return { enabled: false }; } const url = (await this.getClient()).authorizationUrl({ redirect_uri: dto.redirectUri, - scope: this.scope, + scope, state: generators.state(), }); - return { enabled: true, buttonText: this.buttonText, url }; + return { enabled: true, buttonText, url }; } public async callback(dto: OAuthCallbackDto): Promise { @@ -75,9 +58,11 @@ export class OAuthService { // register new user if (!user) { - if (!this.autoRegister) { + const config = await this.immichConfigService.getConfig(); + const { autoRegister } = config.oauth; + if (!autoRegister) { 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.`); } @@ -95,20 +80,31 @@ export class OAuthService { } public async getLogoutEndpoint(): Promise { - if (!this.enabled) { + const config = await this.immichConfigService.getConfig(); + const { enabled } = config.oauth; + + if (!enabled) { return null; } return (await this.getClient()).issuer.metadata.end_session_endpoint || null; } 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'); } - 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 metadata = { ...this.clientMetadata }; if (algorithms[0] === 'HS256') { metadata.id_token_signed_response_alg = algorithms[0]; } diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config-ffmpeg.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config-ffmpeg.dto.ts new file mode 100644 index 0000000000..2b96addb2d --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config-ffmpeg.dto.ts @@ -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; +} diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts new file mode 100644 index 0000000000..1df0e0cd69 --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts @@ -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; +} diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts new file mode 100644 index 0000000000..1cbb5e3666 --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts @@ -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; +} diff --git a/server/apps/immich/src/api-v1/system-config/dto/update-system-config.ts b/server/apps/immich/src/api-v1/system-config/dto/update-system-config.ts deleted file mode 100644 index e762d6d5e8..0000000000 --- a/server/apps/immich/src/api-v1/system-config/dto/update-system-config.ts +++ /dev/null @@ -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; -} diff --git a/server/apps/immich/src/api-v1/system-config/response-dto/system-config-response.dto.ts b/server/apps/immich/src/api-v1/system-config/response-dto/system-config-response.dto.ts deleted file mode 100644 index 6dfd3ae1c9..0000000000 --- a/server/apps/immich/src/api-v1/system-config/response-dto/system-config-response.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/apps/immich/src/api-v1/system-config/system-config.controller.ts b/server/apps/immich/src/api-v1/system-config/system-config.controller.ts index 48e8002fa3..4b8cb29799 100644 --- a/server/apps/immich/src/api-v1/system-config/system-config.controller.ts +++ b/server/apps/immich/src/api-v1/system-config/system-config.controller.ts @@ -1,8 +1,7 @@ import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Authenticated } from '../../decorators/authenticated.decorator'; -import { UpdateSystemConfigDto } from './dto/update-system-config'; -import { SystemConfigResponseDto } from './response-dto/system-config-response.dto'; +import { SystemConfigDto } from './dto/system-config.dto'; import { SystemConfigService } from './system-config.service'; @ApiTags('System Config') @@ -13,12 +12,17 @@ export class SystemConfigController { constructor(private readonly systemConfigService: SystemConfigService) {} @Get() - getConfig(): Promise { + public getConfig(): Promise { return this.systemConfigService.getConfig(); } + @Get('defaults') + public getDefaults(): SystemConfigDto { + return this.systemConfigService.getDefaults(); + } + @Put() - async updateConfig(@Body(ValidationPipe) dto: UpdateSystemConfigDto): Promise { + public updateConfig(@Body(ValidationPipe) dto: SystemConfigDto): Promise { return this.systemConfigService.updateConfig(dto); } } diff --git a/server/apps/immich/src/api-v1/system-config/system-config.service.ts b/server/apps/immich/src/api-v1/system-config/system-config.service.ts index d79f3f5abc..5426d5a310 100644 --- a/server/apps/immich/src/api-v1/system-config/system-config.service.ts +++ b/server/apps/immich/src/api-v1/system-config/system-config.service.ts @@ -1,20 +1,23 @@ import { Injectable } from '@nestjs/common'; import { ImmichConfigService } from 'libs/immich-config/src'; -import { UpdateSystemConfigDto } from './dto/update-system-config'; -import { SystemConfigResponseDto } from './response-dto/system-config-response.dto'; +import { mapConfig, SystemConfigDto } from './dto/system-config.dto'; @Injectable() export class SystemConfigService { constructor(private immichConfigService: ImmichConfigService) {} - async getConfig(): Promise { - const config = await this.immichConfigService.getSystemConfig(); - return { config }; + public async getConfig(): Promise { + const config = await this.immichConfigService.getConfig(); + return mapConfig(config); } - async updateConfig(dto: UpdateSystemConfigDto): Promise { - await this.immichConfigService.updateSystemConfig(dto.config); - const config = await this.immichConfigService.getSystemConfig(); - return { config }; + public getDefaults(): SystemConfigDto { + const config = this.immichConfigService.getDefaults(); + return mapConfig(config); + } + + public async updateConfig(dto: SystemConfigDto): Promise { + await this.immichConfigService.updateConfig(dto); + return this.getConfig(); } } diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index f7a6957b70..fc4c3eaf6c 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -42,16 +42,16 @@ export class VideoTranscodeProcessor { } async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise { - const config = await this.immichConfigService.getSystemConfigMap(); + const config = await this.immichConfigService.getConfig(); return new Promise((resolve, reject) => { ffmpeg(asset.originalPath) .outputOptions([ - `-crf ${config.ffmpeg_crf}`, - `-preset ${config.ffmpeg_preset}`, - `-vcodec ${config.ffmpeg_target_video_codec}`, - `-acodec ${config.ffmpeg_target_audio_codec}`, - `-vf scale=${config.ffmpeg_target_scaling}`, + `-crf ${config.ffmpeg.crf}`, + `-preset ${config.ffmpeg.preset}`, + `-vcodec ${config.ffmpeg.targetVideoCodec}`, + `-acodec ${config.ffmpeg.targetAudioCodec}`, + `-vf scale=${config.ffmpeg.targetScaling}`, ]) .output(savedEncodedPath) .on('start', () => { diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 29533a0361..6c9b3e5ac7 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2086,7 +2086,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigResponseDto" + "$ref": "#/components/schemas/SystemConfigDto" } } } @@ -2109,7 +2109,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSystemConfigDto" + "$ref": "#/components/schemas/SystemConfigDto" } } } @@ -2120,7 +2120,33 @@ "content": { "application/json": { "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" ] }, - "SystemConfigKey": { - "type": "string", - "enum": [ - "ffmpeg_crf", - "ffmpeg_preset", - "ffmpeg_target_video_codec", - "ffmpeg_target_audio_codec", - "ffmpeg_target_scaling" - ] - }, - "SystemConfigResponseItem": { + "SystemConfigFFmpegDto": { "type": "object", "properties": { - "name": { + "crf": { "type": "string" }, - "key": { - "$ref": "#/components/schemas/SystemConfigKey" - }, - "value": { + "preset": { "type": "string" }, - "defaultValue": { + "targetVideoCodec": { + "type": "string" + }, + "targetAudioCodec": { + "type": "string" + }, + "targetScaling": { "type": "string" } }, "required": [ - "name", - "key", - "value", - "defaultValue" + "crf", + "preset", + "targetVideoCodec", + "targetAudioCodec", + "targetScaling" ] }, - "SystemConfigResponseDto": { + "SystemConfigOAuthDto": { "type": "object", "properties": { - "config": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SystemConfigResponseItem" - } + "enabled": { + "type": "boolean" + }, + "issuerUrl": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "buttonText": { + "type": "string" + }, + "autoRegister": { + "type": "boolean" } }, "required": [ - "config" + "enabled", + "issuerUrl", + "clientId", + "clientSecret", + "scope", + "buttonText", + "autoRegister" ] }, - "UpdateSystemConfigDto": { + "SystemConfigDto": { "type": "object", - "properties": {} + "properties": { + "ffmpeg": { + "$ref": "#/components/schemas/SystemConfigFFmpegDto" + }, + "oauth": { + "$ref": "#/components/schemas/SystemConfigOAuthDto" + } + }, + "required": [ + "ffmpeg", + "oauth" + ] } } } diff --git a/server/libs/common/src/config/app.config.ts b/server/libs/common/src/config/app.config.ts index 944fd10bca..44f58975bf 100644 --- a/server/libs/common/src/config/app.config.ts +++ b/server/libs/common/src/config/app.config.ts @@ -16,12 +16,6 @@ const jwtSecretValidator: Joi.CustomValidator = (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 = { envFilePath: '.env', isGlobal: true, @@ -34,12 +28,5 @@ export const immichAppConfig: ConfigModuleOptions = { DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false), REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3), 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, }), }; diff --git a/server/libs/database/src/entities/system-config.entity.ts b/server/libs/database/src/entities/system-config.entity.ts index 32503dd2b9..ce3fd7a96e 100644 --- a/server/libs/database/src/entities/system-config.entity.ts +++ b/server/libs/database/src/entities/system-config.entity.ts @@ -1,27 +1,47 @@ import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_config') -export class SystemConfigEntity { +export class SystemConfigEntity { @PrimaryColumn() key!: SystemConfigKey; - @Column({ type: 'varchar', nullable: true }) - value!: SystemConfigValue; + @Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } }) + value!: T; } -export type SystemConfig = SystemConfigEntity[]; +export type SystemConfigValue = any; +// dot notation matches path in `SystemConfig` export enum SystemConfigKey { - FFMPEG_CRF = 'ffmpeg_crf', - FFMPEG_PRESET = 'ffmpeg_preset', - FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg_target_video_codec', - FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg_target_audio_codec', - FFMPEG_TARGET_SCALING = 'ffmpeg_target_scaling', + FFMPEG_CRF = 'ffmpeg.crf', + FFMPEG_PRESET = 'ffmpeg.preset', + FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec', + FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec', + 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 SystemConfigItem { - key: SystemConfigKey; - value: SystemConfigValue; +export interface SystemConfig { + ffmpeg: { + crf: string; + preset: string; + targetVideoCodec: string; + targetAudioCodec: string; + targetScaling: string; + }; + oauth: { + enabled: boolean; + issuerUrl: string; + clientId: string; + clientSecret: string; + scope: string; + buttonText: string; + autoRegister: boolean; + }; } diff --git a/server/libs/database/src/migrations/1670607437008-TruncateOldConfigItems.ts b/server/libs/database/src/migrations/1670607437008-TruncateOldConfigItems.ts new file mode 100644 index 0000000000..0a82783f89 --- /dev/null +++ b/server/libs/database/src/migrations/1670607437008-TruncateOldConfigItems.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class TruncateOldConfigItems1670607437008 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`TRUNCATE TABLE "system_config"`); + } + + public async down(): Promise { + // noop + } +} diff --git a/server/libs/immich-config/src/immich-config.service.ts b/server/libs/immich-config/src/immich-config.service.ts index a50086fc94..e2656f6fe9 100644 --- a/server/libs/immich-config/src/immich-config.service.ts +++ b/server/libs/immich-config/src/immich-config.service.ts @@ -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 { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; +import * as _ from 'lodash'; +import { DeepPartial, In, Repository } from 'typeorm'; -type SystemConfigMap = Record; - -const configDefaults: Record = { - [SystemConfigKey.FFMPEG_CRF]: { - name: 'FFmpeg Constant Rate Factor (-crf)', - value: '23', +const defaults: SystemConfig = Object.freeze({ + ffmpeg: { + crf: '23', + preset: 'ultrafast', + targetVideoCodec: 'libx264', + targetAudioCodec: 'mp3', + targetScaling: '1280:-2', }, - [SystemConfigKey.FFMPEG_PRESET]: { - name: 'FFmpeg preset (-preset)', - value: 'ultrafast', + oauth: { + enabled: false, + 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() export class ImmichConfigService { @@ -35,38 +30,32 @@ export class ImmichConfigService { private systemConfigRepository: Repository, ) {} - public async getSystemConfig() { - const items = this._getDefaults(); + public getDefaults(): SystemConfig { + return defaults; + } - // override default values + public async getConfig() { const overrides = await this.systemConfigRepository.find(); - for (const override of overrides) { - const item = items.find((_item) => _item.key === override.key); - if (item) { - item.value = override.value; - } + const config: DeepPartial = {}; + for (const { key, value } of overrides) { + // set via dot notation + _.set(config, key, value); } - return items; + return _.defaultsDeep(config, defaults) as SystemConfig; } - public async getSystemConfigMap(): Promise { - const items = await this.getSystemConfig(); - const map: Partial = {}; - - for (const { key, value } of items) { - map[key] = value; - } - - return map as SystemConfigMap; - } - - public async updateSystemConfig(items: SystemConfigEntity[]): Promise { - const deletes: SystemConfigEntity[] = []; + public async updateConfig(config: DeepPartial | null): Promise { const updates: SystemConfigEntity[] = []; + const deletes: SystemConfigEntity[] = []; - for (const item of items) { - if (item.value === null || item.value === this._getDefaultValue(item.key)) { + for (const key of Object.values(SystemConfigKey)) { + // 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); continue; } @@ -82,16 +71,4 @@ export class ImmichConfigService { 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; - } } diff --git a/server/nest-cli.json b/server/nest-cli.json index 99f2ed1511..861e733dab 100644 --- a/server/nest-cli.json +++ b/server/nest-cli.json @@ -71,14 +71,14 @@ "tsConfigPath": "libs/job/tsconfig.lib.json" } }, - "system-config": { + "immich-config": { "type": "library", - "root": "libs/system-config", + "root": "libs/immich-config", "entryFile": "index", - "sourceRoot": "libs/system-config/src", + "sourceRoot": "libs/immich-config/src", "compilerOptions": { - "tsConfigPath": "libs/system-config/tsconfig.lib.json" + "tsConfigPath": "libs/immich-config/tsconfig.lib.json" } } } -} \ No newline at end of file +} diff --git a/server/package.json b/server/package.json index d137cffc5f..834484af84 100644 --- a/server/package.json +++ b/server/package.json @@ -142,7 +142,7 @@ "@app/database/config": "/libs/database/src/config", "@app/common": "/libs/common/src", "^@app/job(|/.*)$": "/libs/job/src/$1", - "^@app/system-config(|/.*)$": "/libs/system-config/src/$1" + "^@app/immich-config(|/.*)$": "/libs/immich-config/src/$1" } } } diff --git a/server/tsconfig.json b/server/tsconfig.json index 8bd2202b78..ee830c64a4 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -22,8 +22,8 @@ "@app/database/*": ["libs/database/src/*"], "@app/job": ["libs/job/src"], "@app/job/*": ["libs/job/src/*"], - "@app/system-config": ["libs/immich-config/src"], - "@app/system-config/*": ["libs/immich-config/src/*"] + "@app/immich-config": ["libs/immich-config/src"], + "@app/immich-config/*": ["libs/immich-config/src/*"] } }, "exclude": ["dist", "node_modules", "upload"] diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 7e126a44b5..fd0a12599f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1428,63 +1428,107 @@ export interface SmartInfoResponseDto { /** * * @export - * @enum {string} + * @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 { +export interface SystemConfigDto { /** * - * @type {Array} - * @memberof SystemConfigResponseDto + * @type {SystemConfigFFmpegDto} + * @memberof SystemConfigDto */ - 'config': Array; + 'ffmpeg': SystemConfigFFmpegDto; + /** + * + * @type {SystemConfigOAuthDto} + * @memberof SystemConfigDto + */ + 'oauth': SystemConfigOAuthDto; } /** * * @export - * @interface SystemConfigResponseItem + * @interface SystemConfigFFmpegDto */ -export interface SystemConfigResponseItem { +export interface SystemConfigFFmpegDto { /** * * @type {string} - * @memberof SystemConfigResponseItem + * @memberof SystemConfigFFmpegDto */ - 'name': string; - /** - * - * @type {SystemConfigKey} - * @memberof SystemConfigResponseItem - */ - 'key': SystemConfigKey; + 'crf': string; /** * * @type {string} - * @memberof SystemConfigResponseItem + * @memberof SystemConfigFFmpegDto */ - 'value': string; + 'preset': 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. * @throws {RequiredError} */ - updateConfig: async (body: object, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'body' is not null or undefined - assertParamExists('updateConfig', 'body', body) + getDefaults: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/system-config/defaults`; + // 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 => { + // verify required parameter 'systemConfigDto' is not null or undefined + assertParamExists('updateConfig', 'systemConfigDto', systemConfigDto) const localVarPath = `/system-config`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5284,7 +5361,7 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(systemConfigDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -5306,18 +5383,27 @@ export const SystemConfigApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async getConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getConfig(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * - * @param {object} body * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateConfig(body: object, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfig(body, options); + async getDefaults(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + 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> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfig(systemConfigDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -5335,17 +5421,25 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getConfig(options?: any): AxiosPromise { + getConfig(options?: any): AxiosPromise { return localVarFp.getConfig(options).then((request) => request(axios, basePath)); }, /** * - * @param {object} body * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateConfig(body: object, options?: any): AxiosPromise { - return localVarFp.updateConfig(body, options).then((request) => request(axios, basePath)); + getDefaults(options?: any): AxiosPromise { + 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 { + 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. * @throws {RequiredError} * @memberof SystemConfigApi */ - public updateConfig(body: object, options?: AxiosRequestConfig) { - return SystemConfigApiFp(this.configuration).updateConfig(body, options).then((request) => request(this.axios, this.basePath)); + public getDefaults(options?: AxiosRequestConfig) { + 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)); } } diff --git a/web/src/app.css b/web/src/app.css index 8d76825517..766d4d19f3 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -59,7 +59,7 @@ input:focus-visible { @layer utilities { .immich-form-input { - @apply bg-slate-100 p-2 rounded-md dark:text-immich-dark-bg focus:border-immich-primary text-sm 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 { diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte new file mode 100644 index 0000000000..4359784d01 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -0,0 +1,126 @@ + + +
+ {#await getConfigs() then} +
+
+ + + + + + + + + + + + +
+ {/await} +
diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte new file mode 100644 index 0000000000..b795332603 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -0,0 +1,147 @@ + + +
+ {#await getConfigs() then} +
+
+
+ +
+ +
+ + + + + + + + + + + +
+ +
+ + + +
+ {/await} +
diff --git a/web/src/lib/components/admin-page/settings/setting-accordion.svelte b/web/src/lib/components/admin-page/settings/setting-accordion.svelte new file mode 100644 index 0000000000..ddbb9e8c84 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-accordion.svelte @@ -0,0 +1,56 @@ + + +
+
+
+

+ {title} +

+ +

{subtitle}

+
+ + +
+ + {#if isOpen} +
    + +
+ {/if} +
+ + diff --git a/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte new file mode 100644 index 0000000000..0bdd9dc834 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-buttons-row.svelte @@ -0,0 +1,35 @@ + + +
+
+ {#if showResetToDefault} + + {/if} +
+ +
+ + + +
+
diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte new file mode 100644 index 0000000000..f45ff08f9b --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -0,0 +1,51 @@ + + + + +
+
+ + {#if required} +
*
+ {/if} + + {#if isEdited} +
+ Unsaved change +
+ {/if} +
+ +
diff --git a/web/src/lib/components/admin-page/settings/setting-switch.svelte b/web/src/lib/components/admin-page/settings/setting-switch.svelte new file mode 100644 index 0000000000..de2e7fdb1a --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-switch.svelte @@ -0,0 +1,81 @@ + + +
+
+

+ {title.toUpperCase()} +

+ +

{subtitle}

+
+ + +
+ + diff --git a/web/src/lib/components/admin-page/settings/settings-panel.svelte b/web/src/lib/components/admin-page/settings/settings-panel.svelte deleted file mode 100644 index 95b51ee4a4..0000000000 --- a/web/src/lib/components/admin-page/settings/settings-panel.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - -
- - - - - - - - - {#each items as item, i} - - - - - {/each} - -
SettingValue
- {item.name} - - -
- -
- -
-
diff --git a/web/src/lib/components/admin-page/user-management.svelte b/web/src/lib/components/admin-page/user-management.svelte deleted file mode 100644 index f52f826a65..0000000000 --- a/web/src/lib/components/admin-page/user-management.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - {#each allUsers as user, i} - - - - - - - {/each} - -
EmailFirst nameLast nameAction
{user.email}{user.firstName}{user.lastName} - {#if !isDeleted(user)} - - - {/if} - {#if isDeleted(user)} - - {/if} -
- - diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte index dbde92320c..5e6fad4899 100644 --- a/web/src/lib/components/shared-components/full-screen-modal.svelte +++ b/web/src/lib/components/shared-components/full-screen-modal.svelte @@ -9,7 +9,7 @@
dispatch('clickOutside')}> diff --git a/web/src/lib/components/shared-components/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar.svelte index 2759dea56e..b66f47099b 100644 --- a/web/src/lib/components/shared-components/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar.svelte @@ -7,6 +7,7 @@ import { clickOutside } from '../../utils/click-outside'; import { api, UserResponseDto } from '@api'; import ThemeButton from './theme-button.svelte'; + import { AppRoute } from '../../constants'; export let user: UserResponseDto; export let shouldShowUploadButton = true; @@ -70,7 +71,7 @@
- {#if $page.url.pathname !== '/admin' && shouldShowUploadButton} + {#if !$page.url.pathname.includes('/admin') && shouldShowUploadButton} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-button.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-button.svelte index 3ea4b4e1fc..d5247112cf 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-button.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-button.svelte @@ -3,22 +3,12 @@ // TODO: why `any` here? There should be a expected type for this // eslint-disable-next-line @typescript-eslint/no-explicit-any export let logo: any; - export let actionType: AdminSideBarSelection | AppSideBarSelection; export let isSelected: boolean; import { createEventDispatcher } from 'svelte'; - import type { - AdminSideBarSelection, - AppSideBarSelection - } from '../../../models/admin-sidebar-selection'; const dispatch = createEventDispatcher(); - - const onButtonClicked = () => { - dispatch('selected', { - actionType - }); - }; + const onButtonClicked = () => dispatch('selected');
- import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection'; - import { onMount } from 'svelte'; import { page } from '$app/stores'; import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte'; import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; @@ -11,28 +9,12 @@ import { api } from '@api'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../loading-spinner.svelte'; - - let selectedAction: AppSideBarSelection; + import { AppRoute } from '../../../constants'; let showAssetCount = false; let showSharingCount = 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 { data: assetCount } = await api.assetApi.getAssetCountByUserId(); @@ -56,14 +38,13 @@
{#await getAssetCount()} @@ -91,16 +71,11 @@
- +
{#await getAlbumCount()} @@ -129,16 +103,11 @@

LIBRARY

-
+
+ 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 ''; + } + }; + + + + Administration - Immich + + + +
- +
+
+ goto(AppRoute.ADMIN_USER_MANAGEMENT)} + /> + goto(AppRoute.ADMIN_JOBS)} + /> + goto(AppRoute.ADMIN_SETTINGS)} + /> + goto(AppRoute.ADMIN_STATS)} + /> +
+ +
+
+ +
+
+

+ {getPageTitle($page.route.id)} +

+
+
+ +
+
+ +
+
+
+
diff --git a/web/src/routes/admin/+page.server.ts b/web/src/routes/admin/+page.server.ts index 01c8609ffc..f8c5996154 100644 --- a/web/src/routes/admin/+page.server.ts +++ b/web/src/routes/admin/+page.server.ts @@ -1,5 +1,4 @@ import { redirect } from '@sveltejs/kit'; -import { serverApi } from '@api'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ parent }) => { @@ -11,7 +10,5 @@ export const load: PageServerLoad = async ({ parent }) => { throw redirect(302, '/photos'); } - const { data: allUsers } = await serverApi.userApi.getAllUsers(false); - - return { user, allUsers }; + throw redirect(302, '/admin/user-management'); }; diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 42388f6868..e69de29bb2 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -1,249 +0,0 @@ - - - - Administration - Immich - - - - -{#if shouldShowCreateUserForm} - (shouldShowCreateUserForm = false)}> - - -{/if} - -{#if shouldShowEditUserForm} - (shouldShowEditUserForm = false)}> - - -{/if} - -{#if shouldShowDeleteConfirmDialog} - (shouldShowDeleteConfirmDialog = false)}> - - -{/if} - -{#if shouldShowRestoreDialog} - (shouldShowRestoreDialog = false)}> - - -{/if} - -{#if shouldShowInfoPanel} - (shouldShowInfoPanel = false)}> -
-

Password reset success

- -

- The user's password has been reset to the default password -
- Please inform the user, and they will need to change the password at the next log-on. -

- -
- -
-
-
-{/if} - -
-
- - - - -
- -
-
-
-
-

- {selectedAction} -

-
-
- -
-
- {#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT} - (shouldShowCreateUserForm = true)} - on:edit-user={editUserHandler} - on:delete-user={deleteUserHandler} - on:restore-user={restoreUserHandler} - /> - {/if} - {#if selectedAction === AdminSideBarSelection.JOBS} - - {/if} - {#if selectedAction === AdminSideBarSelection.SETTINGS} - - {/if} - {#if selectedAction === AdminSideBarSelection.STATS && serverStat} - - {/if} -
-
-
-
diff --git a/web/src/routes/admin/jobs-status/+page.server.ts b/web/src/routes/admin/jobs-status/+page.server.ts new file mode 100644 index 0000000000..ccfbb564f1 --- /dev/null +++ b/web/src/routes/admin/jobs-status/+page.server.ts @@ -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'); + } +}; diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte new file mode 100644 index 0000000000..ac4ef81646 --- /dev/null +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -0,0 +1,11 @@ + + + + Jobs Status - Immich + + +
+ +
diff --git a/web/src/routes/admin/server-status/+page.server.ts b/web/src/routes/admin/server-status/+page.server.ts new file mode 100644 index 0000000000..01c8609ffc --- /dev/null +++ b/web/src/routes/admin/server-status/+page.server.ts @@ -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 }; +}; diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte new file mode 100644 index 0000000000..1ab8940f77 --- /dev/null +++ b/web/src/routes/admin/server-status/+page.svelte @@ -0,0 +1,29 @@ + + + + Jobs Status - Immich + + +{#if $page.data.allUsers && serverStat} + +{/if} diff --git a/web/src/routes/admin/settings/+page.server.ts b/web/src/routes/admin/settings/+page.server.ts new file mode 100644 index 0000000000..fb2c1cc213 --- /dev/null +++ b/web/src/routes/admin/settings/+page.server.ts @@ -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 }; +}; diff --git a/web/src/routes/admin/settings/+page.svelte b/web/src/routes/admin/settings/+page.svelte new file mode 100644 index 0000000000..aa2fc4c56e --- /dev/null +++ b/web/src/routes/admin/settings/+page.svelte @@ -0,0 +1,33 @@ + + +
+ {#await getConfig()} + + {:then configs} + + + + + + + + {/await} +
diff --git a/web/src/routes/admin/user-management/+page.server.ts b/web/src/routes/admin/user-management/+page.server.ts new file mode 100644 index 0000000000..01c8609ffc --- /dev/null +++ b/web/src/routes/admin/user-management/+page.server.ts @@ -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 }; +}; diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte new file mode 100644 index 0000000000..a5cb6de6ea --- /dev/null +++ b/web/src/routes/admin/user-management/+page.svelte @@ -0,0 +1,232 @@ + + + + User Management - Immich + + +
+ {#if shouldShowCreateUserForm} + (shouldShowCreateUserForm = false)}> + + + {/if} + + {#if shouldShowEditUserForm} + (shouldShowEditUserForm = false)}> + + + {/if} + + {#if shouldShowDeleteConfirmDialog} + (shouldShowDeleteConfirmDialog = false)}> + + + {/if} + + {#if shouldShowRestoreDialog} + (shouldShowRestoreDialog = false)}> + + + {/if} + + {#if shouldShowInfoPanel} + (shouldShowInfoPanel = false)}> +
+

Password reset success

+ +

+ The user's password has been reset to the default password +
+ Please inform the user, and they will need to change the password at the next log-on. +

+ +
+ +
+
+
+ {/if} + + + + + + + + + + + + {#if allUsers} + {#each allUsers as user, i} + + + + + + + {/each} + {/if} + +
EmailFirst nameLast nameAction
{user.email}{user.firstName}{user.lastName} + {#if !isDeleted(user)} + + + {/if} + {#if isDeleted(user)} + + {/if} +
+ + +