se agrego envio de correos caarga masiva
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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...',
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user