UNPKG

@diagramers/api

Version:

Diagramers API - A comprehensive Node.js API template with TypeScript, Firebase Functions, and Socket.io

419 lines (388 loc) 12.3 kB
import * as fs from 'fs'; import * as path from 'path'; import * as dotenv from 'dotenv'; import { AppConfig, ConfigValidationResult } from './interfaces'; export class ConfigManager { private static instance: ConfigManager; private config: AppConfig | null = null; private readonly configPath: string; constructor() { this.configPath = path.join(process.cwd(), 'config'); this.loadEnvironmentVariables(); } private loadEnvironmentVariables(): void { const environment = process.env.NODE_ENV || 'development'; // Load .env file first (base variables) const envPath = path.join(process.cwd(), '.env'); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); } // Load environment-specific .env file const envSpecificPath = path.join(process.cwd(), `.env.${environment}`); if (fs.existsSync(envSpecificPath)) { dotenv.config({ path: envSpecificPath }); } // Load local .env file (for overrides) const localEnvPath = path.join(process.cwd(), '.env.local'); if (fs.existsSync(localEnvPath)) { dotenv.config({ path: localEnvPath }); } } static getInstance(): ConfigManager { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); } return ConfigManager.instance; } /** * Load configuration for the current environment */ async load(environment: string = process.env.NODE_ENV || 'development'): Promise<AppConfig> { if (this.config) { return this.config; } try { // Load environment-specific config const envConfigPath = path.join(this.configPath, `${environment}.ts`); let envConfig: Partial<AppConfig> = {}; if (fs.existsSync(envConfigPath)) { // In a real implementation, you'd import the config file // For now, we'll use a basic structure envConfig = await this.loadEnvironmentConfig(environment); } // Load base config const baseConfig = await this.loadBaseConfig(); // Merge configurations this.config = this.mergeConfigs(baseConfig, envConfig); // Validate configuration const validation = this.validate(this.config); if (!validation.isValid) { throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`); } return this.config; } catch (error: any) { throw new Error(`Failed to load configuration: ${error.message}`); } } /** * Get current configuration */ get(): AppConfig { if (!this.config) { throw new Error('Configuration not loaded. Call load() first.'); } return this.config; } /** * Reload configuration */ async reload(environment?: string): Promise<AppConfig> { this.config = null; return await this.load(environment); } /** * Validate configuration */ validate(config: AppConfig): ConfigValidationResult { const errors: string[] = []; const warnings: string[] = []; // Validate required fields if (!config.port || config.port <= 0) { errors.push('Port must be a positive number'); } if (!config.host) { errors.push('Host is required'); } if (!config.auth?.jwtSecret) { errors.push('JWT secret is required'); } if (!config.database?.host) { errors.push('Database host is required'); } // Validate auth providers if (config.auth?.providers?.firebase?.enabled && !config.auth.providers.firebase.projectId) { errors.push('Firebase project ID is required when Firebase auth is enabled'); } // Validate notification providers if (config.notifications?.email?.enabled && !config.notifications.email.defaults?.from) { warnings.push('Email from address is recommended when email notifications are enabled'); } return { isValid: errors.length === 0, errors, warnings }; } /** * Get configuration for a specific module */ getModuleConfig(moduleName: string): any { const config = this.get(); return config.plugins?.find(p => p.name === moduleName)?.config || {}; } /** * Update configuration dynamically */ async updateConfig(updates: Partial<AppConfig>): Promise<void> { if (!this.config) { throw new Error('Configuration not loaded'); } this.config = { ...this.config, ...updates }; // Validate updated config const validation = this.validate(this.config); if (!validation.isValid) { throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`); } } private async loadEnvironmentConfig(environment: string): Promise<Partial<AppConfig>> { return { environment: environment as any, port: parseInt(process.env.PORT || '3000'), host: process.env.HOST || 'localhost', logging: { level: (process.env.LOG_LEVEL || (environment === 'production' ? 'warn' : 'debug')) as 'warn' | 'debug' | 'error' | 'info', file: process.env.LOG_FILE || 'logs/app.log' }, cors: { origin: process.env.CORS_ORIGIN || '*', credentials: process.env.CORS_CREDENTIALS === 'true' }, security: { rateLimit: { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100') }, helmet: process.env.HELMET_ENABLED !== 'false', compression: process.env.COMPRESSION_ENABLED !== 'false' }, database: { host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '3306'), username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'diagramers', dialect: (process.env.DB_DIALECT || 'mysql') as 'mysql' | 'postgres' | 'sqlite' | 'mariadb' | 'mongodb', logging: process.env.DB_LOGGING === 'true' }, auth: { jwtSecret: process.env.JWT_SECRET || 'your-secret-key', jwtExpiresIn: process.env.JWT_EXPIRES_IN || '1h', refreshTokenExpiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d', providers: { internal: true, firebase: { enabled: process.env.FIREBASE_ENABLED === 'true', projectId: process.env.FIREBASE_PROJECT_ID || '', privateKeyId: process.env.FIREBASE_PRIVATE_KEY_ID || '', privateKey: process.env.FIREBASE_PRIVATE_KEY || '', clientEmail: process.env.FIREBASE_CLIENT_EMAIL || '', clientId: process.env.FIREBASE_CLIENT_ID || '', authUri: process.env.FIREBASE_AUTH_URI || '', tokenUri: process.env.FIREBASE_TOKEN_URI || '', authProviderX509CertUrl: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL || '', clientX509CertUrl: process.env.FIREBASE_CLIENT_X509_CERT_URL || '' }, oauth: { enabled: process.env.OAUTH_ENABLED === 'true', providers: { google: { enabled: process.env.GOOGLE_CLIENT_ID ? true : false, clientId: process.env.GOOGLE_CLIENT_ID || '', clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', callbackUrl: process.env.GOOGLE_CALLBACK_URL || '', scope: ['email', 'profile'] }, github: { enabled: false, clientId: '', clientSecret: '', callbackUrl: '', scope: [] }, facebook: { enabled: false, clientId: '', clientSecret: '', callbackUrl: '', scope: [] } } }, sms: { enabled: process.env.SMS_ENABLED === 'true', provider: (process.env.SMS_PROVIDER || 'twilio') as 'twilio' | 'aws-sns' | 'custom', twilio: { accountSid: process.env.TWILIO_ACCOUNT_SID || '', authToken: process.env.TWILIO_AUTH_TOKEN || '', fromNumber: process.env.TWILIO_PHONE_NUMBER || '' } }, email: { enabled: process.env.EMAIL_ENABLED === 'true', provider: 'nodemailer' } } }, notifications: { email: { enabled: process.env.EMAIL_ENABLED === 'true', provider: 'nodemailer', templates: { path: './templates/email', engine: 'handlebars' }, defaults: { from: process.env.EMAIL_FROM || '', replyTo: process.env.EMAIL_FROM || '' } }, sms: { enabled: process.env.SMS_ENABLED === 'true', provider: 'twilio', defaults: { from: process.env.TWILIO_PHONE_NUMBER || '' } }, push: { enabled: false, provider: 'firebase' } } }; } private async loadBaseConfig(): Promise<Partial<AppConfig>> { // Load base configuration return { environment: 'development', port: parseInt(process.env.PORT || '3000'), host: 'localhost', cors: { origin: '*', credentials: false }, database: { host: 'localhost', port: 3306, username: 'root', password: '', database: 'diagramers', dialect: 'mysql', logging: false }, auth: { jwtSecret: process.env.JWT_SECRET || 'your-secret-key', jwtExpiresIn: '1h', refreshTokenExpiresIn: '7d', providers: { internal: true, firebase: { enabled: false, projectId: '', privateKeyId: '', privateKey: '', clientEmail: '', clientId: '', authUri: '', tokenUri: '', authProviderX509CertUrl: '', clientX509CertUrl: '' }, oauth: { enabled: false, providers: { google: { enabled: false, clientId: '', clientSecret: '', callbackUrl: '', scope: [] }, github: { enabled: false, clientId: '', clientSecret: '', callbackUrl: '', scope: [] }, facebook: { enabled: false, clientId: '', clientSecret: '', callbackUrl: '', scope: [] } } }, sms: { enabled: false, provider: 'twilio' }, email: { enabled: false, provider: 'nodemailer' } } }, notifications: { email: { enabled: false, provider: 'nodemailer', templates: { path: './templates/email', engine: 'handlebars' }, defaults: { from: '', replyTo: '' } }, sms: { enabled: false, provider: 'twilio', defaults: { from: '' } }, push: { enabled: false, provider: 'firebase' } }, plugins: [], logging: { level: 'info' }, security: { rateLimit: { windowMs: 15 * 60 * 1000, // 15 minutes max: 100 }, helmet: true, compression: true } }; } private mergeConfigs(base: Partial<AppConfig>, env: Partial<AppConfig>): AppConfig { // Deep merge configurations return { ...base, ...env, auth: { ...base.auth, ...env.auth, providers: { ...base.auth?.providers, ...env.auth?.providers } }, notifications: { ...base.notifications, ...env.notifications }, database: { ...base.database, ...env.database } } as AppConfig; } }