200 lines
5.8 KiB
TypeScript
200 lines
5.8 KiB
TypeScript
/**
|
|
* 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<typeof envSchema>
|
|
}
|
|
|
|
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<typeof envSchema>
|
|
|
|
// 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'
|
|
}`
|
|
)
|
|
}
|