/** * Environment variable validation and management * Ensures all required environment variables are present and valid */ import { z } from 'zod' // Define the schema for environment variables const envSchema = z.object({ // Node environment NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), // Server configuration PORT: z.string().default('3006').transform(Number), // Database MONGODB_URI: z.string().min(1, 'MongoDB URI is required'), // Redis REDIS_URL: z.string().min(1, 'Redis URL is required'), // Authentication secrets (required) SESSION_SECRET: z.string().min(32, 'Session secret must be at least 32 characters'), JWT_SECRET: z.string().min(1, 'JWT secret is required'), JWT_REFRESH_SECRET: z.string().min(1, 'JWT refresh secret is required'), // Optional configuration NEXT_PUBLIC_SITE_URL: z.string().url().optional(), NEXT_PUBLIC_APP_URL: z.string().url().optional(), // Analytics (optional) NEXT_PUBLIC_GA_MEASUREMENT_ID: z.string().optional(), NEXT_PUBLIC_PLAUSIBLE_DOMAIN: z.string().optional(), // Email (optional) SMTP_HOST: z.string().optional(), SMTP_PORT: z .string() .optional() .transform((val) => (val ? Number(val) : undefined)), SMTP_USER: z.string().optional(), SMTP_PASS: z.string().optional(), // File storage (optional) - using dev-portfolio naming convention MINIO_ENDPOINT: z.string().optional(), MINIO_PORT: z .string() .optional() .transform((val) => (val ? Number(val) : undefined)), MINIO_KEY: z.string().optional(), MINIO_SECRET: z.string().optional(), MINIO_IMAGE_BUCKET: z.string().optional(), // Legacy names for backward compatibility MINIO_ACCESS_KEY: z.string().optional(), MINIO_SECRET_KEY: z.string().optional(), MINIO_BUCKET: z.string().optional(), // External file upload API FILE_UPLOAD_TOKEN: z.string().optional(), // Monitoring (optional) SENTRY_DSN: z.string().optional(), SENTRY_ORG: z.string().optional(), SENTRY_PROJECT: z.string().optional(), }) // Parse and validate environment variables function validateEnv() { // Skip validation during build if SKIP_ENV_VALIDATION is set if (process.env.SKIP_ENV_VALIDATION === 'true') { console.log('⚠️ Skipping environment validation (SKIP_ENV_VALIDATION=true)') // Return process.env with unknown first to bypass TypeScript's type checking return process.env as unknown as z.infer } try { return envSchema.parse(process.env) } catch (error) { if (error instanceof z.ZodError) { const missingVars = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`) console.error('❌ Invalid environment variables:') missingVars.forEach((error) => console.error(` - ${error}`)) throw new Error('Environment validation failed') } throw error } } // Export validated environment variables export const env = validateEnv() as z.infer // Utility functions for environment checks export const isDevelopment = env.NODE_ENV === 'development' export const isProduction = env.NODE_ENV === 'production' export const isTest = env.NODE_ENV === 'test' // Database configuration export const dbConfig = { uri: env.MONGODB_URI, } // Redis configuration export const redisConfig = { url: env.REDIS_URL, } // Authentication configuration export const authConfig = { sessionSecret: env.SESSION_SECRET, jwtSecret: env.JWT_SECRET, jwtRefreshSecret: env.JWT_REFRESH_SECRET, } // App configuration export const appConfig = { port: env.PORT, siteUrl: env.NEXT_PUBLIC_SITE_URL || env.NEXT_PUBLIC_APP_URL || `http://localhost:${env.PORT}`, appUrl: env.NEXT_PUBLIC_APP_URL || env.NEXT_PUBLIC_SITE_URL || `http://localhost:${env.PORT}`, } // Analytics configuration export const analyticsConfig = { googleAnalytics: { measurementId: env.NEXT_PUBLIC_GA_MEASUREMENT_ID, enabled: !!env.NEXT_PUBLIC_GA_MEASUREMENT_ID && isProduction, }, plausible: { domain: env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN, enabled: !!env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && isProduction, }, } // Email configuration export const emailConfig = { smtp: { host: env.SMTP_HOST, port: env.SMTP_PORT || 587, user: env.SMTP_USER, pass: env.SMTP_PASS, }, enabled: !!(env.SMTP_HOST && env.SMTP_USER && env.SMTP_PASS), } // File storage configuration - prioritize dev-portfolio naming export const storageConfig = { minio: { endpoint: env.MINIO_ENDPOINT, port: env.MINIO_PORT || 9000, accessKey: env.MINIO_KEY || env.MINIO_ACCESS_KEY, secretKey: env.MINIO_SECRET || env.MINIO_SECRET_KEY, bucket: env.MINIO_IMAGE_BUCKET || env.MINIO_BUCKET, }, enabled: !!( env.MINIO_ENDPOINT && (env.MINIO_KEY || env.MINIO_ACCESS_KEY) && (env.MINIO_SECRET || env.MINIO_SECRET_KEY) ), } // Monitoring configuration export const monitoringConfig = { sentry: { dsn: env.SENTRY_DSN, org: env.SENTRY_ORG, project: env.SENTRY_PROJECT, }, enabled: !!env.SENTRY_DSN && isProduction, } // Environment info for debugging export const envInfo = { nodeEnv: env.NODE_ENV, port: env.PORT, features: { analytics: analyticsConfig.googleAnalytics.enabled || analyticsConfig.plausible.enabled, email: emailConfig.enabled, storage: storageConfig.enabled, monitoring: monitoringConfig.enabled, }, } // Log environment info in development if (isDevelopment) { console.log('🔧 Environment configuration:') console.log(` - Node: ${env.NODE_ENV}`) console.log(` - Port: ${env.PORT}`) console.log(` - Database: ${env.MONGODB_URI.includes('localhost') ? 'Local' : 'Remote'}`) console.log(` - Redis: ${env.REDIS_URL.includes('localhost') ? 'Local' : 'Remote'}`) console.log( ` - Features: ${ Object.entries(envInfo.features) .filter(([, enabled]) => enabled) .map(([name]) => name) .join(', ') || 'None' }` ) }