39948-vm/backend/src/utils/env-validation.ts
2026-07-03 15:59:42 +02:00

293 lines
9.6 KiB
TypeScript

import Joi from 'joi';
import { logger } from './logger.ts';
import type { NodeEnvironment, ValidatedEnvironment } from '../types/index.ts';
type EnvBooleanString = 'true' | 'false';
type EnvLogLevel = ValidatedEnvironment['LOG_LEVEL'];
const envSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production', 'dev_stage')
.default('development'),
PORT: Joi.number().default(8080),
NEXT_PUBLIC_BACK_API: Joi.string().uri().allow('').default(''),
DB_HOST: Joi.string().default('localhost'),
DB_PORT: Joi.number().default(5432),
DB_NAME: Joi.string().default('db_tour_builder_platform'),
DB_USER: Joi.string().default('postgres'),
DB_PASS: Joi.string().allow('').default(''),
SECRET_KEY: Joi.string()
.min(16)
.default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'),
ADMIN_PASS: Joi.string().default('88dbeaf8'),
USER_PASS: Joi.string().default('c3baadeda5c6'),
ADMIN_EMAIL: Joi.string().email().default('admin@flatlogic.com'),
GOOGLE_CLIENT_ID: Joi.string().allow('').default(''),
GOOGLE_CLIENT_SECRET: Joi.string().allow('').default(''),
MS_CLIENT_ID: Joi.string().allow('').default(''),
MS_CLIENT_SECRET: Joi.string().allow('').default(''),
AWS_ACCESS_KEY_ID: Joi.string().allow('').default(''),
AWS_SECRET_ACCESS_KEY: Joi.string().allow('').default(''),
AWS_S3_BUCKET: Joi.string().allow('').default(''),
AWS_S3_REGION: Joi.string().default('us-east-1'),
AWS_S3_PREFIX: Joi.string().default('afeefb9d49f5b7977577876b99532ac7'),
AWS_S3_CONNECTION_TIMEOUT: Joi.number().integer().positive().default(5000),
AWS_S3_REQUEST_TIMEOUT: Joi.number().integer().positive().default(30000),
AWS_S3_MAX_ATTEMPTS: Joi.number().integer().positive().default(3),
AWS_S3_MAX_SOCKETS: Joi.number().integer().positive().default(50),
AWS_S3_KEEP_ALIVE: Joi.string().valid('true', 'false').default('true'),
AWS_S3_PRESIGN_EXPIRY: Joi.number().integer().positive().default(3600),
FILE_STORAGE_PROVIDER: Joi.string()
.trim()
.lowercase()
.valid('', 's3', 'gcloud', 'local')
.default(''),
GC_PROJECT_ID: Joi.string().allow('').default(''),
GC_CLIENT_EMAIL: Joi.string().allow('').default(''),
GC_PRIVATE_KEY: Joi.string().allow('').default(''),
S3_CACHE_DIR: Joi.string().allow('').default(''),
S3_CACHE_ENABLED: Joi.string().valid('true', 'false').default('true'),
S3_CACHE_MAX_AGE: Joi.number().integer().positive().default(86400),
FFMPEG_REVERSE_TIMEOUT_MS: Joi.number().integer().positive().default(600000),
FFPROBE_TIMEOUT_MS: Joi.number().integer().positive().default(30000),
FFMPEG_BREAKER_FAILURE_THRESHOLD: Joi.number()
.integer()
.positive()
.default(3),
FFMPEG_BREAKER_COOLDOWN_MS: Joi.number()
.integer()
.positive()
.default(120000),
FFMPEG_BREAKER_SUCCESS_THRESHOLD: Joi.number()
.integer()
.positive()
.default(1),
FILE_STORAGE_BREAKER_FAILURE_THRESHOLD: Joi.number()
.integer()
.positive()
.default(5),
FILE_STORAGE_BREAKER_COOLDOWN_MS: Joi.number()
.integer()
.positive()
.default(30000),
FILE_STORAGE_BREAKER_SUCCESS_THRESHOLD: Joi.number()
.integer()
.positive()
.default(2),
EMAIL_USER: Joi.string().allow('').default(''),
EMAIL_PASS: Joi.string().allow('').default(''),
EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string()
.valid('true', 'false')
.default('true'),
PEXELS_KEY: Joi.string().allow('').default(''),
LOG_LEVEL: Joi.string()
.valid('fatal', 'error', 'warn', 'info', 'debug', 'trace')
.default('info'),
LOG_PRETTY: Joi.string().valid('true', 'false').default('false'),
}).unknown(true);
function isNodeEnvironment(value: unknown): value is NodeEnvironment {
return (
value === 'development' ||
value === 'test' ||
value === 'production' ||
value === 'dev_stage'
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function isEnvBooleanString(value: unknown): value is EnvBooleanString {
return value === 'true' || value === 'false';
}
function isEnvLogLevel(value: unknown): value is EnvLogLevel {
return (
value === 'fatal' ||
value === 'error' ||
value === 'warn' ||
value === 'info' ||
value === 'debug' ||
value === 'trace'
);
}
function readString(
values: Record<string, unknown>,
key: keyof ValidatedEnvironment,
fallback: string,
): string {
const value = values[key];
return typeof value === 'string' ? value : fallback;
}
function readNumber(
values: Record<string, unknown>,
key: keyof ValidatedEnvironment,
fallback: number,
): number {
const value = values[key];
return typeof value === 'number' ? value : fallback;
}
function toValidatedEnvironment(
values: Record<string, unknown>,
): ValidatedEnvironment {
const nodeEnv = values.NODE_ENV;
const tlsRejectUnauthorized = values.EMAIL_TLS_REJECT_UNAUTHORIZED;
const logLevel = values.LOG_LEVEL;
const s3KeepAlive = values.AWS_S3_KEEP_ALIVE;
const fileStorageProvider = values.FILE_STORAGE_PROVIDER;
const s3CacheEnabled = values.S3_CACHE_ENABLED;
const logPretty = values.LOG_PRETTY;
return {
NODE_ENV: isNodeEnvironment(nodeEnv) ? nodeEnv : 'development',
PORT: readNumber(values, 'PORT', 8080),
NEXT_PUBLIC_BACK_API: readString(values, 'NEXT_PUBLIC_BACK_API', ''),
DB_HOST: readString(values, 'DB_HOST', 'localhost'),
DB_PORT: readNumber(values, 'DB_PORT', 5432),
DB_NAME: readString(values, 'DB_NAME', 'db_tour_builder_platform'),
DB_USER: readString(values, 'DB_USER', 'postgres'),
DB_PASS: readString(values, 'DB_PASS', ''),
SECRET_KEY: readString(
values,
'SECRET_KEY',
'88dbeaf8-e906-405e-9e41-c3baadeda5c6',
),
ADMIN_PASS: readString(values, 'ADMIN_PASS', '88dbeaf8'),
USER_PASS: readString(values, 'USER_PASS', 'c3baadeda5c6'),
ADMIN_EMAIL: readString(values, 'ADMIN_EMAIL', 'admin@flatlogic.com'),
GOOGLE_CLIENT_ID: readString(values, 'GOOGLE_CLIENT_ID', ''),
GOOGLE_CLIENT_SECRET: readString(values, 'GOOGLE_CLIENT_SECRET', ''),
MS_CLIENT_ID: readString(values, 'MS_CLIENT_ID', ''),
MS_CLIENT_SECRET: readString(values, 'MS_CLIENT_SECRET', ''),
AWS_ACCESS_KEY_ID: readString(values, 'AWS_ACCESS_KEY_ID', ''),
AWS_SECRET_ACCESS_KEY: readString(values, 'AWS_SECRET_ACCESS_KEY', ''),
AWS_S3_BUCKET: readString(values, 'AWS_S3_BUCKET', ''),
AWS_S3_REGION: readString(values, 'AWS_S3_REGION', 'us-east-1'),
AWS_S3_PREFIX: readString(
values,
'AWS_S3_PREFIX',
'afeefb9d49f5b7977577876b99532ac7',
),
AWS_S3_CONNECTION_TIMEOUT: readNumber(
values,
'AWS_S3_CONNECTION_TIMEOUT',
5000,
),
AWS_S3_REQUEST_TIMEOUT: readNumber(
values,
'AWS_S3_REQUEST_TIMEOUT',
30000,
),
AWS_S3_MAX_ATTEMPTS: readNumber(values, 'AWS_S3_MAX_ATTEMPTS', 3),
AWS_S3_MAX_SOCKETS: readNumber(values, 'AWS_S3_MAX_SOCKETS', 50),
AWS_S3_KEEP_ALIVE: isEnvBooleanString(s3KeepAlive) ? s3KeepAlive : 'true',
AWS_S3_PRESIGN_EXPIRY: readNumber(
values,
'AWS_S3_PRESIGN_EXPIRY',
3600,
),
FILE_STORAGE_PROVIDER:
fileStorageProvider === 's3' ||
fileStorageProvider === 'gcloud' ||
fileStorageProvider === 'local'
? fileStorageProvider
: '',
GC_PROJECT_ID: readString(values, 'GC_PROJECT_ID', ''),
GC_CLIENT_EMAIL: readString(values, 'GC_CLIENT_EMAIL', ''),
GC_PRIVATE_KEY: readString(values, 'GC_PRIVATE_KEY', ''),
S3_CACHE_DIR: readString(values, 'S3_CACHE_DIR', ''),
S3_CACHE_ENABLED: isEnvBooleanString(s3CacheEnabled)
? s3CacheEnabled
: 'true',
S3_CACHE_MAX_AGE: readNumber(values, 'S3_CACHE_MAX_AGE', 86400),
FFMPEG_REVERSE_TIMEOUT_MS: readNumber(
values,
'FFMPEG_REVERSE_TIMEOUT_MS',
600000,
),
FFPROBE_TIMEOUT_MS: readNumber(values, 'FFPROBE_TIMEOUT_MS', 30000),
FFMPEG_BREAKER_FAILURE_THRESHOLD: readNumber(
values,
'FFMPEG_BREAKER_FAILURE_THRESHOLD',
3,
),
FFMPEG_BREAKER_COOLDOWN_MS: readNumber(
values,
'FFMPEG_BREAKER_COOLDOWN_MS',
120000,
),
FFMPEG_BREAKER_SUCCESS_THRESHOLD: readNumber(
values,
'FFMPEG_BREAKER_SUCCESS_THRESHOLD',
1,
),
FILE_STORAGE_BREAKER_FAILURE_THRESHOLD: readNumber(
values,
'FILE_STORAGE_BREAKER_FAILURE_THRESHOLD',
5,
),
FILE_STORAGE_BREAKER_COOLDOWN_MS: readNumber(
values,
'FILE_STORAGE_BREAKER_COOLDOWN_MS',
30000,
),
FILE_STORAGE_BREAKER_SUCCESS_THRESHOLD: readNumber(
values,
'FILE_STORAGE_BREAKER_SUCCESS_THRESHOLD',
2,
),
EMAIL_USER: readString(values, 'EMAIL_USER', ''),
EMAIL_PASS: readString(values, 'EMAIL_PASS', ''),
EMAIL_TLS_REJECT_UNAUTHORIZED: isEnvBooleanString(tlsRejectUnauthorized)
? tlsRejectUnauthorized
: 'true',
PEXELS_KEY: readString(values, 'PEXELS_KEY', ''),
LOG_LEVEL: isEnvLogLevel(logLevel) ? logLevel : 'info',
LOG_PRETTY: isEnvBooleanString(logPretty) ? logPretty : 'false',
};
}
function validateEnv(): ValidatedEnvironment {
const result: Joi.ValidationResult<Record<string, unknown>> =
envSchema.validate(process.env, {
abortEarly: false,
stripUnknown: false,
});
if (result.error) {
const messages = result.error.details.map(
(detail) => ` - ${detail.message}`,
);
logger.error({ errors: messages }, 'Environment validation failed');
if (process.env.NODE_ENV === 'production') {
process.exit(1);
} else {
logger.warn('Continuing with default values in non-production mode');
}
}
const resultValue: unknown = result.value;
return toValidatedEnvironment(isRecord(resultValue) ? resultValue : {});
}
export { envSchema, validateEnv };