From 59bb7276361d80bae5a4a18b323a09028c85a523 Mon Sep 17 00:00:00 2001
From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Date: Fri, 25 Aug 2023 19:44:52 +0200
Subject: [PATCH] feat(web, server): Ability to use config file instead of
admin UI (#3836)
* implement method to read config file
* getConfig returns config file if present
* return isConfigFile for http requests
* disable elements if config file is used, show message if config file is set, copy existing config to clipboard
* fix allowing partial configuration files
* add new env variable to docs
* fix tests
* minor refactoring, address review
* adapt config type in frontend
* remove unnecessary imports
* move config file reading to system-config repo
* add documentation
* fix code formatting in system settings page
* add validator for config file
* fix formatting in docs
* update generated files
* throw error when trying to update config. e.g. via cli or api
* switch to feature flags for isConfigFile
* refactoring
* refactor: config file
* chore: open api
* feat: always show copy/export buttons
* fix: default flags
* refactor: copy to clipboard
---------
Co-authored-by: Jason Rasmussen
---
cli/src/api/open-api/api.ts | 6 ++
docs/docs/install/config-file.md | 91 ++++++++++++++++++
docs/docs/install/environment-variables.md | 5 +
docs/docs/install/post-install.mdx | 2 +-
mobile/openapi/doc/ServerFeaturesDto.md | Bin 640 -> 672 bytes
.../lib/model/server_features_dto.dart | Bin 4740 -> 5008 bytes
.../test/server_features_dto_test.dart | Bin 1300 -> 1403 bytes
server/immich-openapi-specs.json | 4 +
.../src/domain/server-info/server-info.dto.ts | 1 +
.../server-info/server-info.service.spec.ts | 1 +
.../system-config/system-config.core.ts | 63 ++++++++++--
.../system-config/system-config.repository.ts | 1 +
.../system-config.service.spec.ts | 45 +++++++++
.../repositories/system-config.repository.ts | 3 +
.../system-config.repository.mock.ts | 1 +
web/src/api/open-api/api.ts | 6 ++
web/src/api/utils.ts | 14 +++
.../settings/ffmpeg/ffmpeg-settings.svelte | 13 +++
.../settings/job-settings/job-settings.svelte | 3 +
.../machine-learning-settings.svelte | 12 ++-
.../settings/oauth/oauth-settings.svelte | 24 ++---
.../password-login-settings.svelte | 3 +
.../settings/setting-buttons-row.svelte | 5 +-
.../admin-page/settings/setting-select.svelte | 2 +
.../storage-template-settings.svelte | 4 +
.../thumbnail/thumbnail-settings.svelte | 4 +
.../components/forms/api-key-secret.svelte | 18 +---
.../create-shared-link-modal.svelte | 9 +-
web/src/lib/stores/feature-flags.store.ts | 1 +
web/src/lib/utils/asset-utils.ts | 2 +-
.../(user)/sharing/sharedlinks/+page.svelte | 9 +-
web/src/routes/+error.svelte | 22 +----
.../routes/admin/system-settings/+page.svelte | 53 ++++++++--
33 files changed, 344 insertions(+), 83 deletions(-)
create mode 100644 docs/docs/install/config-file.md
diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts
index 7ffb1f7b6f..9a47f53b7f 100644
--- a/cli/src/api/open-api/api.ts
+++ b/cli/src/api/open-api/api.ts
@@ -2173,6 +2173,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'clipEncode': boolean;
+ /**
+ *
+ * @type {boolean}
+ * @memberof ServerFeaturesDto
+ */
+ 'configFile': boolean;
/**
*
* @type {boolean}
diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md
new file mode 100644
index 0000000000..0cf131a02f
--- /dev/null
+++ b/docs/docs/install/config-file.md
@@ -0,0 +1,91 @@
+# Config File
+
+A config file can be provided as an alternative to the UI configuration.
+
+### Step 1 - Create a new config file
+
+In JSON format, create a new config file (e.g. `immich.config`) and put it in a location that can be accessed by Immich.
+The default configuration looks like this:
+
+```json
+{
+ "ffmpeg": {
+ "crf": 23,
+ "threads": 0,
+ "preset": "ultrafast",
+ "targetVideoCodec": "h264",
+ "targetAudioCodec": "aac",
+ "targetResolution": "720",
+ "maxBitrate": "0",
+ "twoPass": false,
+ "transcode": "required",
+ "tonemap": "hable",
+ "accel": "disabled"
+ },
+ "job": {
+ "backgroundTask": {
+ "concurrency": 5
+ },
+ "clipEncoding": {
+ "concurrency": 2
+ },
+ "metadataExtraction": {
+ "concurrency": 5
+ },
+ "objectTagging": {
+ "concurrency": 2
+ },
+ "recognizeFaces": {
+ "concurrency": 2
+ },
+ "search": {
+ "concurrency": 5
+ },
+ "sidecar": {
+ "concurrency": 5
+ },
+ "storageTemplateMigration": {
+ "concurrency": 5
+ },
+ "thumbnailGeneration": {
+ "concurrency": 5
+ },
+ "videoConversion": {
+ "concurrency": 1
+ }
+ },
+ "oauth": {
+ "enabled": false,
+ "issuerUrl": "",
+ "clientId": "",
+ "clientSecret": "",
+ "mobileOverrideEnabled": false,
+ "mobileRedirectUri": "",
+ "scope": "openid email profile",
+ "storageLabelClaim": "preferred_username",
+ "buttonText": "Login with OAuth",
+ "autoRegister": true,
+ "autoLaunch": false
+ },
+ "passwordLogin": {
+ "enabled": true
+ },
+ "storageTemplate": {
+ "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
+ },
+ "thumbnail": {
+ "webpSize": 250,
+ "jpegSize": 1440
+ }
+}
+```
+
+:::tip
+In Administration > Settings is a button to copy the current configuration to your clipboard.
+So you can just grab it from there, paste it into a file and you're pretty much good to go.
+:::
+
+### Step 2 - Specify the file location
+
+In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
+For more information, refer to the [Environment Variables](https://docs.immich.app/docs/install/environment-variables) section.
diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md
index f0ecbbb970..a344b6fd93 100644
--- a/docs/docs/install/environment-variables.md
+++ b/docs/docs/install/environment-variables.md
@@ -1,3 +1,7 @@
+---
+sidebar_position: 90
+---
+
# Environment Variables
## Docker Compose
@@ -22,6 +26,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
| `PUBLIC_LOGIN_PAGE_MESSAGE` | Public Login Page Message | | web |
+| `IMMICH_CONFIG_FILE` | Path to config file | | server |
:::tip
diff --git a/docs/docs/install/post-install.mdx b/docs/docs/install/post-install.mdx
index 82fecc52f3..131c83a4ff 100644
--- a/docs/docs/install/post-install.mdx
+++ b/docs/docs/install/post-install.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 100
+sidebar_position: 80
---
import RegisterAdminUser from '../partials/_register-admin.md';
diff --git a/mobile/openapi/doc/ServerFeaturesDto.md b/mobile/openapi/doc/ServerFeaturesDto.md
index 9abd465140d0d441192d733ffa04f909ecf82fda..168479b9908cb3af0bdc0cb7005bed5a264b1016 100644
GIT binary patch
delta 25
gcmZo*UBEiw4_9)2URq|lTV_t`SRSGk;$7GA9KQmHg_{=Gm68xsS38X3UJ=$Wz3C?
zNNPn`6&Xe0vZ=NzaQ0>+HX}w^I5$;2R>4-GBqOs}4=%Pjg#7~(!m!B~xfKw+&1PI@
L*b&^xKLxu1)1gCd
delta 45
zcmV+|0Mh@ECxj)ivjLOQ0pqjC0w4ji4+L=mvz!GE0kgseF9EY22;c&g2slikxunW&tJ^W&r;E3~~Sf
delta 12
Tcmey(HHB+~Gt=f(Oc$8|A(8~k
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index e244c31a7f..7fec0d18eb 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -6478,6 +6478,9 @@
"clipEncode": {
"type": "boolean"
},
+ "configFile": {
+ "type": "boolean"
+ },
"facialRecognition": {
"type": "boolean"
},
@@ -6501,6 +6504,7 @@
}
},
"required": [
+ "configFile",
"clipEncode",
"facialRecognition",
"sidecar",
diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts
index 1256f12241..b9cdd181f3 100644
--- a/server/src/domain/server-info/server-info.dto.ts
+++ b/server/src/domain/server-info/server-info.dto.ts
@@ -80,6 +80,7 @@ export class ServerMediaTypesResponseDto {
}
export class ServerFeaturesDto implements FeatureFlags {
+ configFile!: boolean;
clipEncode!: boolean;
facialRecognition!: boolean;
sidecar!: boolean;
diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts
index fefebead85..6c8d194641 100644
--- a/server/src/domain/server-info/server-info.service.spec.ts
+++ b/server/src/domain/server-info/server-info.service.spec.ts
@@ -155,6 +155,7 @@ describe(ServerInfoService.name, () => {
search: true,
sidecar: true,
tagImage: true,
+ configFile: false,
});
expect(configMock.load).toHaveBeenCalled();
});
diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts
index 0b76228f5b..5fa55a7092 100644
--- a/server/src/domain/system-config/system-config.core.ts
+++ b/server/src/domain/system-config/system-config.core.ts
@@ -10,10 +10,13 @@ import {
VideoCodec,
} from '@app/infra/entities';
import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
+import { plainToClass } from 'class-transformer';
+import { validate } from 'class-validator';
import * as _ from 'lodash';
import { Subject } from 'rxjs';
import { DeepPartial } from 'typeorm';
import { QueueName } from '../job/job.constants';
+import { SystemConfigDto } from './dto';
import { ISystemConfigRepository } from './system-config.repository';
export type SystemConfigValidator = (config: SystemConfig) => void | Promise;
@@ -87,6 +90,7 @@ export enum FeatureFlag {
OAUTH = 'oauth',
OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
PASSWORD_LOGIN = 'passwordLogin',
+ CONFIG_FILE = 'configFile',
}
export type FeatureFlags = Record;
@@ -97,6 +101,7 @@ const singleton = new Subject();
export class SystemConfigCore {
private logger = new Logger(SystemConfigCore.name);
private validators: SystemConfigValidator[] = [];
+ private configCache: SystemConfig | null = null;
public config$ = singleton;
@@ -120,6 +125,8 @@ export class SystemConfigCore {
throw new BadRequestException('OAuth is not enabled');
case FeatureFlag.PASSWORD_LOGIN:
throw new BadRequestException('Password login is not enabled');
+ case FeatureFlag.CONFIG_FILE:
+ throw new BadRequestException('Config file is not set');
default:
throw new ForbiddenException(`Missing required feature: ${feature}`);
}
@@ -146,6 +153,7 @@ export class SystemConfigCore {
[FeatureFlag.OAUTH]: config.oauth.enabled,
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
+ [FeatureFlag.CONFIG_FILE]: !!process.env.IMMICH_CONFIG_FILE,
};
}
@@ -157,18 +165,16 @@ export class SystemConfigCore {
this.validators.push(validator);
}
- public async getConfig() {
- const overrides = await this.repository.load();
- const config: DeepPartial = {};
- for (const { key, value } of overrides) {
- // set via dot notation
- _.set(config, key, value);
- }
-
- return _.defaultsDeep(config, defaults) as SystemConfig;
+ public getConfig(force = false): Promise {
+ const configFilePath = process.env.IMMICH_CONFIG_FILE;
+ return configFilePath ? this.loadFromFile(configFilePath, force) : this.loadFromDatabase();
}
public async updateConfig(config: SystemConfig): Promise {
+ if (await this.hasFeature(FeatureFlag.CONFIG_FILE)) {
+ throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
+ }
+
try {
for (const validator of this.validators) {
await validator(config);
@@ -211,8 +217,45 @@ export class SystemConfigCore {
}
public async refreshConfig() {
- const newConfig = await this.getConfig();
+ const newConfig = await this.getConfig(true);
this.config$.next(newConfig);
}
+
+ private async loadFromDatabase() {
+ const config: DeepPartial = {};
+ const overrides = await this.repository.load();
+ for (const { key, value } of overrides) {
+ // set via dot notation
+ _.set(config, key, value);
+ }
+
+ return _.defaultsDeep(config, defaults) as SystemConfig;
+ }
+
+ private async loadFromFile(filepath: string, force = false) {
+ if (force || !this.configCache) {
+ try {
+ const overrides = JSON.parse((await this.repository.readFile(filepath)).toString());
+ const config = plainToClass(SystemConfigDto, _.defaultsDeep(overrides, defaults));
+
+ const errors = await validate(config, {
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ forbidUnknownValues: true,
+ });
+ if (errors.length > 0) {
+ this.logger.error('Validation error', errors);
+ throw new Error(`Invalid value(s) in file: ${errors}`);
+ }
+
+ this.configCache = config;
+ } catch (error: Error | any) {
+ this.logger.error(`Unable to load configuration file: ${filepath} due to ${error}`, error?.stack);
+ throw new Error('Invalid configuration file');
+ }
+ }
+
+ return this.configCache;
+ }
}
diff --git a/server/src/domain/system-config/system-config.repository.ts b/server/src/domain/system-config/system-config.repository.ts
index 6b04669ccc..f999792071 100644
--- a/server/src/domain/system-config/system-config.repository.ts
+++ b/server/src/domain/system-config/system-config.repository.ts
@@ -4,6 +4,7 @@ export const ISystemConfigRepository = 'ISystemConfigRepository';
export interface ISystemConfigRepository {
load(): Promise;
+ readFile(filename: string): Promise;
saveAll(items: SystemConfigEntity[]): Promise;
deleteKeys(keys: string[]): Promise;
}
diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts
index 6735b17bc9..fd450a2963 100644
--- a/server/src/domain/system-config/system-config.service.spec.ts
+++ b/server/src/domain/system-config/system-config.service.spec.ts
@@ -84,6 +84,7 @@ describe(SystemConfigService.name, () => {
let jobMock: jest.Mocked;
beforeEach(async () => {
+ delete process.env.IMMICH_CONFIG_FILE;
configMock = newSystemConfigRepositoryMock();
jobMock = newJobRepositoryMock();
sut = new SystemConfigService(configMock, jobMock);
@@ -126,6 +127,43 @@ describe(SystemConfigService.name, () => {
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
});
+
+ it('should load the config from a file', async () => {
+ process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+ const partialConfig = { ffmpeg: { crf: 30 }, oauth: { autoLaunch: true } };
+ configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig)));
+
+ await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
+
+ expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
+ });
+
+ it('should accept an empty configuration file', async () => {
+ process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+ configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify({})));
+
+ await expect(sut.getConfig()).resolves.toEqual(defaults);
+
+ expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
+ });
+
+ const tests = [
+ { should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
+ { should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },
+ { should: 'validate enums', config: { ffmpeg: { transcode: 'unknown' } } },
+ { should: 'validate top level unknown options', config: { unknownOption: true } },
+ { should: 'validate nested unknown options', config: { ffmpeg: { unknownOption: true } } },
+ { should: 'validate required oauth fields', config: { oauth: { enabled: true } } },
+ ];
+
+ for (const test of tests) {
+ it(`should ${test.should}`, async () => {
+ process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+ configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(test.config)));
+
+ await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
+ });
+ }
});
describe('getStorageTemplateOptions', () => {
@@ -176,6 +214,13 @@ describe(SystemConfigService.name, () => {
expect(validator).toHaveBeenCalledWith(updatedConfig);
expect(configMock.saveAll).not.toHaveBeenCalled();
});
+
+ it('should throw an error if a config file is in use', async () => {
+ process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+ configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify({})));
+ await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
+ expect(configMock.saveAll).not.toHaveBeenCalled();
+ });
});
describe('refreshConfig', () => {
diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts
index 0ce7c07a56..cfe0eab3d5 100644
--- a/server/src/infra/repositories/system-config.repository.ts
+++ b/server/src/infra/repositories/system-config.repository.ts
@@ -1,5 +1,6 @@
import { ISystemConfigRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
+import { readFile } from 'fs/promises';
import { In, Repository } from 'typeorm';
import { SystemConfigEntity } from '../entities';
@@ -13,6 +14,8 @@ export class SystemConfigRepository implements ISystemConfigRepository {
return this.repository.find();
}
+ readFile = readFile;
+
saveAll(items: SystemConfigEntity[]): Promise {
return this.repository.save(items);
}
diff --git a/server/test/repositories/system-config.repository.mock.ts b/server/test/repositories/system-config.repository.mock.ts
index 258ded0a76..254f3bad23 100644
--- a/server/test/repositories/system-config.repository.mock.ts
+++ b/server/test/repositories/system-config.repository.mock.ts
@@ -3,6 +3,7 @@ import { ISystemConfigRepository } from '@app/domain';
export const newSystemConfigRepositoryMock = (): jest.Mocked => {
return {
load: jest.fn().mockResolvedValue([]),
+ readFile: jest.fn(),
saveAll: jest.fn().mockResolvedValue([]),
deleteKeys: jest.fn(),
};
diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts
index 7ffb1f7b6f..9a47f53b7f 100644
--- a/web/src/api/open-api/api.ts
+++ b/web/src/api/open-api/api.ts
@@ -2173,6 +2173,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'clipEncode': boolean;
+ /**
+ *
+ * @type {boolean}
+ * @memberof ServerFeaturesDto
+ */
+ 'configFile': boolean;
/**
*
* @type {boolean}
diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts
index 75f3b47863..26b6550666 100644
--- a/web/src/api/utils.ts
+++ b/web/src/api/utils.ts
@@ -1,9 +1,23 @@
import type { AxiosError, AxiosPromise } from 'axios';
+import {
+ notificationController,
+ NotificationType,
+} from '../lib/components/shared-components/notification/notification';
+import { handleError } from '../lib/utils/handle-error';
import { api } from './api';
import type { UserResponseDto } from './open-api';
export type ApiError = AxiosError<{ message: string }>;
+export const copyToClipboard = async (secret: string) => {
+ try {
+ await navigator.clipboard.writeText(secret);
+ notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
+ } catch (error) {
+ handleError(error, 'Cannot copy to clipboard, make sure you are accessing the page through https');
+ }
+};
+
export const oauth = {
isCallback: (location: Location) => {
const search = location.search;
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
index 3a08a171b5..e840cd04de 100644
--- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte
@@ -20,6 +20,7 @@
import { fade } from 'svelte/transition';
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
+ export let disabled = false;
let savedConfig: SystemConfigFFmpegDto;
let defaultConfig: SystemConfigFFmpegDto;
@@ -90,6 +91,7 @@
diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
index 5ec9a34d54..b795db7d30 100644
--- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
@@ -11,6 +11,7 @@
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
+ export let disabled = false;
let savedConfig: SystemConfigJobDto;
let defaultConfig: SystemConfigJobDto;
@@ -78,6 +79,7 @@
diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
index 4b10e8535f..f8ac971cf2 100644
--- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
@@ -11,6 +11,8 @@
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
+ export let disabled = false;
+
let config: SystemConfigDto;
let defaultConfig: SystemConfigDto;
@@ -56,6 +58,7 @@
@@ -67,7 +70,7 @@
desc="URL of machine learning server"
bind:value={config.machineLearning.url}
required={true}
- disabled={!config.machineLearning.enabled}
+ disabled={disabled || !config.machineLearning.enabled}
isEdited={!(config.machineLearning.url === config.machineLearning.url)}
/>
@@ -75,20 +78,20 @@
title="SMART SEARCH"
subtitle="Extract CLIP embeddings for smart search"
bind:checked={config.machineLearning.clipEncodeEnabled}
- disabled={!config.machineLearning.enabled}
+ disabled={disabled || !config.machineLearning.enabled}
/>
@@ -97,6 +100,7 @@
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!isEqual(config, defaultConfig)}
+ {disabled}
/>
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
index 9e5ad58a96..0c6c4a5629 100644
--- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte
@@ -13,6 +13,7 @@
import SettingSwitch from '../setting-switch.svelte';
export let oauthConfig: SystemConfigOAuthDto;
+ export let disabled = false;
let savedConfig: SystemConfigOAuthDto;
let defaultConfig: SystemConfigOAuthDto;
@@ -117,14 +118,14 @@
>.
-
+
@@ -133,7 +134,7 @@
label="CLIENT ID"
bind:value={oauthConfig.clientId}
required={true}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
/>
@@ -142,7 +143,7 @@
label="CLIENT SECRET"
bind:value={oauthConfig.clientSecret}
required={true}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
/>
@@ -151,7 +152,7 @@
label="SCOPE"
bind:value={oauthConfig.scope}
required={true}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
isEdited={!(oauthConfig.scope == savedConfig.scope)}
/>
@@ -161,7 +162,7 @@
desc="Automatically set the user's storage label to the value of this claim."
bind:value={oauthConfig.storageLabelClaim}
required={true}
- disabled={!oauthConfig.storageLabelClaim}
+ disabled={disabled || !oauthConfig.storageLabelClaim}
isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
/>
@@ -170,7 +171,7 @@
label="BUTTON TEXT"
bind:value={oauthConfig.buttonText}
required={false}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
/>
@@ -178,20 +179,20 @@
title="AUTO REGISTER"
subtitle="Automatically register new users after signing in with OAuth"
bind:checked={oauthConfig.autoRegister}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
/>
handleToggleOverride()}
bind:checked={oauthConfig.mobileOverrideEnabled}
/>
@@ -202,7 +203,7 @@
label="MOBILE REDIRECT URI"
bind:value={oauthConfig.mobileRedirectUri}
required={true}
- disabled={!oauthConfig.enabled}
+ disabled={disabled || !oauthConfig.enabled}
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
/>
{/if}
@@ -212,6 +213,7 @@
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
+ {disabled}
/>
diff --git a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte
index fc9c938c5e..2c11512f54 100644
--- a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte
@@ -12,6 +12,7 @@
import SettingSwitch from '../setting-switch.svelte';
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
+ export let disabled = false;
let savedConfig: SystemConfigPasswordLoginDto;
let defaultConfig: SystemConfigPasswordLoginDto;
@@ -100,6 +101,7 @@