- Make unified config lazy-loaded to avoid initialization issues - Replace direct config exports with getter functions - Rewrite utils/env.ts to use unified config instead of scattered access - Add show-config.js helper script for configuration management - Type-safe configuration access throughout the app - Smart defaults for all environments with environment overrides - Eliminates scattered process.env and import.meta.env access
961 lines
26 KiB
TypeScript
961 lines
26 KiB
TypeScript
/**
|
|
* Unified Application Configuration System
|
|
*
|
|
* This file serves as the single source of truth for all application configuration.
|
|
* It supports multiple environments (development, staging, production) and provides
|
|
* type-safe configuration for both frontend and backend components.
|
|
*
|
|
* Configuration sources (in order of precedence):
|
|
* 1. Environment variables
|
|
* 2. Environment-specific config files
|
|
* 3. Default values
|
|
*/
|
|
|
|
export type Environment = 'development' | 'staging' | 'production' | 'test';
|
|
|
|
export interface UnifiedConfig {
|
|
// Application Identity
|
|
app: {
|
|
name: string;
|
|
version: string;
|
|
environment: Environment;
|
|
baseUrl: string;
|
|
port: number;
|
|
};
|
|
|
|
// Database Configuration
|
|
database: {
|
|
url: string;
|
|
username: string;
|
|
password: string;
|
|
name: string;
|
|
useMock: boolean;
|
|
connectionTimeout: number;
|
|
retryAttempts: number;
|
|
};
|
|
|
|
// Container & Registry Configuration
|
|
container: {
|
|
registry: string;
|
|
repository: string;
|
|
tag: string;
|
|
imageUrl: string;
|
|
};
|
|
|
|
// Kubernetes Configuration
|
|
kubernetes: {
|
|
namespace: string;
|
|
ingressHost: string;
|
|
ingressClass: string;
|
|
certIssuer: string;
|
|
storageClass: string;
|
|
storageSize: string;
|
|
replicas: {
|
|
frontend: number;
|
|
database: number;
|
|
};
|
|
resources: {
|
|
frontend: {
|
|
requests: { memory: string; cpu: string };
|
|
limits: { memory: string; cpu: string };
|
|
};
|
|
database: {
|
|
requests: { memory: string; cpu: string };
|
|
limits: { memory: string; cpu: string };
|
|
};
|
|
};
|
|
};
|
|
|
|
// Authentication Configuration
|
|
auth: {
|
|
jwtSecret: string;
|
|
jwtExpiresIn: string;
|
|
refreshTokenExpiresIn: string;
|
|
emailVerificationExpiresIn: string;
|
|
bcryptRounds: number;
|
|
};
|
|
|
|
// Email Configuration
|
|
email: {
|
|
provider: 'mailgun' | 'smtp' | 'console';
|
|
mailgun?: {
|
|
apiKey: string;
|
|
domain: string;
|
|
baseUrl: string;
|
|
};
|
|
smtp?: {
|
|
host: string;
|
|
port: number;
|
|
secure: boolean;
|
|
username: string;
|
|
password: string;
|
|
};
|
|
fromName: string;
|
|
fromEmail: string;
|
|
};
|
|
|
|
// OAuth Configuration
|
|
oauth: {
|
|
google?: {
|
|
clientId: string;
|
|
clientSecret?: string;
|
|
};
|
|
github?: {
|
|
clientId: string;
|
|
clientSecret?: string;
|
|
};
|
|
};
|
|
|
|
// Feature Flags
|
|
features: {
|
|
enableEmailVerification: boolean;
|
|
enableOAuth: boolean;
|
|
enableAdminInterface: boolean;
|
|
enableMonitoring: boolean;
|
|
enableMetrics: boolean;
|
|
enableTracing: boolean;
|
|
enableRateLimiting: boolean;
|
|
enableSecurityHeaders: boolean;
|
|
debugMode: boolean;
|
|
hotReload: boolean;
|
|
};
|
|
|
|
// Performance & Monitoring
|
|
performance: {
|
|
cacheTimeout: number;
|
|
requestTimeout: number;
|
|
maxConnections: number;
|
|
enableCors: boolean;
|
|
corsOrigin: string | string[];
|
|
};
|
|
|
|
// Logging Configuration
|
|
logging: {
|
|
level: 'debug' | 'info' | 'warn' | 'error';
|
|
format: 'json' | 'text';
|
|
enableTimestamp: boolean;
|
|
enableColors: boolean;
|
|
};
|
|
|
|
// Security Configuration
|
|
security: {
|
|
enableHttps: boolean;
|
|
enableHsts: boolean;
|
|
enableCsp: boolean;
|
|
sessionSecret: string;
|
|
rateLimitRequests: number;
|
|
rateLimitWindow: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Environment-specific configuration overrides
|
|
*/
|
|
const environmentConfigs: Record<Environment, Partial<UnifiedConfig>> = {
|
|
development: {
|
|
app: {
|
|
name: 'rxminder',
|
|
version: '1.0.0',
|
|
environment: 'development',
|
|
baseUrl: 'http://localhost:5173',
|
|
port: 5173,
|
|
},
|
|
database: {
|
|
url: 'http://localhost:5984',
|
|
username: 'admin',
|
|
password: 'password',
|
|
name: 'rxminder_dev',
|
|
useMock: false,
|
|
connectionTimeout: 5000,
|
|
retryAttempts: 3,
|
|
},
|
|
kubernetes: {
|
|
namespace: 'rxminder-dev',
|
|
ingressHost: 'rxminder-dev.local',
|
|
ingressClass: 'nginx',
|
|
certIssuer: 'selfsigned',
|
|
storageClass: 'standard',
|
|
storageSize: '1Gi',
|
|
replicas: {
|
|
frontend: 1,
|
|
database: 1,
|
|
},
|
|
resources: {
|
|
frontend: {
|
|
requests: { memory: '128Mi', cpu: '50m' },
|
|
limits: { memory: '256Mi', cpu: '200m' },
|
|
},
|
|
database: {
|
|
requests: { memory: '256Mi', cpu: '100m' },
|
|
limits: { memory: '512Mi', cpu: '500m' },
|
|
},
|
|
},
|
|
},
|
|
features: {
|
|
enableEmailVerification: false,
|
|
enableOAuth: false,
|
|
enableAdminInterface: true,
|
|
enableMonitoring: false,
|
|
enableMetrics: false,
|
|
enableTracing: false,
|
|
enableRateLimiting: false,
|
|
enableSecurityHeaders: false,
|
|
debugMode: true,
|
|
hotReload: true,
|
|
},
|
|
logging: {
|
|
level: 'debug',
|
|
format: 'text',
|
|
enableTimestamp: true,
|
|
enableColors: true,
|
|
},
|
|
performance: {
|
|
cacheTimeout: 300, // 5 minutes
|
|
requestTimeout: 30000,
|
|
maxConnections: 100,
|
|
enableCors: true,
|
|
corsOrigin: '*',
|
|
},
|
|
},
|
|
|
|
staging: {
|
|
app: {
|
|
name: 'rxminder',
|
|
version: '1.0.0',
|
|
environment: 'staging',
|
|
baseUrl: 'https://staging.rxminder.com',
|
|
port: 80,
|
|
},
|
|
kubernetes: {
|
|
namespace: 'rxminder-staging',
|
|
ingressHost: 'staging.rxminder.com',
|
|
ingressClass: 'nginx',
|
|
certIssuer: 'letsencrypt-staging',
|
|
storageClass: 'ssd',
|
|
storageSize: '5Gi',
|
|
replicas: {
|
|
frontend: 2,
|
|
database: 1,
|
|
},
|
|
resources: {
|
|
frontend: {
|
|
requests: { memory: '256Mi', cpu: '100m' },
|
|
limits: { memory: '512Mi', cpu: '500m' },
|
|
},
|
|
database: {
|
|
requests: { memory: '512Mi', cpu: '200m' },
|
|
limits: { memory: '1Gi', cpu: '1000m' },
|
|
},
|
|
},
|
|
},
|
|
features: {
|
|
enableEmailVerification: true,
|
|
enableOAuth: true,
|
|
enableAdminInterface: true,
|
|
enableMonitoring: true,
|
|
enableMetrics: true,
|
|
enableTracing: false,
|
|
enableRateLimiting: true,
|
|
enableSecurityHeaders: true,
|
|
debugMode: false,
|
|
hotReload: false,
|
|
},
|
|
logging: {
|
|
level: 'info',
|
|
format: 'json',
|
|
enableTimestamp: true,
|
|
enableColors: false,
|
|
},
|
|
performance: {
|
|
cacheTimeout: 1800, // 30 minutes
|
|
requestTimeout: 60000,
|
|
maxConnections: 150,
|
|
enableCors: true,
|
|
corsOrigin: 'https://staging.rxminder.com',
|
|
},
|
|
},
|
|
|
|
production: {
|
|
app: {
|
|
name: 'rxminder',
|
|
version: '1.0.0',
|
|
environment: 'production',
|
|
baseUrl: 'https://rxminder.com',
|
|
port: 80,
|
|
},
|
|
database: {
|
|
url: 'http://rxminder-couchdb-service:5984',
|
|
username: 'admin',
|
|
password: 'secure_password',
|
|
name: 'rxminder_prod',
|
|
useMock: false,
|
|
connectionTimeout: 10000,
|
|
retryAttempts: 5,
|
|
},
|
|
kubernetes: {
|
|
namespace: 'rxminder-prod',
|
|
ingressHost: 'rxminder.com',
|
|
ingressClass: 'nginx',
|
|
certIssuer: 'letsencrypt-prod',
|
|
storageClass: 'ssd',
|
|
storageSize: '10Gi',
|
|
replicas: {
|
|
frontend: 3,
|
|
database: 1,
|
|
},
|
|
resources: {
|
|
frontend: {
|
|
requests: { memory: '256Mi', cpu: '100m' },
|
|
limits: { memory: '512Mi', cpu: '500m' },
|
|
},
|
|
database: {
|
|
requests: { memory: '512Mi', cpu: '200m' },
|
|
limits: { memory: '1Gi', cpu: '1000m' },
|
|
},
|
|
},
|
|
},
|
|
features: {
|
|
enableEmailVerification: true,
|
|
enableOAuth: true,
|
|
enableAdminInterface: true,
|
|
enableMonitoring: true,
|
|
enableMetrics: true,
|
|
enableTracing: true,
|
|
enableRateLimiting: true,
|
|
enableSecurityHeaders: true,
|
|
debugMode: false,
|
|
hotReload: false,
|
|
},
|
|
logging: {
|
|
level: 'warn',
|
|
format: 'json',
|
|
enableTimestamp: true,
|
|
enableColors: false,
|
|
},
|
|
performance: {
|
|
cacheTimeout: 3600, // 1 hour
|
|
requestTimeout: 120000,
|
|
maxConnections: 200,
|
|
enableCors: true,
|
|
corsOrigin: 'https://rxminder.com',
|
|
},
|
|
security: {
|
|
enableHttps: true,
|
|
enableHsts: true,
|
|
enableCsp: true,
|
|
sessionSecret: 'secure-session-secret-key',
|
|
rateLimitRequests: 100,
|
|
rateLimitWindow: 900000, // 15 minutes
|
|
},
|
|
},
|
|
|
|
test: {
|
|
app: {
|
|
name: 'rxminder',
|
|
version: '1.0.0',
|
|
environment: 'test',
|
|
baseUrl: 'http://localhost:3000',
|
|
port: 3000,
|
|
},
|
|
database: {
|
|
url: 'http://localhost:5984',
|
|
username: 'test',
|
|
password: 'test',
|
|
name: 'rxminder_test',
|
|
useMock: true,
|
|
connectionTimeout: 3000,
|
|
retryAttempts: 1,
|
|
},
|
|
features: {
|
|
enableEmailVerification: false,
|
|
enableOAuth: false,
|
|
enableAdminInterface: false,
|
|
enableMonitoring: false,
|
|
enableMetrics: false,
|
|
enableTracing: false,
|
|
enableRateLimiting: false,
|
|
enableSecurityHeaders: false,
|
|
debugMode: true,
|
|
hotReload: false,
|
|
},
|
|
logging: {
|
|
level: 'error',
|
|
format: 'text',
|
|
enableTimestamp: false,
|
|
enableColors: false,
|
|
},
|
|
email: {
|
|
provider: 'console' as const,
|
|
fromName: 'Test RxMinder',
|
|
fromEmail: 'test@rxminder.local',
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Default configuration values
|
|
*/
|
|
const defaultConfig: UnifiedConfig = {
|
|
app: {
|
|
name: 'RxMinder',
|
|
version: '1.0.0',
|
|
environment: 'development',
|
|
baseUrl: 'http://localhost:5173',
|
|
port: 5173,
|
|
},
|
|
|
|
database: {
|
|
url: 'http://localhost:5984',
|
|
username: 'admin',
|
|
password: 'changeme',
|
|
name: 'meds_app',
|
|
useMock: false,
|
|
connectionTimeout: 30000,
|
|
retryAttempts: 3,
|
|
},
|
|
|
|
container: {
|
|
registry: 'gitea-http.taildb3494.ts.net',
|
|
repository: 'will/meds',
|
|
tag: 'latest',
|
|
imageUrl: '', // Will be computed
|
|
},
|
|
|
|
kubernetes: {
|
|
namespace: 'rxminder',
|
|
ingressHost: 'rxminder.local',
|
|
ingressClass: 'nginx',
|
|
certIssuer: 'letsencrypt-prod',
|
|
storageClass: 'longhorn',
|
|
storageSize: '1Gi',
|
|
replicas: {
|
|
frontend: 1,
|
|
database: 1,
|
|
},
|
|
resources: {
|
|
frontend: {
|
|
requests: { memory: '128Mi', cpu: '50m' },
|
|
limits: { memory: '256Mi', cpu: '200m' },
|
|
},
|
|
database: {
|
|
requests: { memory: '256Mi', cpu: '100m' },
|
|
limits: { memory: '512Mi', cpu: '500m' },
|
|
},
|
|
},
|
|
},
|
|
|
|
auth: {
|
|
jwtSecret: 'your-super-secret-jwt-key-change-in-production',
|
|
jwtExpiresIn: '1h',
|
|
refreshTokenExpiresIn: '7d',
|
|
emailVerificationExpiresIn: '24h',
|
|
bcryptRounds: 12,
|
|
},
|
|
|
|
email: {
|
|
provider: 'console',
|
|
mailgun: {
|
|
apiKey: '',
|
|
domain: '',
|
|
baseUrl: 'https://api.mailgun.net/v3',
|
|
},
|
|
fromName: 'RxMinder',
|
|
fromEmail: 'noreply@rxminder.com',
|
|
},
|
|
|
|
oauth: {
|
|
google: {
|
|
clientId: '',
|
|
},
|
|
github: {
|
|
clientId: '',
|
|
},
|
|
},
|
|
|
|
features: {
|
|
enableEmailVerification: true,
|
|
enableOAuth: true,
|
|
enableAdminInterface: true,
|
|
enableMonitoring: false,
|
|
enableMetrics: false,
|
|
enableTracing: false,
|
|
enableRateLimiting: false,
|
|
enableSecurityHeaders: false,
|
|
debugMode: false,
|
|
hotReload: false,
|
|
},
|
|
|
|
performance: {
|
|
cacheTimeout: 1800,
|
|
requestTimeout: 30000,
|
|
maxConnections: 100,
|
|
enableCors: true,
|
|
corsOrigin: '*',
|
|
},
|
|
|
|
logging: {
|
|
level: 'info',
|
|
format: 'json',
|
|
enableTimestamp: true,
|
|
enableColors: false,
|
|
},
|
|
|
|
security: {
|
|
enableHttps: false,
|
|
enableHsts: false,
|
|
enableCsp: false,
|
|
sessionSecret: 'your-session-secret-change-in-production',
|
|
rateLimitRequests: 100,
|
|
rateLimitWindow: 900000, // 15 minutes
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Get environment variable with fallback
|
|
* Reads from .env file via process.env or import.meta.env
|
|
*/
|
|
function getEnvVar(key: string, fallback: string = ''): string {
|
|
// First try process.env (Node.js, includes .env via dotenv or build process)
|
|
if (typeof process !== 'undefined' && process.env && process.env[key]) {
|
|
return process.env[key]!;
|
|
}
|
|
|
|
// Then try import.meta.env (Vite/browser, includes .env via Vite's loadEnv)
|
|
if (
|
|
typeof import.meta !== 'undefined' &&
|
|
import.meta.env &&
|
|
import.meta.env[key]
|
|
) {
|
|
return import.meta.env[key];
|
|
}
|
|
|
|
// Use fallback if not found
|
|
return fallback;
|
|
}
|
|
|
|
/**
|
|
* Get boolean environment variable
|
|
*/
|
|
function getBoolEnvVar(key: string, fallback: boolean = false): boolean {
|
|
const value = getEnvVar(key);
|
|
if (value === '') return fallback;
|
|
return value.toLowerCase() === 'true' || value === '1';
|
|
}
|
|
|
|
/**
|
|
* Get number environment variable
|
|
*/
|
|
function getNumberEnvVar(key: string, fallback: number = 0): number {
|
|
const value = getEnvVar(key);
|
|
if (value === '') return fallback;
|
|
const parsed = parseInt(value, 10);
|
|
return isNaN(parsed) ? fallback : parsed;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from environment variables (.env file)
|
|
* This provides environment-specific overrides for the unified config
|
|
*/
|
|
function loadFromEnvironment(config: UnifiedConfig): UnifiedConfig {
|
|
return {
|
|
...config,
|
|
app: {
|
|
...config.app,
|
|
name: getEnvVar('APP_NAME', config.app.name),
|
|
version: getEnvVar('APP_VERSION', config.app.version),
|
|
baseUrl: getEnvVar('APP_BASE_URL', config.app.baseUrl),
|
|
port: getNumberEnvVar('PORT', config.app.port),
|
|
},
|
|
|
|
database: {
|
|
...config.database,
|
|
url:
|
|
getEnvVar('VITE_COUCHDB_URL') ||
|
|
getEnvVar('COUCHDB_URL', config.database.url),
|
|
username:
|
|
getEnvVar('VITE_COUCHDB_USER') ||
|
|
getEnvVar('COUCHDB_USER', config.database.username),
|
|
password:
|
|
getEnvVar('VITE_COUCHDB_PASSWORD') ||
|
|
getEnvVar('COUCHDB_PASSWORD', config.database.password),
|
|
name: getEnvVar('COUCHDB_DATABASE_NAME', config.database.name),
|
|
useMock: getBoolEnvVar('USE_MOCK_DB', config.database.useMock),
|
|
},
|
|
|
|
container: {
|
|
...config.container,
|
|
registry: getEnvVar('CONTAINER_REGISTRY', config.container.registry),
|
|
repository: getEnvVar(
|
|
'CONTAINER_REPOSITORY',
|
|
config.container.repository
|
|
),
|
|
tag: getEnvVar('CONTAINER_TAG', config.container.tag),
|
|
},
|
|
|
|
kubernetes: {
|
|
...config.kubernetes,
|
|
ingressHost: getEnvVar('INGRESS_HOST', config.kubernetes.ingressHost),
|
|
storageClass: getEnvVar('STORAGE_CLASS', config.kubernetes.storageClass),
|
|
storageSize: getEnvVar('STORAGE_SIZE', config.kubernetes.storageSize),
|
|
},
|
|
|
|
auth: {
|
|
...config.auth,
|
|
jwtSecret: getEnvVar('JWT_SECRET', config.auth.jwtSecret),
|
|
jwtExpiresIn: getEnvVar('JWT_EXPIRES_IN', config.auth.jwtExpiresIn),
|
|
},
|
|
|
|
security: {
|
|
...config.security,
|
|
sessionSecret: getEnvVar('SESSION_SECRET', config.security.sessionSecret),
|
|
},
|
|
|
|
email: {
|
|
...config.email,
|
|
provider:
|
|
(getEnvVar('EMAIL_PROVIDER', config.email.provider) as
|
|
| 'mailgun'
|
|
| 'smtp'
|
|
| 'console') || config.email.provider,
|
|
mailgun: {
|
|
...config.email.mailgun!,
|
|
apiKey:
|
|
getEnvVar('VITE_MAILGUN_API_KEY') ||
|
|
getEnvVar('MAILGUN_API_KEY', config.email.mailgun?.apiKey || ''),
|
|
domain:
|
|
getEnvVar('VITE_MAILGUN_DOMAIN') ||
|
|
getEnvVar('MAILGUN_DOMAIN', config.email.mailgun?.domain || ''),
|
|
},
|
|
fromName:
|
|
getEnvVar('VITE_MAILGUN_FROM_NAME') ||
|
|
getEnvVar('MAILGUN_FROM_NAME', config.email.fromName),
|
|
fromEmail:
|
|
getEnvVar('VITE_MAILGUN_FROM_EMAIL') ||
|
|
getEnvVar('MAILGUN_FROM_EMAIL', config.email.fromEmail),
|
|
},
|
|
|
|
oauth: {
|
|
google: {
|
|
clientId:
|
|
getEnvVar('VITE_GOOGLE_CLIENT_ID') ||
|
|
getEnvVar('GOOGLE_CLIENT_ID', config.oauth.google?.clientId || ''),
|
|
clientSecret: getEnvVar('GOOGLE_CLIENT_SECRET'),
|
|
},
|
|
github: {
|
|
clientId:
|
|
getEnvVar('VITE_GITHUB_CLIENT_ID') ||
|
|
getEnvVar('GITHUB_CLIENT_ID', config.oauth.github?.clientId || ''),
|
|
clientSecret: getEnvVar('GITHUB_CLIENT_SECRET'),
|
|
},
|
|
},
|
|
|
|
features: {
|
|
...config.features,
|
|
enableEmailVerification: getBoolEnvVar(
|
|
'ENABLE_EMAIL_VERIFICATION',
|
|
config.features.enableEmailVerification
|
|
),
|
|
enableOAuth: getBoolEnvVar('ENABLE_OAUTH', config.features.enableOAuth),
|
|
enableAdminInterface: getBoolEnvVar(
|
|
'ENABLE_ADMIN_INTERFACE',
|
|
config.features.enableAdminInterface
|
|
),
|
|
enableMonitoring: getBoolEnvVar(
|
|
'ENABLE_MONITORING',
|
|
config.features.enableMonitoring
|
|
),
|
|
enableMetrics: getBoolEnvVar(
|
|
'ENABLE_METRICS',
|
|
config.features.enableMetrics
|
|
),
|
|
debugMode: getBoolEnvVar('DEBUG_MODE', config.features.debugMode),
|
|
},
|
|
|
|
logging: {
|
|
...config.logging,
|
|
level:
|
|
(getEnvVar('LOG_LEVEL', config.logging.level) as
|
|
| 'debug'
|
|
| 'info'
|
|
| 'warn'
|
|
| 'error') || config.logging.level,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deep merge two objects
|
|
*/
|
|
function deepMerge<T>(target: T, source: Partial<T>): T {
|
|
const result = { ...target };
|
|
for (const key in source) {
|
|
if (
|
|
source[key] &&
|
|
typeof source[key] === 'object' &&
|
|
!Array.isArray(source[key])
|
|
) {
|
|
result[key as keyof T] = deepMerge(
|
|
result[key as keyof T],
|
|
source[key] as Partial<T[keyof T]>
|
|
);
|
|
} else if (source[key] !== undefined) {
|
|
result[key as keyof T] = source[key] as T[keyof T];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Validate configuration
|
|
*/
|
|
function validateConfig(config: UnifiedConfig): void {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// Production-specific validations (only when NODE_ENV is explicitly production)
|
|
const isActualProduction = getEnvVar('NODE_ENV') === 'production';
|
|
if (config.app.environment === 'production' && isActualProduction) {
|
|
if (config.auth.jwtSecret === defaultConfig.auth.jwtSecret) {
|
|
errors.push('JWT_SECRET must be changed in production');
|
|
}
|
|
if (
|
|
config.security.sessionSecret === defaultConfig.security.sessionSecret
|
|
) {
|
|
errors.push('SESSION_SECRET must be changed in production');
|
|
}
|
|
if (config.database.password === defaultConfig.database.password) {
|
|
warnings.push(
|
|
'Consider changing default database password in production'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Email configuration validation
|
|
if (
|
|
config.features.enableEmailVerification &&
|
|
config.email.provider === 'mailgun'
|
|
) {
|
|
if (!config.email.mailgun?.apiKey) {
|
|
warnings.push(
|
|
'MAILGUN_API_KEY not configured - email will fall back to console'
|
|
);
|
|
}
|
|
if (!config.email.mailgun?.domain) {
|
|
warnings.push('MAILGUN_DOMAIN not configured - email may not work');
|
|
}
|
|
}
|
|
|
|
// OAuth configuration validation
|
|
if (config.features.enableOAuth) {
|
|
if (!config.oauth.google?.clientId && !config.oauth.github?.clientId) {
|
|
warnings.push('No OAuth client IDs configured - OAuth will be disabled');
|
|
}
|
|
}
|
|
|
|
// Log warnings and throw errors
|
|
if (warnings.length > 0) {
|
|
console.warn('⚠️ Configuration warnings:', warnings);
|
|
}
|
|
if (errors.length > 0) {
|
|
console.error('❌ Configuration errors:', errors);
|
|
throw new Error(`Configuration validation failed: ${errors.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create unified configuration
|
|
* Uses .env file as primary source for environment-specific overrides
|
|
*/
|
|
export function createUnifiedConfig(): UnifiedConfig {
|
|
// Determine environment from .env file or NODE_ENV
|
|
const nodeEnv = getEnvVar('NODE_ENV', 'development') as Environment;
|
|
const environment = ['development', 'staging', 'production', 'test'].includes(
|
|
nodeEnv
|
|
)
|
|
? (nodeEnv as Environment)
|
|
: 'development';
|
|
|
|
return createUnifiedConfigForEnvironment(environment);
|
|
}
|
|
|
|
/**
|
|
* Create unified configuration for specific environment
|
|
* Priority: .env file overrides > environment-specific config > defaults
|
|
*/
|
|
export function createUnifiedConfigForEnvironment(
|
|
environment: Environment
|
|
): UnifiedConfig {
|
|
// Start with default config
|
|
let config = { ...defaultConfig };
|
|
config.app.environment = environment;
|
|
|
|
// Apply environment-specific overrides (lower priority)
|
|
if (environmentConfigs[environment]) {
|
|
config = deepMerge(config, environmentConfigs[environment]);
|
|
}
|
|
|
|
// Apply .env file overrides (highest priority)
|
|
config = loadFromEnvironment(config);
|
|
|
|
// Compute derived values after all overrides
|
|
config.container.imageUrl = `${config.container.registry}/${config.container.repository}:${config.container.tag}`;
|
|
|
|
// Validate configuration
|
|
if (environment !== 'test') {
|
|
validateConfig(config);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Lazy-loaded singleton configuration instance
|
|
*/
|
|
let _unifiedConfig: UnifiedConfig | null = null;
|
|
|
|
export const unifiedConfig: UnifiedConfig = new Proxy({} as UnifiedConfig, {
|
|
get(target, prop) {
|
|
if (!_unifiedConfig) {
|
|
_unifiedConfig = createUnifiedConfig();
|
|
}
|
|
return _unifiedConfig[prop as keyof UnifiedConfig];
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Export specific configuration sections for convenience
|
|
*/
|
|
export const getAppConfig = () => unifiedConfig.app;
|
|
export const getDatabaseConfig = () => unifiedConfig.database;
|
|
export const getContainerConfig = () => unifiedConfig.container;
|
|
export const getKubernetesConfig = () => unifiedConfig.kubernetes;
|
|
export const getAuthConfig = () => unifiedConfig.auth;
|
|
export const getEmailConfig = () => unifiedConfig.email;
|
|
export const getOAuthConfig = () => unifiedConfig.oauth;
|
|
export const getFeatureFlags = () => unifiedConfig.features;
|
|
export const getPerformanceConfig = () => unifiedConfig.performance;
|
|
export const getLoggingConfig = () => unifiedConfig.logging;
|
|
export const getSecurityConfig = () => unifiedConfig.security;
|
|
|
|
/**
|
|
* Utility functions
|
|
*/
|
|
export const isProduction = () =>
|
|
unifiedConfig.app.environment === 'production';
|
|
export const isDevelopment = () =>
|
|
unifiedConfig.app.environment === 'development';
|
|
export const isStaging = () => unifiedConfig.app.environment === 'staging';
|
|
export const isTest = () => unifiedConfig.app.environment === 'test';
|
|
|
|
/**
|
|
* Export configuration as environment variables (for scripts and Kubernetes)
|
|
*/
|
|
export function exportAsEnvVars(
|
|
config?: UnifiedConfig
|
|
): Record<string, string> {
|
|
const configToUse = config || unifiedConfig;
|
|
|
|
return {
|
|
// Application
|
|
APP_NAME: configToUse.app.name,
|
|
APP_VERSION: configToUse.app.version,
|
|
NODE_ENV: configToUse.app.environment,
|
|
APP_BASE_URL: configToUse.app.baseUrl,
|
|
PORT: configToUse.app.port.toString(),
|
|
|
|
// Database
|
|
COUCHDB_URL: configToUse.database.url,
|
|
COUCHDB_USER: configToUse.database.username,
|
|
COUCHDB_PASSWORD: configToUse.database.password,
|
|
COUCHDB_DATABASE_NAME: configToUse.database.name,
|
|
USE_MOCK_DB: configToUse.database.useMock.toString(),
|
|
|
|
// Container
|
|
CONTAINER_REGISTRY: configToUse.container.registry,
|
|
CONTAINER_REPOSITORY: configToUse.container.repository,
|
|
CONTAINER_TAG: configToUse.container.tag,
|
|
DOCKER_IMAGE: configToUse.container.imageUrl,
|
|
|
|
// Kubernetes
|
|
KUBERNETES_NAMESPACE: configToUse.kubernetes.namespace,
|
|
INGRESS_HOST: configToUse.kubernetes.ingressHost,
|
|
INGRESS_CLASS: configToUse.kubernetes.ingressClass,
|
|
CERT_MANAGER_ISSUER: configToUse.kubernetes.certIssuer,
|
|
STORAGE_CLASS: configToUse.kubernetes.storageClass,
|
|
STORAGE_SIZE: configToUse.kubernetes.storageSize,
|
|
|
|
// Features
|
|
ENABLE_EMAIL_VERIFICATION:
|
|
configToUse.features.enableEmailVerification.toString(),
|
|
ENABLE_OAUTH: configToUse.features.enableOAuth.toString(),
|
|
ENABLE_ADMIN_INTERFACE:
|
|
configToUse.features.enableAdminInterface.toString(),
|
|
ENABLE_MONITORING: configToUse.features.enableMonitoring.toString(),
|
|
ENABLE_METRICS: configToUse.features.enableMetrics.toString(),
|
|
DEBUG_MODE: configToUse.features.debugMode.toString(),
|
|
|
|
// Logging
|
|
LOG_LEVEL: configToUse.logging.level,
|
|
LOG_FORMAT: configToUse.logging.format,
|
|
|
|
// Performance
|
|
CACHE_TTL: configToUse.performance.cacheTimeout.toString(),
|
|
REQUEST_TIMEOUT: configToUse.performance.requestTimeout.toString(),
|
|
MAX_CONNECTIONS: configToUse.performance.maxConnections.toString(),
|
|
ENABLE_CORS: configToUse.performance.enableCors.toString(),
|
|
CORS_ORIGIN: Array.isArray(configToUse.performance.corsOrigin)
|
|
? configToUse.performance.corsOrigin.join(',')
|
|
: configToUse.performance.corsOrigin,
|
|
|
|
// Replicas (for Kubernetes)
|
|
FRONTEND_REPLICAS: configToUse.kubernetes.replicas.frontend.toString(),
|
|
DATABASE_REPLICAS: configToUse.kubernetes.replicas.database.toString(),
|
|
|
|
// Resources (for Kubernetes)
|
|
FRONTEND_MEMORY_REQUEST:
|
|
configToUse.kubernetes.resources.frontend.requests.memory,
|
|
FRONTEND_CPU_REQUEST:
|
|
configToUse.kubernetes.resources.frontend.requests.cpu,
|
|
FRONTEND_MEMORY_LIMIT:
|
|
configToUse.kubernetes.resources.frontend.limits.memory,
|
|
FRONTEND_CPU_LIMIT: configToUse.kubernetes.resources.frontend.limits.cpu,
|
|
DATABASE_MEMORY_REQUEST:
|
|
configToUse.kubernetes.resources.database.requests.memory,
|
|
DATABASE_CPU_REQUEST:
|
|
configToUse.kubernetes.resources.database.requests.cpu,
|
|
DATABASE_MEMORY_LIMIT:
|
|
configToUse.kubernetes.resources.database.limits.memory,
|
|
DATABASE_CPU_LIMIT: configToUse.kubernetes.resources.database.limits.cpu,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Debug helper to log current configuration
|
|
*/
|
|
export function logConfig(): void {
|
|
if (unifiedConfig.features.debugMode) {
|
|
console.warn('🔧 Unified Configuration (Single Source of Truth):', {
|
|
environment: unifiedConfig.app.environment,
|
|
app: unifiedConfig.app.name,
|
|
version: unifiedConfig.app.version,
|
|
baseUrl: unifiedConfig.app.baseUrl,
|
|
database: {
|
|
url: unifiedConfig.database.url,
|
|
useMock: unifiedConfig.database.useMock,
|
|
},
|
|
container: {
|
|
registry: unifiedConfig.container.registry,
|
|
repository: unifiedConfig.container.repository,
|
|
imageUrl: unifiedConfig.container.imageUrl,
|
|
},
|
|
features: unifiedConfig.features,
|
|
configSource: '.env file overrides applied',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Auto-log in development
|
|
if (isDevelopment() && typeof window !== 'undefined') {
|
|
logConfig();
|
|
}
|