diff --git a/server/package-lock.json b/server/package-lock.json index 54862802d1..3752dd95b5 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", - "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.2.2", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.2.2", @@ -63,7 +62,8 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35" + "ua-parser-js": "^1.0.35", + "validator": "^13.12.0" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -2108,20 +2108,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, - "node_modules/@nestjs/config": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "dependencies": { - "dotenv": "16.4.5", - "dotenv-expand": "10.0.0", - "lodash": "4.17.21" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "rxjs": "^7.1.0" - } - }, "node_modules/@nestjs/core": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.5.tgz", @@ -8050,14 +8036,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "engines": { - "node": ">=12" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -14901,9 +14879,10 @@ } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -16665,16 +16644,6 @@ } } }, - "@nestjs/config": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "requires": { - "dotenv": "16.4.5", - "dotenv-expand": "10.0.0", - "lodash": "4.17.21" - } - }, "@nestjs/core": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.5.tgz", @@ -20806,11 +20775,6 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, - "dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" - }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -25625,9 +25589,9 @@ } }, "validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" }, "vary": { "version": "1.1.2", diff --git a/server/package.json b/server/package.json index 4716783371..1d53dffcc6 100644 --- a/server/package.json +++ b/server/package.json @@ -36,7 +36,6 @@ "dependencies": { "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", - "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.2.2", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.2.2", @@ -88,7 +87,8 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35" + "ua-parser-js": "^1.0.35", + "validator": "^13.12.0" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index ef02416e58..8ed9d5f6ed 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,13 +1,11 @@ import { BullModule } from '@nestjs/bullmq'; import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ClsModule } from 'nestjs-cls'; import { OpenTelemetryModule } from 'nestjs-otel'; import { commands } from 'src/commands'; -import { immichAppConfig } from 'src/config'; import { controllers } from 'src/controllers'; import { entities } from 'src/entities'; import { ImmichWorker } from 'src/enum'; @@ -43,7 +41,6 @@ const imports = [ BullModule.forRoot(bull.config), BullModule.registerQueue(...bull.queues), ClsModule.forRoot(cls.config), - ConfigModule.forRoot(immichAppConfig), OpenTelemetryModule.forRoot(otel), TypeOrmModule.forRootAsync({ inject: [ModuleRef], diff --git a/server/src/config.ts b/server/src/config.ts index 7bc93d7608..12e6e6576b 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,12 +1,9 @@ -import { ConfigModuleOptions } from '@nestjs/config'; import { CronExpression } from '@nestjs/schedule'; -import Joi, { Root } from 'joi'; import { AudioCodec, Colorspace, CQMode, ImageFormat, - ImmichEnvironment, LogLevel, ToneMapping, TranscodeHWAccel, @@ -306,48 +303,3 @@ export const defaults = Object.freeze({ deleteDelay: 7, }, }); - -const WHEN_DB_URL_SET = Joi.when('DB_URL', { - is: Joi.exist(), - then: Joi.string().optional(), - otherwise: Joi.string().required(), -}); - -export const immichAppConfig: ConfigModuleOptions = { - envFilePath: '.env', - isGlobal: true, - validationSchema: Joi.object({ - IMMICH_ENV: Joi.string() - .optional() - .valid(...Object.values(ImmichEnvironment)) - .default(ImmichEnvironment.PRODUCTION), - IMMICH_LOG_LEVEL: Joi.string() - .optional() - .valid(...Object.values(LogLevel)), - - DB_USERNAME: WHEN_DB_URL_SET, - DB_PASSWORD: WHEN_DB_URL_SET, - DB_DATABASE_NAME: WHEN_DB_URL_SET, - DB_URL: Joi.string().optional(), - DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'), - DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false), - - IMMICH_PORT: Joi.number().optional(), - IMMICH_API_METRICS_PORT: Joi.number().optional(), - IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(), - - IMMICH_TRUSTED_PROXIES: Joi.extend((joi: Root) => ({ - type: 'stringArray', - base: joi.array(), - coerce: (value) => (value.split ? value.split(',') : value), - })) - .stringArray() - .single() - .items( - Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'optional', - }), - ), - }), -}; diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts new file mode 100644 index 0000000000..6c238252a6 --- /dev/null +++ b/server/src/dtos/env.dto.ts @@ -0,0 +1,190 @@ +import { Transform, Type } from 'class-transformer'; +import { IsEnum, IsInt, IsString } from 'class-validator'; +import { ImmichEnvironment, LogLevel } from 'src/enum'; +import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; + +export class EnvDto { + @IsInt() + @Optional() + @Type(() => Number) + IMMICH_API_METRICS_PORT?: number; + + @IsString() + @Optional() + IMMICH_BUILD_DATA?: string; + + @IsString() + @Optional() + IMMICH_BUILD?: string; + + @IsString() + @Optional() + IMMICH_BUILD_URL?: string; + + @IsString() + @Optional() + IMMICH_BUILD_IMAGE?: string; + + @IsString() + @Optional() + IMMICH_BUILD_IMAGE_URL?: string; + + @IsString() + @Optional() + IMMICH_CONFIG_FILE?: string; + + @IsEnum(ImmichEnvironment) + @Optional() + IMMICH_ENV?: ImmichEnvironment; + + @IsString() + @Optional() + IMMICH_HOST?: string; + + @ValidateBoolean({ optional: true }) + IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean; + + @IsEnum(LogLevel) + @Optional() + IMMICH_LOG_LEVEL?: LogLevel; + + @IsInt() + @Optional() + @Type(() => Number) + IMMICH_MICROSERVICES_METRICS_PORT?: number; + + @IsInt() + @Optional() + @Type(() => Number) + IMMICH_PORT?: number; + + @IsString() + @Optional() + IMMICH_REPOSITORY?: string; + + @IsString() + @Optional() + IMMICH_REPOSITORY_URL?: string; + + @IsString() + @Optional() + IMMICH_SOURCE_REF?: string; + + @IsString() + @Optional() + IMMICH_SOURCE_COMMIT?: string; + + @IsString() + @Optional() + IMMICH_SOURCE_URL?: string; + + @IsString() + @Optional() + IMMICH_TELEMETRY_INCLUDE?: string; + + @IsString() + @Optional() + IMMICH_TELEMETRY_EXCLUDE?: string; + + @IsString() + @Optional() + IMMICH_THIRD_PARTY_SOURCE_URL?: string; + + @IsString() + @Optional() + IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string; + + @IsString() + @Optional() + IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string; + + @IsString() + @Optional() + IMMICH_THIRD_PARTY_SUPPORT_URL?: string; + + @IsIPRange({ requireCIDR: false }, { each: true }) + @Transform(({ value }) => + value && typeof value === 'string' + ? value + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + : value, + ) + @Optional() + IMMICH_TRUSTED_PROXIES?: string[]; + + @IsString() + @Optional() + IMMICH_WORKERS_INCLUDE?: string; + + @IsString() + @Optional() + IMMICH_WORKERS_EXCLUDE?: string; + + @IsString() + @Optional() + DB_DATABASE_NAME?: string; + + @IsString() + @Optional() + DB_HOSTNAME?: string; + + @IsString() + @Optional() + DB_PASSWORD?: string; + + @IsInt() + @Optional() + @Type(() => Number) + DB_PORT?: number; + + @ValidateBoolean({ optional: true }) + DB_SKIP_MIGRATIONS?: boolean; + + @IsString() + @Optional() + DB_URL?: string; + + @IsString() + @Optional() + DB_USERNAME?: string; + + @IsEnum(['pgvector', 'pgvecto.rs']) + @Optional() + DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs'; + + @IsString() + @Optional() + NO_COLOR?: string; + + @IsString() + @Optional() + REDIS_HOSTNAME?: string; + + @IsInt() + @Optional() + @Type(() => Number) + REDIS_PORT?: number; + + @IsInt() + @Optional() + @Type(() => Number) + REDIS_DBINDEX?: number; + + @IsString() + @Optional() + REDIS_USERNAME?: string; + + @IsString() + @Optional() + REDIS_PASSWORD?: string; + + @IsString() + @Optional() + REDIS_SOCKET?: string; + + @IsString() + @Optional() + REDIS_URL?: string; +} diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index d2777a2e46..2ff5f53073 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -8,6 +8,7 @@ const getEnv = () => { const resetEnv = () => { for (const env of [ + 'IMMICH_ENV', 'IMMICH_WORKERS_INCLUDE', 'IMMICH_WORKERS_EXCLUDE', 'IMMICH_TRUSTED_PROXIES', @@ -62,6 +63,18 @@ describe('getEnv', () => { resetEnv(); }); + it('should use defaults', () => { + const config = getEnv(); + + expect(config).toMatchObject({ + host: undefined, + port: 2283, + environment: 'production', + configFile: undefined, + logLevel: undefined, + }); + }); + describe('database', () => { it('should use defaults', () => { const { database } = getEnv(); @@ -202,6 +215,11 @@ describe('getEnv', () => { trustedProxies: ['10.1.0.0', '10.2.0.0', '169.254.0.0/16'], }); }); + + it('should reject invalid trusted proxies', () => { + process.env.IMMICH_TRUSTED_PROXIES = '10.1'; + expect(() => getEnv()).toThrowError('Invalid environment variables: IMMICH_TRUSTED_PROXIES'); + }); }); describe('telemetry', () => { diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 4fdda028e3..76b0bb0c83 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -1,17 +1,18 @@ import { Injectable } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { Request, Response } from 'express'; import { CLS_ID } from 'nestjs-cls'; import { join, resolve } from 'node:path'; import { citiesFile, excludePaths } from 'src/constants'; import { Telemetry } from 'src/decorators'; -import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum'; +import { EnvDto } from 'src/dtos/env.dto'; +import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum'; import { EnvData, IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { QueueName } from 'src/interfaces/job.interface'; import { setDifference } from 'src/utils/set'; -// TODO replace src/config validation with class-validator, here - const productionKeys = { client: 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=', @@ -35,8 +36,16 @@ const asSet = (value: string | undefined, defaults: T[]) => { }; const getEnv = (): EnvData => { - const includedWorkers = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]); - const excludedWorkers = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []); + const dto = plainToInstance(EnvDto, process.env); + const errors = validateSync(dto); + if (errors.length > 0) { + throw new Error( + `Invalid environment variables: ${errors.map((error) => `${error.property}=${error.value}`).join(', ')}`, + ); + } + + const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]); + const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []); const workers = [...setDifference(includedWorkers, excludedWorkers)]; for (const worker of workers) { if (!WORKER_TYPES.has(worker)) { @@ -44,9 +53,9 @@ const getEnv = (): EnvData => { } } - const environment = process.env.IMMICH_ENV as ImmichEnvironment; + const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION; const isProd = environment === ImmichEnvironment.PRODUCTION; - const buildFolder = process.env.IMMICH_BUILD_DATA || '/build'; + const buildFolder = dto.IMMICH_BUILD_DATA || '/build'; const folders = { // eslint-disable-next-line unicorn/prefer-module dist: resolve(`${__dirname}/..`), @@ -54,18 +63,18 @@ const getEnv = (): EnvData => { web: join(buildFolder, 'www'), }; - const databaseUrl = process.env.DB_URL; + const databaseUrl = dto.DB_URL; let redisConfig = { - host: process.env.REDIS_HOSTNAME || 'redis', - port: Number.parseInt(process.env.REDIS_PORT || '') || 6379, - db: Number.parseInt(process.env.REDIS_DBINDEX || '') || 0, - username: process.env.REDIS_USERNAME || undefined, - password: process.env.REDIS_PASSWORD || undefined, - path: process.env.REDIS_SOCKET || undefined, + host: dto.REDIS_HOSTNAME || 'redis', + port: dto.REDIS_PORT || 6379, + db: dto.REDIS_DBINDEX || 0, + username: dto.REDIS_USERNAME || undefined, + password: dto.REDIS_PASSWORD || undefined, + path: dto.REDIS_SOCKET || undefined, }; - const redisUrl = process.env.REDIS_URL; + const redisUrl = dto.REDIS_URL; if (redisUrl && redisUrl.startsWith('ioredis://')) { try { redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString()); @@ -75,11 +84,11 @@ const getEnv = (): EnvData => { } const includedTelemetries = - process.env.IMMICH_TELEMETRY_INCLUDE === 'all' + dto.IMMICH_TELEMETRY_INCLUDE === 'all' ? new Set(Object.values(ImmichTelemetry)) - : asSet(process.env.IMMICH_TELEMETRY_INCLUDE, []); + : asSet(dto.IMMICH_TELEMETRY_INCLUDE, []); - const excludedTelemetries = asSet(process.env.IMMICH_TELEMETRY_EXCLUDE, []); + const excludedTelemetries = asSet(dto.IMMICH_TELEMETRY_EXCLUDE, []); const telemetries = setDifference(includedTelemetries, excludedTelemetries); for (const telemetry of telemetries) { if (!TELEMETRY_TYPES.has(telemetry)) { @@ -88,26 +97,26 @@ const getEnv = (): EnvData => { } return { - host: process.env.IMMICH_HOST, - port: Number(process.env.IMMICH_PORT) || 2283, + host: dto.IMMICH_HOST, + port: dto.IMMICH_PORT || 2283, environment, - configFile: process.env.IMMICH_CONFIG_FILE, - logLevel: process.env.IMMICH_LOG_LEVEL as LogLevel, + configFile: dto.IMMICH_CONFIG_FILE, + logLevel: dto.IMMICH_LOG_LEVEL, buildMetadata: { - build: process.env.IMMICH_BUILD, - buildUrl: process.env.IMMICH_BUILD_URL, - buildImage: process.env.IMMICH_BUILD_IMAGE, - buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL, - repository: process.env.IMMICH_REPOSITORY, - repositoryUrl: process.env.IMMICH_REPOSITORY_URL, - sourceRef: process.env.IMMICH_SOURCE_REF, - sourceCommit: process.env.IMMICH_SOURCE_COMMIT, - sourceUrl: process.env.IMMICH_SOURCE_URL, - thirdPartySourceUrl: process.env.IMMICH_THIRD_PARTY_SOURCE_URL, - thirdPartyBugFeatureUrl: process.env.IMMICH_THIRD_PARTY_BUG_FEATURE_URL, - thirdPartyDocumentationUrl: process.env.IMMICH_THIRD_PARTY_DOCUMENTATION_URL, - thirdPartySupportUrl: process.env.IMMICH_THIRD_PARTY_SUPPORT_URL, + build: dto.IMMICH_BUILD, + buildUrl: dto.IMMICH_BUILD_URL, + buildImage: dto.IMMICH_BUILD_IMAGE, + buildImageUrl: dto.IMMICH_BUILD_IMAGE_URL, + repository: dto.IMMICH_REPOSITORY, + repositoryUrl: dto.IMMICH_REPOSITORY_URL, + sourceRef: dto.IMMICH_SOURCE_REF, + sourceCommit: dto.IMMICH_SOURCE_COMMIT, + sourceUrl: dto.IMMICH_SOURCE_URL, + thirdPartySourceUrl: dto.IMMICH_THIRD_PARTY_SOURCE_URL, + thirdPartyBugFeatureUrl: dto.IMMICH_THIRD_PARTY_BUG_FEATURE_URL, + thirdPartyDocumentationUrl: dto.IMMICH_THIRD_PARTY_DOCUMENTATION_URL, + thirdPartySupportUrl: dto.IMMICH_THIRD_PARTY_SUPPORT_URL, }, bull: { @@ -153,26 +162,22 @@ const getEnv = (): EnvData => { ? { connectionType: 'url', url: databaseUrl } : { connectionType: 'parts', - host: process.env.DB_HOSTNAME || 'database', - port: Number(process.env.DB_PORT) || 5432, - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_DATABASE_NAME || 'immich', + host: dto.DB_HOSTNAME || 'database', + port: dto.DB_PORT || 5432, + username: dto.DB_USERNAME || 'postgres', + password: dto.DB_PASSWORD || 'postgres', + database: dto.DB_DATABASE_NAME || 'immich', }), }, - skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true', - vectorExtension: - process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS, + skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false, + vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS, }, licensePublicKey: isProd ? productionKeys : stagingKeys, network: { - trustedProxies: (process.env.IMMICH_TRUSTED_PROXIES ?? '') - .split(',') - .map((value) => value.trim()) - .filter(Boolean), + trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [], }, otel: { @@ -203,18 +208,18 @@ const getEnv = (): EnvData => { }, storage: { - ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true', + ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS, }, telemetry: { - apiPort: Number(process.env.IMMICH_API_METRICS_PORT || '') || 8081, - microservicesPort: Number(process.env.IMMICH_MICROSERVICES_METRICS_PORT || '') || 8082, + apiPort: dto.IMMICH_API_METRICS_PORT || 8081, + microservicesPort: dto.IMMICH_MICROSERVICES_METRICS_PORT || 8082, metrics: telemetries, }, workers, - noColor: !!process.env.NO_COLOR, + noColor: !!dto.NO_COLOR, }; }; diff --git a/server/src/validation.ts b/server/src/validation.ts index 81b309d663..180d53ed56 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -25,6 +25,7 @@ import { import { CronJob } from 'cron'; import { DateTime } from 'luxon'; import sanitize from 'sanitize-filename'; +import { isIP, isIPRange } from 'validator'; @Injectable() export class ParseMeUUIDPipe extends ParseUUIDPipe { @@ -228,3 +229,32 @@ export function MaxDateString( validationOptions, ); } + +type IsIPRangeOptions = { requireCIDR?: boolean }; +export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator { + const { requireCIDR } = { requireCIDR: true, ...options }; + + return ValidateBy( + { + name: 'isIPRange', + validator: { + validate: (value): boolean => { + if (isIPRange(value)) { + return true; + } + + if (!requireCIDR && isIP(value)) { + return true; + } + + return false; + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range', + validationOptions, + ), + }, + }, + validationOptions, + ); +}