/** * Configuration Reader Service * Reads admin configurations from database for use in backend logic */ import { sequelize } from '@config/database'; import { QueryTypes } from 'sequelize'; import logger from '@utils/logger'; // Cache configurations in memory for performance let configCache: Map = new Map(); let cacheExpiry: Date | null = null; const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes // Sensitive config keys that should be masked in logs const SENSITIVE_CONFIG_PATTERNS = [ 'API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'CREDENTIAL', 'PRIVATE', 'AUTH', 'KEY', 'VAPID' ]; /** * Check if a config key contains sensitive data */ function isSensitiveConfig(configKey: string): boolean { const upperKey = configKey.toUpperCase(); return SENSITIVE_CONFIG_PATTERNS.some(pattern => upperKey.includes(pattern)); } /** * Mask sensitive value for logging (show first 4 and last 2 chars) */ function maskSensitiveValue(value: string): string { if (!value || value.length <= 8) { return '***REDACTED***'; } return `${value.substring(0, 4)}****${value.substring(value.length - 2)}`; } /** * Get a configuration value from database (with caching) */ export async function getConfigValue(configKey: string, defaultValue: string = ''): Promise { try { // Check cache first if (configCache.has(configKey) && cacheExpiry && new Date() < cacheExpiry) { return configCache.get(configKey)!; } // Query database const result = await sequelize.query(` SELECT config_value FROM admin_configurations WHERE config_key = :configKey LIMIT 1 `, { replacements: { configKey }, type: QueryTypes.SELECT }); if (result && result.length > 0) { const value = (result[0] as any).config_value; configCache.set(configKey, value); // Always update cache expiry when loading from database cacheExpiry = new Date(Date.now() + CACHE_DURATION_MS); // Mask sensitive values in logs for security const logValue = isSensitiveConfig(configKey) ? maskSensitiveValue(value) : value; logger.info(`[ConfigReader] Loaded config '${configKey}' = '${logValue}' from database (cached for 5min)`); return value; } // Mask sensitive default values in logs for security const logDefault = isSensitiveConfig(configKey) ? maskSensitiveValue(defaultValue) : defaultValue; logger.warn(`[ConfigReader] Config key '${configKey}' not found, using default: ${logDefault}`); return defaultValue; } catch (error) { logger.error(`[ConfigReader] Error reading config '${configKey}':`, error); return defaultValue; } } /** * Get number configuration */ export async function getConfigNumber(configKey: string, defaultValue: number): Promise { const value = await getConfigValue(configKey, String(defaultValue)); return parseFloat(value) || defaultValue; } /** * Get boolean configuration */ export async function getConfigBoolean(configKey: string, defaultValue: boolean): Promise { const value = await getConfigValue(configKey, String(defaultValue)); return value === 'true' || value === '1'; } /** * Get TAT thresholds from database */ export async function getTatThresholds(): Promise<{ first: number; second: number }> { const first = await getConfigNumber('TAT_REMINDER_THRESHOLD_1', 50); const second = await getConfigNumber('TAT_REMINDER_THRESHOLD_2', 75); return { first, second }; } /** * Get working hours from database */ export async function getWorkingHours(): Promise<{ startHour: number; endHour: number }> { const startHour = await getConfigNumber('WORK_START_HOUR', 9); const endHour = await getConfigNumber('WORK_END_HOUR', 18); return { startHour, endHour }; } /** * Clear configuration cache (call after updating configs) */ export function clearConfigCache(): void { configCache.clear(); cacheExpiry = null; logger.info('[ConfigReader] Configuration cache cleared'); } /** * Preload all configurations into cache */ export async function preloadConfigurations(): Promise { try { const results = await sequelize.query(` SELECT config_key, config_value FROM admin_configurations `, { type: QueryTypes.SELECT }); results.forEach((row: any) => { configCache.set(row.config_key, row.config_value); }); cacheExpiry = new Date(Date.now() + CACHE_DURATION_MS); logger.info(`[ConfigReader] Preloaded ${results.length} configurations into cache`); } catch (error) { logger.error('[ConfigReader] Error preloading configurations:', error); } } /** * Get AI provider configurations */ export async function getAIProviderConfig(): Promise<{ provider: string; claudeKey: string; openaiKey: string; geminiKey: string; claudeModel: string; openaiModel: string; geminiModel: string; enabled: boolean; }> { const provider = await getConfigValue('AI_PROVIDER', 'claude'); const claudeKey = await getConfigValue('CLAUDE_API_KEY', ''); const openaiKey = await getConfigValue('OPENAI_API_KEY', ''); const geminiKey = await getConfigValue('GEMINI_API_KEY', ''); // Get models from database config, fallback to env, then to defaults const claudeModel = await getConfigValue('CLAUDE_MODEL', process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514'); const openaiModel = await getConfigValue('OPENAI_MODEL', process.env.OPENAI_MODEL || 'gpt-4o'); const geminiModel = await getConfigValue('GEMINI_MODEL', process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite'); const enabled = await getConfigBoolean('AI_ENABLED', true); return { provider, claudeKey, openaiKey, geminiKey, claudeModel, openaiModel, geminiModel, enabled }; }