diff --git a/e2e/src/specs/server/api/jobs.e2e-spec.ts b/e2e/src/specs/server/api/jobs.e2e-spec.ts index f9d8b75c4..b4228eeb9 100644 --- a/e2e/src/specs/server/api/jobs.e2e-spec.ts +++ b/e2e/src/specs/server/api/jobs.e2e-spec.ts @@ -45,7 +45,7 @@ describe('/jobs', () => { config.machineLearning.enabled = false; config.metadata.faces.import = false; config.machineLearning.clip.enabled = false; - await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); + await updateConfig({ adminConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); }); it('should queue metadata extraction for missing assets', async () => { diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index 3b85e9e4c..10d3343a7 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -1,7 +1,7 @@ import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server'; import { + AdminConfigOAuthDto, LoginResponseDto, - SystemConfigOAuthDto, getConfigDefaults, getMyUser, getSessions, @@ -70,7 +70,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => return { url: redirectUrl, state, codeVerifier }; }; -const setupOAuth = async (token: string, dto: Partial) => { +const setupOAuth = async (token: string, dto: Partial) => { const options = { headers: asBearerAuth(token) }; const defaults = await getConfigDefaults(options); const merged = { @@ -80,7 +80,7 @@ const setupOAuth = async (token: string, dto: Partial) => allowInsecureRequests: true, ...dto, }; - await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options); + await updateConfig({ adminConfigDto: { ...defaults, oauth: merged } }, options); }; describe(`/oauth`, () => { diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 6490806bf..e4216ae57 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -647,7 +647,7 @@ export const utils = { resetAdminConfig: async (accessToken: string) => { const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); - await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); + await updateConfig({ adminConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); }, isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => { diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 8b6727168..4c6358472 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -333,6 +333,155 @@ "x-immich-state": "Stable" } }, + "/admin/config": { + "get": { + "description": "Retrieve admin configuration.", + "operationId": "getAdminConfig", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the admin configuration", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.read", + "x-immich-state": "Alpha" + }, + "put": { + "description": "Update the system configuration with a new system configuration.", + "operationId": "updateAdminConfig", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update the system configuration", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.update", + "x-immich-state": "Alpha" + } + }, + "/admin/config/defaults": { + "get": { + "description": "Retrieve the default value of every system configuration property.", + "operationId": "getAdminConfigDefaults", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the system configuration defaults", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.read", + "x-immich-state": "Alpha" + } + }, "/admin/database-backups": { "delete": { "description": "Delete a backup by its filename", @@ -1169,7 +1318,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigSmtpDto" + "$ref": "#/components/schemas/AdminConfigSmtpDto" } } }, @@ -5712,6 +5861,98 @@ "x-immich-state": "Stable" } }, + "/config": { + "get": { + "description": "Retrieve the system configuration properties that are visible to logged in users.", + "operationId": "getUserConfig", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the configuration with user visibility", + "tags": [ + "Config (user)" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "userConfig.read", + "x-immich-state": "Alpha" + } + }, + "/config/defaults": { + "get": { + "description": "Retrieve the default value of the configuration properties that are visible to logged in users.", + "operationId": "getUserConfigDefaults", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the default configuration with user visibility", + "tags": [ + "Config (user)" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "userConfig.read", + "x-immich-state": "Alpha" + } + }, "/download/archive": { "post": { "description": "Download a ZIP archive containing the specified assets. The assets must have been previously requested via the \"getDownloadInfo\" endpoint.", @@ -9807,6 +10048,74 @@ "x-immich-permission": "plugin.read" } }, + "/public/config": { + "get": { + "description": "Retrieve the system configuration properties that are visible to everyone.", + "operationId": "getPublicConfig", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicConfigDto" + } + } + }, + "description": "" + } + }, + "summary": "Get the public configuration", + "tags": [ + "Config (public)" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, + "/public/config/defaults": { + "get": { + "description": "Retrieve the default value of the configuration properties that are visible to everyone.", + "operationId": "getPublicConfigDefaults", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicConfigDto" + } + } + }, + "description": "" + } + }, + "summary": "Get the public configuration defaults", + "tags": [ + "Config (public)" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, "/queues": { "get": { "description": "Retrieves a list of queues.", @@ -11243,6 +11552,7 @@ }, "/server/config": { "get": { + "deprecated": true, "description": "Retrieve the current server configuration.", "operationId": "getServerConfig", "parameters": [], @@ -11260,7 +11570,8 @@ }, "summary": "Get config", "tags": [ - "Server" + "Server", + "Deprecated" ], "x-immich-history": [ { @@ -11274,13 +11585,19 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getPublicConfig" } ], - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/server/features": { "get": { + "deprecated": true, "description": "Retrieve available features supported by this server.", "operationId": "getServerFeatures", "parameters": [], @@ -11298,7 +11615,8 @@ }, "summary": "Get features", "tags": [ - "Server" + "Server", + "Deprecated" ], "x-immich-history": [ { @@ -11312,9 +11630,14 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getPublicConfig" } ], - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/server/license": { @@ -13357,6 +13680,7 @@ }, "/system-config": { "get": { + "deprecated": true, "description": "Retrieve the current system configuration.", "operationId": "getConfig", "parameters": [], @@ -13365,7 +13689,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -13385,7 +13709,8 @@ ], "summary": "Get system configuration", "tags": [ - "System config" + "System config", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -13400,12 +13725,18 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getAdminConfig" } ], "x-immich-permission": "systemConfig.read", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" }, "put": { + "deprecated": true, "description": "Update the system configuration with a new system configuration.", "operationId": "updateConfig", "parameters": [], @@ -13413,7 +13744,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -13424,7 +13755,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -13444,7 +13775,8 @@ ], "summary": "Update system configuration", "tags": [ - "System config" + "System config", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -13459,14 +13791,20 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "updateAdminConfig" } ], "x-immich-permission": "systemConfig.update", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/system-config/defaults": { "get": { + "deprecated": true, "description": "Retrieve the default values for the system configuration.", "operationId": "getConfigDefaults", "parameters": [], @@ -13475,7 +13813,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -13495,7 +13833,8 @@ ], "summary": "Get system configuration defaults", "tags": [ - "System config" + "System config", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -13510,10 +13849,15 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getAdminConfigDefaults" } ], "x-immich-permission": "systemConfig.read", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/system-config/storage-template-options": { @@ -16420,6 +16764,18 @@ "name": "Authentication (admin)", "description": "Administrative endpoints related to authentication." }, + { + "name": "Config (user)", + "description": "The system configuration properties that are visible to logged in users." + }, + { + "name": "Config (admin)", + "description": "Endpoints to view and modify the full system configuration." + }, + { + "name": "Config (public)", + "description": "The system configuration properties that are visible to everyone." + }, { "name": "Database Backups (admin)", "description": "Manage backups of the Immich database." @@ -16683,6 +17039,1211 @@ ], "type": "object" }, + "AdminConfigBackupsDto": { + "properties": { + "database": { + "$ref": "#/components/schemas/AdminConfigDatabaseBackupDto" + } + }, + "required": [ + "database" + ], + "type": "object" + }, + "AdminConfigClipDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "modelName": { + "description": "Name of the model to use", + "type": "string" + } + }, + "required": [ + "enabled", + "modelName" + ], + "type": "object" + }, + "AdminConfigDatabaseBackupDto": { + "properties": { + "cronExpression": { + "description": "Cron expression", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "keepLastAmount": { + "description": "Keep last amount", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "cronExpression", + "enabled", + "keepLastAmount" + ], + "type": "object" + }, + "AdminConfigDto": { + "description": "Configuration properties that are visible to the admin", + "properties": { + "backup": { + "$ref": "#/components/schemas/AdminConfigBackupsDto" + }, + "ffmpeg": { + "$ref": "#/components/schemas/AdminConfigFFmpegDto" + }, + "image": { + "$ref": "#/components/schemas/AdminConfigImageDto" + }, + "integrityChecks": { + "$ref": "#/components/schemas/AdminConfigIntegrityChecksDto" + }, + "job": { + "$ref": "#/components/schemas/AdminConfigJobDto" + }, + "library": { + "$ref": "#/components/schemas/AdminConfigLibraryDto" + }, + "logging": { + "$ref": "#/components/schemas/AdminConfigLoggingDto" + }, + "machineLearning": { + "$ref": "#/components/schemas/AdminConfigMachineLearningDto" + }, + "map": { + "$ref": "#/components/schemas/AdminConfigMapDto" + }, + "metadata": { + "$ref": "#/components/schemas/AdminConfigMetadataDto" + }, + "newVersionCheck": { + "$ref": "#/components/schemas/AdminConfigNewVersionCheckDto" + }, + "nightlyTasks": { + "$ref": "#/components/schemas/AdminConfigNightlyTasksDto" + }, + "notifications": { + "$ref": "#/components/schemas/AdminConfigNotificationsDto" + }, + "oauth": { + "$ref": "#/components/schemas/AdminConfigOAuthDto" + }, + "passwordLogin": { + "$ref": "#/components/schemas/AdminConfigPasswordLoginDto" + }, + "reverseGeocoding": { + "$ref": "#/components/schemas/AdminConfigReverseGeocodingDto" + }, + "server": { + "$ref": "#/components/schemas/AdminConfigServerDto" + }, + "storageTemplate": { + "$ref": "#/components/schemas/AdminConfigStorageTemplateDto" + }, + "templates": { + "$ref": "#/components/schemas/AdminConfigTemplatesDto" + }, + "theme": { + "$ref": "#/components/schemas/AdminConfigThemeDto" + }, + "trash": { + "$ref": "#/components/schemas/AdminConfigTrashDto" + }, + "user": { + "$ref": "#/components/schemas/AdminConfigUserDto" + } + }, + "required": [ + "backup", + "ffmpeg", + "image", + "integrityChecks", + "job", + "library", + "logging", + "machineLearning", + "map", + "metadata", + "newVersionCheck", + "nightlyTasks", + "notifications", + "oauth", + "passwordLogin", + "reverseGeocoding", + "server", + "storageTemplate", + "templates", + "theme", + "trash", + "user" + ], + "type": "object" + }, + "AdminConfigDuplicateDetectionDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "maxDistance": { + "description": "Maximum distance threshold for duplicate detection", + "format": "double", + "maximum": 0.1, + "minimum": 0.001, + "type": "number" + } + }, + "required": [ + "enabled", + "maxDistance" + ], + "type": "object" + }, + "AdminConfigFFmpegDto": { + "properties": { + "accel": { + "$ref": "#/components/schemas/TranscodeHWAccel" + }, + "accelDecode": { + "description": "Accelerated decode", + "type": "boolean" + }, + "acceptedAudioCodecs": { + "description": "Accepted audio codecs", + "items": { + "$ref": "#/components/schemas/AudioCodec" + }, + "type": "array" + }, + "acceptedContainers": { + "description": "Accepted containers", + "items": { + "$ref": "#/components/schemas/VideoContainer" + }, + "type": "array" + }, + "acceptedVideoCodecs": { + "description": "Accepted video codecs", + "items": { + "$ref": "#/components/schemas/VideoCodec" + }, + "type": "array" + }, + "bframes": { + "description": "B-frames", + "maximum": 16, + "minimum": -1, + "type": "integer" + }, + "cqMode": { + "$ref": "#/components/schemas/CQMode" + }, + "crf": { + "description": "CRF", + "maximum": 51, + "minimum": 0, + "type": "integer" + }, + "gopSize": { + "description": "GOP size", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "maxBitrate": { + "description": "Max bitrate", + "type": "string" + }, + "preferredHwDevice": { + "description": "Preferred hardware device", + "type": "string" + }, + "preset": { + "description": "Preset", + "type": "string" + }, + "realtime": { + "$ref": "#/components/schemas/AdminConfigFFmpegRealtimeDto" + }, + "refs": { + "description": "References", + "maximum": 6, + "minimum": 0, + "type": "integer" + }, + "targetAudioCodec": { + "$ref": "#/components/schemas/AudioCodec" + }, + "targetResolution": { + "description": "Target resolution", + "type": "string" + }, + "targetVideoCodec": { + "$ref": "#/components/schemas/VideoCodec" + }, + "temporalAQ": { + "description": "Temporal AQ", + "type": "boolean" + }, + "threads": { + "description": "Threads", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "tonemap": { + "$ref": "#/components/schemas/ToneMapping" + }, + "transcode": { + "$ref": "#/components/schemas/TranscodePolicy" + }, + "twoPass": { + "description": "Two pass", + "type": "boolean" + } + }, + "required": [ + "accel", + "accelDecode", + "acceptedAudioCodecs", + "acceptedContainers", + "acceptedVideoCodecs", + "bframes", + "cqMode", + "crf", + "gopSize", + "maxBitrate", + "preferredHwDevice", + "preset", + "realtime", + "refs", + "targetAudioCodec", + "targetResolution", + "targetVideoCodec", + "temporalAQ", + "threads", + "tonemap", + "transcode", + "twoPass" + ], + "type": "object" + }, + "AdminConfigFFmpegRealtimeDto": { + "properties": { + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + }, + "resolutions": { + "description": "Resolutions to use for real-time HLS transcoding", + "items": { + "$ref": "#/components/schemas/HlsVideoResolution" + }, + "type": "array" + }, + "videoCodecs": { + "description": "Video codecs to use for real-time HLS transcoding", + "items": { + "$ref": "#/components/schemas/VideoCodec" + }, + "type": "array" + } + }, + "required": [ + "enabled", + "resolutions", + "videoCodecs" + ], + "type": "object" + }, + "AdminConfigFacesDto": { + "properties": { + "import": { + "description": "Import", + "type": "boolean" + } + }, + "required": [ + "import" + ], + "type": "object" + }, + "AdminConfigFacialRecognitionDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "maxDistance": { + "description": "Maximum distance threshold for face recognition", + "format": "double", + "maximum": 2, + "minimum": 0.1, + "type": "number" + }, + "minFaces": { + "description": "Minimum number of faces required for recognition", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minScore": { + "description": "Minimum confidence score for face detection", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" + }, + "modelName": { + "description": "Name of the model to use", + "type": "string" + } + }, + "required": [ + "enabled", + "maxDistance", + "minFaces", + "minScore", + "modelName" + ], + "type": "object" + }, + "AdminConfigGeneratedFullsizeImageDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/ImageFormat" + }, + "progressive": { + "description": "Progressive", + "type": "boolean" + }, + "quality": { + "description": "Quality", + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "enabled", + "format", + "quality" + ], + "type": "object" + }, + "AdminConfigGeneratedImageDto": { + "properties": { + "format": { + "$ref": "#/components/schemas/ImageFormat" + }, + "progressive": { + "description": "Progressive", + "type": "boolean" + }, + "quality": { + "description": "Quality", + "maximum": 100, + "minimum": 1, + "type": "integer" + }, + "size": { + "description": "Size", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "format", + "quality", + "size" + ], + "type": "object" + }, + "AdminConfigImageDto": { + "properties": { + "colorspace": { + "$ref": "#/components/schemas/Colorspace" + }, + "extractEmbedded": { + "description": "Extract embedded", + "type": "boolean" + }, + "fullsize": { + "$ref": "#/components/schemas/AdminConfigGeneratedFullsizeImageDto" + }, + "preview": { + "$ref": "#/components/schemas/AdminConfigGeneratedImageDto" + }, + "thumbnail": { + "$ref": "#/components/schemas/AdminConfigGeneratedImageDto" + } + }, + "required": [ + "colorspace", + "extractEmbedded", + "fullsize", + "preview", + "thumbnail" + ], + "type": "object" + }, + "AdminConfigIntegrityChecksDto": { + "description": "Integrity checks config", + "properties": { + "checksumFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityChecksumJobDto" + }, + "missingFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityJobDto" + }, + "untrackedFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityJobDto" + } + }, + "required": [ + "checksumFiles", + "missingFiles", + "untrackedFiles" + ], + "type": "object" + }, + "AdminConfigIntegrityChecksumJobDto": { + "description": "Integrity checksum job config", + "properties": { + "cronExpression": { + "description": "Cron expression for when the integrity check should run", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "percentageLimit": { + "description": "Percentage limit of the integrity checksum job", + "format": "double", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "timeLimit": { + "description": "How long the integrity checksum job may run for", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cronExpression", + "enabled", + "percentageLimit", + "timeLimit" + ], + "type": "object" + }, + "AdminConfigIntegrityJobDto": { + "description": "Integrity job config", + "properties": { + "cronExpression": { + "description": "Cron expression for when the integrity check should run", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "cronExpression", + "enabled" + ], + "type": "object" + }, + "AdminConfigJobDto": { + "properties": { + "backgroundTask": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "editor": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "faceDetection": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "integrityCheck": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "library": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "metadataExtraction": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "migration": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "notifications": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "ocr": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "search": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "sidecar": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "smartSearch": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "thumbnailGeneration": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "videoConversion": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "workflow": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + } + }, + "required": [ + "backgroundTask", + "editor", + "faceDetection", + "integrityCheck", + "library", + "metadataExtraction", + "migration", + "notifications", + "ocr", + "search", + "sidecar", + "smartSearch", + "thumbnailGeneration", + "videoConversion", + "workflow" + ], + "type": "object" + }, + "AdminConfigJobSettingsDto": { + "properties": { + "concurrency": { + "description": "Concurrency", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "concurrency" + ], + "type": "object" + }, + "AdminConfigLibraryDto": { + "properties": { + "scan": { + "$ref": "#/components/schemas/AdminConfigLibraryScanDto" + }, + "watch": { + "$ref": "#/components/schemas/AdminConfigLibraryWatchDto" + } + }, + "required": [ + "scan", + "watch" + ], + "type": "object" + }, + "AdminConfigLibraryScanDto": { + "properties": { + "cronExpression": { + "description": "Cron expression", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "cronExpression", + "enabled" + ], + "type": "object" + }, + "AdminConfigLibraryWatchDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "AdminConfigLoggingDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "level": { + "$ref": "#/components/schemas/LogLevel" + } + }, + "required": [ + "enabled", + "level" + ], + "type": "object" + }, + "AdminConfigMachineLearningAvailabilityChecksDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "interval": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "timeout": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "enabled", + "interval", + "timeout" + ], + "type": "object" + }, + "AdminConfigMachineLearningDto": { + "properties": { + "availabilityChecks": { + "$ref": "#/components/schemas/AdminConfigMachineLearningAvailabilityChecksDto" + }, + "clip": { + "$ref": "#/components/schemas/AdminConfigClipDto" + }, + "duplicateDetection": { + "$ref": "#/components/schemas/AdminConfigDuplicateDetectionDto" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "facialRecognition": { + "$ref": "#/components/schemas/AdminConfigFacialRecognitionDto" + }, + "ocr": { + "$ref": "#/components/schemas/AdminConfigOcrDto" + }, + "urls": { + "description": "ML service URLs", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "availabilityChecks", + "clip", + "duplicateDetection", + "enabled", + "facialRecognition", + "ocr", + "urls" + ], + "type": "object" + }, + "AdminConfigMapDto": { + "properties": { + "darkStyle": { + "description": "Dark map style URL", + "format": "uri", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "lightStyle": { + "description": "Light map style URL", + "format": "uri", + "type": "string" + } + }, + "required": [ + "darkStyle", + "enabled", + "lightStyle" + ], + "type": "object" + }, + "AdminConfigMetadataDto": { + "properties": { + "faces": { + "$ref": "#/components/schemas/AdminConfigFacesDto" + } + }, + "required": [ + "faces" + ], + "type": "object" + }, + "AdminConfigNewVersionCheckDto": { + "properties": { + "channel": { + "$ref": "#/components/schemas/ReleaseChannel" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "channel", + "enabled" + ], + "type": "object" + }, + "AdminConfigNightlyTasksDto": { + "properties": { + "clusterNewFaces": { + "description": "Cluster new faces", + "type": "boolean" + }, + "databaseCleanup": { + "description": "Database cleanup", + "type": "boolean" + }, + "generateMemories": { + "description": "Generate memories", + "type": "boolean" + }, + "missingThumbnails": { + "description": "Missing thumbnails", + "type": "boolean" + }, + "startTime": { + "description": "Start time (HH:MM)", + "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$", + "type": "string" + }, + "syncQuotaUsage": { + "description": "Sync quota usage", + "type": "boolean" + } + }, + "required": [ + "clusterNewFaces", + "databaseCleanup", + "generateMemories", + "missingThumbnails", + "startTime", + "syncQuotaUsage" + ], + "type": "object" + }, + "AdminConfigNotificationsDto": { + "properties": { + "smtp": { + "$ref": "#/components/schemas/AdminConfigSmtpDto" + } + }, + "required": [ + "smtp" + ], + "type": "object" + }, + "AdminConfigOAuthDto": { + "properties": { + "allowInsecureRequests": { + "description": "Allow insecure requests", + "type": "boolean" + }, + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" + }, + "autoRegister": { + "description": "Auto register", + "type": "boolean" + }, + "buttonText": { + "description": "Button text", + "type": "string" + }, + "clientId": { + "description": "Client ID", + "type": "string" + }, + "clientSecret": { + "description": "Client secret", + "type": "string" + }, + "defaultStorageQuota": { + "description": "Default storage quota", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "endSessionEndpoint": { + "description": "End session endpoint", + "type": "string" + }, + "issuerUrl": { + "description": "Issuer URL", + "type": "string" + }, + "mobileOverrideEnabled": { + "description": "Mobile override enabled", + "type": "boolean" + }, + "mobileRedirectUri": { + "description": "Mobile redirect URI (set to empty string to disable)", + "type": "string" + }, + "profileSigningAlgorithm": { + "description": "Profile signing algorithm", + "type": "string" + }, + "prompt": { + "description": "OAuth prompt parameter (e.g. select_account, login, consent)", + "type": "string" + }, + "roleClaim": { + "description": "Role claim", + "type": "string" + }, + "scope": { + "description": "Scope", + "type": "string" + }, + "signingAlgorithm": { + "description": "Signing algorithm", + "type": "string" + }, + "storageLabelClaim": { + "description": "Storage label claim", + "type": "string" + }, + "storageQuotaClaim": { + "description": "Storage quota claim", + "type": "string" + }, + "timeout": { + "description": "Timeout", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "tokenEndpointAuthMethod": { + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" + } + }, + "required": [ + "allowInsecureRequests", + "autoLaunch", + "autoRegister", + "buttonText", + "clientId", + "clientSecret", + "defaultStorageQuota", + "enabled", + "endSessionEndpoint", + "issuerUrl", + "mobileOverrideEnabled", + "mobileRedirectUri", + "profileSigningAlgorithm", + "prompt", + "roleClaim", + "scope", + "signingAlgorithm", + "storageLabelClaim", + "storageQuotaClaim", + "timeout", + "tokenEndpointAuthMethod" + ], + "type": "object" + }, + "AdminConfigOcrDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "maxResolution": { + "description": "Maximum resolution for OCR processing", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minDetectionScore": { + "description": "Minimum confidence score for text detection", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" + }, + "minRecognitionScore": { + "description": "Minimum confidence score for text recognition", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" + }, + "modelName": { + "description": "Name of the model to use", + "type": "string" + } + }, + "required": [ + "enabled", + "maxResolution", + "minDetectionScore", + "minRecognitionScore", + "modelName" + ], + "type": "object" + }, + "AdminConfigPasswordLoginDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "AdminConfigReverseGeocodingDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "AdminConfigServerDto": { + "properties": { + "externalDomain": { + "description": "External domain", + "type": "string" + }, + "loginPageMessage": { + "description": "Login page message", + "type": "string" + }, + "publicUsers": { + "description": "Public users", + "type": "boolean" + } + }, + "required": [ + "externalDomain", + "loginPageMessage", + "publicUsers" + ], + "type": "object" + }, + "AdminConfigSmtpDto": { + "properties": { + "enabled": { + "description": "Whether SMTP email notifications are enabled", + "type": "boolean" + }, + "from": { + "description": "Email address to send from", + "type": "string" + }, + "replyTo": { + "description": "Email address for replies", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/AdminConfigSmtpTransportDto" + } + }, + "required": [ + "enabled", + "from", + "replyTo", + "transport" + ], + "type": "object" + }, + "AdminConfigSmtpTransportDto": { + "properties": { + "host": { + "description": "SMTP server hostname", + "type": "string" + }, + "ignoreCert": { + "description": "Whether to ignore SSL certificate errors", + "type": "boolean" + }, + "password": { + "description": "SMTP password", + "type": "string" + }, + "port": { + "description": "SMTP server port", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "secure": { + "description": "Whether to use secure connection (TLS/SSL)", + "type": "boolean" + }, + "username": { + "description": "SMTP username", + "type": "string" + } + }, + "required": [ + "host", + "ignoreCert", + "password", + "port", + "secure", + "username" + ], + "type": "object" + }, + "AdminConfigStorageTemplateDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "hashVerificationEnabled": { + "description": "Hash verification enabled", + "type": "boolean" + }, + "template": { + "description": "Template", + "type": "string" + } + }, + "required": [ + "enabled", + "hashVerificationEnabled", + "template" + ], + "type": "object" + }, + "AdminConfigTemplateEmailsDto": { + "properties": { + "albumInviteTemplate": { + "description": "Album invite template", + "type": "string" + }, + "albumUpdateTemplate": { + "description": "Album update template", + "type": "string" + }, + "welcomeTemplate": { + "description": "Welcome template", + "type": "string" + } + }, + "required": [ + "albumInviteTemplate", + "albumUpdateTemplate", + "welcomeTemplate" + ], + "type": "object" + }, + "AdminConfigTemplatesDto": { + "properties": { + "email": { + "$ref": "#/components/schemas/AdminConfigTemplateEmailsDto" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "AdminConfigThemeDto": { + "properties": { + "customCss": { + "description": "Custom CSS for theming", + "type": "string" + } + }, + "required": [ + "customCss" + ], + "type": "object" + }, + "AdminConfigTrashDto": { + "properties": { + "days": { + "description": "Days", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "days", + "enabled" + ], + "type": "object" + }, + "AdminConfigUserDto": { + "properties": { + "deleteDelay": { + "description": "Delete delay", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "deleteDelay" + ], + "type": "object" + }, "AdminOnboardingUpdateDto": { "properties": { "isOnboarded": { @@ -18468,23 +20029,6 @@ ], "type": "object" }, - "CLIPConfig": { - "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "modelName": { - "description": "Name of the model to use", - "type": "string" - } - }, - "required": [ - "enabled", - "modelName" - ], - "type": "object" - }, "CQMode": { "description": "CQ mode", "enum": [ @@ -18781,30 +20325,6 @@ ], "type": "object" }, - "DatabaseBackupConfig": { - "properties": { - "cronExpression": { - "description": "Cron expression", - "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "keepLastAmount": { - "description": "Keep last amount", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "cronExpression", - "enabled", - "keepLastAmount" - ], - "type": "object" - }, "DatabaseBackupDeleteDto": { "properties": { "backups": { @@ -19002,26 +20522,6 @@ }, "type": "object" }, - "DuplicateDetectionConfig": { - "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "maxDistance": { - "description": "Maximum distance threshold for duplicate detection", - "format": "double", - "maximum": 0.1, - "minimum": 0.001, - "type": "number" - } - }, - "required": [ - "enabled", - "maxDistance" - ], - "type": "object" - }, "DuplicateResolveDto": { "properties": { "groups": { @@ -19306,46 +20806,6 @@ ], "type": "object" }, - "FacialRecognitionConfig": { - "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "maxDistance": { - "description": "Maximum distance threshold for face recognition", - "format": "double", - "maximum": 2, - "minimum": 0.1, - "type": "number" - }, - "minFaces": { - "description": "Minimum number of faces required for recognition", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "minScore": { - "description": "Minimum confidence score for face detection", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" - }, - "modelName": { - "description": "Name of the model to use", - "type": "string" - } - }, - "required": [ - "enabled", - "maxDistance", - "minFaces", - "minScore", - "modelName" - ], - "type": "object" - }, "FoldersResponse": { "properties": { "enabled": { @@ -19549,20 +21009,6 @@ ], "type": "string" }, - "JobSettingsDto": { - "properties": { - "concurrency": { - "description": "Concurrency", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "concurrency" - ], - "type": "object" - }, "LibraryResponseDto": { "properties": { "assetCount": { @@ -19794,30 +21240,6 @@ ], "type": "object" }, - "MachineLearningAvailabilityChecksDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "interval": { - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "timeout": { - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - } - }, - "required": [ - "enabled", - "interval", - "timeout" - ], - "type": "object" - }, "MaintenanceAction": { "description": "Maintenance action", "enum": [ @@ -20799,46 +22221,6 @@ ], "type": "string" }, - "OcrConfig": { - "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "maxResolution": { - "description": "Maximum resolution for OCR processing", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "minDetectionScore": { - "description": "Minimum confidence score for text detection", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" - }, - "minRecognitionScore": { - "description": "Minimum confidence score for text recognition", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" - }, - "modelName": { - "description": "Name of the model to use", - "type": "string" - } - }, - "required": [ - "enabled", - "maxResolution", - "minDetectionScore", - "minRecognitionScore", - "modelName" - ], - "type": "object" - }, "OnThisDayDto": { "properties": { "year": { @@ -21148,6 +22530,9 @@ "backup.download", "backup.upload", "backup.delete", + "adminConfig.read", + "adminConfig.update", + "userConfig.read", "duplicate.read", "duplicate.delete", "face.create", @@ -21683,6 +23068,88 @@ ], "type": "object" }, + "PublicConfigDto": { + "description": "Configuration properties that are visible to everyone", + "properties": { + "oauth": { + "$ref": "#/components/schemas/PublicConfigOAuthDto" + }, + "passwordLogin": { + "$ref": "#/components/schemas/PublicConfigPasswordLoginDto" + }, + "server": { + "$ref": "#/components/schemas/PublicConfigServerDto" + }, + "theme": { + "$ref": "#/components/schemas/PublicConfigThemeDto" + } + }, + "required": [ + "oauth", + "passwordLogin", + "server", + "theme" + ], + "type": "object" + }, + "PublicConfigOAuthDto": { + "properties": { + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" + }, + "buttonText": { + "description": "Button text", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "autoLaunch", + "buttonText", + "enabled" + ], + "type": "object" + }, + "PublicConfigPasswordLoginDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "PublicConfigServerDto": { + "properties": { + "loginPageMessage": { + "description": "Login page message", + "type": "string" + } + }, + "required": [ + "loginPageMessage" + ], + "type": "object" + }, + "PublicConfigThemeDto": { + "properties": { + "customCss": { + "description": "Custom CSS for theming", + "type": "string" + } + }, + "required": [ + "customCss" + ], + "type": "object" + }, "PurchaseResponse": { "properties": { "hideBuyButtonUntil": { @@ -25782,976 +27249,6 @@ ], "type": "object" }, - "SystemConfigBackupsDto": { - "properties": { - "database": { - "$ref": "#/components/schemas/DatabaseBackupConfig" - } - }, - "required": [ - "database" - ], - "type": "object" - }, - "SystemConfigDto": { - "description": "System configuration", - "properties": { - "backup": { - "$ref": "#/components/schemas/SystemConfigBackupsDto" - }, - "ffmpeg": { - "$ref": "#/components/schemas/SystemConfigFFmpegDto" - }, - "image": { - "$ref": "#/components/schemas/SystemConfigImageDto" - }, - "integrityChecks": { - "$ref": "#/components/schemas/SystemConfigIntegrityChecks" - }, - "job": { - "$ref": "#/components/schemas/SystemConfigJobDto" - }, - "library": { - "$ref": "#/components/schemas/SystemConfigLibraryDto" - }, - "logging": { - "$ref": "#/components/schemas/SystemConfigLoggingDto" - }, - "machineLearning": { - "$ref": "#/components/schemas/SystemConfigMachineLearningDto" - }, - "map": { - "$ref": "#/components/schemas/SystemConfigMapDto" - }, - "metadata": { - "$ref": "#/components/schemas/SystemConfigMetadataDto" - }, - "newVersionCheck": { - "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" - }, - "nightlyTasks": { - "$ref": "#/components/schemas/SystemConfigNightlyTasksDto" - }, - "notifications": { - "$ref": "#/components/schemas/SystemConfigNotificationsDto" - }, - "oauth": { - "$ref": "#/components/schemas/SystemConfigOAuthDto" - }, - "passwordLogin": { - "$ref": "#/components/schemas/SystemConfigPasswordLoginDto" - }, - "reverseGeocoding": { - "$ref": "#/components/schemas/SystemConfigReverseGeocodingDto" - }, - "server": { - "$ref": "#/components/schemas/SystemConfigServerDto" - }, - "storageTemplate": { - "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" - }, - "templates": { - "$ref": "#/components/schemas/SystemConfigTemplatesDto" - }, - "theme": { - "$ref": "#/components/schemas/SystemConfigThemeDto" - }, - "trash": { - "$ref": "#/components/schemas/SystemConfigTrashDto" - }, - "user": { - "$ref": "#/components/schemas/SystemConfigUserDto" - } - }, - "required": [ - "backup", - "ffmpeg", - "image", - "integrityChecks", - "job", - "library", - "logging", - "machineLearning", - "map", - "metadata", - "newVersionCheck", - "nightlyTasks", - "notifications", - "oauth", - "passwordLogin", - "reverseGeocoding", - "server", - "storageTemplate", - "templates", - "theme", - "trash", - "user" - ], - "type": "object" - }, - "SystemConfigFFmpegDto": { - "properties": { - "accel": { - "$ref": "#/components/schemas/TranscodeHWAccel" - }, - "accelDecode": { - "description": "Accelerated decode", - "type": "boolean" - }, - "acceptedAudioCodecs": { - "description": "Accepted audio codecs", - "items": { - "$ref": "#/components/schemas/AudioCodec" - }, - "type": "array" - }, - "acceptedContainers": { - "description": "Accepted containers", - "items": { - "$ref": "#/components/schemas/VideoContainer" - }, - "type": "array" - }, - "acceptedVideoCodecs": { - "description": "Accepted video codecs", - "items": { - "$ref": "#/components/schemas/VideoCodec" - }, - "type": "array" - }, - "bframes": { - "description": "B-frames", - "maximum": 16, - "minimum": -1, - "type": "integer" - }, - "cqMode": { - "$ref": "#/components/schemas/CQMode" - }, - "crf": { - "description": "CRF", - "maximum": 51, - "minimum": 0, - "type": "integer" - }, - "gopSize": { - "description": "GOP size", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "maxBitrate": { - "description": "Max bitrate", - "type": "string" - }, - "preferredHwDevice": { - "description": "Preferred hardware device", - "type": "string" - }, - "preset": { - "description": "Preset", - "type": "string" - }, - "realtime": { - "$ref": "#/components/schemas/SystemConfigFFmpegRealtimeDto" - }, - "refs": { - "description": "References", - "maximum": 6, - "minimum": 0, - "type": "integer" - }, - "targetAudioCodec": { - "$ref": "#/components/schemas/AudioCodec" - }, - "targetResolution": { - "description": "Target resolution", - "type": "string" - }, - "targetVideoCodec": { - "$ref": "#/components/schemas/VideoCodec" - }, - "temporalAQ": { - "description": "Temporal AQ", - "type": "boolean" - }, - "threads": { - "description": "Threads", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "tonemap": { - "$ref": "#/components/schemas/ToneMapping" - }, - "transcode": { - "$ref": "#/components/schemas/TranscodePolicy" - }, - "twoPass": { - "description": "Two pass", - "type": "boolean" - } - }, - "required": [ - "accel", - "accelDecode", - "acceptedAudioCodecs", - "acceptedContainers", - "acceptedVideoCodecs", - "bframes", - "cqMode", - "crf", - "gopSize", - "maxBitrate", - "preferredHwDevice", - "preset", - "realtime", - "refs", - "targetAudioCodec", - "targetResolution", - "targetVideoCodec", - "temporalAQ", - "threads", - "tonemap", - "transcode", - "twoPass" - ], - "type": "object" - }, - "SystemConfigFFmpegRealtimeDto": { - "properties": { - "enabled": { - "description": "Enable real-time HLS transcoding (alpha)", - "type": "boolean" - }, - "resolutions": { - "description": "Resolutions to use for real-time HLS transcoding", - "items": { - "$ref": "#/components/schemas/HlsVideoResolution" - }, - "type": "array" - }, - "videoCodecs": { - "description": "Video codecs to use for real-time HLS transcoding", - "items": { - "$ref": "#/components/schemas/VideoCodec" - }, - "type": "array" - } - }, - "required": [ - "enabled", - "resolutions", - "videoCodecs" - ], - "type": "object" - }, - "SystemConfigFacesDto": { - "properties": { - "import": { - "description": "Import", - "type": "boolean" - } - }, - "required": [ - "import" - ], - "type": "object" - }, - "SystemConfigGeneratedFullsizeImageDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "format": { - "$ref": "#/components/schemas/ImageFormat" - }, - "progressive": { - "description": "Progressive", - "type": "boolean" - }, - "quality": { - "description": "Quality", - "maximum": 100, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "enabled", - "format", - "quality" - ], - "type": "object" - }, - "SystemConfigGeneratedImageDto": { - "properties": { - "format": { - "$ref": "#/components/schemas/ImageFormat" - }, - "progressive": { - "description": "Progressive", - "type": "boolean" - }, - "quality": { - "description": "Quality", - "maximum": 100, - "minimum": 1, - "type": "integer" - }, - "size": { - "description": "Size", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "format", - "quality", - "size" - ], - "type": "object" - }, - "SystemConfigImageDto": { - "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" - }, - "extractEmbedded": { - "description": "Extract embedded", - "type": "boolean" - }, - "fullsize": { - "$ref": "#/components/schemas/SystemConfigGeneratedFullsizeImageDto" - }, - "preview": { - "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" - }, - "thumbnail": { - "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" - } - }, - "required": [ - "colorspace", - "extractEmbedded", - "fullsize", - "preview", - "thumbnail" - ], - "type": "object" - }, - "SystemConfigIntegrityChecks": { - "description": "Integrity checks config", - "properties": { - "checksumFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityChecksumJob" - }, - "missingFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityJob" - }, - "untrackedFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityJob" - } - }, - "required": [ - "checksumFiles", - "missingFiles", - "untrackedFiles" - ], - "type": "object" - }, - "SystemConfigIntegrityChecksumJob": { - "description": "Integrity checksum job config", - "properties": { - "cronExpression": { - "description": "Cron expression for when the integrity check should run", - "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "percentageLimit": { - "description": "Percentage limit of the integrity checksum job", - "format": "double", - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "timeLimit": { - "description": "How long the integrity checksum job may run for", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "cronExpression", - "enabled", - "percentageLimit", - "timeLimit" - ], - "type": "object" - }, - "SystemConfigIntegrityJob": { - "description": "Integrity job config", - "properties": { - "cronExpression": { - "description": "Cron expression for when the integrity check should run", - "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "cronExpression", - "enabled" - ], - "type": "object" - }, - "SystemConfigJobDto": { - "properties": { - "backgroundTask": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "editor": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "faceDetection": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "integrityCheck": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "library": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "metadataExtraction": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "migration": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "notifications": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "ocr": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "search": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "sidecar": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "smartSearch": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "thumbnailGeneration": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "videoConversion": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "workflow": { - "$ref": "#/components/schemas/JobSettingsDto" - } - }, - "required": [ - "backgroundTask", - "editor", - "faceDetection", - "integrityCheck", - "library", - "metadataExtraction", - "migration", - "notifications", - "ocr", - "search", - "sidecar", - "smartSearch", - "thumbnailGeneration", - "videoConversion", - "workflow" - ], - "type": "object" - }, - "SystemConfigLibraryDto": { - "properties": { - "scan": { - "$ref": "#/components/schemas/SystemConfigLibraryScanDto" - }, - "watch": { - "$ref": "#/components/schemas/SystemConfigLibraryWatchDto" - } - }, - "required": [ - "scan", - "watch" - ], - "type": "object" - }, - "SystemConfigLibraryScanDto": { - "properties": { - "cronExpression": { - "description": "Cron expression", - "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "cronExpression", - "enabled" - ], - "type": "object" - }, - "SystemConfigLibraryWatchDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "SystemConfigLoggingDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "level": { - "$ref": "#/components/schemas/LogLevel" - } - }, - "required": [ - "enabled", - "level" - ], - "type": "object" - }, - "SystemConfigMachineLearningDto": { - "properties": { - "availabilityChecks": { - "$ref": "#/components/schemas/MachineLearningAvailabilityChecksDto" - }, - "clip": { - "$ref": "#/components/schemas/CLIPConfig" - }, - "duplicateDetection": { - "$ref": "#/components/schemas/DuplicateDetectionConfig" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "facialRecognition": { - "$ref": "#/components/schemas/FacialRecognitionConfig" - }, - "ocr": { - "$ref": "#/components/schemas/OcrConfig" - }, - "urls": { - "description": "ML service URLs", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "availabilityChecks", - "clip", - "duplicateDetection", - "enabled", - "facialRecognition", - "ocr", - "urls" - ], - "type": "object" - }, - "SystemConfigMapDto": { - "properties": { - "darkStyle": { - "description": "Dark map style URL", - "format": "uri", - "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "lightStyle": { - "description": "Light map style URL", - "format": "uri", - "type": "string" - } - }, - "required": [ - "darkStyle", - "enabled", - "lightStyle" - ], - "type": "object" - }, - "SystemConfigMetadataDto": { - "properties": { - "faces": { - "$ref": "#/components/schemas/SystemConfigFacesDto" - } - }, - "required": [ - "faces" - ], - "type": "object" - }, - "SystemConfigNewVersionCheckDto": { - "properties": { - "channel": { - "$ref": "#/components/schemas/ReleaseChannel" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "channel", - "enabled" - ], - "type": "object" - }, - "SystemConfigNightlyTasksDto": { - "properties": { - "clusterNewFaces": { - "description": "Cluster new faces", - "type": "boolean" - }, - "databaseCleanup": { - "description": "Database cleanup", - "type": "boolean" - }, - "generateMemories": { - "description": "Generate memories", - "type": "boolean" - }, - "missingThumbnails": { - "description": "Missing thumbnails", - "type": "boolean" - }, - "startTime": { - "description": "Start time (HH:MM)", - "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$", - "type": "string" - }, - "syncQuotaUsage": { - "description": "Sync quota usage", - "type": "boolean" - } - }, - "required": [ - "clusterNewFaces", - "databaseCleanup", - "generateMemories", - "missingThumbnails", - "startTime", - "syncQuotaUsage" - ], - "type": "object" - }, - "SystemConfigNotificationsDto": { - "properties": { - "smtp": { - "$ref": "#/components/schemas/SystemConfigSmtpDto" - } - }, - "required": [ - "smtp" - ], - "type": "object" - }, - "SystemConfigOAuthDto": { - "properties": { - "allowInsecureRequests": { - "description": "Allow insecure requests", - "type": "boolean" - }, - "autoLaunch": { - "description": "Auto launch", - "type": "boolean" - }, - "autoRegister": { - "description": "Auto register", - "type": "boolean" - }, - "buttonText": { - "description": "Button text", - "type": "string" - }, - "clientId": { - "description": "Client ID", - "type": "string" - }, - "clientSecret": { - "description": "Client secret", - "type": "string" - }, - "defaultStorageQuota": { - "description": "Default storage quota", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "endSessionEndpoint": { - "description": "End session endpoint", - "type": "string" - }, - "issuerUrl": { - "description": "Issuer URL", - "type": "string" - }, - "mobileOverrideEnabled": { - "description": "Mobile override enabled", - "type": "boolean" - }, - "mobileRedirectUri": { - "description": "Mobile redirect URI (set to empty string to disable)", - "type": "string" - }, - "profileSigningAlgorithm": { - "description": "Profile signing algorithm", - "type": "string" - }, - "prompt": { - "description": "OAuth prompt parameter (e.g. select_account, login, consent)", - "type": "string" - }, - "roleClaim": { - "description": "Role claim", - "type": "string" - }, - "scope": { - "description": "Scope", - "type": "string" - }, - "signingAlgorithm": { - "description": "Signing algorithm", - "type": "string" - }, - "storageLabelClaim": { - "description": "Storage label claim", - "type": "string" - }, - "storageQuotaClaim": { - "description": "Storage quota claim", - "type": "string" - }, - "timeout": { - "description": "Timeout", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "tokenEndpointAuthMethod": { - "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" - } - }, - "required": [ - "allowInsecureRequests", - "autoLaunch", - "autoRegister", - "buttonText", - "clientId", - "clientSecret", - "defaultStorageQuota", - "enabled", - "endSessionEndpoint", - "issuerUrl", - "mobileOverrideEnabled", - "mobileRedirectUri", - "profileSigningAlgorithm", - "prompt", - "roleClaim", - "scope", - "signingAlgorithm", - "storageLabelClaim", - "storageQuotaClaim", - "timeout", - "tokenEndpointAuthMethod" - ], - "type": "object" - }, - "SystemConfigPasswordLoginDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "SystemConfigReverseGeocodingDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "SystemConfigServerDto": { - "properties": { - "externalDomain": { - "description": "External domain", - "type": "string" - }, - "loginPageMessage": { - "description": "Login page message", - "type": "string" - }, - "publicUsers": { - "description": "Public users", - "type": "boolean" - } - }, - "required": [ - "externalDomain", - "loginPageMessage", - "publicUsers" - ], - "type": "object" - }, - "SystemConfigSmtpDto": { - "properties": { - "enabled": { - "description": "Whether SMTP email notifications are enabled", - "type": "boolean" - }, - "from": { - "description": "Email address to send from", - "type": "string" - }, - "replyTo": { - "description": "Email address for replies", - "type": "string" - }, - "transport": { - "$ref": "#/components/schemas/SystemConfigSmtpTransportDto" - } - }, - "required": [ - "enabled", - "from", - "replyTo", - "transport" - ], - "type": "object" - }, - "SystemConfigSmtpTransportDto": { - "properties": { - "host": { - "description": "SMTP server hostname", - "type": "string" - }, - "ignoreCert": { - "description": "Whether to ignore SSL certificate errors", - "type": "boolean" - }, - "password": { - "description": "SMTP password", - "type": "string" - }, - "port": { - "description": "SMTP server port", - "maximum": 65535, - "minimum": 0, - "type": "integer" - }, - "secure": { - "description": "Whether to use secure connection (TLS/SSL)", - "type": "boolean" - }, - "username": { - "description": "SMTP username", - "type": "string" - } - }, - "required": [ - "host", - "ignoreCert", - "password", - "port", - "secure", - "username" - ], - "type": "object" - }, - "SystemConfigStorageTemplateDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "hashVerificationEnabled": { - "description": "Hash verification enabled", - "type": "boolean" - }, - "template": { - "description": "Template", - "type": "string" - } - }, - "required": [ - "enabled", - "hashVerificationEnabled", - "template" - ], - "type": "object" - }, - "SystemConfigTemplateEmailsDto": { - "properties": { - "albumInviteTemplate": { - "description": "Album invite template", - "type": "string" - }, - "albumUpdateTemplate": { - "description": "Album update template", - "type": "string" - }, - "welcomeTemplate": { - "description": "Welcome template", - "type": "string" - } - }, - "required": [ - "albumInviteTemplate", - "albumUpdateTemplate", - "welcomeTemplate" - ], - "type": "object" - }, "SystemConfigTemplateStorageOptionDto": { "properties": { "dayOptions": { @@ -26823,62 +27320,6 @@ ], "type": "object" }, - "SystemConfigTemplatesDto": { - "properties": { - "email": { - "$ref": "#/components/schemas/SystemConfigTemplateEmailsDto" - } - }, - "required": [ - "email" - ], - "type": "object" - }, - "SystemConfigThemeDto": { - "properties": { - "customCss": { - "description": "Custom CSS for theming", - "type": "string" - } - }, - "required": [ - "customCss" - ], - "type": "object" - }, - "SystemConfigTrashDto": { - "properties": { - "days": { - "description": "Days", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "days", - "enabled" - ], - "type": "object" - }, - "SystemConfigUserDto": { - "properties": { - "deleteDelay": { - "description": "Delete delay", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "deleteDelay" - ], - "type": "object" - }, "TagBulkAssetsDto": { "properties": { "assetIds": { @@ -27780,6 +28221,362 @@ ], "type": "string" }, + "UserConfigClipDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigDto": { + "description": "Configuration properties that are visible to a logged user", + "properties": { + "ffmpeg": { + "$ref": "#/components/schemas/UserConfigFFmpegDto" + }, + "image": { + "$ref": "#/components/schemas/UserConfigImageDto" + }, + "machineLearning": { + "$ref": "#/components/schemas/UserConfigMachineLearningDto" + }, + "map": { + "$ref": "#/components/schemas/UserConfigMapDto" + }, + "oauth": { + "$ref": "#/components/schemas/UserConfigOAuthDto" + }, + "passwordLogin": { + "$ref": "#/components/schemas/UserConfigPasswordLoginDto" + }, + "reverseGeocoding": { + "$ref": "#/components/schemas/UserConfigReverseGeocodingDto" + }, + "server": { + "$ref": "#/components/schemas/UserConfigServerDto" + }, + "theme": { + "$ref": "#/components/schemas/UserConfigThemeDto" + }, + "trash": { + "$ref": "#/components/schemas/UserConfigTrashDto" + }, + "user": { + "$ref": "#/components/schemas/UserConfigUserDto" + } + }, + "required": [ + "ffmpeg", + "image", + "machineLearning", + "map", + "oauth", + "passwordLogin", + "reverseGeocoding", + "server", + "theme", + "trash", + "user" + ], + "type": "object" + }, + "UserConfigDuplicateDetectionDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigFFmpegDto": { + "properties": { + "realtime": { + "$ref": "#/components/schemas/UserConfigFFmpegRealtimeDto" + } + }, + "required": [ + "realtime" + ], + "type": "object" + }, + "UserConfigFFmpegRealtimeDto": { + "properties": { + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + }, + "resolutions": { + "description": "Resolutions to use for real-time HLS transcoding", + "items": { + "$ref": "#/components/schemas/HlsVideoResolution" + }, + "type": "array" + }, + "videoCodecs": { + "description": "Video codecs to use for real-time HLS transcoding", + "items": { + "$ref": "#/components/schemas/VideoCodec" + }, + "type": "array" + } + }, + "required": [ + "enabled", + "resolutions", + "videoCodecs" + ], + "type": "object" + }, + "UserConfigFacialRecognitionDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "minFaces": { + "description": "Minimum number of faces required for recognition", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "enabled", + "minFaces" + ], + "type": "object" + }, + "UserConfigGeneratedFullsizeImageDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigGeneratedImageDto": { + "properties": { + "size": { + "description": "Size", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "size" + ], + "type": "object" + }, + "UserConfigImageDto": { + "properties": { + "fullsize": { + "$ref": "#/components/schemas/UserConfigGeneratedFullsizeImageDto" + }, + "preview": { + "$ref": "#/components/schemas/UserConfigGeneratedImageDto" + }, + "thumbnail": { + "$ref": "#/components/schemas/UserConfigGeneratedImageDto" + } + }, + "required": [ + "fullsize", + "preview", + "thumbnail" + ], + "type": "object" + }, + "UserConfigMachineLearningDto": { + "properties": { + "clip": { + "$ref": "#/components/schemas/UserConfigClipDto" + }, + "duplicateDetection": { + "$ref": "#/components/schemas/UserConfigDuplicateDetectionDto" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "facialRecognition": { + "$ref": "#/components/schemas/UserConfigFacialRecognitionDto" + }, + "ocr": { + "$ref": "#/components/schemas/UserConfigOcrDto" + } + }, + "required": [ + "clip", + "duplicateDetection", + "enabled", + "facialRecognition", + "ocr" + ], + "type": "object" + }, + "UserConfigMapDto": { + "properties": { + "darkStyle": { + "description": "Dark map style URL", + "format": "uri", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "lightStyle": { + "description": "Light map style URL", + "format": "uri", + "type": "string" + } + }, + "required": [ + "darkStyle", + "enabled", + "lightStyle" + ], + "type": "object" + }, + "UserConfigOAuthDto": { + "properties": { + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" + }, + "buttonText": { + "description": "Button text", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "autoLaunch", + "buttonText", + "enabled" + ], + "type": "object" + }, + "UserConfigOcrDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigPasswordLoginDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigReverseGeocodingDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigServerDto": { + "properties": { + "externalDomain": { + "description": "External domain", + "type": "string" + }, + "loginPageMessage": { + "description": "Login page message", + "type": "string" + }, + "publicUsers": { + "description": "Public users", + "type": "boolean" + } + }, + "required": [ + "externalDomain", + "loginPageMessage", + "publicUsers" + ], + "type": "object" + }, + "UserConfigThemeDto": { + "properties": { + "customCss": { + "description": "Custom CSS for theming", + "type": "string" + } + }, + "required": [ + "customCss" + ], + "type": "object" + }, + "UserConfigTrashDto": { + "properties": { + "days": { + "description": "Days", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "days", + "enabled" + ], + "type": "object" + }, + "UserConfigUserDto": { + "properties": { + "deleteDelay": { + "description": "Delete delay", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "deleteDelay" + ], + "type": "object" + }, "UserLicense": { "properties": { "activatedAt": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 7e0fb963f..2d20f9c5d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -54,6 +54,380 @@ export type ActivityStatisticsResponseDto = { /** Number of likes */ likes: number; }; +export type AdminConfigDatabaseBackupDto = { + /** Cron expression */ + cronExpression: string; + /** Enabled */ + enabled: boolean; + /** Keep last amount */ + keepLastAmount: number; +}; +export type AdminConfigBackupsDto = { + database: AdminConfigDatabaseBackupDto; +}; +export type AdminConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; + /** Resolutions to use for real-time HLS transcoding */ + resolutions: HlsVideoResolution[]; + /** Video codecs to use for real-time HLS transcoding */ + videoCodecs: VideoCodec[]; +}; +export type AdminConfigFFmpegDto = { + accel: TranscodeHWAccel; + /** Accelerated decode */ + accelDecode: boolean; + /** Accepted audio codecs */ + acceptedAudioCodecs: AudioCodec[]; + /** Accepted containers */ + acceptedContainers: VideoContainer[]; + /** Accepted video codecs */ + acceptedVideoCodecs: VideoCodec[]; + /** B-frames */ + bframes: number; + cqMode: CQMode; + /** CRF */ + crf: number; + /** GOP size */ + gopSize: number; + /** Max bitrate */ + maxBitrate: string; + /** Preferred hardware device */ + preferredHwDevice: string; + /** Preset */ + preset: string; + realtime: AdminConfigFFmpegRealtimeDto; + /** References */ + refs: number; + targetAudioCodec: AudioCodec; + /** Target resolution */ + targetResolution: string; + targetVideoCodec: VideoCodec; + /** Temporal AQ */ + temporalAQ: boolean; + /** Threads */ + threads: number; + tonemap: ToneMapping; + transcode: TranscodePolicy; + /** Two pass */ + twoPass: boolean; +}; +export type AdminConfigGeneratedFullsizeImageDto = { + /** Enabled */ + enabled: boolean; + format: ImageFormat; + /** Progressive */ + progressive?: boolean; + /** Quality */ + quality: number; +}; +export type AdminConfigGeneratedImageDto = { + format: ImageFormat; + /** Progressive */ + progressive?: boolean; + /** Quality */ + quality: number; + /** Size */ + size: number; +}; +export type AdminConfigImageDto = { + colorspace: Colorspace; + /** Extract embedded */ + extractEmbedded: boolean; + fullsize: AdminConfigGeneratedFullsizeImageDto; + preview: AdminConfigGeneratedImageDto; + thumbnail: AdminConfigGeneratedImageDto; +}; +export type AdminConfigIntegrityChecksumJobDto = { + /** Cron expression for when the integrity check should run */ + cronExpression: string; + /** Enabled */ + enabled: boolean; + /** Percentage limit of the integrity checksum job */ + percentageLimit: number; + /** How long the integrity checksum job may run for */ + timeLimit: number; +}; +export type AdminConfigIntegrityJobDto = { + /** Cron expression for when the integrity check should run */ + cronExpression: string; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigIntegrityChecksDto = { + checksumFiles: AdminConfigIntegrityChecksumJobDto; + missingFiles: AdminConfigIntegrityJobDto; + untrackedFiles: AdminConfigIntegrityJobDto; +}; +export type AdminConfigJobSettingsDto = { + /** Concurrency */ + concurrency: number; +}; +export type AdminConfigJobDto = { + backgroundTask: AdminConfigJobSettingsDto; + editor: AdminConfigJobSettingsDto; + faceDetection: AdminConfigJobSettingsDto; + integrityCheck: AdminConfigJobSettingsDto; + library: AdminConfigJobSettingsDto; + metadataExtraction: AdminConfigJobSettingsDto; + migration: AdminConfigJobSettingsDto; + notifications: AdminConfigJobSettingsDto; + ocr: AdminConfigJobSettingsDto; + search: AdminConfigJobSettingsDto; + sidecar: AdminConfigJobSettingsDto; + smartSearch: AdminConfigJobSettingsDto; + thumbnailGeneration: AdminConfigJobSettingsDto; + videoConversion: AdminConfigJobSettingsDto; + workflow: AdminConfigJobSettingsDto; +}; +export type AdminConfigLibraryScanDto = { + /** Cron expression */ + cronExpression: string; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigLibraryWatchDto = { + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigLibraryDto = { + scan: AdminConfigLibraryScanDto; + watch: AdminConfigLibraryWatchDto; +}; +export type AdminConfigLoggingDto = { + /** Enabled */ + enabled: boolean; + level: LogLevel; +}; +export type AdminConfigMachineLearningAvailabilityChecksDto = { + /** Enabled */ + enabled: boolean; + interval: number; + timeout: number; +}; +export type AdminConfigClipDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Name of the model to use */ + modelName: string; +}; +export type AdminConfigDuplicateDetectionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum distance threshold for duplicate detection */ + maxDistance: number; +}; +export type AdminConfigFacialRecognitionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum distance threshold for face recognition */ + maxDistance: number; + /** Minimum number of faces required for recognition */ + minFaces: number; + /** Minimum confidence score for face detection */ + minScore: number; + /** Name of the model to use */ + modelName: string; +}; +export type AdminConfigOcrDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum resolution for OCR processing */ + maxResolution: number; + /** Minimum confidence score for text detection */ + minDetectionScore: number; + /** Minimum confidence score for text recognition */ + minRecognitionScore: number; + /** Name of the model to use */ + modelName: string; +}; +export type AdminConfigMachineLearningDto = { + availabilityChecks: AdminConfigMachineLearningAvailabilityChecksDto; + clip: AdminConfigClipDto; + duplicateDetection: AdminConfigDuplicateDetectionDto; + /** Enabled */ + enabled: boolean; + facialRecognition: AdminConfigFacialRecognitionDto; + ocr: AdminConfigOcrDto; + /** ML service URLs */ + urls: string[]; +}; +export type AdminConfigMapDto = { + /** Dark map style URL */ + darkStyle: string; + /** Enabled */ + enabled: boolean; + /** Light map style URL */ + lightStyle: string; +}; +export type AdminConfigFacesDto = { + /** Import */ + "import": boolean; +}; +export type AdminConfigMetadataDto = { + faces: AdminConfigFacesDto; +}; +export type AdminConfigNewVersionCheckDto = { + channel: ReleaseChannel; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigNightlyTasksDto = { + /** Cluster new faces */ + clusterNewFaces: boolean; + /** Database cleanup */ + databaseCleanup: boolean; + /** Generate memories */ + generateMemories: boolean; + /** Missing thumbnails */ + missingThumbnails: boolean; + /** Start time (HH:MM) */ + startTime: string; + /** Sync quota usage */ + syncQuotaUsage: boolean; +}; +export type AdminConfigSmtpTransportDto = { + /** SMTP server hostname */ + host: string; + /** Whether to ignore SSL certificate errors */ + ignoreCert: boolean; + /** SMTP password */ + password: string; + /** SMTP server port */ + port: number; + /** Whether to use secure connection (TLS/SSL) */ + secure: boolean; + /** SMTP username */ + username: string; +}; +export type AdminConfigSmtpDto = { + /** Whether SMTP email notifications are enabled */ + enabled: boolean; + /** Email address to send from */ + "from": string; + /** Email address for replies */ + replyTo: string; + transport: AdminConfigSmtpTransportDto; +}; +export type AdminConfigNotificationsDto = { + smtp: AdminConfigSmtpDto; +}; +export type AdminConfigOAuthDto = { + /** Allow insecure requests */ + allowInsecureRequests: boolean; + /** Auto launch */ + autoLaunch: boolean; + /** Auto register */ + autoRegister: boolean; + /** Button text */ + buttonText: string; + /** Client ID */ + clientId: string; + /** Client secret */ + clientSecret: string; + /** Default storage quota */ + defaultStorageQuota: number | null; + /** Enabled */ + enabled: boolean; + /** End session endpoint */ + endSessionEndpoint: string; + /** Issuer URL */ + issuerUrl: string; + /** Mobile override enabled */ + mobileOverrideEnabled: boolean; + /** Mobile redirect URI (set to empty string to disable) */ + mobileRedirectUri: string; + /** Profile signing algorithm */ + profileSigningAlgorithm: string; + /** OAuth prompt parameter (e.g. select_account, login, consent) */ + prompt: string; + /** Role claim */ + roleClaim: string; + /** Scope */ + scope: string; + /** Signing algorithm */ + signingAlgorithm: string; + /** Storage label claim */ + storageLabelClaim: string; + /** Storage quota claim */ + storageQuotaClaim: string; + /** Timeout */ + timeout: number; + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; +}; +export type AdminConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigReverseGeocodingDto = { + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigServerDto = { + /** External domain */ + externalDomain: string; + /** Login page message */ + loginPageMessage: string; + /** Public users */ + publicUsers: boolean; +}; +export type AdminConfigStorageTemplateDto = { + /** Enabled */ + enabled: boolean; + /** Hash verification enabled */ + hashVerificationEnabled: boolean; + /** Template */ + template: string; +}; +export type AdminConfigTemplateEmailsDto = { + /** Album invite template */ + albumInviteTemplate: string; + /** Album update template */ + albumUpdateTemplate: string; + /** Welcome template */ + welcomeTemplate: string; +}; +export type AdminConfigTemplatesDto = { + email: AdminConfigTemplateEmailsDto; +}; +export type AdminConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type AdminConfigTrashDto = { + /** Days */ + days: number; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigUserDto = { + /** Delete delay */ + deleteDelay: number; +}; +export type AdminConfigDto = { + backup: AdminConfigBackupsDto; + ffmpeg: AdminConfigFFmpegDto; + image: AdminConfigImageDto; + integrityChecks: AdminConfigIntegrityChecksDto; + job: AdminConfigJobDto; + library: AdminConfigLibraryDto; + logging: AdminConfigLoggingDto; + machineLearning: AdminConfigMachineLearningDto; + map: AdminConfigMapDto; + metadata: AdminConfigMetadataDto; + newVersionCheck: AdminConfigNewVersionCheckDto; + nightlyTasks: AdminConfigNightlyTasksDto; + notifications: AdminConfigNotificationsDto; + oauth: AdminConfigOAuthDto; + passwordLogin: AdminConfigPasswordLoginDto; + reverseGeocoding: AdminConfigReverseGeocodingDto; + server: AdminConfigServerDto; + storageTemplate: AdminConfigStorageTemplateDto; + templates: AdminConfigTemplatesDto; + theme: AdminConfigThemeDto; + trash: AdminConfigTrashDto; + user: AdminConfigUserDto; +}; export type DatabaseBackupDeleteDto = { /** Backup filenames to delete */ backups: string[]; @@ -165,29 +539,6 @@ export type TemplateResponseDto = { /** Template name */ name: string; }; -export type SystemConfigSmtpTransportDto = { - /** SMTP server hostname */ - host: string; - /** Whether to ignore SSL certificate errors */ - ignoreCert: boolean; - /** SMTP password */ - password: string; - /** SMTP server port */ - port: number; - /** Whether to use secure connection (TLS/SSL) */ - secure: boolean; - /** SMTP username */ - username: string; -}; -export type SystemConfigSmtpDto = { - /** Whether SMTP email notifications are enabled */ - enabled: boolean; - /** Email address to send from */ - "from": string; - /** Email address for replies */ - replyTo: string; - transport: SystemConfigSmtpTransportDto; -}; export type TestEmailResponseDto = { /** Email message ID */ messageId: string; @@ -1123,6 +1474,115 @@ export type ValidateAccessTokenResponseDto = { /** Authentication status */ authStatus: boolean; }; +export type UserConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; + /** Resolutions to use for real-time HLS transcoding */ + resolutions: HlsVideoResolution[]; + /** Video codecs to use for real-time HLS transcoding */ + videoCodecs: VideoCodec[]; +}; +export type UserConfigFFmpegDto = { + realtime: UserConfigFFmpegRealtimeDto; +}; +export type UserConfigGeneratedFullsizeImageDto = { + /** Enabled */ + enabled: boolean; +}; +export type UserConfigGeneratedImageDto = { + /** Size */ + size: number; +}; +export type UserConfigImageDto = { + fullsize: UserConfigGeneratedFullsizeImageDto; + preview: UserConfigGeneratedImageDto; + thumbnail: UserConfigGeneratedImageDto; +}; +export type UserConfigClipDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigDuplicateDetectionDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigFacialRecognitionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Minimum number of faces required for recognition */ + minFaces: number; +}; +export type UserConfigOcrDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigMachineLearningDto = { + clip: UserConfigClipDto; + duplicateDetection: UserConfigDuplicateDetectionDto; + /** Enabled */ + enabled: boolean; + facialRecognition: UserConfigFacialRecognitionDto; + ocr: UserConfigOcrDto; +}; +export type UserConfigMapDto = { + /** Dark map style URL */ + darkStyle: string; + /** Enabled */ + enabled: boolean; + /** Light map style URL */ + lightStyle: string; +}; +export type UserConfigOAuthDto = { + /** Auto launch */ + autoLaunch: boolean; + /** Button text */ + buttonText: string; + /** Enabled */ + enabled: boolean; +}; +export type UserConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type UserConfigReverseGeocodingDto = { + /** Enabled */ + enabled: boolean; +}; +export type UserConfigServerDto = { + /** External domain */ + externalDomain: string; + /** Login page message */ + loginPageMessage: string; + /** Public users */ + publicUsers: boolean; +}; +export type UserConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type UserConfigTrashDto = { + /** Days */ + days: number; + /** Enabled */ + enabled: boolean; +}; +export type UserConfigUserDto = { + /** Delete delay */ + deleteDelay: number; +}; +export type UserConfigDto = { + ffmpeg: UserConfigFFmpegDto; + image: UserConfigImageDto; + machineLearning: UserConfigMachineLearningDto; + map: UserConfigMapDto; + oauth: UserConfigOAuthDto; + passwordLogin: UserConfigPasswordLoginDto; + reverseGeocoding: UserConfigReverseGeocodingDto; + server: UserConfigServerDto; + theme: UserConfigThemeDto; + trash: UserConfigTrashDto; + user: UserConfigUserDto; +}; export type DownloadArchiveDto = { /** Asset IDs */ assetIds: string[]; @@ -1592,6 +2052,32 @@ export type PluginTemplateResponseDto = { /** Ui hints, for example "smart-album" */ uiHints: string[]; }; +export type PublicConfigOAuthDto = { + /** Auto launch */ + autoLaunch: boolean; + /** Button text */ + buttonText: string; + /** Enabled */ + enabled: boolean; +}; +export type PublicConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type PublicConfigServerDto = { + /** Login page message */ + loginPageMessage: string; +}; +export type PublicConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type PublicConfigDto = { + oauth: PublicConfigOAuthDto; + passwordLogin: PublicConfigPasswordLoginDto; + server: PublicConfigServerDto; + theme: PublicConfigThemeDto; +}; export type QueueResponseDto = { /** Whether the queue is paused */ isPaused: boolean; @@ -2298,357 +2784,6 @@ export type SyncStreamDto = { /** Sync request types */ types: SyncRequestType[]; }; -export type DatabaseBackupConfig = { - /** Cron expression */ - cronExpression: string; - /** Enabled */ - enabled: boolean; - /** Keep last amount */ - keepLastAmount: number; -}; -export type SystemConfigBackupsDto = { - database: DatabaseBackupConfig; -}; -export type SystemConfigFFmpegRealtimeDto = { - /** Enable real-time HLS transcoding (alpha) */ - enabled: boolean; - /** Resolutions to use for real-time HLS transcoding */ - resolutions: HlsVideoResolution[]; - /** Video codecs to use for real-time HLS transcoding */ - videoCodecs: VideoCodec[]; -}; -export type SystemConfigFFmpegDto = { - accel: TranscodeHWAccel; - /** Accelerated decode */ - accelDecode: boolean; - /** Accepted audio codecs */ - acceptedAudioCodecs: AudioCodec[]; - /** Accepted containers */ - acceptedContainers: VideoContainer[]; - /** Accepted video codecs */ - acceptedVideoCodecs: VideoCodec[]; - /** B-frames */ - bframes: number; - cqMode: CQMode; - /** CRF */ - crf: number; - /** GOP size */ - gopSize: number; - /** Max bitrate */ - maxBitrate: string; - /** Preferred hardware device */ - preferredHwDevice: string; - /** Preset */ - preset: string; - realtime: SystemConfigFFmpegRealtimeDto; - /** References */ - refs: number; - targetAudioCodec: AudioCodec; - /** Target resolution */ - targetResolution: string; - targetVideoCodec: VideoCodec; - /** Temporal AQ */ - temporalAQ: boolean; - /** Threads */ - threads: number; - tonemap: ToneMapping; - transcode: TranscodePolicy; - /** Two pass */ - twoPass: boolean; -}; -export type SystemConfigGeneratedFullsizeImageDto = { - /** Enabled */ - enabled: boolean; - format: ImageFormat; - /** Progressive */ - progressive?: boolean; - /** Quality */ - quality: number; -}; -export type SystemConfigGeneratedImageDto = { - format: ImageFormat; - /** Progressive */ - progressive?: boolean; - /** Quality */ - quality: number; - /** Size */ - size: number; -}; -export type SystemConfigImageDto = { - colorspace: Colorspace; - /** Extract embedded */ - extractEmbedded: boolean; - fullsize: SystemConfigGeneratedFullsizeImageDto; - preview: SystemConfigGeneratedImageDto; - thumbnail: SystemConfigGeneratedImageDto; -}; -export type SystemConfigIntegrityChecksumJob = { - /** Cron expression for when the integrity check should run */ - cronExpression: string; - /** Enabled */ - enabled: boolean; - /** Percentage limit of the integrity checksum job */ - percentageLimit: number; - /** How long the integrity checksum job may run for */ - timeLimit: number; -}; -export type SystemConfigIntegrityJob = { - /** Cron expression for when the integrity check should run */ - cronExpression: string; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigIntegrityChecks = { - checksumFiles: SystemConfigIntegrityChecksumJob; - missingFiles: SystemConfigIntegrityJob; - untrackedFiles: SystemConfigIntegrityJob; -}; -export type JobSettingsDto = { - /** Concurrency */ - concurrency: number; -}; -export type SystemConfigJobDto = { - backgroundTask: JobSettingsDto; - editor: JobSettingsDto; - faceDetection: JobSettingsDto; - integrityCheck: JobSettingsDto; - library: JobSettingsDto; - metadataExtraction: JobSettingsDto; - migration: JobSettingsDto; - notifications: JobSettingsDto; - ocr: JobSettingsDto; - search: JobSettingsDto; - sidecar: JobSettingsDto; - smartSearch: JobSettingsDto; - thumbnailGeneration: JobSettingsDto; - videoConversion: JobSettingsDto; - workflow: JobSettingsDto; -}; -export type SystemConfigLibraryScanDto = { - /** Cron expression */ - cronExpression: string; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigLibraryWatchDto = { - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigLibraryDto = { - scan: SystemConfigLibraryScanDto; - watch: SystemConfigLibraryWatchDto; -}; -export type SystemConfigLoggingDto = { - /** Enabled */ - enabled: boolean; - level: LogLevel; -}; -export type MachineLearningAvailabilityChecksDto = { - /** Enabled */ - enabled: boolean; - interval: number; - timeout: number; -}; -export type ClipConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Name of the model to use */ - modelName: string; -}; -export type DuplicateDetectionConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum distance threshold for duplicate detection */ - maxDistance: number; -}; -export type FacialRecognitionConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum distance threshold for face recognition */ - maxDistance: number; - /** Minimum number of faces required for recognition */ - minFaces: number; - /** Minimum confidence score for face detection */ - minScore: number; - /** Name of the model to use */ - modelName: string; -}; -export type OcrConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum resolution for OCR processing */ - maxResolution: number; - /** Minimum confidence score for text detection */ - minDetectionScore: number; - /** Minimum confidence score for text recognition */ - minRecognitionScore: number; - /** Name of the model to use */ - modelName: string; -}; -export type SystemConfigMachineLearningDto = { - availabilityChecks: MachineLearningAvailabilityChecksDto; - clip: ClipConfig; - duplicateDetection: DuplicateDetectionConfig; - /** Enabled */ - enabled: boolean; - facialRecognition: FacialRecognitionConfig; - ocr: OcrConfig; - /** ML service URLs */ - urls: string[]; -}; -export type SystemConfigMapDto = { - /** Dark map style URL */ - darkStyle: string; - /** Enabled */ - enabled: boolean; - /** Light map style URL */ - lightStyle: string; -}; -export type SystemConfigFacesDto = { - /** Import */ - "import": boolean; -}; -export type SystemConfigMetadataDto = { - faces: SystemConfigFacesDto; -}; -export type SystemConfigNewVersionCheckDto = { - channel: ReleaseChannel; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigNightlyTasksDto = { - /** Cluster new faces */ - clusterNewFaces: boolean; - /** Database cleanup */ - databaseCleanup: boolean; - /** Generate memories */ - generateMemories: boolean; - /** Missing thumbnails */ - missingThumbnails: boolean; - /** Start time (HH:MM) */ - startTime: string; - /** Sync quota usage */ - syncQuotaUsage: boolean; -}; -export type SystemConfigNotificationsDto = { - smtp: SystemConfigSmtpDto; -}; -export type SystemConfigOAuthDto = { - /** Allow insecure requests */ - allowInsecureRequests: boolean; - /** Auto launch */ - autoLaunch: boolean; - /** Auto register */ - autoRegister: boolean; - /** Button text */ - buttonText: string; - /** Client ID */ - clientId: string; - /** Client secret */ - clientSecret: string; - /** Default storage quota */ - defaultStorageQuota: number | null; - /** Enabled */ - enabled: boolean; - /** End session endpoint */ - endSessionEndpoint: string; - /** Issuer URL */ - issuerUrl: string; - /** Mobile override enabled */ - mobileOverrideEnabled: boolean; - /** Mobile redirect URI (set to empty string to disable) */ - mobileRedirectUri: string; - /** Profile signing algorithm */ - profileSigningAlgorithm: string; - /** OAuth prompt parameter (e.g. select_account, login, consent) */ - prompt: string; - /** Role claim */ - roleClaim: string; - /** Scope */ - scope: string; - /** Signing algorithm */ - signingAlgorithm: string; - /** Storage label claim */ - storageLabelClaim: string; - /** Storage quota claim */ - storageQuotaClaim: string; - /** Timeout */ - timeout: number; - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; -}; -export type SystemConfigPasswordLoginDto = { - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigReverseGeocodingDto = { - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigServerDto = { - /** External domain */ - externalDomain: string; - /** Login page message */ - loginPageMessage: string; - /** Public users */ - publicUsers: boolean; -}; -export type SystemConfigStorageTemplateDto = { - /** Enabled */ - enabled: boolean; - /** Hash verification enabled */ - hashVerificationEnabled: boolean; - /** Template */ - template: string; -}; -export type SystemConfigTemplateEmailsDto = { - /** Album invite template */ - albumInviteTemplate: string; - /** Album update template */ - albumUpdateTemplate: string; - /** Welcome template */ - welcomeTemplate: string; -}; -export type SystemConfigTemplatesDto = { - email: SystemConfigTemplateEmailsDto; -}; -export type SystemConfigThemeDto = { - /** Custom CSS for theming */ - customCss: string; -}; -export type SystemConfigTrashDto = { - /** Days */ - days: number; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigUserDto = { - /** Delete delay */ - deleteDelay: number; -}; -export type SystemConfigDto = { - backup: SystemConfigBackupsDto; - ffmpeg: SystemConfigFFmpegDto; - image: SystemConfigImageDto; - integrityChecks: SystemConfigIntegrityChecks; - job: SystemConfigJobDto; - library: SystemConfigLibraryDto; - logging: SystemConfigLoggingDto; - machineLearning: SystemConfigMachineLearningDto; - map: SystemConfigMapDto; - metadata: SystemConfigMetadataDto; - newVersionCheck: SystemConfigNewVersionCheckDto; - nightlyTasks: SystemConfigNightlyTasksDto; - notifications: SystemConfigNotificationsDto; - oauth: SystemConfigOAuthDto; - passwordLogin: SystemConfigPasswordLoginDto; - reverseGeocoding: SystemConfigReverseGeocodingDto; - server: SystemConfigServerDto; - storageTemplate: SystemConfigStorageTemplateDto; - templates: SystemConfigTemplatesDto; - theme: SystemConfigThemeDto; - trash: SystemConfigTrashDto; - user: SystemConfigUserDto; -}; export type SystemConfigTemplateStorageOptionDto = { /** Available day format options for storage template */ dayOptions: string[]; @@ -3452,6 +3587,43 @@ export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { method: "POST" })); } +/** + * Get the admin configuration + */ +export function getAdminConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config", { + ...opts + })); +} +/** + * Update the system configuration + */ +export function updateAdminConfig({ adminConfigDto }: { + adminConfigDto: AdminConfigDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config", oazapfts.json({ + ...opts, + method: "PUT", + body: adminConfigDto + }))); +} +/** + * Get the system configuration defaults + */ +export function getAdminConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config/defaults", { + ...opts + })); +} /** * Delete database backup */ @@ -3659,8 +3831,8 @@ export function getNotificationTemplateAdmin({ name, templateDto }: { /** * Send test email */ -export function sendTestEmailAdmin({ systemConfigSmtpDto }: { - systemConfigSmtpDto: SystemConfigSmtpDto; +export function sendTestEmailAdmin({ adminConfigSmtpDto }: { + adminConfigSmtpDto: AdminConfigSmtpDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3668,7 +3840,7 @@ export function sendTestEmailAdmin({ systemConfigSmtpDto }: { }>("/admin/notifications/test-email", oazapfts.json({ ...opts, method: "POST", - body: systemConfigSmtpDto + body: adminConfigSmtpDto }))); } /** @@ -4695,6 +4867,28 @@ export function validateAccessToken(opts?: Oazapfts.RequestOpts) { method: "POST" })); } +/** + * Get the configuration with user visibility + */ +export function getUserConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserConfigDto; + }>("/config", { + ...opts + })); +} +/** + * Get the default configuration with user visibility + */ +export function getUserConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserConfigDto; + }>("/config/defaults", { + ...opts + })); +} /** * Download asset archive */ @@ -5640,6 +5834,28 @@ export function getPlugin({ id }: { ...opts })); } +/** + * Get the public configuration + */ +export function getPublicConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PublicConfigDto; + }>("/public/config", { + ...opts + })); +} +/** + * Get the public configuration defaults + */ +export function getPublicConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PublicConfigDto; + }>("/public/config/defaults", { + ...opts + })); +} /** * List all queues */ @@ -6450,7 +6666,7 @@ export function getSyncStream({ syncStreamDto }: { export function getConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config", { ...opts })); @@ -6458,16 +6674,16 @@ export function getConfig(opts?: Oazapfts.RequestOpts) { /** * Update system configuration */ -export function updateConfig({ systemConfigDto }: { - systemConfigDto: SystemConfigDto; +export function updateConfig({ adminConfigDto }: { + adminConfigDto: AdminConfigDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config", oazapfts.json({ ...opts, method: "PUT", - body: systemConfigDto + body: adminConfigDto }))); } /** @@ -6476,7 +6692,7 @@ export function updateConfig({ systemConfigDto }: { export function getConfigDefaults(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config/defaults", { ...opts })); @@ -7164,6 +7380,80 @@ export enum UserAvatarColor { Gray = "gray", Amber = "amber" } +export enum TranscodeHWAccel { + Nvenc = "nvenc", + Qsv = "qsv", + Vaapi = "vaapi", + Rkmpp = "rkmpp", + Disabled = "disabled" +} +export enum AudioCodec { + Mp3 = "mp3", + Aac = "aac", + Opus = "opus", + PcmS16Le = "pcm_s16le" +} +export enum VideoContainer { + Mov = "mov", + Mp4 = "mp4", + Ogg = "ogg", + Webm = "webm" +} +export enum VideoCodec { + H264 = "h264", + Hevc = "hevc", + Vp9 = "vp9", + Av1 = "av1" +} +export enum CQMode { + Auto = "auto", + Cqp = "cqp", + Icq = "icq" +} +export enum HlsVideoResolution { + $480 = 480, + $720 = 720, + $1080 = 1080, + $1440 = 1440, + $2160 = 2160 +} +export enum ToneMapping { + Hable = "hable", + Mobius = "mobius", + Reinhard = "reinhard", + Disabled = "disabled" +} +export enum TranscodePolicy { + All = "all", + Optimal = "optimal", + Bitrate = "bitrate", + Required = "required", + Disabled = "disabled" +} +export enum Colorspace { + Srgb = "srgb", + P3 = "p3" +} +export enum ImageFormat { + Jpeg = "jpeg", + Webp = "webp" +} +export enum LogLevel { + Verbose = "verbose", + Debug = "debug", + Log = "log", + Warn = "warn", + Error = "error", + Fatal = "fatal" +} +export enum ReleaseChannel { + Stable = "stable", + ReleaseCandidate = "releaseCandidate" +} +export enum OAuthTokenEndpointAuthMethod { + ClientSecretPost = "client_secret_post", + ClientSecretBasic = "client_secret_basic" +} export enum IntegrityReport { UntrackedFile = "untracked_file", MissingFile = "missing_file", @@ -7272,6 +7562,9 @@ export enum Permission { BackupDownload = "backup.download", BackupUpload = "backup.upload", BackupDelete = "backup.delete", + AdminConfigRead = "adminConfig.read", + AdminConfigUpdate = "adminConfig.update", + UserConfigRead = "userConfig.read", DuplicateRead = "duplicate.read", DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", @@ -7679,80 +7972,6 @@ export enum SyncRequestType { AssetFacesV2 = "AssetFacesV2", UserMetadataV1 = "UserMetadataV1" } -export enum TranscodeHWAccel { - Nvenc = "nvenc", - Qsv = "qsv", - Vaapi = "vaapi", - Rkmpp = "rkmpp", - Disabled = "disabled" -} -export enum AudioCodec { - Mp3 = "mp3", - Aac = "aac", - Opus = "opus", - PcmS16Le = "pcm_s16le" -} -export enum VideoContainer { - Mov = "mov", - Mp4 = "mp4", - Ogg = "ogg", - Webm = "webm" -} -export enum VideoCodec { - H264 = "h264", - Hevc = "hevc", - Vp9 = "vp9", - Av1 = "av1" -} -export enum CQMode { - Auto = "auto", - Cqp = "cqp", - Icq = "icq" -} -export enum HlsVideoResolution { - $480 = 480, - $720 = 720, - $1080 = 1080, - $1440 = 1440, - $2160 = 2160 -} -export enum ToneMapping { - Hable = "hable", - Mobius = "mobius", - Reinhard = "reinhard", - Disabled = "disabled" -} -export enum TranscodePolicy { - All = "all", - Optimal = "optimal", - Bitrate = "bitrate", - Required = "required", - Disabled = "disabled" -} -export enum Colorspace { - Srgb = "srgb", - P3 = "p3" -} -export enum ImageFormat { - Jpeg = "jpeg", - Webp = "webp" -} -export enum LogLevel { - Verbose = "verbose", - Debug = "debug", - Log = "log", - Warn = "warn", - Error = "error", - Fatal = "fatal" -} -export enum ReleaseChannel { - Stable = "stable", - ReleaseCandidate = "releaseCandidate" -} -export enum OAuthTokenEndpointAuthMethod { - ClientSecretPost = "client_secret_post", - ClientSecretBasic = "client_secret_basic" -} export enum AssetOrderBy { TakenAt = "takenAt", CreatedAt = "createdAt" diff --git a/server/src/config.ts b/server/src/config.ts deleted file mode 100644 index 55304080a..000000000 --- a/server/src/config.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { CronExpression } from '@nestjs/schedule'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { - AudioCodec, - Colorspace, - CQMode, - HlsVideoResolution, - ImageFormat, - LogLevel, - OAuthTokenEndpointAuthMethod, - QueueName, - ToneMapping, - TranscodeHardwareAcceleration, - TranscodePolicy, - VideoCodec, - VideoContainer, -} from 'src/enum'; -import { ConcurrentQueueName, FullsizeImageOptions, ImageOptions } from 'src/types'; - -export type SystemConfig = { - backup: { - database: { - enabled: boolean; - cronExpression: string; - keepLastAmount: number; - }; - }; - ffmpeg: { - crf: number; - threads: number; - preset: string; - targetVideoCodec: VideoCodec; - acceptedVideoCodecs: VideoCodec[]; - targetAudioCodec: AudioCodec; - acceptedAudioCodecs: AudioCodec[]; - acceptedContainers: VideoContainer[]; - targetResolution: string; - maxBitrate: string; - bframes: number; - refs: number; - gopSize: number; - temporalAQ: boolean; - cqMode: CQMode; - twoPass: boolean; - preferredHwDevice: string; - transcode: TranscodePolicy; - accel: TranscodeHardwareAcceleration; - accelDecode: boolean; - tonemap: ToneMapping; - realtime: { - enabled: boolean; - videoCodecs: VideoCodec[]; - resolutions: HlsVideoResolution[]; - }; - }; - integrityChecks: { - missingFiles: { - enabled: boolean; - cronExpression: string; - }; - untrackedFiles: { - enabled: boolean; - cronExpression: string; - }; - checksumFiles: { - enabled: boolean; - cronExpression: string; - timeLimit: number; - percentageLimit: number; - }; - }; - job: Record; - logging: { - enabled: boolean; - level: LogLevel; - }; - machineLearning: { - enabled: boolean; - urls: string[]; - availabilityChecks: { - enabled: boolean; - timeout: number; - interval: number; - }; - clip: { - enabled: boolean; - modelName: string; - }; - duplicateDetection: { - enabled: boolean; - maxDistance: number; - }; - facialRecognition: { - enabled: boolean; - modelName: string; - minScore: number; - minFaces: number; - maxDistance: number; - }; - ocr: { - enabled: boolean; - modelName: string; - minDetectionScore: number; - minRecognitionScore: number; - maxResolution: number; - }; - }; - map: { - enabled: boolean; - lightStyle: string; - darkStyle: string; - }; - reverseGeocoding: { - enabled: boolean; - }; - metadata: { - faces: { - import: boolean; - }; - }; - oauth: { - autoLaunch: boolean; - autoRegister: boolean; - buttonText: string; - clientId: string; - clientSecret: string; - defaultStorageQuota: number | null; - enabled: boolean; - issuerUrl: string; - endSessionEndpoint: string; - mobileOverrideEnabled: boolean; - mobileRedirectUri: string; - prompt: string; - scope: string; - signingAlgorithm: string; - profileSigningAlgorithm: string; - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; - timeout: number; - allowInsecureRequests: boolean; - storageLabelClaim: string; - storageQuotaClaim: string; - roleClaim: string; - }; - passwordLogin: { - enabled: boolean; - }; - storageTemplate: { - enabled: boolean; - hashVerificationEnabled: boolean; - template: string; - }; - image: { - thumbnail: ImageOptions; - preview: ImageOptions; - colorspace: Colorspace; - extractEmbedded: boolean; - fullsize: FullsizeImageOptions; - }; - newVersionCheck: { - enabled: boolean; - channel: ReleaseChannel; - }; - nightlyTasks: { - startTime: string; - databaseCleanup: boolean; - missingThumbnails: boolean; - clusterNewFaces: boolean; - generateMemories: boolean; - syncQuotaUsage: boolean; - }; - trash: { - enabled: boolean; - days: number; - }; - theme: { - customCss: string; - }; - library: { - scan: { - enabled: boolean; - cronExpression: string; - }; - watch: { - enabled: boolean; - }; - }; - notifications: { - smtp: { - enabled: boolean; - from: string; - replyTo: string; - transport: { - ignoreCert: boolean; - host: string; - port: number; - secure: boolean; - username: string; - password: string; - }; - }; - }; - templates: { - email: { - welcomeTemplate: string; - albumInviteTemplate: string; - albumUpdateTemplate: string; - }; - }; - server: { - externalDomain: string; - loginPageMessage: string; - publicUsers: boolean; - }; - user: { - deleteDelay: number; - }; -}; - -export type MachineLearningConfig = SystemConfig['machineLearning']; - -export const defaults = Object.freeze({ - backup: { - database: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_2AM, - keepLastAmount: 14, - }, - }, - ffmpeg: { - crf: 23, - threads: 0, - preset: 'ultrafast', - targetVideoCodec: VideoCodec.H264, - acceptedVideoCodecs: [VideoCodec.H264], - targetAudioCodec: AudioCodec.Aac, - acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.Opus], - acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], - targetResolution: '720', - maxBitrate: '0', - bframes: -1, - refs: 0, - gopSize: 0, - temporalAQ: false, - cqMode: CQMode.Auto, - twoPass: false, - preferredHwDevice: 'auto', - transcode: TranscodePolicy.Required, - tonemap: ToneMapping.Hable, - accel: TranscodeHardwareAcceleration.Disabled, - accelDecode: true, - realtime: { - enabled: false, - videoCodecs: [VideoCodec.H264, VideoCodec.Hevc], - resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080], - }, - }, - integrityChecks: { - missingFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - }, - untrackedFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - }, - checksumFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - timeLimit: 60 * 60 * 1000, // 1 hour - percentageLimit: 1, // 100% of assets - }, - }, - job: { - [QueueName.BackgroundTask]: { concurrency: 5 }, - [QueueName.SmartSearch]: { concurrency: 2 }, - [QueueName.MetadataExtraction]: { concurrency: 5 }, - [QueueName.FaceDetection]: { concurrency: 2 }, - [QueueName.Search]: { concurrency: 5 }, - [QueueName.Sidecar]: { concurrency: 5 }, - [QueueName.Library]: { concurrency: 5 }, - [QueueName.Migration]: { concurrency: 5 }, - [QueueName.ThumbnailGeneration]: { concurrency: 3 }, - [QueueName.VideoConversion]: { concurrency: 1 }, - [QueueName.Notification]: { concurrency: 5 }, - [QueueName.Ocr]: { concurrency: 1 }, - [QueueName.Workflow]: { concurrency: 5 }, - [QueueName.IntegrityCheck]: { concurrency: 1 }, - [QueueName.Editor]: { concurrency: 2 }, - }, - logging: { - enabled: true, - level: LogLevel.Log, - }, - machineLearning: { - enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', - urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], - availabilityChecks: { - enabled: true, - timeout: 2000, - interval: 30_000, - }, - clip: { - enabled: true, - modelName: 'ViT-B-32__openai', - }, - duplicateDetection: { - enabled: true, - maxDistance: 0.01, - }, - facialRecognition: { - enabled: true, - modelName: 'buffalo_l', - minScore: 0.7, - maxDistance: 0.5, - minFaces: 3, - }, - ocr: { - enabled: true, - modelName: 'PP-OCRv5_mobile', - minDetectionScore: 0.5, - minRecognitionScore: 0.8, - maxResolution: 736, - }, - }, - map: { - enabled: true, - lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', - darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', - }, - reverseGeocoding: { - enabled: true, - }, - metadata: { - faces: { - import: false, - }, - }, - oauth: { - autoLaunch: false, - autoRegister: true, - buttonText: 'Login with OAuth', - clientId: '', - clientSecret: '', - defaultStorageQuota: null, - enabled: false, - issuerUrl: '', - endSessionEndpoint: '', - mobileOverrideEnabled: false, - mobileRedirectUri: '', - prompt: '', - scope: 'openid email profile', - signingAlgorithm: 'RS256', - profileSigningAlgorithm: 'none', - storageLabelClaim: 'preferred_username', - storageQuotaClaim: 'immich_quota', - roleClaim: 'immich_role', - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, - timeout: 30_000, - allowInsecureRequests: false, - }, - passwordLogin: { - enabled: true, - }, - storageTemplate: { - enabled: false, - hashVerificationEnabled: true, - template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', - }, - image: { - thumbnail: { - format: ImageFormat.Webp, - size: 250, - quality: 80, - progressive: false, - }, - preview: { - format: ImageFormat.Jpeg, - size: 1440, - quality: 80, - progressive: false, - }, - colorspace: Colorspace.P3, - extractEmbedded: false, - fullsize: { - enabled: false, - format: ImageFormat.Jpeg, - quality: 80, - progressive: false, - }, - }, - newVersionCheck: { - enabled: true, - channel: ReleaseChannel.Stable, - }, - nightlyTasks: { - startTime: '00:00', - databaseCleanup: true, - generateMemories: true, - syncQuotaUsage: true, - missingThumbnails: true, - clusterNewFaces: true, - }, - trash: { - enabled: true, - days: 30, - }, - theme: { - customCss: '', - }, - library: { - scan: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT, - }, - watch: { - enabled: false, - }, - }, - server: { - externalDomain: '', - loginPageMessage: '', - publicUsers: true, - }, - notifications: { - smtp: { - enabled: false, - from: '', - replyTo: '', - transport: { - ignoreCert: false, - host: '', - port: 587, - secure: false, - username: '', - password: '', - }, - }, - }, - templates: { - email: { - welcomeTemplate: '', - albumInviteTemplate: '', - albumUpdateTemplate: '', - }, - }, - user: { - deleteDelay: 7, - }, -}); diff --git a/server/src/constants.ts b/server/src/constants.ts index 815a400ed..a8b2216b3 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -150,6 +150,9 @@ export const endpointTags: Record = { [ApiTag.Assets]: 'An asset is an image or video that has been uploaded to Immich.', [ApiTag.Authentication]: 'Endpoints related to user authentication, including OAuth.', [ApiTag.AuthenticationAdmin]: 'Administrative endpoints related to authentication.', + [ApiTag.ConfigUser]: 'The system configuration properties that are visible to logged in users.', + [ApiTag.ConfigAdmin]: 'Endpoints to view and modify the full system configuration.', + [ApiTag.ConfigPublic]: 'The system configuration properties that are visible to everyone.', [ApiTag.DatabaseBackups]: 'Manage backups of the Immich database.', [ApiTag.Deprecated]: 'Deprecated endpoints that are planned for removal in the next major release.', [ApiTag.Download]: 'Endpoints for downloading assets or collections of assets.', diff --git a/server/src/controllers/config-admin.controller.ts b/server/src/controllers/config-admin.controller.ts new file mode 100644 index 000000000..b135b0e62 --- /dev/null +++ b/server/src/controllers/config-admin.controller.ts @@ -0,0 +1,46 @@ +import { Body, Controller, Get, Put } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AdminConfigDto } from 'src/dtos/config.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigAdmin) +@Controller('admin/config') +export class ConfigAdminController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ permission: Permission.AdminConfigRead, admin: true }) + @Endpoint({ + summary: 'Get the admin configuration', + description: 'Retrieve admin configuration.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getAdminConfig(): Promise { + return this.service.getAdminConfig(); + } + + @Get('defaults') + @Authenticated({ permission: Permission.AdminConfigRead, admin: true }) + @Endpoint({ + summary: 'Get the system configuration defaults', + description: 'Retrieve the default value of every system configuration property.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getAdminConfigDefaults(): AdminConfigDto { + return this.service.getAdminConfigDefaults(); + } + + @Put() + @Authenticated({ permission: Permission.AdminConfigUpdate, admin: true }) + @Endpoint({ + summary: 'Update the system configuration', + description: 'Update the system configuration with a new system configuration.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + updateAdminConfig(@Body() dto: AdminConfigDto): Promise { + return this.service.updateAdminConfig(dto); + } +} diff --git a/server/src/controllers/config-public.controller.ts b/server/src/controllers/config-public.controller.ts new file mode 100644 index 000000000..87f7bb14c --- /dev/null +++ b/server/src/controllers/config-public.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { PublicConfigDto } from 'src/dtos/config.dto'; +import { ApiTag } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigPublic) +@Controller('public/config') +export class ConfigPublicController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ public: true }) + @Endpoint({ + summary: 'Get the public configuration', + description: 'Retrieve the system configuration properties that are visible to everyone.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getPublicConfig(): Promise { + return this.service.getPublicConfig(); + } + + @Get('defaults') + @Authenticated({ public: true }) + @Endpoint({ + summary: 'Get the public configuration defaults', + description: 'Retrieve the default value of the configuration properties that are visible to everyone.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getPublicConfigDefaults(): PublicConfigDto { + return this.service.getPublicConfigDefaults(); + } +} diff --git a/server/src/controllers/config-user.controller.ts b/server/src/controllers/config-user.controller.ts new file mode 100644 index 000000000..8f49d119f --- /dev/null +++ b/server/src/controllers/config-user.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { UserConfigDto } from 'src/dtos/config.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigUser) +@Controller('config') +export class ConfigUserController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ permission: Permission.UserConfigRead }) + @Endpoint({ + summary: 'Get the configuration with user visibility', + description: 'Retrieve the system configuration properties that are visible to logged in users.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getUserConfig(): Promise { + return this.service.getUserConfig(); + } + + @Get('defaults') + @Authenticated({ permission: Permission.UserConfigRead }) + @Endpoint({ + summary: 'Get the default configuration with user visibility', + description: 'Retrieve the default value of the configuration properties that are visible to logged in users.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getUserConfigDefaults(): UserConfigDto { + return this.service.getUserConfigDefaults(); + } +} diff --git a/server/src/controllers/config.controller.spec.ts b/server/src/controllers/config.controller.spec.ts new file mode 100644 index 000000000..fdd6a76e7 --- /dev/null +++ b/server/src/controllers/config.controller.spec.ts @@ -0,0 +1,116 @@ +import _ from 'lodash'; +import { ConfigAdminController } from 'src/controllers/config-admin.controller'; +import { ConfigPublicController } from 'src/controllers/config-public.controller'; +import { ConfigUserController } from 'src/controllers/config-user.controller'; +import { defaults, mapPublicConfig, mapUserConfig } from 'src/dtos/config.dto'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +/** Returns a full config that passes Zod validation (required URLs and min lengths). */ +function validConfig() { + const config = _.cloneDeep(defaults) as typeof defaults & { + oauth: { mobileRedirectUri: string }; + notifications: { smtp: { from: string; transport: { host: string } } }; + server: { externalDomain: string }; + }; + config.oauth.mobileRedirectUri ||= 'https://example.com'; + config.server.externalDomain ||= 'https://example.com'; + config.notifications.smtp.from ||= 'noreply@example.com'; + config.notifications.smtp.transport.host ||= 'localhost'; + return config; +} + +describe('config controllers', () => { + let ctx: ControllerContext; + const service = mockBaseService(SystemConfigService); + + beforeAll(async () => { + ctx = await controllerSetup( + [ConfigAdminController, ConfigUserController, ConfigPublicController], + [{ provide: SystemConfigService, useValue: service }], + ); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /admin/config', () => { + it('should return the full config', async () => { + service.getAdminConfig.mockResolvedValue(validConfig()); + + const { status, body } = await request(ctx.getHttpServer()).get('/admin/config'); + + expect(status).toBe(200); + expect(body.oauth.clientSecret).toBeDefined(); + expect(body.job.thumbnailGeneration).toBeDefined(); + }); + }); + + describe('PUT /admin/config', () => { + it('should accept a valid config', async () => { + service.updateAdminConfig.mockImplementation((dto) => Promise.resolve(dto)); + + const { status } = await request(ctx.getHttpServer()).put('/admin/config').send(validConfig()); + + expect(status).toBe(200); + }); + + it('should reject an invalid config', async () => { + const config = validConfig(); + config.nightlyTasks.startTime = 'invalid'; + + const { status, body } = await request(ctx.getHttpServer()).put('/admin/config').send(config); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { + path: ['nightlyTasks', 'startTime'], + message: 'Invalid input: expected string in HH:MM format, received string', + }, + ]), + ); + expect(service.updateAdminConfig).not.toHaveBeenCalled(); + }); + }); + + describe('GET /config', () => { + it('should return the properties visible to logged in users', async () => { + service.getUserConfig.mockResolvedValue(mapUserConfig(validConfig())); + + const { status, body } = await request(ctx.getHttpServer()).get('/config'); + + expect(status).toBe(200); + expect(body.image).toEqual({ + thumbnail: { size: defaults.image.thumbnail.size }, + preview: { size: defaults.image.preview.size }, + fullsize: { enabled: defaults.image.fullsize.enabled }, + }); + expect(body.oauth.clientSecret).toBeUndefined(); + expect(body.job).toBeUndefined(); + }); + }); + + describe('GET /public/config', () => { + it('should return the properties visible to everyone', async () => { + service.getPublicConfig.mockResolvedValue(mapPublicConfig(validConfig())); + + const { status, body } = await request(ctx.getHttpServer()).get('/public/config'); + + expect(status).toBe(200); + expect(body.server).toEqual({ loginPageMessage: defaults.server.loginPageMessage }); + expect(body.oauth).toEqual({ + autoLaunch: defaults.oauth.autoLaunch, + buttonText: defaults.oauth.buttonText, + enabled: defaults.oauth.enabled, + }); + expect(body.image).toBeUndefined(); + expect(body.trash).toBeUndefined(); + }); + }); +}); diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index e7a01643a..f6ea6d424 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -6,6 +6,9 @@ import { AssetMediaController } from 'src/controllers/asset-media.controller'; import { AssetController } from 'src/controllers/asset.controller'; import { AuthAdminController } from 'src/controllers/auth-admin.controller'; import { AuthController } from 'src/controllers/auth.controller'; +import { ConfigAdminController } from 'src/controllers/config-admin.controller'; +import { ConfigPublicController } from 'src/controllers/config-public.controller'; +import { ConfigUserController } from 'src/controllers/config-user.controller'; import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; @@ -49,6 +52,9 @@ export const controllers = [ AssetMediaController, AuthController, AuthAdminController, + ConfigUserController, + ConfigAdminController, + ConfigPublicController, DatabaseBackupController, DownloadController, DuplicateController, diff --git a/server/src/controllers/notification-admin.controller.ts b/server/src/controllers/notification-admin.controller.ts index c322c5a2b..7a5585fad 100644 --- a/server/src/controllers/notification-admin.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/com import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { NotificationCreateDto, NotificationDto, @@ -9,7 +10,6 @@ import { TemplateResponseDto, TestEmailResponseDto, } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { ApiTag } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { EmailTemplate } from 'src/repositories/email.repository'; diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 640715549..ef9f408e8 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -101,7 +101,11 @@ export class ServerController { @Endpoint({ summary: 'Get features', description: 'Retrieve available features supported by this server.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getPublicConfig' }), }) getServerFeatures(): Promise { return this.service.getFeatures(); @@ -112,7 +116,11 @@ export class ServerController { @Endpoint({ summary: 'Get config', description: 'Retrieve the current server configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getPublicConfig' }), }) getServerConfig(): Promise { return this.service.getSystemConfig(); diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 7d40f1258..0ae8741c6 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { defaults } from 'src/config'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { defaults } from 'src/dtos/config.dto'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { SystemConfigService } from 'src/services/system-config.service'; import request from 'supertest'; diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 6b79b38d9..b7c5f0099 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, ConfigTemplateStorageOptionDto } from 'src/dtos/config.dto'; import { ApiTag, Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { StorageTemplateService } from 'src/services/storage-template.service'; @@ -20,10 +20,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Get system configuration', description: 'Retrieve the current system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getAdminConfig' }), }) - getConfig(): Promise { - return this.service.getSystemConfig(); + getConfig(): Promise { + return this.service.getAdminConfig(); } @Get('defaults') @@ -31,10 +35,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Get system configuration defaults', description: 'Retrieve the default values for the system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getAdminConfigDefaults' }), }) - getConfigDefaults(): SystemConfigDto { - return this.service.getDefaults(); + getConfigDefaults(): AdminConfigDto { + return this.service.getAdminConfigDefaults(); } @Put() @@ -42,10 +50,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Update system configuration', description: 'Update the system configuration with a new system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'updateAdminConfig' }), }) - updateConfig(@Body() dto: SystemConfigDto): Promise { - return this.service.updateSystemConfig(dto); + updateConfig(@Body() dto: AdminConfigDto): Promise { + return this.service.updateAdminConfig(dto); } @Get('storage-template-options') @@ -55,7 +67,7 @@ export class SystemConfigController { description: 'Retrieve exemplary storage template options.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { + getStorageTemplateOptions(): ConfigTemplateStorageOptionDto { return this.storageTemplateService.getStorageTemplateOptions(); } } diff --git a/server/src/dtos/config.dto.spec.ts b/server/src/dtos/config.dto.spec.ts new file mode 100644 index 000000000..ce4f5a5d0 --- /dev/null +++ b/server/src/dtos/config.dto.spec.ts @@ -0,0 +1,68 @@ +import { + AdminConfigDto, + defaults, + mapPublicConfig, + mapUserConfig, + PublicConfigDto, + UserConfigDto, +} from 'src/dtos/config.dto'; +import { getKeysDeep } from 'src/utils/misc'; +import z from 'zod'; + +const PUBLIC_PROPERTIES = [ + 'oauth.autoLaunch', + 'oauth.buttonText', + 'oauth.enabled', + 'passwordLogin.enabled', + 'server.loginPageMessage', + 'theme.customCss', +]; + +describe('config visibility', () => { + it('should expose every property to admins', () => { + const paths = getKeysDeep(defaults); + + expect(paths).toEqual(expect.arrayContaining(PUBLIC_PROPERTIES)); + expect(paths).toContain('oauth.clientSecret'); + expect(paths.length).toBeGreaterThan(100); + }); + + it('should expose the public properties to everyone', () => { + expect(getKeysDeep(mapPublicConfig(defaults)).sort()).toEqual(PUBLIC_PROPERTIES); + }); + + it('should expose everything public to logged in users as well', () => { + expect(getKeysDeep(mapUserConfig(defaults))).toEqual(expect.arrayContaining(PUBLIC_PROPERTIES)); + }); + + it('should accept the defaults with the admin schema', () => { + expect(AdminConfigDto.schema.safeParse(defaults)).toEqual(expect.objectContaining({ success: true })); + }); + + it('should map the defaults onto the user and public schemas', () => { + expect(UserConfigDto.schema.safeParse(mapUserConfig(defaults))).toEqual(expect.objectContaining({ success: true })); + expect(PublicConfigDto.schema.safeParse(mapPublicConfig(defaults))).toEqual( + expect.objectContaining({ success: true }), + ); + }); + + it('should not leak admin properties into the public config', () => { + const config = mapPublicConfig(defaults) as Record; + + expect(config.oauth).toEqual({ + autoLaunch: defaults.oauth.autoLaunch, + buttonText: defaults.oauth.buttonText, + enabled: defaults.oauth.enabled, + }); + expect(config.job).toBeUndefined(); + expect(config.image).toBeUndefined(); + expect(config.notifications).toBeUndefined(); + }); + + it('should keep the visibility metadata out of the schemas', () => { + for (const schema of [AdminConfigDto.schema, UserConfigDto.schema, PublicConfigDto.schema]) { + const json = JSON.stringify(z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' })); + expect(json).not.toContain('visibility'); + } + }); +}); diff --git a/server/src/dtos/config.dto.ts b/server/src/dtos/config.dto.ts new file mode 100644 index 000000000..a7dbaf0cc --- /dev/null +++ b/server/src/dtos/config.dto.ts @@ -0,0 +1,773 @@ +import { CronExpression } from '@nestjs/schedule'; +import { validateCronExpression } from 'cron'; +import { createZodDto } from 'nestjs-zod'; +import { + AudioCodec, + AudioCodecSchema, + Colorspace, + ColorspaceSchema, + ConfigVisibility, + CQMode, + CQModeSchema, + HlsVideoResolution, + HlsVideoResolutionSchema, + ImageFormat, + ImageFormatSchema, + LogLevel, + LogLevelSchema, + OAuthTokenEndpointAuthMethod, + OAuthTokenEndpointAuthMethodSchema, + ReleaseChannel, + ReleaseChannelSchema, + ToneMapping, + ToneMappingSchema, + TranscodeHardwareAcceleration, + TranscodeHardwareAccelerationSchema, + TranscodePolicy, + TranscodePolicySchema, + VideoCodec, + VideoCodecSchema, + VideoContainer, + VideoContainerSchema, +} from 'src/enum'; +import { DeepPartial } from 'src/types'; +import z from 'zod'; + +const { Admin, User, Public } = ConfigVisibility; + +const configBool = z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .meta({ type: 'boolean' }); + +const cronExpressionSchema = z + .string() + .superRefine((value, ctx) => { + const validated = validateCronExpression(value); + if (!validated.valid) { + ctx.addIssue({ + code: 'custom', + message: `Invalid cron expression. ${validated.error?.message ?? ''}`, + input: value, + }); + } + }) + .describe('Cron expression'); + +const emptyOrUrl = (error: string) => + z.string().refine((url) => url.length === 0 || z.url().safeParse(url).success, { error }); + +const AdminConfigIntegrityJobSchema = z + .object({ + enabled: z.boolean().describe('Enabled'), + cronExpression: cronExpressionSchema.describe('Cron expression for when the integrity check should run'), + }) + .describe('Integrity job config') + .meta({ id: 'AdminConfigIntegrityJobDto' }); + +const AdminConfigJobSettingsSchema = z + .object({ concurrency: z.int().min(1).describe('Concurrency') }) + .meta({ id: 'AdminConfigJobSettingsDto' }); + +const AdminConfigMachineLearningTaskSchema = z.object({ + enabled: z.boolean().describe('Whether the task is enabled').meta({ visibility: User }), +}); + +const AdminConfigMachineLearningModelSchema = AdminConfigMachineLearningTaskSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); + +const AdminConfigGeneratedImageSchema = z + .object({ + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + size: z.int().min(1).describe('Size').meta({ visibility: User }), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'AdminConfigGeneratedImageDto' }); + +const AdminConfigFFmpegSchema = z + .object({ + crf: z.coerce.number().int().min(0).max(51).describe('CRF'), + threads: z.coerce.number().int().min(0).describe('Threads'), + preset: z.string().describe('Preset'), + targetVideoCodec: VideoCodecSchema, + acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), + targetAudioCodec: AudioCodecSchema, + acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'), + acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), + targetResolution: z.string().describe('Target resolution'), + maxBitrate: z.string().describe('Max bitrate'), + bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), + refs: z.coerce.number().int().min(0).max(6).describe('References'), + gopSize: z.coerce.number().int().min(0).describe('GOP size'), + temporalAQ: configBool.describe('Temporal AQ'), + cqMode: CQModeSchema, + twoPass: configBool.describe('Two pass'), + preferredHwDevice: z.string().describe('Preferred hardware device'), + transcode: TranscodePolicySchema, + accel: TranscodeHardwareAccelerationSchema, + accelDecode: configBool.describe('Accelerated decode'), + tonemap: ToneMappingSchema, + realtime: z + .object({ + enabled: configBool.describe('Enable real-time HLS transcoding (alpha)').meta({ visibility: User }), + videoCodecs: z + .array(VideoCodecSchema) + .describe('Video codecs to use for real-time HLS transcoding') + .meta({ visibility: User }), + resolutions: z + .array(HlsVideoResolutionSchema) + .describe('Resolutions to use for real-time HLS transcoding') + .meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigFFmpegRealtimeDto' }), + }) + .meta({ id: 'AdminConfigFFmpegDto' }); + +const AdminConfigSmtpSchema = z + .object({ + enabled: configBool.describe('Whether SMTP email notifications are enabled'), + from: z.string().describe('Email address to send from'), + replyTo: z.string().describe('Email address for replies'), + transport: z + .object({ + ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), + host: z.string().describe('SMTP server hostname'), + port: z.int().min(0).max(65_535).describe('SMTP server port'), + secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), + username: z.string().describe('SMTP username'), + password: z.string().describe('SMTP password'), + }) + .meta({ id: 'AdminConfigSmtpTransportDto' }), + }) + .meta({ id: 'AdminConfigSmtpDto' }); + +const AdminConfigSchemaWithVisibility = z + .object({ + backup: z + .object({ + database: z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.int().min(1).describe('Keep last amount'), + }) + .meta({ id: 'AdminConfigDatabaseBackupDto' }), + }) + .meta({ id: 'AdminConfigBackupsDto' }), + ffmpeg: AdminConfigFFmpegSchema, + integrityChecks: z + .object({ + missingFiles: AdminConfigIntegrityJobSchema, + untrackedFiles: AdminConfigIntegrityJobSchema, + checksumFiles: AdminConfigIntegrityJobSchema.extend({ + timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'), + percentageLimit: z + .float32() + .nonnegative() + .max(1) + .describe('Percentage limit of the integrity checksum job') + .meta({ format: 'double' }), + }) + .describe('Integrity checksum job config') + .meta({ id: 'AdminConfigIntegrityChecksumJobDto' }), + }) + .describe('Integrity checks config') + .meta({ id: 'AdminConfigIntegrityChecksDto' }), + job: z + .object({ + thumbnailGeneration: AdminConfigJobSettingsSchema, + metadataExtraction: AdminConfigJobSettingsSchema, + videoConversion: AdminConfigJobSettingsSchema, + faceDetection: AdminConfigJobSettingsSchema, + smartSearch: AdminConfigJobSettingsSchema, + backgroundTask: AdminConfigJobSettingsSchema, + migration: AdminConfigJobSettingsSchema, + search: AdminConfigJobSettingsSchema, + sidecar: AdminConfigJobSettingsSchema, + library: AdminConfigJobSettingsSchema, + notifications: AdminConfigJobSettingsSchema, + ocr: AdminConfigJobSettingsSchema, + workflow: AdminConfigJobSettingsSchema, + editor: AdminConfigJobSettingsSchema, + integrityCheck: AdminConfigJobSettingsSchema, + }) + .meta({ id: 'AdminConfigJobDto' }), + logging: z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'AdminConfigLoggingDto' }), + machineLearning: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + urls: z.array(z.string()).min(1).describe('ML service URLs'), + availabilityChecks: z + .object({ + enabled: configBool.describe('Enabled'), + timeout: z.int(), + interval: z.int(), + }) + .meta({ id: 'AdminConfigMachineLearningAvailabilityChecksDto' }), + clip: AdminConfigMachineLearningModelSchema.meta({ id: 'AdminConfigClipDto' }), + duplicateDetection: AdminConfigMachineLearningTaskSchema.extend({ + maxDistance: z + .number() + .min(0.001) + .max(0.1) + .describe('Maximum distance threshold for duplicate detection') + .meta({ format: 'double' }), + }).meta({ id: 'AdminConfigDuplicateDetectionDto' }), + facialRecognition: AdminConfigMachineLearningModelSchema.extend({ + minScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for face detection') + .meta({ format: 'double' }), + maxDistance: z + .number() + .min(0.1) + .max(2) + .describe('Maximum distance threshold for face recognition') + .meta({ format: 'double' }), + minFaces: z + .int() + .min(1) + .describe('Minimum number of faces required for recognition') + .meta({ visibility: User }), + }).meta({ id: 'AdminConfigFacialRecognitionDto' }), + ocr: AdminConfigMachineLearningModelSchema.extend({ + maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), + minDetectionScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for text detection') + .meta({ format: 'double' }), + minRecognitionScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for text recognition') + .meta({ format: 'double' }), + }).meta({ id: 'AdminConfigOcrDto' }), + }) + .meta({ id: 'AdminConfigMachineLearningDto' }), + map: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + lightStyle: z.url().describe('Light map style URL').meta({ visibility: User }), + darkStyle: z.url().describe('Dark map style URL').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigMapDto' }), + reverseGeocoding: z + .object({ enabled: configBool.describe('Enabled').meta({ visibility: User }) }) + .meta({ id: 'AdminConfigReverseGeocodingDto' }), + metadata: z + .object({ + faces: z.object({ import: configBool.describe('Import') }).meta({ id: 'AdminConfigFacesDto' }), + }) + .meta({ id: 'AdminConfigMetadataDto' }), + oauth: z + .object({ + autoLaunch: configBool.describe('Auto launch').meta({ visibility: Public }), + autoRegister: configBool.describe('Auto register'), + buttonText: z.string().describe('Button text').meta({ visibility: Public }), + clientId: z.string().describe('Client ID'), + clientSecret: z.string().describe('Client secret'), + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, + timeout: z.int().min(1).describe('Timeout'), + allowInsecureRequests: configBool.describe('Allow insecure requests'), + defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), + enabled: configBool.describe('Enabled').meta({ visibility: Public }), + issuerUrl: emptyOrUrl('Issuer URL must be an empty string or a valid URL').describe('Issuer URL'), + scope: z.string().describe('Scope'), + prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), + endSessionEndpoint: emptyOrUrl('endSessionEndpoint must be an empty string or a valid URL').describe( + 'End session endpoint', + ), + signingAlgorithm: z.string().describe('Signing algorithm'), + profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), + storageLabelClaim: z.string().describe('Storage label claim'), + storageQuotaClaim: z.string().describe('Storage quota claim'), + roleClaim: z.string().describe('Role claim'), + mobileOverrideEnabled: configBool.describe('Mobile override enabled'), + mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), + }) + .transform((value, ctx) => { + if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { + return value; + } + + if (!z.url().safeParse(value.mobileRedirectUri).success) { + ctx.issues.push({ + code: 'custom', + message: 'Mobile redirect URI must be an empty string or a valid URL', + input: value.mobileRedirectUri, + }); + return z.NEVER; + } + + return value; + }) + .meta({ id: 'AdminConfigOAuthDto' }), + passwordLogin: z + .object({ enabled: configBool.describe('Enabled').meta({ visibility: Public }) }) + .meta({ id: 'AdminConfigPasswordLoginDto' }), + storageTemplate: z + .object({ + enabled: configBool.describe('Enabled'), + hashVerificationEnabled: configBool.describe('Hash verification enabled'), + template: z.string().describe('Template'), + }) + .meta({ id: 'AdminConfigStorageTemplateDto' }), + image: z + .object({ + thumbnail: AdminConfigGeneratedImageSchema, + preview: AdminConfigGeneratedImageSchema, + fullsize: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'AdminConfigGeneratedFullsizeImageDto' }), + colorspace: ColorspaceSchema, + extractEmbedded: configBool.describe('Extract embedded'), + }) + .meta({ id: 'AdminConfigImageDto' }), + newVersionCheck: z + .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) + .meta({ id: 'AdminConfigNewVersionCheckDto' }), + nightlyTasks: z + .object({ + startTime: z.iso + .time({ + precision: -1, + error: (iss) => `Invalid input: expected string in HH:MM format, received ${typeof iss.input}`, + }) + .describe('Start time (HH:MM)'), + databaseCleanup: configBool.describe('Database cleanup'), + missingThumbnails: configBool.describe('Missing thumbnails'), + clusterNewFaces: configBool.describe('Cluster new faces'), + generateMemories: configBool.describe('Generate memories'), + syncQuotaUsage: configBool.describe('Sync quota usage'), + }) + .meta({ id: 'AdminConfigNightlyTasksDto' }), + trash: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + days: z.int().min(0).describe('Days').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigTrashDto' }), + theme: z + .object({ customCss: z.string().describe('Custom CSS for theming').meta({ visibility: Public }) }) + .meta({ id: 'AdminConfigThemeDto' }), + library: z + .object({ + scan: z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'AdminConfigLibraryScanDto' }), + watch: z.object({ enabled: configBool.describe('Enabled') }).meta({ id: 'AdminConfigLibraryWatchDto' }), + }) + .meta({ id: 'AdminConfigLibraryDto' }), + notifications: z.object({ smtp: AdminConfigSmtpSchema }).meta({ id: 'AdminConfigNotificationsDto' }), + templates: z + .object({ + email: z + .object({ + welcomeTemplate: z.string().describe('Welcome template'), + albumInviteTemplate: z.string().describe('Album invite template'), + albumUpdateTemplate: z.string().describe('Album update template'), + }) + .meta({ id: 'AdminConfigTemplateEmailsDto' }), + }) + .meta({ id: 'AdminConfigTemplatesDto' }), + server: z + .object({ + externalDomain: emptyOrUrl('External domain must be an empty string or a valid URL') + .describe('External domain') + .meta({ visibility: User }), + loginPageMessage: z.string().describe('Login page message').meta({ visibility: Public }), + publicUsers: configBool.describe('Public users').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigServerDto' }), + user: z + .object({ deleteDelay: z.int().min(1).describe('Delete delay').meta({ visibility: User }) }) + .meta({ id: 'AdminConfigUserDto' }), + }) + .describe('Configuration properties that are visible to the admin') + .meta({ id: 'AdminConfigDto' }); + +export type SystemConfig = z.infer; +export type MachineLearningConfig = SystemConfig['machineLearning']; + +const visibilities = [Public, User, Admin]; + +const isVisible = (property: ConfigVisibility, visibility: ConfigVisibility) => + visibilities.indexOf(property) <= visibilities.indexOf(visibility); + +const getMeta = (schema: z.ZodType) => + (z.globalRegistry.get(schema) ?? {}) as { id?: string; description?: string; visibility?: ConfigVisibility }; + +const unwrap = (schema: z.ZodType) => (schema instanceof z.ZodPipe ? (schema.def.in as z.ZodType) : schema); + +const visibleSchemas = new Map>(); + +const applyVisibility = (visibility: ConfigVisibility): z.ZodType | undefined => { + const map: Record = { + [Admin]: 'Configuration properties that are visible to the admin', + [User]: 'Configuration properties that are visible to a logged user', + [Public]: 'Configuration properties that are visible to everyone', + }; + + return applyVisibilityRecursive(AdminConfigSchemaWithVisibility, visibility, map[visibility]); +}; + +const applyVisibilityRecursive = ( + schema: z.ZodType, + visibility: ConfigVisibility, + override?: string, +): z.ZodType | undefined => { + const object = unwrap(schema); + const { id, description, visibility: property } = getMeta(schema); + + if (!(object instanceof z.ZodObject)) { + return isVisible(property ?? Admin, visibility) ? schema : undefined; + } + + let cache = visibleSchemas.get(schema); + if (!cache) { + cache = new Map(); + visibleSchemas.set(schema, cache); + } + + if (cache.has(visibility)) { + return cache.get(visibility); + } + + const shape: Record = {}; + for (const [key, value] of Object.entries(object.shape as Record)) { + const visible = applyVisibilityRecursive(value, visibility); + if (visible) { + shape[key] = visible; + } + } + + let visible: z.ZodType | undefined; + if (Object.keys(shape).length > 0) { + visible = z.object(shape).meta({ + ...(id && { id: `${visibility}${id.slice(Admin.length)}` }), + ...((override ?? description) && { description: override ?? description }), + }); + } + + cache.set(visibility, visible); + + return visible; +}; + +const stripVisibilityMetadata = (schema: T): T => { + const object = unwrap(schema); + if (object instanceof z.ZodObject) { + for (const value of Object.values(object.shape as Record)) { + stripVisibilityMetadata(value); + } + + return schema; + } + + const { visibility, ...meta } = getMeta(schema); + if (visibility) { + z.globalRegistry.add(schema, meta); + } + + return schema; +}; + +const AdminConfigSchema = applyVisibility(Admin)! as z.ZodType; +const UserConfigSchema = applyVisibility(User)! as z.ZodType>; +const PublicConfigSchema = applyVisibility(Public)! as z.ZodType>; + +// prevent visibility metadata from leaking to openapi spec +// eslint-disable-next-line unicorn/no-top-level-side-effects +stripVisibilityMetadata(AdminConfigSchemaWithVisibility); + +const ConfigTemplateStorageOptionSchema = z + .object({ + yearOptions: z.array(z.string()).describe('Available year format options for storage template'), + monthOptions: z.array(z.string()).describe('Available month format options for storage template'), + weekOptions: z.array(z.string()).describe('Available week format options for storage template'), + dayOptions: z.array(z.string()).describe('Available day format options for storage template'), + hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), + minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), + secondOptions: z.array(z.string()).describe('Available second format options for storage template'), + presetOptions: z.array(z.string()).describe('Available preset template options'), + }) + .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); + +export class AdminConfigDto extends createZodDto(AdminConfigSchema) {} +export class UserConfigDto extends createZodDto(UserConfigSchema) {} +export class PublicConfigDto extends createZodDto(PublicConfigSchema) {} +export class ConfigFFmpegDto extends createZodDto(AdminConfigFFmpegSchema) {} +export class ConfigSmtpDto extends createZodDto(AdminConfigSmtpSchema) {} +export class ConfigTemplateStorageOptionDto extends createZodDto(ConfigTemplateStorageOptionSchema) {} + +/** @deprecated the `/system-config` endpoints these are named after are on their way out */ +export { AdminConfigDto as SystemConfigDto, ConfigSmtpDto as SystemConfigSmtpDto }; + +export function mapAdminConfig(config: SystemConfig): AdminConfigDto { + return config; +} + +export function mapUserConfig(config: SystemConfig): UserConfigDto { + return UserConfigSchema.parse(config); +} + +export function mapPublicConfig(config: SystemConfig): PublicConfigDto { + return PublicConfigSchema.parse(config); +} + +export const defaults = Object.freeze({ + backup: { + database: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_2AM, + keepLastAmount: 14, + }, + }, + ffmpeg: { + crf: 23, + threads: 0, + preset: 'ultrafast', + targetVideoCodec: VideoCodec.H264, + acceptedVideoCodecs: [VideoCodec.H264], + targetAudioCodec: AudioCodec.Aac, + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.Opus], + acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], + targetResolution: '720', + maxBitrate: '0', + bframes: -1, + refs: 0, + gopSize: 0, + temporalAQ: false, + cqMode: CQMode.Auto, + twoPass: false, + preferredHwDevice: 'auto', + transcode: TranscodePolicy.Required, + tonemap: ToneMapping.Hable, + accel: TranscodeHardwareAcceleration.Disabled, + accelDecode: true, + realtime: { + enabled: false, + videoCodecs: [VideoCodec.H264, VideoCodec.Hevc], + resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080], + }, + }, + integrityChecks: { + missingFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + }, + untrackedFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + }, + checksumFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + timeLimit: 60 * 60 * 1000, // 1 hour + percentageLimit: 1, // 100% of assets + }, + }, + job: { + thumbnailGeneration: { concurrency: 3 }, + metadataExtraction: { concurrency: 5 }, + videoConversion: { concurrency: 1 }, + faceDetection: { concurrency: 2 }, + smartSearch: { concurrency: 2 }, + backgroundTask: { concurrency: 5 }, + migration: { concurrency: 5 }, + search: { concurrency: 5 }, + sidecar: { concurrency: 5 }, + library: { concurrency: 5 }, + notifications: { concurrency: 5 }, + ocr: { concurrency: 1 }, + workflow: { concurrency: 5 }, + editor: { concurrency: 2 }, + integrityCheck: { concurrency: 1 }, + }, + logging: { + enabled: true, + level: LogLevel.Log, + }, + machineLearning: { + enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', + urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], + availabilityChecks: { + enabled: true, + timeout: 2000, + interval: 30_000, + }, + clip: { + enabled: true, + modelName: 'ViT-B-32__openai', + }, + duplicateDetection: { + enabled: true, + maxDistance: 0.01, + }, + facialRecognition: { + enabled: true, + modelName: 'buffalo_l', + minScore: 0.7, + maxDistance: 0.5, + minFaces: 3, + }, + ocr: { + enabled: true, + modelName: 'PP-OCRv5_mobile', + minDetectionScore: 0.5, + minRecognitionScore: 0.8, + maxResolution: 736, + }, + }, + map: { + enabled: true, + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', + }, + reverseGeocoding: { + enabled: true, + }, + metadata: { + faces: { + import: false, + }, + }, + oauth: { + autoLaunch: false, + autoRegister: true, + buttonText: 'Login with OAuth', + clientId: '', + clientSecret: '', + defaultStorageQuota: null, + enabled: false, + issuerUrl: '', + endSessionEndpoint: '', + mobileOverrideEnabled: false, + mobileRedirectUri: '', + prompt: '', + scope: 'openid email profile', + signingAlgorithm: 'RS256', + profileSigningAlgorithm: 'none', + storageLabelClaim: 'preferred_username', + storageQuotaClaim: 'immich_quota', + roleClaim: 'immich_role', + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, + timeout: 30_000, + allowInsecureRequests: false, + }, + passwordLogin: { + enabled: true, + }, + storageTemplate: { + enabled: false, + hashVerificationEnabled: true, + template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', + }, + image: { + thumbnail: { + format: ImageFormat.Webp, + size: 250, + quality: 80, + progressive: false, + }, + preview: { + format: ImageFormat.Jpeg, + size: 1440, + quality: 80, + progressive: false, + }, + colorspace: Colorspace.P3, + extractEmbedded: false, + fullsize: { + enabled: false, + format: ImageFormat.Jpeg, + quality: 80, + progressive: false, + }, + }, + newVersionCheck: { + enabled: true, + channel: ReleaseChannel.Stable, + }, + nightlyTasks: { + startTime: '00:00', + databaseCleanup: true, + generateMemories: true, + syncQuotaUsage: true, + missingThumbnails: true, + clusterNewFaces: true, + }, + trash: { + enabled: true, + days: 30, + }, + theme: { + customCss: '', + }, + library: { + scan: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT, + }, + watch: { + enabled: false, + }, + }, + server: { + externalDomain: '', + loginPageMessage: '', + publicUsers: true, + }, + notifications: { + smtp: { + enabled: false, + from: '', + replyTo: '', + transport: { + ignoreCert: false, + host: '', + port: 587, + secure: false, + username: '', + password: '', + }, + }, + }, + templates: { + email: { + welcomeTemplate: '', + albumInviteTemplate: '', + albumUpdateTemplate: '', + }, + }, + user: { + deleteDelay: 7, + }, +}); diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts deleted file mode 100644 index 2ba6f0c36..000000000 --- a/server/src/dtos/model-config.dto.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createZodDto } from 'nestjs-zod'; -import z from 'zod'; - -const TaskConfigSchema = z - .object({ - enabled: z.boolean().describe('Whether the task is enabled'), - }) - .meta({ id: 'TaskConfig' }); - -const ModelConfigSchema = TaskConfigSchema.extend({ - modelName: z.string().describe('Name of the model to use'), -}); - -export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); - -export const DuplicateDetectionConfigSchema = TaskConfigSchema.extend({ - maxDistance: z - .number() - .meta({ format: 'double' }) - .min(0.001) - .max(0.1) - .describe('Maximum distance threshold for duplicate detection'), -}).meta({ id: 'DuplicateDetectionConfig' }); - -export const FacialRecognitionConfigSchema = ModelConfigSchema.extend({ - minScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for face detection'), - maxDistance: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(2) - .describe('Maximum distance threshold for face recognition'), - minFaces: z.int().min(1).describe('Minimum number of faces required for recognition'), -}).meta({ id: 'FacialRecognitionConfig' }); - -export const OcrConfigSchema = ModelConfigSchema.extend({ - maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), - minDetectionScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for text detection'), - minRecognitionScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for text recognition'), -}).meta({ id: 'OcrConfig' }); - -export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts deleted file mode 100644 index a50b7abe8..000000000 --- a/server/src/dtos/system-config.dto.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { validateCronExpression } from 'cron'; -import { createZodDto } from 'nestjs-zod'; -import { SystemConfig } from 'src/config'; -import { - CLIPConfigSchema, - DuplicateDetectionConfigSchema, - FacialRecognitionConfigSchema, - OcrConfigSchema, -} from 'src/dtos/model-config.dto'; -import { - AudioCodecSchema, - ColorspaceSchema, - CQModeSchema, - HlsVideoResolutionSchema, - ImageFormatSchema, - LogLevelSchema, - OAuthTokenEndpointAuthMethodSchema, - ToneMappingSchema, - TranscodeHardwareAccelerationSchema, - TranscodePolicySchema, - VideoCodecSchema, - VideoContainerSchema, -} from 'src/enum'; -import z from 'zod'; - -/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */ -const configBool = z - .preprocess((val) => { - if (val === 'true') { - return true; - } - if (val === 'false') { - return false; - } - return val; - }, z.boolean()) - .meta({ type: 'boolean' }); - -const JobSettingsSchema = z - .object({ - concurrency: z.int().min(1).describe('Concurrency'), - }) - .meta({ id: 'JobSettingsDto' }); - -const cronExpressionSchema = z - .string() - .superRefine((value, ctx) => { - const validated = validateCronExpression(value); - if (!validated.valid) { - ctx.addIssue({ - code: 'custom', - message: `Invalid cron expression. ${validated.error?.message ?? ''}`, - input: value, - }); - } - }) - .describe('Cron expression'); - -const DatabaseBackupSchema = z - .object({ - enabled: configBool.describe('Enabled'), - cronExpression: cronExpressionSchema, - keepLastAmount: z.int().min(1).describe('Keep last amount'), - }) - .meta({ id: 'DatabaseBackupConfig' }); - -const SystemConfigIntegrityJobSchema = z - .object({ - enabled: z.boolean().describe('Enabled'), - cronExpression: cronExpressionSchema.describe('Cron expression for when the integrity check should run'), - }) - .describe('Integrity job config') - .meta({ id: 'SystemConfigIntegrityJob' }); - -const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({ - timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'), - percentageLimit: z - .float32() - .nonnegative() - .max(1) - .describe('Percentage limit of the integrity checksum job') - .meta({ format: 'double' }), -}) - .describe('Integrity checksum job config') - .meta({ id: 'SystemConfigIntegrityChecksumJob' }); - -const SystemConfigIntegrityChecksSchema = z - .object({ - missingFiles: SystemConfigIntegrityJobSchema, - untrackedFiles: SystemConfigIntegrityJobSchema, - checksumFiles: SystemConfigIntegrityChecksumJobSchema, - }) - .describe('Integrity checks config') - .meta({ id: 'SystemConfigIntegrityChecks' }); - -const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); - -const SystemConfigFFmpegSchema = z - .object({ - crf: z.coerce.number().int().min(0).max(51).describe('CRF'), - threads: z.coerce.number().int().min(0).describe('Threads'), - preset: z.string().describe('Preset'), - targetVideoCodec: VideoCodecSchema, - acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), - targetAudioCodec: AudioCodecSchema, - acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'), - acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), - targetResolution: z.string().describe('Target resolution'), - maxBitrate: z.string().describe('Max bitrate'), - bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), - refs: z.coerce.number().int().min(0).max(6).describe('References'), - gopSize: z.coerce.number().int().min(0).describe('GOP size'), - temporalAQ: configBool.describe('Temporal AQ'), - cqMode: CQModeSchema, - twoPass: configBool.describe('Two pass'), - preferredHwDevice: z.string().describe('Preferred hardware device'), - transcode: TranscodePolicySchema, - accel: TranscodeHardwareAccelerationSchema, - accelDecode: configBool.describe('Accelerated decode'), - tonemap: ToneMappingSchema, - realtime: z - .object({ - enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'), - videoCodecs: z.array(VideoCodecSchema).describe('Video codecs to use for real-time HLS transcoding'), - resolutions: z.array(HlsVideoResolutionSchema).describe('Resolutions to use for real-time HLS transcoding'), - }) - .meta({ id: 'SystemConfigFFmpegRealtimeDto' }), - }) - .meta({ id: 'SystemConfigFFmpegDto' }); - -const SystemConfigJobSchema = z - .object({ - thumbnailGeneration: JobSettingsSchema, - metadataExtraction: JobSettingsSchema, - videoConversion: JobSettingsSchema, - faceDetection: JobSettingsSchema, - smartSearch: JobSettingsSchema, - backgroundTask: JobSettingsSchema, - migration: JobSettingsSchema, - search: JobSettingsSchema, - sidecar: JobSettingsSchema, - library: JobSettingsSchema, - notifications: JobSettingsSchema, - ocr: JobSettingsSchema, - workflow: JobSettingsSchema, - editor: JobSettingsSchema, - integrityCheck: JobSettingsSchema, - }) - .meta({ id: 'SystemConfigJobDto' }); - -const SystemConfigLibraryScanSchema = z - .object({ - enabled: configBool.describe('Enabled'), - cronExpression: cronExpressionSchema, - }) - .meta({ id: 'SystemConfigLibraryScanDto' }); - -const SystemConfigLibraryWatchSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigLibraryWatchDto' }); - -const SystemConfigLibrarySchema = z - .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) - .meta({ id: 'SystemConfigLibraryDto' }); - -const SystemConfigLoggingSchema = z - .object({ - enabled: configBool.describe('Enabled'), - level: LogLevelSchema, - }) - .meta({ id: 'SystemConfigLoggingDto' }); - -const MachineLearningAvailabilityChecksSchema = z - .object({ - enabled: configBool.describe('Enabled'), - timeout: z.int(), - interval: z.int(), - }) - .meta({ id: 'MachineLearningAvailabilityChecksDto' }); - -const SystemConfigMachineLearningSchema = z - .object({ - enabled: configBool.describe('Enabled'), - urls: z.array(z.string()).min(1).describe('ML service URLs'), - availabilityChecks: MachineLearningAvailabilityChecksSchema, - clip: CLIPConfigSchema, - duplicateDetection: DuplicateDetectionConfigSchema, - facialRecognition: FacialRecognitionConfigSchema, - ocr: OcrConfigSchema, - }) - .meta({ id: 'SystemConfigMachineLearningDto' }); - -const SystemConfigMapSchema = z - .object({ - enabled: configBool.describe('Enabled'), - lightStyle: z.url().describe('Light map style URL'), - darkStyle: z.url().describe('Dark map style URL'), - }) - .meta({ id: 'SystemConfigMapDto' }); - -export enum ReleaseChannel { - Stable = 'stable', - ReleaseCandidate = 'releaseCandidate', -} - -const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); - -const SystemConfigNewVersionCheckSchema = z - .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) - .meta({ id: 'SystemConfigNewVersionCheckDto' }); - -const SystemConfigNightlyTasksSchema = z - .object({ - startTime: z.iso - .time({ - precision: -1, - error: (iss) => `Invalid input: expected string in HH:MM format, received ${typeof iss.input}`, - }) - .describe('Start time (HH:MM)'), - databaseCleanup: configBool.describe('Database cleanup'), - missingThumbnails: configBool.describe('Missing thumbnails'), - clusterNewFaces: configBool.describe('Cluster new faces'), - generateMemories: configBool.describe('Generate memories'), - syncQuotaUsage: configBool.describe('Sync quota usage'), - }) - .meta({ id: 'SystemConfigNightlyTasksDto' }); - -const SystemConfigOAuthSchema = z - .object({ - autoLaunch: configBool.describe('Auto launch'), - autoRegister: configBool.describe('Auto register'), - buttonText: z.string().describe('Button text'), - clientId: z.string().describe('Client ID'), - clientSecret: z.string().describe('Client secret'), - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, - timeout: z.int().min(1).describe('Timeout'), - allowInsecureRequests: configBool.describe('Allow insecure requests'), - defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), - enabled: configBool.describe('Enabled'), - issuerUrl: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'Issuer URL must be an empty string or a valid URL', - }) - .describe('Issuer URL'), - scope: z.string().describe('Scope'), - prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), - endSessionEndpoint: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'endSessionEndpoint must be an empty string or a valid URL', - }) - .describe('End session endpoint'), - signingAlgorithm: z.string().describe('Signing algorithm'), - profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), - storageLabelClaim: z.string().describe('Storage label claim'), - storageQuotaClaim: z.string().describe('Storage quota claim'), - roleClaim: z.string().describe('Role claim'), - mobileOverrideEnabled: configBool.describe('Mobile override enabled'), - mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), - }) - .transform((value, ctx) => { - if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { - return value; - } - - if (!z.url().safeParse(value.mobileRedirectUri).success) { - ctx.issues.push({ - code: 'custom', - message: 'Mobile redirect URI must be an empty string or a valid URL', - input: value.mobileRedirectUri, - }); - return z.NEVER; - } - - return value; - }) - .meta({ - id: 'SystemConfigOAuthDto', - }); - -const SystemConfigPasswordLoginSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigPasswordLoginDto' }); - -const SystemConfigReverseGeocodingSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigReverseGeocodingDto' }); - -const SystemConfigFacesSchema = z - .object({ import: configBool.describe('Import') }) - .meta({ id: 'SystemConfigFacesDto' }); -const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); - -const SystemConfigServerSchema = z - .object({ - externalDomain: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'External domain must be an empty string or a valid URL', - }) - .describe('External domain'), - loginPageMessage: z.string().describe('Login page message'), - publicUsers: configBool.describe('Public users'), - }) - .meta({ id: 'SystemConfigServerDto' }); - -const SystemConfigSmtpTransportSchema = z - .object({ - ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), - host: z.string().describe('SMTP server hostname'), - port: z.int().min(0).max(65_535).describe('SMTP server port'), - secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), - username: z.string().describe('SMTP username'), - password: z.string().describe('SMTP password'), - }) - .meta({ id: 'SystemConfigSmtpTransportDto' }); - -const SystemConfigSmtpSchema = z - .object({ - enabled: configBool.describe('Whether SMTP email notifications are enabled'), - from: z.string().describe('Email address to send from'), - replyTo: z.string().describe('Email address for replies'), - transport: SystemConfigSmtpTransportSchema, - }) - .meta({ id: 'SystemConfigSmtpDto' }); - -const SystemConfigNotificationsSchema = z - .object({ smtp: SystemConfigSmtpSchema }) - .meta({ id: 'SystemConfigNotificationsDto' }); - -const SystemConfigTemplateEmailsSchema = z - .object({ - albumInviteTemplate: z.string().describe('Album invite template'), - welcomeTemplate: z.string().describe('Welcome template'), - albumUpdateTemplate: z.string().describe('Album update template'), - }) - .meta({ id: 'SystemConfigTemplateEmailsDto' }); -const SystemConfigTemplatesSchema = z - .object({ email: SystemConfigTemplateEmailsSchema }) - .meta({ id: 'SystemConfigTemplatesDto' }); - -const SystemConfigStorageTemplateSchema = z - .object({ - enabled: configBool.describe('Enabled'), - hashVerificationEnabled: configBool.describe('Hash verification enabled'), - template: z.string().describe('Template'), - }) - .meta({ id: 'SystemConfigStorageTemplateDto' }); - -const SystemConfigTemplateStorageOptionSchema = z - .object({ - yearOptions: z.array(z.string()).describe('Available year format options for storage template'), - monthOptions: z.array(z.string()).describe('Available month format options for storage template'), - weekOptions: z.array(z.string()).describe('Available week format options for storage template'), - dayOptions: z.array(z.string()).describe('Available day format options for storage template'), - hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), - minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), - secondOptions: z.array(z.string()).describe('Available second format options for storage template'), - presetOptions: z.array(z.string()).describe('Available preset template options'), - }) - .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); - -const SystemConfigThemeSchema = z - .object({ customCss: z.string().describe('Custom CSS for theming') }) - .meta({ id: 'SystemConfigThemeDto' }); - -const SystemConfigGeneratedImageSchema = z - .object({ - format: ImageFormatSchema, - quality: z.int().min(1).max(100).describe('Quality'), - size: z.int().min(1).describe('Size'), - progressive: configBool.default(false).optional().describe('Progressive'), - }) - .meta({ id: 'SystemConfigGeneratedImageDto' }); - -const SystemConfigGeneratedFullsizeImageSchema = z - .object({ - enabled: configBool.describe('Enabled'), - format: ImageFormatSchema, - quality: z.int().min(1).max(100).describe('Quality'), - progressive: configBool.default(false).optional().describe('Progressive'), - }) - .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' }); - -const SystemConfigImageSchema = z - .object({ - thumbnail: SystemConfigGeneratedImageSchema, - preview: SystemConfigGeneratedImageSchema, - fullsize: SystemConfigGeneratedFullsizeImageSchema, - colorspace: ColorspaceSchema, - extractEmbedded: configBool.describe('Extract embedded'), - }) - .meta({ id: 'SystemConfigImageDto' }); - -const SystemConfigTrashSchema = z - .object({ - enabled: configBool.describe('Enabled'), - days: z.int().min(0).describe('Days'), - }) - .meta({ id: 'SystemConfigTrashDto' }); - -const SystemConfigUserSchema = z - .object({ - deleteDelay: z.int().min(1).describe('Delete delay'), - }) - .meta({ id: 'SystemConfigUserDto' }); - -export const SystemConfigSchema = z - .object({ - backup: SystemConfigBackupsSchema, - ffmpeg: SystemConfigFFmpegSchema, - logging: SystemConfigLoggingSchema, - machineLearning: SystemConfigMachineLearningSchema, - map: SystemConfigMapSchema, - newVersionCheck: SystemConfigNewVersionCheckSchema, - nightlyTasks: SystemConfigNightlyTasksSchema, - oauth: SystemConfigOAuthSchema, - passwordLogin: SystemConfigPasswordLoginSchema, - reverseGeocoding: SystemConfigReverseGeocodingSchema, - metadata: SystemConfigMetadataSchema, - storageTemplate: SystemConfigStorageTemplateSchema, - job: SystemConfigJobSchema, - image: SystemConfigImageSchema, - trash: SystemConfigTrashSchema, - theme: SystemConfigThemeSchema, - library: SystemConfigLibrarySchema, - notifications: SystemConfigNotificationsSchema, - templates: SystemConfigTemplatesSchema, - server: SystemConfigServerSchema, - user: SystemConfigUserSchema, - integrityChecks: SystemConfigIntegrityChecksSchema, - }) - .describe('System configuration') - .meta({ id: 'SystemConfigDto' }); - -export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {} -export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {} -export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {} -export class SystemConfigDto extends createZodDto(SystemConfigSchema) {} - -export function mapConfig(config: SystemConfig): SystemConfigDto { - return config; -} diff --git a/server/src/enum.ts b/server/src/enum.ts index b6e2f6e46..3402afbc0 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -161,6 +161,11 @@ export enum Permission { BackupUpload = 'backup.upload', BackupDelete = 'backup.delete', + AdminConfigRead = 'adminConfig.read', + AdminConfigUpdate = 'adminConfig.update', + + UserConfigRead = 'userConfig.read', + DuplicateRead = 'duplicate.read', DuplicateDelete = 'duplicate.delete', @@ -1165,12 +1170,25 @@ export const AssetVisibilitySchema = z .describe('Asset visibility') .meta({ id: 'AssetVisibility' }); +export enum ReleaseChannel { + Stable = 'stable', + ReleaseCandidate = 'releaseCandidate', +} + +export const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); + export enum CronJob { LibraryScan = 'LibraryScan', NightlyJobs = 'NightlyJobs', VersionCheck = 'VersionCheck', } +export enum ConfigVisibility { + Public = 'Public', + User = 'User', + Admin = 'Admin', +} + export enum ApiTag { Activities = 'Activities', Albums = 'Albums', @@ -1178,6 +1196,9 @@ export enum ApiTag { Authentication = 'Authentication', AuthenticationAdmin = 'Authentication (admin)', Assets = 'Assets', + ConfigUser = 'Config (user)', + ConfigAdmin = 'Config (admin)', + ConfigPublic = 'Config (public)', DatabaseBackups = 'Database Backups (admin)', Deprecated = 'Deprecated', Download = 'Download', diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 7fedc4eb3..d377215e5 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import _ from 'lodash'; import { Socket } from 'socket.io'; -import { SystemConfig } from 'src/config'; import { Asset } from 'src/database'; import { EventConfig } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index a05ddbc86..c1b14fae0 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Duration } from 'luxon'; import { readFile } from 'node:fs/promises'; -import { MachineLearningConfig } from 'src/config'; -import { CLIPConfig } from 'src/dtos/model-config.dto'; +import { MachineLearningConfig } from 'src/dtos/config.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; export interface BoundingBox { @@ -205,7 +204,7 @@ export class MachineLearningRepository { }; } - async encodeImage(imagePath: string, { modelName }: CLIPConfig) { + async encodeImage(imagePath: string, { modelName }: MachineLearningConfig['clip']) { const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } }; const response = await this.predict({ imagePath }, request); return response[ModelTask.SEARCH]; diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 5cfded148..4b0e13356 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,7 +4,7 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; +import { ReleaseChannel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 6ed8ce925..9204ccea4 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -1,10 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; import sanitize from 'sanitize-filename'; -import { SystemConfig } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { UserAdmin } from 'src/database'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; diff --git a/server/src/services/database-backup.service.spec.ts b/server/src/services/database-backup.service.spec.ts index 502638625..ac808af8a 100644 --- a/server/src/services/database-backup.service.spec.ts +++ b/server/src/services/database-backup.service.spec.ts @@ -1,8 +1,8 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; import { PassThrough, Readable } from 'node:stream'; -import { defaults, SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { DatabaseBackupService } from 'src/services/database-backup.service'; diff --git a/server/src/services/hls.service.ts b/server/src/services/hls.service.ts index 94a8a46ac..d2f7133c0 100644 --- a/server/src/services/hls.service.ts +++ b/server/src/services/hls.service.ts @@ -5,7 +5,7 @@ import { HLS_SEGMENT_DURATION, HLS_SEGMENT_FILENAME_REGEX, HLS_VARIANTS, HLS_VER import { StorageCore } from 'src/cores/storage.core'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { ConfigFFmpegDto } from 'src/dtos/config.dto'; import { CacheControl, ImmichWorker, Permission } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; @@ -124,7 +124,7 @@ export class HlsService extends BaseService { this.websocketRepository.serverSend('HlsSessionEnd', { sessionId }); } - private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) { + private generateMainPlaylist(sessionId: string, ffmpeg: ConfigFFmpegDto, asset: AssetWithStreamInfo) { const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration; const roundedFps = fps.toFixed(3); const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index ae0623260..575de8759 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { Stats } from 'node:fs'; -import { defaults, SystemConfig } from 'src/config'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index d64e0088e..c9cd395cc 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,7 +1,7 @@ import { ShallowDehydrateObject } from 'kysely'; import { OutputInfo } from 'sharp'; -import { SystemConfig } from 'src/config'; import { Exif } from 'src/database'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 19b1688e8..0839eba6a 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; import { AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; +import { ConfigFFmpegDto, SystemConfig } from 'src/dtos/config.dto'; import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetFileType, AssetType, @@ -626,7 +625,7 @@ export class MediaService extends BaseService { } private getTranscodeTarget( - config: SystemConfigFFmpegDto, + config: ConfigFFmpegDto, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo, ): TranscodeTarget { @@ -648,7 +647,7 @@ export class MediaService extends BaseService { return TranscodeTarget.None; } - private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: AudioStreamInfo): boolean { + private isAudioTranscodeRequired(ffmpegConfig: ConfigFFmpegDto, stream?: AudioStreamInfo): boolean { if (!stream) { return false; } @@ -671,7 +670,7 @@ export class MediaService extends BaseService { } } - private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean { + private isVideoTranscodeRequired(ffmpegConfig: ConfigFFmpegDto, stream: VideoStreamInfo): boolean { const isScalingEnabled = ffmpegConfig.targetResolution !== 'original'; const targetRes = Number.parseInt(ffmpegConfig.targetResolution); const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes; @@ -703,7 +702,7 @@ export class MediaService extends BaseService { } } - private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { + private isRemuxRequired(ffmpegConfig: ConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { if (ffmpegConfig.transcode === TranscodePolicy.Disabled) { return false; } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 57c029961..bcd347c56 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -2,7 +2,7 @@ import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; -import { defaults } from 'src/config'; +import { defaults } from 'src/dtos/config.dto'; import { AssetFileType, AssetType, diff --git a/server/src/services/notification-admin.service.spec.ts b/server/src/services/notification-admin.service.spec.ts index c20089771..12fb3e71f 100644 --- a/server/src/services/notification-admin.service.spec.ts +++ b/server/src/services/notification-admin.service.spec.ts @@ -1,4 +1,4 @@ -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/notification-admin.service.ts b/server/src/services/notification-admin.service.ts index 2fc4584dc..63c1da0d2 100644 --- a/server/src/services/notification-admin.service.ts +++ b/server/src/services/notification-admin.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { mapNotification, NotificationCreateDto } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { NotificationLevel, NotificationType } from 'src/enum'; import { EmailTemplate } from 'src/repositories/email.repository'; import { BaseService } from 'src/services/base.service'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index d6fcefbe6..47b9b7257 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,5 +1,4 @@ -import { defaults, SystemConfig } from 'src/config'; -import { SystemConfigDto } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, defaults, SystemConfig } from 'src/dtos/config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; import { NotificationService } from 'src/services/notification.service'; import { AlbumFactory } from 'test/factories/album.factory'; @@ -100,7 +99,7 @@ describe(NotificationService.name, () => { it('skips smtp validation with DTO when there are no changes', async () => { const oldConfig = { ...configs.smtpEnabled }; - const newConfig = configs.smtpEnabled as SystemConfigDto; + const newConfig = configs.smtpEnabled as AdminConfigDto; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index a650b466b..a341468bb 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -3,6 +3,7 @@ import { OnEvent, OnJob } from 'src/decorators'; import { MapAlbumDto } from 'src/dtos/album.dto'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { mapNotification, NotificationDeleteAllDto, @@ -11,7 +12,6 @@ import { NotificationUpdateAllDto, NotificationUpdateDto, } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 5643c5ece..3490c634e 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum'; import { QueueService } from 'src/services/queue.service'; import { factory } from 'test/small.factory'; diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index 1f0bf6c76..7130146c1 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { mapQueueLegacy, mapQueuesLegacy, diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 6bd0a3c9b..354672a6e 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { SmartInfoService } from 'src/services/smart-info.service'; import { getCLIPModelInfo } from 'src/utils/misc'; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 19a177449..e147d3da7 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { OnEvent, OnJob } from 'src/decorators'; import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 8f11a1dfa..72d8802e1 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,5 +1,5 @@ import { Stats } from 'node:fs'; -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { AssetPathType, AssetType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { AlbumFactory } from 'test/factories/album.factory'; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index de731f46c..c2aaf986e 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { ConfigTemplateStorageOptionDto } from 'src/dtos/config.dto'; import { AssetFileType, AssetPathType, @@ -129,7 +129,7 @@ export class StorageTemplateService extends BaseService { } } - getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { + getStorageTemplateOptions(): ConfigTemplateStorageOptionDto { return { ...storageTokens, presetOptions: storagePresets }; } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 08851da96..90d91ccf4 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,6 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { defaults, SystemConfig } from 'src/config'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { AudioCodec, Colorspace, @@ -10,6 +9,7 @@ import { LogLevel, OAuthTokenEndpointAuthMethod, QueueName, + ReleaseChannel, ToneMapping, TranscodeHardwareAcceleration, TranscodePolicy, @@ -269,7 +269,7 @@ describe(SystemConfigService.name, () => { it('should return the default config', () => { mocks.systemMetadata.get.mockResolvedValue(partialConfig); - expect(sut.getDefaults()).toEqual(defaults); + expect(sut.getAdminConfigDefaults()).toEqual(defaults); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); }); @@ -278,7 +278,7 @@ describe(SystemConfigService.name, () => { it('should return the default config', async () => { mocks.systemMetadata.get.mockResolvedValue({}); - await expect(sut.getSystemConfig()).resolves.toEqual(defaults); + await expect(sut.getAdminConfig()).resolves.toEqual(defaults); }); it('should merge the overrides', async () => { @@ -289,14 +289,14 @@ describe(SystemConfigService.name, () => { user: { deleteDelay: 15 }, }); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); }); it('should load the config from a json file', async () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); }); @@ -305,7 +305,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } })); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ ffmpeg: expect.objectContaining({ twoPass: false }), }); }); @@ -314,7 +314,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } })); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ ffmpeg: expect.objectContaining({ threads: 42 }), }); }); @@ -325,7 +325,7 @@ describe(SystemConfigService.name, () => { JSON.stringify({ library: { scan: { cronExpression: '0 0 */3 * *' } } }), ); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ library: { scan: { enabled: true, @@ -339,7 +339,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ oauth: { issuerUrl: 'accounts.google.com' } })); - await expect(sut.getSystemConfig()).rejects.toThrow( + await expect(sut.getAdminConfig()).rejects.toThrow( '[oauth.issuerUrl] Issuer URL must be an empty string or a valid URL', ); }); @@ -348,7 +348,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } })); - await expect(sut.getSystemConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); + await expect(sut.getAdminConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); }); it('should log errors with the config file', async () => { @@ -356,7 +356,7 @@ describe(SystemConfigService.name, () => { mocks.systemMetadata.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`); - await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error); + await expect(sut.getAdminConfig()).rejects.toBeInstanceOf(Error); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); expect(mocks.logger.error).toHaveBeenCalledTimes(2); @@ -380,7 +380,7 @@ describe(SystemConfigService.name, () => { `; mocks.systemMetadata.readFile.mockResolvedValue(partialConfig); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.yaml'); }); @@ -389,7 +389,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({})); - await expect(sut.getSystemConfig()).resolves.toEqual(defaults); + await expect(sut.getAdminConfig()).resolves.toEqual(defaults); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); }); @@ -399,7 +399,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } }; mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); expect(config.machineLearning.urls).toEqual(['immich_machine_learning']); }); @@ -420,7 +420,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { server: { externalDomain } }; mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app'); }); } @@ -432,7 +432,7 @@ describe(SystemConfigService.name, () => { `; mocks.systemMetadata.readFile.mockResolvedValue(partialConfig); - await sut.getSystemConfig(); + await sut.getAdminConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); }); @@ -467,12 +467,12 @@ describe(SystemConfigService.name, () => { mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config)); if (test.throws) { - await expect(sut.getSystemConfig()).rejects.toThrow(test.throws); + await expect(sut.getAdminConfig()).rejects.toThrow(test.throws); } else if (test.warn) { - await sut.getSystemConfig(); + await sut.getAdminConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); } else { - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); test.check!(config); } }); @@ -482,7 +482,7 @@ describe(SystemConfigService.name, () => { describe('updateConfig', () => { it('should update the config and emit an event', async () => { mocks.systemMetadata.get.mockResolvedValue(partialConfig); - await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig); + await expect(sut.updateAdminConfig(updatedConfig)).resolves.toEqual(updatedConfig); expect(mocks.event.emit).toHaveBeenCalledWith( 'ConfigUpdate', expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }), @@ -492,7 +492,7 @@ describe(SystemConfigService.name, () => { it('should throw an error if a config file is in use', async () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({})); - await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.updateAdminConfig(defaults)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index faa4f8d42..055a21f1c 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,8 +1,15 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import _ from 'lodash'; -import { defaults } from 'src/config'; import { OnEvent } from 'src/decorators'; -import { mapConfig, SystemConfigDto } from 'src/dtos/system-config.dto'; +import { + AdminConfigDto, + defaults, + mapAdminConfig, + mapPublicConfig, + mapUserConfig, + PublicConfigDto, + UserConfigDto, +} from 'src/dtos/config.dto'; import { BootstrapEventPriority } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; @@ -22,13 +29,31 @@ export class SystemConfigService extends BaseService { this.machineLearningRepository.teardown(); } - async getSystemConfig(): Promise { + async getAdminConfig(): Promise { const config = await this.getConfig({ withCache: false }); - return mapConfig(config); + return mapAdminConfig(config); } - getDefaults(): SystemConfigDto { - return mapConfig(defaults); + getAdminConfigDefaults(): AdminConfigDto { + return mapAdminConfig(defaults); + } + + async getUserConfig(): Promise { + const config = await this.getConfig({ withCache: false }); + return mapUserConfig(config); + } + + getUserConfigDefaults(): UserConfigDto { + return mapUserConfig(defaults); + } + + async getPublicConfig(): Promise { + const config = await this.getConfig({ withCache: false }); + return mapPublicConfig(config); + } + + getPublicConfigDefaults(): PublicConfigDto { + return mapPublicConfig(defaults); } @OnEvent({ name: 'ConfigInit', priority: -100 }) @@ -56,7 +81,7 @@ export class SystemConfigService extends BaseService { } } - async updateSystemConfig(dto: SystemConfigDto): Promise { + async updateAdminConfig(dto: AdminConfigDto): Promise { const { configFile } = this.configRepository.getEnv(); if (configFile) { throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use'); @@ -75,7 +100,7 @@ export class SystemConfigService extends BaseService { await this.eventRepository.emit('ConfigUpdate', { newConfig, oldConfig }); - return mapConfig(newConfig); + return mapAdminConfig(newConfig); } async getCustomCss(): Promise { diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 0044730ce..b5fc6764f 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -1,8 +1,7 @@ import { DateTime } from 'luxon'; import { SemVer } from 'semver'; -import { defaults } from 'src/config'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; +import { defaults } from 'src/dtos/config.dto'; +import { CronJob, JobName, JobStatus, ReleaseChannel, SystemMetadataKey } from 'src/enum'; import { VersionService } from 'src/services/version.service'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index f1abeedb3..85bf579fb 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -4,8 +4,16 @@ import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum'; +import { + CronJob, + DatabaseLock, + ImmichWorker, + JobName, + JobStatus, + QueueName, + ReleaseChannel, + SystemMetadataKey, +} from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { VersionCheckMetadata } from 'src/types'; diff --git a/server/src/types.ts b/server/src/types.ts index d31841ffd..3b5a93724 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,9 +1,9 @@ import { ShallowDehydrateObject } from 'kysely'; -import { SystemConfig } from 'src/config'; import { VECTOR_EXTENSIONS } from 'src/constants'; import { AssetFile } from 'src/database'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { @@ -34,10 +34,10 @@ import { Mocked } from 'vitest'; export type DeepPartial = T extends Date ? T - : T extends Record - ? { [K in keyof T]?: DeepPartial } - : T extends Array - ? DeepPartial[] + : T extends Array + ? DeepPartial[] + : T extends object + ? { [K in keyof T]?: DeepPartial } : T; export type RepositoryInterface = Pick; diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index a6073471d..38e5facb0 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -1,8 +1,7 @@ import AsyncLock from 'async-lock'; import { load as loadYaml } from 'js-yaml'; import * as _ from 'lodash'; -import { SystemConfig, defaults } from 'src/config'; -import { SystemConfigSchema } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, SystemConfig, defaults } from 'src/dtos/config.dto'; import { DatabaseLock, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -100,7 +99,7 @@ const buildConfig = async (repos: RepoDeps) => { } // validate with Zod schema - const result = SystemConfigSchema.safeParse(rawConfig); + const result = AdminConfigDto.schema.safeParse(rawConfig); if (!result.success) { const messages = ['Invalid system config: ']; for (const issue of result.error.issues) { diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 656e5bf44..49877904e 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,5 +1,5 @@ import { AUDIO_ENCODER, AV1_LEVELS, CodecLevel, H264_LEVELS, HEVC_LEVELS, SUPPORTED_HWA_CODECS } from 'src/constants'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { ConfigFFmpegDto } from 'src/dtos/config.dto'; import { ColorMatrix, ColorPrimaries, @@ -62,18 +62,18 @@ export const getCodecString = (codec: VideoCodec, width: number, height: number, export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; protected constructor( - protected config: SystemConfigFFmpegDto, + protected config: ConfigFFmpegDto, protected tune: VideoTuning = { strictGop: false, lowLatency: false }, ) {} - static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + static create(config: ConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (config.accel === TranscodeHardwareAcceleration.Disabled) { return BaseConfig.getSWCodecConfig(config, tune); } return BaseConfig.getHWCodecConfig(config, interfaces, tune); } - private static getSWCodecConfig(config: SystemConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { + private static getSWCodecConfig(config: ConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { switch (config.targetVideoCodec) { case VideoCodec.H264: { return new H264Config(config, tune); @@ -93,7 +93,7 @@ export class BaseConfig implements VideoCodecSWConfig { } } - private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + private static getHWCodecConfig(config: ConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (!SUPPORTED_HWA_CODECS[config.accel].includes(config.targetVideoCodec)) { throw new Error( `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${SUPPORTED_HWA_CODECS[config.accel]}`, @@ -424,7 +424,7 @@ export class BaseHWConfig extends BaseConfig { protected device: string; constructor( - protected config: SystemConfigFFmpegDto, + protected config: ConfigFFmpegDto, protected interfaces: VideoInterfaces, tune?: VideoTuning, ) { @@ -471,7 +471,7 @@ export class BaseHWConfig extends BaseConfig { } export class ThumbnailConfig extends BaseConfig { - static create(config: SystemConfigFFmpegDto): VideoCodecSWConfig { + static create(config: ConfigFFmpegDto): VideoCodecSWConfig { return new ThumbnailConfig(config); } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index a51400798..84ebc7e98 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -13,9 +13,9 @@ import { writeFileSync } from 'node:fs'; import path from 'node:path'; import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; -import { SystemConfig } from 'src/config'; import { CLIP_MODEL_INFO, JOBS_ASSET_PAGINATION_SIZE, endpointTags, serverVersion } from 'src/constants'; import { extraModels } from 'src/decorators'; +import { SystemConfig } from 'src/dtos/config.dto'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; diff --git a/server/src/utils/profile-image.ts b/server/src/utils/profile-image.ts index ee94dd898..ea2c0d3b6 100644 --- a/server/src/utils/profile-image.ts +++ b/server/src/utils/profile-image.ts @@ -1,6 +1,6 @@ import { join } from 'node:path'; -import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; +import { SystemConfig } from 'src/dtos/config.dto'; import { StorageFolder } from 'src/enum'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { MediaRepository } from 'src/repositories/media.repository'; diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts index 355f2cc1a..6ba655692 100644 --- a/server/test/fixtures/system-config.stub.ts +++ b/server/test/fixtures/system-config.stub.ts @@ -1,4 +1,4 @@ -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { DeepPartial } from 'src/types'; export const systemConfigStub = { diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 1fbe41d96..09cef8947 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -4,9 +4,9 @@ import { createHash, randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { resolve } from 'node:path'; import { Writable } from 'node:stream'; -import { SystemConfig } from 'src/config'; import { AssetFace } from 'src/database'; import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditActionItem, AssetEditsCreateDto } from 'src/dtos/editing.dto'; import { AlbumUserRole, diff --git a/server/test/utils.ts b/server/test/utils.ts index 3bdbe1f5d..f8d3c15e2 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -95,7 +95,9 @@ export type ControllerContext = { close: () => Promise; }; -export const controllerSetup = async (controller: new (...args: any[]) => unknown, providers: Provider[]) => { +type ControllerClass = new (...args: any[]) => unknown; + +export const controllerSetup = async (controller: ControllerClass | ControllerClass[], providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; const upload = multer({ storage: multer.memoryStorage() }); const memoryFileInterceptor = { @@ -117,7 +119,7 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow }, }; const moduleRef = await Test.createTestingModule({ - controllers: [controller], + controllers: Array.isArray(controller) ? controller : [controller], providers: [ { provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_PIPE, useClass: ZodValidationPipe }, diff --git a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte index da58d779e..3948b6bbc 100644 --- a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte +++ b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte @@ -1,15 +1,15 @@