From 7110104e9014f7dacfdde43bc776010be37d745a Mon Sep 17 00:00:00 2001 From: Emilio-11 Date: Thu, 9 Apr 2026 16:59:22 -0600 Subject: [PATCH] se agrego envio de correos caarga masiva --- src/auth/auth.service.ts | 14 +--- src/mail/mail.controller.ts | 8 ++ src/mail/mail.service.ts | 133 +++++++++++++++++++++++++++--- src/main.ts | 9 +- src/sistema/dto/sistema-update.ts | 34 ++++++++ src/sistema/sistema.controller.ts | 28 ++++++- src/sistema/sistema.decorators.ts | 72 +++++++++++++++- src/sistema/sistema.service.ts | 60 ++++++++++++++ 8 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 src/sistema/dto/sistema-update.ts diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 8a06752..fe8b081 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,36 +3,24 @@ import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { - constructor(private jwtService: JwtService) { } + constructor(private jwtService: JwtService) {} generarToken(payload: any) { console.log('JWT_SECRET:', process.env.JWT_SECRET); return this.jwtService.sign(payload, { secret: process.env.JWT_SECRET, - }); } async verificarToken(token: string) { - console.log('JWT_SECRET:', process.env.JWT_SECRET); - console.log('JWT_SECRET:', process.env.JWT_SECRET); - console.log('Verificando token:', token); - console.log('retirar estos console.log'); - try { const result = await this.jwtService.verify(token, { secret: process.env.JWT_SECRET, }); console.log('Resultado de verificación:', result); return result; - } catch (error) { - - throw new Error('Token inválido o expirado'); } - - - } } diff --git a/src/mail/mail.controller.ts b/src/mail/mail.controller.ts index da174b8..afcf14a 100644 --- a/src/mail/mail.controller.ts +++ b/src/mail/mail.controller.ts @@ -47,6 +47,14 @@ export class MailController { return result; } + @Post('masivo') + async enviarMasivo( + @Body() data: SendCorreoDto[], + @Headers() headers: { token: string }, + ) { + return await this.mailService.sendBulkMail(data, headers.token); + } + /* @Post('send-excel') diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index aa35bd9..eb415d0 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -78,12 +78,10 @@ export class MailService { } async sendMail(sendEmail: SendCorreoDto, token: string) { - console.log('token', token); let payload; payload = await this.authService.verificarToken(token); - console.log('payload', payload); if (!payload || !payload.nombre) { throw new UnauthorizedException('Token inválido o incompleto'); } @@ -99,15 +97,11 @@ export class MailService { sistem.email_password_iv, ); - console.log('decryptedPassword', decryptedPassword); - const decryptedToken = decrypt( sistem.refreshToken_encrypted, sistem.refreshToken_iv, ); - console.log('decryptedToken', decryptedToken); - console.log('sistem', { type: 'OAuth2', user: sistem.email, @@ -134,11 +128,6 @@ export class MailService { rateLimit: 1, }); - console.log( - 'estos son los archivos adjuntos en el service', - sendEmail.adjuntos, - ); - const mailOptions = { from: sistem.email, to: sendEmail.to, @@ -164,8 +153,6 @@ export class MailService { }), }; - console.log('mailOptions', mailOptions); - let resMail = await transporter.sendMail(mailOptions); if (!resMail) throw new Error(''); @@ -196,4 +183,124 @@ export class MailService { return statusTexto; } + + private async saveEmailStatus( + dto: SendCorreoDto, + sistem: any, + statusText: string, + ) { + try { + // 1. Buscar el estado en la base de datos (Enviado o Fallido) + let status = await this.statusRepository.findOne({ + where: { status: statusText }, + }); + + // 2. Si el estado no existe en el catálogo, lo creamos para evitar errores de FK + if (!status) { + status = this.statusRepository.create({ status: statusText }); + await this.statusRepository.save(status); + } + + // 3. Crear el registro histórico del correo + const info = this.correoRepository.create({ + id_status: status, + id_sistema: sistem, + fecha_recibido: dto.fecha_recibido || new Date(), // Fecha en que se solicitó + fecha_enviado: new Date(), // Fecha real de salida + destinatario: dto.to, + remitente: sistem.email, + }); + + // 4. Guardar el registro de forma asíncrona + return await this.correoRepository.save(info); + } catch (error) { + // Solo hacemos log del error para no detener el proceso masivo si falla el log en DB + console.error( + `Error crítico al guardar status en DB para ${dto.to}:`, + error.message, + ); + } + } + + async sendBulkMail(emailsData: SendCorreoDto[], token: string) { + // 1. Validación de acceso y obtención de credenciales (una sola vez) + const payload = await this.authService.verificarToken(token); + if (!payload || !payload.nombre) + throw new UnauthorizedException('Token inválido'); + + const sistem = await this.sistemaService.findByNombre(payload.nombre); + if (!sistem) throw new UnauthorizedException('Sistema no encontrado'); + + const decryptedPassword = decrypt( + sistem.email_password_encrypted, + sistem.email_password_iv, + ); + const decryptedToken = decrypt( + sistem.refreshToken_encrypted, + sistem.refreshToken_iv, + ); + + // 2. Crear el transporter único con Pool + const transporter = nodemailer.createTransport({ + service: 'gmail', + pool: true, + maxConnections: 5, // Procesará hasta 5 conexiones SMTP simultáneas + auth: { + type: 'OAuth2', + user: sistem.email, + clientId: sistem.clientId, + clientSecret: decryptedPassword, + refreshToken: decryptedToken, + }, + }); + + const CHUNK_SIZE = 50; // Tamaño de cada lote + const results = { successful: 0, failed: 0 }; + + // 3. Proceso por Chunks (Lotes) + for (let i = 0; i < emailsData.length; i += CHUNK_SIZE) { + const chunk = emailsData.slice(i, i + CHUNK_SIZE); + + // Procesamos el lote actual en paralelo para ganar velocidad + await Promise.all( + chunk.map(async (emailDto) => { + try { + const mailOptions = { + from: sistem.email, + to: emailDto.to, + subject: emailDto.subject, + html: emailDto.html, + attachments: (emailDto.adjuntos || []).map((adj) => ({ + filename: adj.filename || 'adjunto.png', + content: Buffer.from( + adj.content.split('base64,')[1] || adj.content, + 'base64', + ), + cid: adj.cid, + contentType: adj.contentType || 'image/png', + })), + }; + + await transporter.sendMail(mailOptions); + await this.saveEmailStatus(emailDto, sistem, 'Enviado'); + results.successful++; + } catch (error) { + console.error( + `Error en envío masivo a ${emailDto.to}:`, + error.message, + ); + await this.saveEmailStatus(emailDto, sistem, 'Fallido'); + results.failed++; + } + }), + ); + + console.log( + `Lote completado: ${i + chunk.length} de ${emailsData.length}`, + ); + } + + transporter.close(); + return results; + } } diff --git a/src/main.ts b/src/main.ts index 4b9240c..96e92a4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,12 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; - +import { json } from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.use(json({ limit: '50mb' })); app.enableCors(); @@ -19,9 +20,11 @@ async function bootstrap() { .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('documentation', app, document); - + await app.listen(process.env.PORT ?? 3000); - console.log(`Aplicación corriendo en: http://localhost:${process.env.PORT ?? 3000}`); + console.log( + `Aplicación corriendo en: http://localhost:${process.env.PORT ?? 3000}`, + ); } bootstrap(); diff --git a/src/sistema/dto/sistema-update.ts b/src/sistema/dto/sistema-update.ts new file mode 100644 index 0000000..798694e --- /dev/null +++ b/src/sistema/dto/sistema-update.ts @@ -0,0 +1,34 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class UpdateSistemaDto { + @IsOptional() + @IsString() + Nombre?: string; + + @IsOptional() + @IsString() + ip?: string; + + //Password del sistema + @IsNotEmpty() + @IsString() + token: string; + + @IsOptional() + @IsString() + email?: string; + + //clientSecret + @IsOptional() + @IsString() + email_password?: string; + + @IsOptional() + @IsString() + clientId?: string; + + //TU_REFRESH_TOKEN + @IsOptional() + @IsString() + refreshToken?: string; +} diff --git a/src/sistema/sistema.controller.ts b/src/sistema/sistema.controller.ts index 51d5009..6e88785 100644 --- a/src/sistema/sistema.controller.ts +++ b/src/sistema/sistema.controller.ts @@ -1,8 +1,22 @@ -import { Controller, Get, Post, Body, Patch, Param, Query } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Query, +} from '@nestjs/common'; import { SistemaService } from './sistema.service'; import { CreateSistemaDto } from './dto/create-sistema.dto'; import { ComprobarSistemaDto } from './dto/comprobar-sistema'; -import { ApiAccesoSistema, ApiCreateSistema, ApiGenerarToken } from './sistema.decorators'; +import { + ApiAccesoSistema, + ApiCreateSistema, + ApiGenerarToken, + ApiUpdateSistema, +} from './sistema.decorators'; +import { UpdateSistemaDto } from './dto/sistema-update'; @Controller('sistema') export class SistemaController { @@ -18,6 +32,11 @@ export class SistemaController { create(@Body() createSistemaDto: CreateSistemaDto) { return this.sistemaService.create(createSistemaDto); } + @Patch('config') + @ApiUpdateSistema() + update2(@Body() updateSistemaDto: UpdateSistemaDto) { + return this.sistemaService.update(updateSistemaDto); + } @Patch(':nombre_sistema') @ApiGenerarToken() @@ -25,7 +44,10 @@ export class SistemaController { @Param('nombre_sistema') nombre_sistema: string, @Body() comprobarSistemaDto: ComprobarSistemaDto, ) { - return this.sistemaService.Generar_Token(nombre_sistema, comprobarSistemaDto.password); + return this.sistemaService.Generar_Token( + nombre_sistema, + comprobarSistemaDto.password, + ); } @ApiAccesoSistema() diff --git a/src/sistema/sistema.decorators.ts b/src/sistema/sistema.decorators.ts index 74405e4..f775a89 100644 --- a/src/sistema/sistema.decorators.ts +++ b/src/sistema/sistema.decorators.ts @@ -1,6 +1,72 @@ import { applyDecorators } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger'; +export const ApiUpdateSistema = () => { + return applyDecorators( + ApiOperation({ + summary: 'Actualizar configuración del sistema', + description: ` +Este endpoint permite **actualizar parcialmente** la configuración de un sistema existente. + +🔐 **Requiere token del sistema** para autorizar la operación. + +Se pueden actualizar uno o más de los siguientes campos: +- Nombre del sistema +- IP +- Credenciales de correo +- Credenciales OAuth (clientId, refreshToken) + +⚠️ Solo se actualizarán los campos enviados en el cuerpo. + `, + }), + ApiBody({ + description: 'Datos a actualizar del sistema', + schema: { + type: 'object', + properties: { + Nombre: { + type: 'string', + example: 'Sistema de Notificaciones', + description: 'Nuevo nombre del sistema', + }, + ip: { + type: 'string', + example: '192.168.0.20', + description: 'Nueva dirección IP autorizada', + }, + token: { + type: 'string', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + description: 'Token JWT del sistema (obligatorio)', + }, + email: { + type: 'string', + format: 'email', + example: 'sistema@gmail.com', + description: 'Correo del sistema para envío de emails', + }, + email_password: { + type: 'string', + example: 'app-password-123', + description: 'Contraseña o secreto del correo', + }, + clientId: { + type: 'string', + example: '1234567890-abc.apps.googleusercontent.com', + description: 'Client ID de Google OAuth', + }, + refreshToken: { + type: 'string', + example: '1//0gxxxxxxxxxxxxxxxx', + description: 'Refresh Token OAuth del correo', + }, + }, + required: ['token'], + }, + }), + ); +}; + export const ApiCreateSistema = () => { return applyDecorators( ApiOperation({ @@ -43,7 +109,7 @@ Además, se deben proporcionar las credenciales del correo que el sistema usará }, required: ['Nombre', 'ip', 'email', 'email_password', 'password'], }, - }) + }), ); }; @@ -73,7 +139,7 @@ Se debe enviar el nombre del sistema como parámetro de ruta y su contraseña de }, required: ['password'], }, - }) + }), ); }; @@ -88,6 +154,6 @@ export const ApiAccesoSistema = () => { required: true, description: 'Token JWT del sistema', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', - }) + }), ); }; diff --git a/src/sistema/sistema.service.ts b/src/sistema/sistema.service.ts index 5258af2..72c9786 100644 --- a/src/sistema/sistema.service.ts +++ b/src/sistema/sistema.service.ts @@ -9,6 +9,7 @@ import { Sistema } from 'src/typeorm/entidades'; import { Repository } from 'typeorm'; import { AuthService } from 'src/auth/auth.service'; import { encrypt } from 'src/crypto/crypto.util'; +import { UpdateSistemaDto } from './dto/sistema-update'; @Injectable() export class SistemaService { @@ -92,6 +93,65 @@ export class SistemaService { ); } + async update(updateSistemaDto: UpdateSistemaDto) { + const decoded = await this.authService.verificarToken( + updateSistemaDto.token, + ); + console.log('aqui'); + if (!decoded) { + throw new UnauthorizedException('Token inválido'); + } + + let sistem = await this.sistemaRepository.findOneBy({ + id_sistema: decoded.sistemaId, + }); + + if (!sistem) { + throw new UnauthorizedException('Sistema no encontrado'); + } + let encryptedData_token: string | undefined; + let encryptedData_iv: string | undefined; + + let encrypted_password: string | undefined; + let iv_password: string | undefined; + + if (updateSistemaDto.refreshToken) { + ({ encryptedData: encryptedData_token, iv: encryptedData_iv } = encrypt( + updateSistemaDto.refreshToken, + )); + sistem.refreshToken_encrypted = encryptedData_token; + sistem.refreshToken_iv = encryptedData_iv; + } + + if (updateSistemaDto.email_password) { + ({ encryptedData: encrypted_password, iv: iv_password } = encrypt( + updateSistemaDto.email_password, + )); + sistem.email_password_encrypted = encrypted_password; + sistem.email_password_iv = iv_password; + } + + if (updateSistemaDto.Nombre) { + sistem.nombre_sistema = updateSistemaDto.Nombre; + } + + if (updateSistemaDto.ip) { + sistem.ip = updateSistemaDto.ip; + } + + if (updateSistemaDto.email) { + sistem.email = updateSistemaDto.email; + } + + if (updateSistemaDto.clientId) { + sistem.clientId = updateSistemaDto.clientId; + } + + await this.sistemaRepository.save(sistem); + + return 'se guardo correctamente'; + } + async findByNombre(sistema: string) { const sistem = this.sistemaRepository.findOne({ where: { nombre_sistema: sistema },