@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
text/typescript
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;
}
}