[Backend] API admin configuration avec test SMTP #67
@ -37,6 +37,7 @@
|
|||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"joi": "^18.0.0",
|
"joi": "^18.0.0",
|
||||||
"mapped-types": "^0.0.1",
|
"mapped-types": "^0.0.1",
|
||||||
|
"nodemailer": "^6.9.16",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
@ -54,6 +55,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^6.4.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
|
|||||||
232
backend/src/modules/config/config.controller.ts
Normal file
232
backend/src/modules/config/config.controller.ts
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Configuration } from '../../entities/configuration.entity';
|
import { Configuration } from '../../entities/configuration.entity';
|
||||||
import { AppConfigService } from './config.service';
|
import { AppConfigService } from './config.service';
|
||||||
|
import { ConfigController } from './config.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Configuration])],
|
imports: [TypeOrmModule.forFeature([Configuration])],
|
||||||
|
controllers: [ConfigController],
|
||||||
providers: [AppConfigService],
|
providers: [AppConfigService],
|
||||||
exports: [AppConfigService],
|
exports: [AppConfigService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -176,13 +176,77 @@ export class AppConfigService implements OnModuleInit {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test de la configuration SMTP
|
* 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<boolean> {
|
async testSmtpConnection(testEmail?: string): Promise<{ success: boolean; error?: string }> {
|
||||||
// TODO: Implémenter le test SMTP avec Nodemailer
|
try {
|
||||||
// Pour l'instant, on retourne true
|
this.logger.log('🧪 Test de connexion SMTP...');
|
||||||
this.logger.log('🧪 Test de connexion SMTP (à implémenter)');
|
|
||||||
return true;
|
// 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',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
7
backend/src/modules/config/dto/test-smtp.dto.ts
Normal file
7
backend/src/modules/config/dto/test-smtp.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class TestSmtpDto {
|
||||||
|
@IsEmail()
|
||||||
|
testEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
67
backend/src/modules/config/dto/update-config.dto.ts
Normal file
67
backend/src/modules/config/dto/update-config.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user