1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

refactor(server): env validation (#13817)

This commit is contained in:
Jason Rasmussen 2024-10-30 05:00:41 -04:00 committed by GitHub
parent 19eb3ed8b9
commit 0f668fd5c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 305 additions and 149 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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],

View file

@ -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<SystemConfig>({
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',
}),
),
}),
};

190
server/src/dtos/env.dto.ts Normal file
View file

@ -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;
}

View file

@ -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', () => {

View file

@ -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 = <T>(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<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_INCLUDE, []);
: asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_INCLUDE, []);
const excludedTelemetries = asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_EXCLUDE, []);
const excludedTelemetries = asSet<ImmichTelemetry>(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,
};
};

View file

@ -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,
);
}