import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Configuration } from '../../entities/configuration.entity'; import * as crypto from 'crypto'; @Injectable() export class AppConfigService implements OnModuleInit { private readonly logger = new Logger(AppConfigService.name); private cache: Map = new Map(); private readonly ENCRYPTION_KEY: string; private readonly ENCRYPTION_ALGORITHM = 'aes-256-cbc'; private readonly IV_LENGTH = 16; constructor( @InjectRepository(Configuration) private configRepo: Repository, ) { // Clé de chiffrement depuis les variables d'environnement // En production, cette clé doit être générée de manière sécurisée this.ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex'); if (!process.env.CONFIG_ENCRYPTION_KEY) { this.logger.warn( '⚠️ CONFIG_ENCRYPTION_KEY non définie. Utilisation d\'une clé temporaire (NON RECOMMANDÉ EN PRODUCTION)', ); } } /** * Chargement du cache au démarrage de l'application */ async onModuleInit() { await this.loadCache(); } /** * Chargement de toutes les configurations en cache */ async loadCache(): Promise { try { const configs = await this.configRepo.find(); this.logger.log(`📦 Chargement de ${configs.length} configurations en cache`); for (const config of configs) { let value = config.valeur; // Déchiffrement si nécessaire if (config.type === 'encrypted' && value) { try { value = this.decrypt(value); } catch (error) { this.logger.error( `❌ Erreur de déchiffrement pour la clé '${config.cle}'`, error, ); value = null; } } // Conversion de type const convertedValue = this.convertType(value, config.type); this.cache.set(config.cle, convertedValue); } this.logger.log('✅ Cache de configuration chargé avec succès'); } catch (error) { this.logger.error('❌ Erreur lors du chargement du cache', error); throw error; } } /** * Récupération d'une valeur de configuration * @param key Clé de configuration * @param defaultValue Valeur par défaut si la clé n'existe pas * @returns Valeur de configuration */ get(key: string, defaultValue?: T): T { if (this.cache.has(key)) { return this.cache.get(key) as T; } if (defaultValue !== undefined) { return defaultValue; } this.logger.warn(`⚠️ Configuration '${key}' non trouvée et aucune valeur par défaut fournie`); return undefined as T; } /** * Mise à jour d'une valeur de configuration * @param key Clé de configuration * @param value Nouvelle valeur * @param userId ID de l'utilisateur qui modifie */ async set(key: string, value: any, userId?: string): Promise { const config = await this.configRepo.findOne({ where: { cle: key } }); if (!config) { throw new Error(`Configuration '${key}' non trouvée`); } let valueToStore = value !== null && value !== undefined ? String(value) : null; // Chiffrement si nécessaire if (config.type === 'encrypted' && valueToStore) { valueToStore = this.encrypt(valueToStore); } config.valeur = valueToStore; config.modifieLe = new Date(); if (userId) { config.modifiePar = { id: userId } as any; } await this.configRepo.save(config); // Mise à jour du cache (avec la valeur déchiffrée) this.cache.set(key, value); this.logger.log(`✅ Configuration '${key}' mise à jour`); } /** * Récupération de toutes les configurations par catégorie * @param category Catégorie de configuration * @returns Objet clé/valeur des configurations */ async getByCategory(category: string): Promise> { const configs = await this.configRepo.find({ where: { categorie: category as any }, }); const result: Record = {}; for (const config of configs) { let value = config.valeur; // Masquer les mots de passe if (config.type === 'encrypted') { value = value ? '***********' : null; } else { value = this.convertType(value, config.type); } result[config.cle] = { value, type: config.type, description: config.description, }; } return result; } /** * Récupération de toutes les configurations (pour l'admin) * @returns Liste de toutes les configurations */ async getAll(): Promise { const configs = await this.configRepo.find({ order: { categorie: 'ASC', cle: 'ASC' }, }); // Masquer les valeurs chiffrées return configs.map((config) => ({ ...config, valeur: config.type === 'encrypted' && config.valeur ? '***********' : config.valeur, })); } /** * Test de la configuration SMTP * @param testEmail Email de destination pour le test * @returns Objet avec success et error éventuel */ async testSmtpConnection(testEmail?: string): Promise<{ success: boolean; error?: string }> { try { this.logger.log('🧪 Test de connexion SMTP...'); // Récupération de la configuration SMTP const smtpHost = this.get('smtp_host'); const smtpPort = this.get('smtp_port'); const smtpSecure = this.get('smtp_secure'); const smtpAuthRequired = this.get('smtp_auth_required'); const smtpUser = this.get('smtp_user'); const smtpPassword = this.get('smtp_password'); const emailFromName = this.get('email_from_name'); const emailFromAddress = this.get('email_from_address'); // Import dynamique de nodemailer const nodemailer = await import('nodemailer'); // Configuration du transporteur const transportConfig: any = { host: smtpHost, port: smtpPort, secure: smtpSecure, }; if (smtpAuthRequired && smtpUser && smtpPassword) { transportConfig.auth = { user: smtpUser, pass: smtpPassword, }; } const transporter = nodemailer.createTransport(transportConfig); // Vérification de la connexion await transporter.verify(); this.logger.log('✅ Connexion SMTP vérifiée'); // Si un email de test est fourni, on envoie un email if (testEmail) { await transporter.sendMail({ from: `"${emailFromName}" <${emailFromAddress}>`, to: testEmail, subject: '🧪 Test de configuration SMTP - P\'titsPas', text: 'Ceci est un email de test pour vérifier la configuration SMTP de votre application P\'titsPas.', html: `

✅ Test de configuration SMTP réussi !

Ceci est un email de test pour vérifier la configuration SMTP de votre application P'titsPas.

Si vous recevez cet email, cela signifie que votre configuration SMTP fonctionne correctement.


Cet email a été envoyé automatiquement depuis votre application P'titsPas.
Configuration testée le ${new Date().toLocaleString('fr-FR')}

`, }); this.logger.log(`📧 Email de test envoyé à ${testEmail}`); } return { success: true }; } catch (error) { this.logger.error('❌ Échec du test SMTP', error); return { success: false, error: error.message || 'Erreur inconnue lors du test SMTP', }; } } /** * Vérification si la configuration initiale est terminée * @returns true si la configuration est terminée */ isSetupCompleted(): boolean { return this.get('setup_completed', false); } /** * Marquer la configuration initiale comme terminée * @param userId ID de l'utilisateur qui termine la configuration (null si non authentifié) */ async markSetupCompleted(userId: string | null): Promise { await this.set('setup_completed', 'true', userId ?? undefined); this.logger.log('✅ Configuration initiale marquée comme terminée'); } /** * Conversion de type selon le type de configuration * @param value Valeur à convertir * @param type Type cible * @returns Valeur convertie */ private convertType(value: string | null, type: string): any { if (value === null || value === undefined) { return null; } switch (type) { case 'number': return parseFloat(value); case 'boolean': return value === 'true' || value === '1'; case 'json': try { return JSON.parse(value); } catch { return null; } case 'string': case 'encrypted': default: return value; } } /** * Chiffrement AES-256-CBC * @param text Texte à chiffrer * @returns Texte chiffré (format: iv:encrypted) */ private encrypt(text: string): string { const iv = crypto.randomBytes(this.IV_LENGTH); const key = Buffer.from(this.ENCRYPTION_KEY, 'hex'); const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); // Format: iv:encrypted return `${iv.toString('hex')}:${encrypted}`; } /** * Déchiffrement AES-256-CBC * @param encryptedText Texte chiffré (format: iv:encrypted) * @returns Texte déchiffré */ private decrypt(encryptedText: string): string { const parts = encryptedText.split(':'); if (parts.length !== 2) { throw new Error('Format de chiffrement invalide'); } const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const key = Buffer.from(this.ENCRYPTION_KEY, 'hex'); const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } }