- completeSetup: userId = req.user?.id ?? null (plus de fallback 'system') - markSetupCompleted(userId: string | null), set(..., userId ?? undefined) - Corrige 'invalid input syntax for type uuid: "system"' au clic Sauvegarder Co-authored-by: Cursor <cursoragent@cursor.com>
339 lines
10 KiB
TypeScript
339 lines
10 KiB
TypeScript
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<string, any> = 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<Configuration>,
|
|
) {
|
|
// 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<void> {
|
|
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<T = any>(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<void> {
|
|
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<Record<string, any>> {
|
|
const configs = await this.configRepo.find({
|
|
where: { categorie: category as any },
|
|
});
|
|
|
|
const result: Record<string, any> = {};
|
|
|
|
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<Configuration[]> {
|
|
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<string>('smtp_host');
|
|
const smtpPort = this.get<number>('smtp_port');
|
|
const smtpSecure = this.get<boolean>('smtp_secure');
|
|
const smtpAuthRequired = this.get<boolean>('smtp_auth_required');
|
|
const smtpUser = this.get<string>('smtp_user');
|
|
const smtpPassword = this.get<string>('smtp_password');
|
|
const emailFromName = this.get<string>('email_from_name');
|
|
const emailFromAddress = this.get<string>('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: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: #4CAF50;">✅ Test de configuration SMTP réussi !</h2>
|
|
<p>Ceci est un email de test pour vérifier la configuration SMTP de votre application <strong>P'titsPas</strong>.</p>
|
|
<p>Si vous recevez cet email, cela signifie que votre configuration SMTP fonctionne correctement.</p>
|
|
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
|
<p style="color: #666; font-size: 12px;">
|
|
Cet email a été envoyé automatiquement depuis votre application P'titsPas.<br>
|
|
Configuration testée le ${new Date().toLocaleString('fr-FR')}
|
|
</p>
|
|
</div>
|
|
`,
|
|
});
|
|
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<boolean>('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<void> {
|
|
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;
|
|
}
|
|
}
|
|
|