se agrego envio de correos caarga masiva

This commit is contained in:
Emilio-11
2026-04-09 16:59:22 -06:00
parent d0da4e81cf
commit 7110104e90
8 changed files with 323 additions and 35 deletions
+1 -13
View File
@@ -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');
}
}
}
+8
View File
@@ -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')
+120 -13
View File
@@ -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;
}
}
+6 -3
View File
@@ -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();
+34
View File
@@ -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;
}
+25 -3
View File
@@ -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()
+69 -3
View File
@@ -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...',
})
}),
);
};
+60
View File
@@ -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 },