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 { 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, key: keyof ValidatedEnvironment, fallback: string, ): string { const value = values[key]; return typeof value === 'string' ? value : fallback; } function readNumber( values: Record, key: keyof ValidatedEnvironment, fallback: number, ): number { const value = values[key]; return typeof value === 'number' ? value : fallback; } function toValidatedEnvironment( values: Record, ): 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> = 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 };