Se generalizó envio de correos a diferentes sistemas

This commit is contained in:
evenegas
2025-05-02 16:06:58 -06:00
parent 66c81120aa
commit 336bf545b7
19 changed files with 742 additions and 307 deletions
+5 -2
View File
@@ -12,7 +12,10 @@ import { SistemaModule } from './sistema/sistema.module';
@Module({
imports: [
ConfigModule.forRoot(),
ConfigModule.forRoot(
{isGlobal: true}
),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.db_host,
@@ -21,7 +24,7 @@ import { SistemaModule } from './sistema/sistema.module';
password: process.env.db_password,
port: Number(process.env.db_port),
synchronize: true,
dropSchema: true, // elimina la base de datos
dropSchema: false, // elimina la base de datos
// logging: true, // Habilita los logs para depuración
autoLoadEntities: true, // Carga automáticamente las entidades
+22
View File
@@ -0,0 +1,22 @@
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
@Module({
imports: [
ConfigModule, // Ya es global, pero lo puedes dejar
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: '1d' },
}),
}),
],
providers: [AuthService],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
+24
View File
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
generarToken(payload: any) {
return this.jwtService.sign(payload, {
secret: process.env.JWT_SECRET, // <-- Aquí lo defines explícitamente
expiresIn: '1d',
});
}
verificarToken(token: string) {
try {
return this.jwtService.verify(token, {
secret: process.env.JWT_SECRET, // <-- También aquí
});
} catch {
return null;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import * as crypto from 'crypto';
export function encrypt(text: string) {
const secret = process.env.KEY_SECRET;
if (!secret) throw new Error('Falta la variable de entorno KEY_SECRET');
const key = crypto.scryptSync(secret, 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
iv: iv.toString('hex'),
encryptedData: encrypted,
};
}
export function decrypt(encryptedData: string, ivHex: string) {
const secret = process.env.KEY_SECRET;
if (!process.env.KEY_SECRET) {
throw new Error('Falta la variable de entorno KEY_SECRET');
}
if (!secret) throw new Error('Falta la variable de entorno KEY_SECRET');
const key = crypto.scryptSync(secret, 'salt', 32);
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
+29
View File
@@ -0,0 +1,29 @@
import { IsString, IsEmail, IsDate, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class SendCorreoDto {
@IsEmail()
to: string;
@IsString()
@IsOptional()
subject: string;
@IsString()
@IsOptional()
text: string;
@IsDate()
@Type(() => Date)
fecha_recibido: Date;
@IsString()
@IsOptional()
html: string;
@IsOptional()
adjuntos: any;
}
+57 -35
View File
@@ -6,24 +6,30 @@ import {
Headers,
UnauthorizedException,
Get,
UseInterceptors,
} from '@nestjs/common';
import { MailService } from './mail.service';
import { ExcelService } from 'src/excel/excel.service';
import { ApiTags } from '@nestjs/swagger';
import { ApiFindAllMails, ApiSendMail, ApiSendMailExcel} from './mail.decorators';
import { ApiSendMail} from './mail.decorators';
import { Repository } from 'typeorm';
import { Sistema } from 'src/typeorm/votacionesPrueba.entity';
import { Sistema } from 'src/typeorm/entidades';
import { InjectRepository } from '@nestjs/typeorm';
import { SistemaService } from 'src/sistema/sistema.service';
import { JwtService } from '@nestjs/jwt';
import { SendCorreoDto } from 'src/mail/dto/send-email.dto';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthService } from 'src/auth/auth.service';
@ApiTags('Mail') // Grupo de endpoints
@Controller('mail')
export class MailController {
constructor(
private readonly sistemaService: SistemaService,
private readonly mailService: MailService,
private readonly excelService: ExcelService,
@InjectRepository(Sistema) private readonly sistemaRepository: Repository<Sistema>,
// private readonly excelService: ExcelService,
private readonly authService: AuthService
) {}
@@ -35,22 +41,32 @@ export class MailController {
@ApiSendMail()
async sendMail(
@Headers() headers:{token:string},
@Body() body: { to: string; subject: string; text: string; fecha_recibido: Date, html:string, adjuntos:any }
@Body() body: SendCorreoDto
) {
if (!headers.token) {
throw new UnauthorizedException('Header "token" es requerido');
}
const sistem=await this.sistemaRepository.findOne({where:{token:headers.token}})
if(!sistem){
throw new UnauthorizedException('Password incorrecto');
}
let payload
const { to, subject, text, html, fecha_recibido, adjuntos } = body;
payload = await this.authService.verificarToken(headers.token);
if (!payload || !payload.nombre) {
throw new UnauthorizedException('Token inválido o incompleto');
}
const sistem= await this.sistemaService.findByNombre(payload.nombre)
if(!sistem){
throw new UnauthorizedException('sistema No encontrado');
}
const result = await this.mailService.sendMail(to, subject, text,html, fecha_recibido,sistem,adjuntos); // sistem
const result = await this.mailService.sendMail(body,sistem); // sistem
return result;
@@ -58,42 +74,48 @@ export class MailController {
}
/*
@Post('send-excel')
@UseInterceptors(FileInterceptor('file'))
@ApiSendMailExcel()
async sendMailExcel(@Headers() headers:{password:string},@Body() file: Express.Multer.File) {
if (!headers.password) {
throw new UnauthorizedException('Header "Password" es requerido');
async sendMailExcel(
@Headers() headers: { token: string },
@UploadedFile() file: Express.Multer.File
) {
if (!headers.token) {
throw new UnauthorizedException('Header "token" es requerido');
}
const sistem = await this.sistemaRepository.findOne({ where: { token: headers.password } });
let payload;
try {
payload = await this.jwtService.verify(headers.token);
} catch {
throw new UnauthorizedException('Token inválido');
}
const sistem = await this.sistemaService.findByNombre(payload.nombre);
if (!sistem) {
throw new UnauthorizedException('Password incorrecto');
throw new UnauthorizedException('Sistema no encontrado');
}
if (!file) {
throw new Error('Archivo Excel no encontrado');
}
const emails=this.excelService.readEmailsFromExcel(file);
let fechaActual: Date = new Date();
for (const email of await emails) {
}
const emails = await this.excelService.readEmailsFromExcel(file);
const fechaActual: Date = new Date();
for (const email of emails) {
try {
await this.mailService.sendMail(email, "subject", "text","html", fechaActual,sistem);
console.log(`📧 Enviado a: ${email}`);
await this.mailService.sendMail(email, 'subject', 'text', 'html', fechaActual, sistem);
console.log(`Enviado a: ${email}`);
} catch (error) {
console.error(`Error enviando a ${email}:`, error.message);
console.error(`Error enviando a ${email}:`, error.message);
}
}
}
@Get()
@ApiFindAllMails()
async findAll(@Headers() headers:{token:string}){
return await this.mailService.findAll(headers.token)
return { message: 'Correos enviados (si no hubo errores)' };
}
*/
}
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -7,11 +7,13 @@ import {
Status,
Correo,
Sistema,
}from 'src/typeorm/votacionesPrueba.entity'
}from 'src/typeorm/entidades'
import { SistemaModule } from 'src/sistema/sistema.module';
import { JwtService } from '@nestjs/jwt';
import { AuthService } from 'src/auth/auth.service';
@Module({
providers: [MailService],
providers: [MailService,JwtService,AuthService],
imports: [
ExcelModule,
SistemaModule,
+1 -1
View File
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { MailService } from './mail.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Correo, Sistema, Status } from '../typeorm/votacionesPrueba.entity'; // Ajusta si es necesario
import { Correo, Sistema, Status } from '../typeorm/entidades'; // Ajusta si es necesario
describe('MailService', () => {
let service: MailService;
+69 -25
View File
@@ -3,18 +3,25 @@ import * as nodemailer from 'nodemailer';
import * as dotenv from 'dotenv';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Correo, Sistema, Status } from '../typeorm/votacionesPrueba.entity';
import { Correo, Sistema, Status } from '../typeorm/entidades';
import { SistemaService } from 'src/sistema/sistema.service';
import { SendCorreoDto } from 'src/mail/dto/send-email.dto';
import { AuthService } from 'src/auth/auth.service';
import { decrypt } from '../crypto/crypto.util';
dotenv.config();
@Injectable()
export class MailService {
private transporter;
constructor(
private readonly sistemaService: SistemaService,
private readonly authService: AuthService,
@InjectRepository(Correo) private readonly correoRepository: Repository<Correo>,
@InjectRepository(Status) private readonly statusRepository: Repository<Status>,
@InjectRepository(Sistema) private readonly sistemaRepository: Repository<Sistema>
@InjectRepository(Sistema) private readonly sistemaRepositosistemaRepositoryry: Repository<Sistema>
) {
@@ -31,6 +38,8 @@ export class MailService {
},
}); */
/*
this.transporter = nodemailer.createTransport({
host: 'smtp.gmail.com', // o smtprelay.gmail.com (ver punto 2)
port: 587,
@@ -45,7 +54,7 @@ export class MailService {
});
*/
/*
this.transporter = nodemailer.createTransport({
@@ -57,39 +66,74 @@ export class MailService {
}
async findAll(TOKEN:string){
async findAllBySistema(TOKEN:string,nombreSistema:string){
if (!TOKEN) {
throw new UnauthorizedException('Header "Password" es requerido');
}
const sistem = await this.sistemaRepository.findOne({ where: { token:TOKEN } });
if (!sistem) {
const sistem=await this.sistemaService.acceso(TOKEN)
if(!sistem){
throw new UnauthorizedException('Password incorrecto');
}
const result= await this.correoRepository.find()
const result= await this.correoRepository.find({where:{
id_sistema:{nombre_sistema:nombreSistema}
},
relations: {
id_sistema: true,
id_status: true,
},}
)
return result
}
async sendMail(to: string, subject: string, text: string, html: string, fecha_recibido: Date, sistema, adjuntos?: any[] ) {
async sendMail(sendEmail:SendCorreoDto,sistem:Sistema,) {
const decryptedPassword = decrypt(
sistem.email_password_encrypted,
sistem.email_password_iv
);
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com', // o smtprelay.gmail.com (ver punto 2)
port: 587,
secure: false,
requireTLS: true, // TLS STARTTLS
auth: { user:sistem.email, pass:decryptedPassword},
pool: true, // <‑‑ activa reuse
maxConnections: 1, // una sola conexión viva
maxMessages: 100, // reabrir después de 100 envíos
rateDelta: 2000, // ventana 1 s
rateLimit: 1 // máx. 5 mensajes/seg
});
console.log("estos son los archivos adjuntos en el service",adjuntos);
console.log("estos son los archivos adjuntos en el service",sendEmail.adjuntos);
const mailOptions = {
from: process.env.USER_GMAIL,
to,
subject,
text,
html,
from: sistem.email,
to:sendEmail.to,
subject:sendEmail.subject,
text: sendEmail.text,
html:sendEmail.html,
// OJO: nodemailer usa 'attachments', no 'adjuntos'
attachments: (adjuntos || []).map((adj) => {
attachments: (sendEmail.adjuntos || []).map((adj) => {
// Asegúrate de no incluir 'data:image/png;base64,' en 'content'
let base64Clean = adj.content;
// Si viene con prefijo "data:image...", lo quitamos
@@ -114,7 +158,7 @@ export class MailService {
let resMail = await this.transporter.sendMail(mailOptions);
let resMail = await transporter.sendMail(mailOptions);
const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido";
@@ -128,10 +172,10 @@ export class MailService {
const info= this.correoRepository.create({
id_status: status,
id_sistema: sistema,
fecha_recibido: fecha_recibido,
id_sistema: sistem,
fecha_recibido: sendEmail.fecha_recibido,
fecha_enviado: new Date(),
destinatario: to,
destinatario: sendEmail.to,
remitente: process.env.USER_GMAIL,
});
+4 -7
View File
@@ -1,8 +1,5 @@
import { IsString, IsOptional, IsBoolean, IsNotEmpty } from 'class-validator';
// comprobar-sistema.dto.ts
export class ComprobarSistemaDto {
@IsNotEmpty()
@IsString()
token: string;
}
password: string;
}
+10
View File
@@ -13,4 +13,14 @@ export class CreateSistemaDto {
@IsString()
password: string;
@IsNotEmpty()
@IsString()
email: string;
@IsNotEmpty()
@IsString()
email_password:string
}
+14 -5
View File
@@ -1,8 +1,8 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } 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 { ApiCreateSistema, ApiGenerarToken } from './sistema.decorators';
import { ComprobarSistemaDto } from './dto/comprobar-sistema';
import { ApiAccesoSistema, ApiCreateSistema, ApiGenerarToken } from './sistema.decorators';
@Controller('sistema')
export class SistemaController {
@@ -16,7 +16,16 @@ export class SistemaController {
@Patch(':nombre_sistema')
@ApiGenerarToken()
update(@Param('nombre_sistema') nombre_sistema: string, @Body() password: string) {
return this.sistemaService.Generar_Token(nombre_sistema, password);
update(
@Param('nombre_sistema') nombre_sistema: string,
@Body() comprobarSistemaDto: ComprobarSistemaDto,
) {
return this.sistemaService.Generar_Token(nombre_sistema, comprobarSistemaDto.password);
}
@ApiAccesoSistema()
@Get('acceso')
async acceso(@Query('token') token: string) {
return this.sistemaService.acceso(token);
}
}
+59 -16
View File
@@ -1,26 +1,47 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam } from '@nestjs/swagger';
import { ApiBody, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
export const ApiCreateSistema = () => {
return applyDecorators(
ApiOperation({
summary: 'Registrar un nuevo sistema',
description: `Este endpoint permite dar de alta un sistema para poder utilizar funcionalidades autenticadas mediante contraseña.
Se debe proporcionar:
- **Nombre**: Nombre único del sistema.
- **ip**: Dirección IP desde la cual se harán peticiones.
- **password**: Contraseña segura que el sistema usará para autenticarse.`,
description: `Este endpoint permite registrar un sistema que podrá autenticarse y utilizar servicios protegidos.
Debe incluirse la contraseña maestra del administrador para autorizar la creación.
Además, se deben proporcionar las credenciales del correo que el sistema usará para enviar emails.`,
}),
ApiBody({
description: 'Datos del sistema a registrar',
schema: {
type: 'object',
properties: {
Nombre: { type: 'string', example: 'Sistema de Reportes' },
ip: { type: 'string', example: '192.168.1.100' },
password: { type: 'string', example: 'clave-segura-123' },
Nombre: {
type: 'string',
example: 'Sistema de Notificaciones',
description: 'Nombre único del sistema a registrar',
},
ip: {
type: 'string',
example: '192.168.0.10',
description: 'Dirección IP desde donde se harán las peticiones',
},
email: {
type: 'string',
format: 'email',
example: 'sistema@gmail.com',
description: 'Correo electrónico del sistema para enviar emails',
},
email_password: {
type: 'string',
example: 'contraseña-de-aplicacion',
description: 'Contraseña de aplicación del correo proporcionado',
},
password: {
type: 'string',
example: 'clave-admin-secreta',
description: 'Contraseña maestra para autorizar el registro',
},
},
required: ['Nombre', 'ip', 'password'],
required: ['Nombre', 'ip', 'email', 'email_password', 'password'],
},
})
);
@@ -30,21 +51,43 @@ export const ApiGenerarToken = () => {
return applyDecorators(
ApiOperation({
summary: 'Generar token JWT para un sistema',
description: `Este endpoint permite generar un token de acceso válido para el sistema registrado.
Se debe enviar el nombre del sistema como parámetro de ruta y la contraseña como cuerpo.`,
description: `Este endpoint permite generar un token de autenticación JWT para un sistema previamente registrado.
Se debe enviar el nombre del sistema como parámetro de ruta y su contraseña de registro en el cuerpo.`,
}),
ApiParam({
name: 'nombre_sistema',
required: true,
description: 'Nombre exacto del sistema registrado',
example: 'Sistema de Reportes',
example: 'Sistema de Notificaciones',
}),
ApiBody({
description: 'Contraseña del sistema',
description: 'Contraseña del sistema para verificar identidad',
schema: {
type: 'string',
example: 'clave-segura-123',
type: 'object',
properties: {
password: {
type: 'string',
example: 'clave-admin-secreta',
description: 'Contraseña de autenticación del sistema',
},
},
required: ['password'],
},
})
);
};
export const ApiAccesoSistema = () => {
return applyDecorators(
ApiOperation({
summary: 'Verificar acceso del sistema mediante token',
description: `Este endpoint permite verificar si un token JWT enviado por un sistema es válido y tiene acceso.`,
}),
ApiQuery({
name: 'token',
required: true,
description: 'Token JWT del sistema',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
);
};
+4 -2
View File
@@ -2,14 +2,16 @@ import { Module } from '@nestjs/common';
import { SistemaService } from './sistema.service';
import { SistemaController } from './sistema.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Sistema } from 'src/typeorm/votacionesPrueba.entity';
import { Sistema } from 'src/typeorm/entidades';
import { AuthModule } from 'src/auth/auth.module';
@Module({
controllers: [SistemaController],
providers: [SistemaService],
imports:[
TypeOrmModule.forFeature([
Sistema])
Sistema]),
AuthModule
],
exports:[SistemaModule,SistemaService]
})
+47 -39
View File
@@ -1,63 +1,71 @@
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { CreateSistemaDto } from './dto/create-sistema.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Sistema } from 'src/typeorm/votacionesPrueba.entity';
import { Sistema } from 'src/typeorm/entidades';
import { Repository } from 'typeorm';
import { randomBytes } from 'crypto';
import { ComprobarSistemaDto } from './dto/comprobar-sistema';
import { AuthService } from 'src/auth/auth.service';
import { encrypt } from 'src/crypto/crypto.util';
@Injectable()
export class SistemaService {
constructor(
@InjectRepository(Sistema) private readonly sistemaRepository: Repository<Sistema>
@InjectRepository(Sistema) private readonly sistemaRepository: Repository<Sistema>,
private readonly authService: AuthService
) {}
){}
generarPassword(longitud = 12): string {
return randomBytes(longitud)
.toString('base64')
.slice(0, longitud)
.replace(/[+/=]/g, () => String.fromCharCode(33 + Math.floor(Math.random() * 94))); // reemplaza caracteres raros
}
async Generar_Token(nombreSistema:string,adminPasword:string) {
if(adminPasword!==process.env.ADMIN_PASSWORD){
throw new UnauthorizedException('Password erroneo');
async Generar_Token(nombreSistema: string, adminPassword: string) {
if (adminPassword !== process.env.ADMIN_PASSWORD) {
throw new UnauthorizedException('Password erróneo');
}
const sistema = await this.sistemaRepository.findOneBy({nombre_sistema:nombreSistema})
if(!sistema){
throw new NotFoundException('Sistema no encontrado')
}
sistema.token=this.generarPassword()
this.sistemaRepository.save(sistema)
return sistema.token;
const sistema = await this.sistemaRepository.findOneBy({ nombre_sistema: nombreSistema });
if (!sistema) {
throw new NotFoundException('Sistema no encontrado');
}
const token = this.authService.generarToken({ sistemaId: sistema.id_sistema, nombre: sistema.nombre_sistema, ip:sistema.ip });
return token;
}
async acceso(TOKEN:string){
const result = await this.sistemaRepository.findOne({where:{token:TOKEN}})
if(!result){
throw new UnauthorizedException('token erroneo')
async acceso(token: string) {
const decoded = this.authService.verificarToken(token);
if (!decoded) {
throw new UnauthorizedException('Token inválido');
}
return true
const sistema = await this.sistemaRepository.findOneBy({ id_sistema: decoded.sistemaId });
if (!sistema) {
throw new UnauthorizedException('Sistema no encontrado');
}
return true;
}
async create(createSistemaDto: CreateSistemaDto) {
if(createSistemaDto.password!==process.env.ADMIN_PASSWORD){
throw new UnauthorizedException('Password erroneo');
if (createSistemaDto.password !== process.env.ADMIN_PASSWORD) {
throw new UnauthorizedException('Password erróneo');
}
const sistem = this.sistemaRepository.create({
nombre_sistema:createSistemaDto.Nombre,
ip:createSistemaDto.ip,
});
await this.sistemaRepository.save(sistem)
const { encryptedData, iv } = encrypt(createSistemaDto.email_password);
return this.Generar_Token(createSistemaDto.Nombre,createSistemaDto.password);
const sistema = this.sistemaRepository.create({
nombre_sistema: createSistemaDto.Nombre,
ip: createSistemaDto.ip,
email:createSistemaDto.email,
email_password_encrypted: encryptedData,
email_password_iv: iv
});
await this.sistemaRepository.save(sistema);
return this.Generar_Token(createSistemaDto.Nombre, createSistemaDto.password);
}
async findByNombre(sistema:string){
const sistem= this.sistemaRepository.findOne({where:{nombre_sistema:sistema}})
if(!sistem){
throw new UnauthorizedException('Password erróneo');
}
return sistem;
}
}
@@ -6,15 +6,21 @@ export class Sistema {
@PrimaryGeneratedColumn()
id_sistema: number;
@Column({ type: 'varchar', length: 255, nullable: true })
token: string;
@Column({ type: 'varchar', length: 50 })
nombre_sistema: string;
@Column({ type: 'varchar', length: 50 })
ip:string;
@Column()
email: string;
@Column()
email_password_encrypted: string;
@Column()
email_password_iv: string;
@OneToMany(() => Correo, (correo) => correo.id_sistema)
correos: Correo[];
}