mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
refactor(server): env validation (#13817)
This commit is contained in:
parent
19eb3ed8b9
commit
0f668fd5c6
8 changed files with 305 additions and 149 deletions
54
server/package-lock.json
generated
54
server/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
190
server/src/dtos/env.dto.ts
Normal 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;
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue