From eb1583b35bff3dee1578ec9bde00d3488f4984ae Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 28 Nov 2025 17:00:55 +0100 Subject: [PATCH] feat(backend): API admin configuration avec test SMTP (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation de l'API REST pour la gestion de la configuration système par les administrateurs. Nouveaux fichiers : - modules/config/config.controller.ts : Controller REST - modules/config/dto/update-config.dto.ts : DTO mise à jour - modules/config/dto/test-smtp.dto.ts : DTO test SMTP Endpoints créés : ✅ GET /api/v1/configuration/setup/status → Vérifier si la configuration initiale est terminée ✅ POST /api/v1/configuration/setup/complete → Marquer la configuration comme terminée ✅ POST /api/v1/configuration/test-smtp → Tester la connexion SMTP + envoi email de test ✅ PATCH /api/v1/configuration/bulk → Mise à jour multiple des configurations ✅ GET /api/v1/configuration → Récupérer toutes les configurations (admin) ✅ GET /api/v1/configuration/:category → Récupérer par catégorie (email/app/security) Fonctionnalités : - Validation des données avec class-validator - Test SMTP avec Nodemailer - Envoi d'email de test HTML - Gestion d'erreurs complète - Rechargement automatique du cache - Traçabilité des modifications Sécurité : - Guards commentés (à activer avec JWT) - Validation des catégories - Mots de passe masqués dans les réponses Dépendances ajoutées : - nodemailer ^6.9.16 - @types/nodemailer ^6.4.16 Tests effectués : ✅ GET /setup/status → {setupCompleted: false} ✅ GET /email → 8 configurations email ✅ Build Docker réussi ✅ Toutes les routes mappées correctement Ref: #6 --- backend/package.json | 2 + .../src/modules/config/config.controller.ts | 232 ++++++++++++++++++ backend/src/modules/config/config.module.ts | 2 + backend/src/modules/config/config.service.ts | 76 +++++- .../src/modules/config/dto/test-smtp.dto.ts | 7 + .../modules/config/dto/update-config.dto.ts | 67 +++++ 6 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 backend/src/modules/config/config.controller.ts create mode 100644 backend/src/modules/config/dto/test-smtp.dto.ts create mode 100644 backend/src/modules/config/dto/update-config.dto.ts diff --git a/backend/package.json b/backend/package.json index b0f43a4..6ffec65 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "class-validator": "^0.14.2", "joi": "^18.0.0", "mapped-types": "^0.0.1", + "nodemailer": "^6.9.16", "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", @@ -54,6 +55,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", diff --git a/backend/src/modules/config/config.controller.ts b/backend/src/modules/config/config.controller.ts new file mode 100644 index 0000000..ee1c9ba --- /dev/null +++ b/backend/src/modules/config/config.controller.ts @@ -0,0 +1,232 @@ +import { + Controller, + Get, + Patch, + Post, + Body, + Param, + UseGuards, + Request, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { AppConfigService } from './config.service'; +import { UpdateConfigDto } from './dto/update-config.dto'; +import { TestSmtpDto } from './dto/test-smtp.dto'; + +@Controller('configuration') +export class ConfigController { + constructor(private readonly configService: AppConfigService) {} + + /** + * Vérifier si la configuration initiale est terminée + * GET /api/v1/configuration/setup/status + */ + @Get('setup/status') + async getSetupStatus() { + try { + const isCompleted = this.configService.isSetupCompleted(); + return { + success: true, + data: { + setupCompleted: isCompleted, + }, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la vérification du statut de configuration', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Marquer la configuration initiale comme terminée + * POST /api/v1/configuration/setup/complete + */ + @Post('setup/complete') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async completeSetup(@Request() req: any) { + try { + // TODO: Récupérer l'ID utilisateur depuis le JWT + const userId = req.user?.id || 'system'; + + await this.configService.markSetupCompleted(userId); + + return { + success: true, + message: 'Configuration initiale terminée avec succès', + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la finalisation de la configuration', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Test de la connexion SMTP + * POST /api/v1/configuration/test-smtp + */ + @Post('test-smtp') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async testSmtp(@Body() testSmtpDto: TestSmtpDto) { + try { + const result = await this.configService.testSmtpConnection(testSmtpDto.testEmail); + + if (result.success) { + return { + success: true, + message: 'Connexion SMTP réussie. Email de test envoyé.', + }; + } else { + return { + success: false, + message: 'Échec du test SMTP', + error: result.error, + }; + } + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors du test SMTP', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Mise à jour multiple des configurations + * PATCH /api/v1/configuration/bulk + */ + @Patch('bulk') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async updateBulk(@Body() updateConfigDto: UpdateConfigDto, @Request() req: any) { + try { + // TODO: Récupérer l'ID utilisateur depuis le JWT + const userId = req.user?.id || null; + + let updated = 0; + const errors: string[] = []; + + // Parcourir toutes les clés du DTO + for (const [key, value] of Object.entries(updateConfigDto)) { + if (value !== undefined) { + try { + await this.configService.set(key, value, userId); + updated++; + } catch (error) { + errors.push(`${key}: ${error.message}`); + } + } + } + + // Recharger le cache après les modifications + await this.configService.loadCache(); + + if (errors.length > 0) { + return { + success: false, + message: 'Certaines configurations n\'ont pas pu être mises à jour', + updated, + errors, + }; + } + + return { + success: true, + message: 'Configuration mise à jour avec succès', + updated, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la mise à jour des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Récupérer toutes les configurations (pour l'admin) + * GET /api/v1/configuration + */ + @Get() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async getAll() { + try { + const configs = await this.configService.getAll(); + return { + success: true, + data: configs, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la récupération des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Récupérer les configurations par catégorie + * GET /api/v1/configuration/:category + */ + @Get(':category') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async getByCategory(@Param('category') category: string) { + try { + if (!['email', 'app', 'security'].includes(category)) { + throw new HttpException( + { + success: false, + message: 'Catégorie invalide. Valeurs acceptées: email, app, security', + }, + HttpStatus.BAD_REQUEST, + ); + } + + const configs = await this.configService.getByCategory(category); + return { + success: true, + data: configs, + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + { + success: false, + message: 'Erreur lors de la récupération des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/backend/src/modules/config/config.module.ts b/backend/src/modules/config/config.module.ts index 5b03c3d..aed9842 100644 --- a/backend/src/modules/config/config.module.ts +++ b/backend/src/modules/config/config.module.ts @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Configuration } from '../../entities/configuration.entity'; import { AppConfigService } from './config.service'; +import { ConfigController } from './config.controller'; @Module({ imports: [TypeOrmModule.forFeature([Configuration])], + controllers: [ConfigController], providers: [AppConfigService], exports: [AppConfigService], }) diff --git a/backend/src/modules/config/config.service.ts b/backend/src/modules/config/config.service.ts index 2119f87..421ccae 100644 --- a/backend/src/modules/config/config.service.ts +++ b/backend/src/modules/config/config.service.ts @@ -176,13 +176,77 @@ export class AppConfigService implements OnModuleInit { /** * Test de la configuration SMTP - * @returns true si la connexion SMTP fonctionne + * @param testEmail Email de destination pour le test + * @returns Objet avec success et error éventuel */ - async testSmtpConnection(): Promise { - // TODO: Implémenter le test SMTP avec Nodemailer - // Pour l'instant, on retourne true - this.logger.log('🧪 Test de connexion SMTP (à implémenter)'); - return true; + 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', + }; + } } /** diff --git a/backend/src/modules/config/dto/test-smtp.dto.ts b/backend/src/modules/config/dto/test-smtp.dto.ts new file mode 100644 index 0000000..42968b2 --- /dev/null +++ b/backend/src/modules/config/dto/test-smtp.dto.ts @@ -0,0 +1,7 @@ +import { IsEmail } from 'class-validator'; + +export class TestSmtpDto { + @IsEmail() + testEmail: string; +} + diff --git a/backend/src/modules/config/dto/update-config.dto.ts b/backend/src/modules/config/dto/update-config.dto.ts new file mode 100644 index 0000000..04e392d --- /dev/null +++ b/backend/src/modules/config/dto/update-config.dto.ts @@ -0,0 +1,67 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, IsEmail, IsUrl } from 'class-validator'; + +export class UpdateConfigDto { + // Configuration Email (SMTP) + @IsOptional() + @IsString() + smtp_host?: string; + + @IsOptional() + @IsNumber() + smtp_port?: number; + + @IsOptional() + @IsBoolean() + smtp_secure?: boolean; + + @IsOptional() + @IsBoolean() + smtp_auth_required?: boolean; + + @IsOptional() + @IsString() + smtp_user?: string; + + @IsOptional() + @IsString() + smtp_password?: string; + + @IsOptional() + @IsString() + email_from_name?: string; + + @IsOptional() + @IsEmail() + email_from_address?: string; + + // Configuration Application + @IsOptional() + @IsString() + app_name?: string; + + @IsOptional() + @IsUrl() + app_url?: string; + + @IsOptional() + @IsString() + app_logo_url?: string; + + // Configuration Sécurité + @IsOptional() + @IsNumber() + password_reset_token_expiry_days?: number; + + @IsOptional() + @IsNumber() + jwt_expiry_hours?: number; + + @IsOptional() + @IsNumber() + max_upload_size_mb?: number; + + @IsOptional() + @IsNumber() + bcrypt_rounds?: number; +} + -- 2.47.2