Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 648e980fa8 | |||
| cbdcd0ca1e | |||
| 7110104e90 | |||
| d0da4e81cf | |||
| 58997d6d8c | |||
| 8f39fb86b3 | |||
| c4fd905317 | |||
| 4a24eafa0d | |||
| a220f2933d | |||
| 904db0549f | |||
| ccf476bd6e | |||
| 8949c57bdb | |||
| 336bf545b7 | |||
| 66c81120aa | |||
| 9e8d339e11 | |||
| 50ac4081a9 | |||
| 58b49ba93d | |||
| b2e992dd04 | |||
| 2df592ee89 | |||
| ee622197c1 | |||
| 6c9d003cb9 | |||
| 1895e29cf2 |
@@ -1,6 +1,7 @@
|
||||
# Gmail
|
||||
USER_GMAIL=
|
||||
PASS_GMAIL=
|
||||
ADMIN_PASSWORD=12345
|
||||
|
||||
# MariaDB database settings
|
||||
db_type=mysql
|
||||
|
||||
Generated
+5849
-3721
File diff suppressed because it is too large
Load Diff
+30
-23
@@ -20,51 +20,58 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/bull": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.0.5",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"@nestjs/swagger": "^11.4.5",
|
||||
"@nestjs/typeorm": "^11.0.3",
|
||||
"bull": "^4.16.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.4",
|
||||
"csv-parser": "^3.2.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"mysql2": "^3.23.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"passport": "^0.7.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.21"
|
||||
"typeorm": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@eslint/js": "^9.39.5",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@nestjs/testing": "^11.1.28",
|
||||
"@swc/cli": "^0.8.1",
|
||||
"@swc/core": "^1.15.46",
|
||||
"@types/bull": "^3.15.9",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/multer": "^1.4.13",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier": "^3.9.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"supertest": "^7.2.2",
|
||||
"ts-jest": "^29.4.12",
|
||||
"ts-loader": "^9.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
"typescript-eslint": "^8.65.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
BIN
Binary file not shown.
+18
-7
@@ -2,36 +2,47 @@ import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
import {ExcelModule} from './excel/excel.module'
|
||||
import { ExcelModule } from './excel/excel.module'
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SistemaModule } from './sistema/sistema.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
ConfigModule.forRoot(
|
||||
{ isGlobal: true }
|
||||
),
|
||||
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
|
||||
}),
|
||||
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'mysql',
|
||||
type: 'mysql',
|
||||
host: process.env.db_host,
|
||||
username: process.env.db_username,
|
||||
database: process.env.db_database,
|
||||
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
|
||||
|
||||
|
||||
}),
|
||||
|
||||
|
||||
MailModule,
|
||||
ExcelModule,
|
||||
SistemaModule,
|
||||
|
||||
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.register({
|
||||
secret: process.env.JWT_SECRET
|
||||
})
|
||||
|
||||
|
||||
],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService, JwtModule],
|
||||
})
|
||||
export class AuthModule { }
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
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) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,14 +7,14 @@ import { ApiReadEmails } from './excel.decorators';
|
||||
@ApiTags('Excel') // Agrupa los endpoints en Swagger
|
||||
@Controller('excel')
|
||||
export class ExcelController {
|
||||
constructor(private readonly excelService: ExcelService) {}
|
||||
constructor(private readonly excelService: ExcelService) { }
|
||||
|
||||
@Post('read-emails')
|
||||
@UseInterceptors(FileInterceptor('file')) // Permite subir archivos
|
||||
@ApiConsumes('multipart/form-data') // Indica que se usa un formulario con archivos
|
||||
@ApiReadEmails()
|
||||
async getEmails(@UploadedFile() file: Express.Multer.File) {
|
||||
const emails = await this.excelService.readEmailsFromExcel(file);
|
||||
return { emails };
|
||||
}
|
||||
// @Post('read-emails')
|
||||
// @UseInterceptors(FileInterceptor('file')) // Permite subir archivos
|
||||
// @ApiConsumes('multipart/form-data') // Indica que se usa un formulario con archivos
|
||||
// @ApiReadEmails()
|
||||
// async getEmails(@UploadedFile() file: Express.Multer.File) {
|
||||
// const emails = await this.excelService.readEmailsFromExcel(file);
|
||||
// return { emails };
|
||||
// }
|
||||
}
|
||||
|
||||
+27
-27
@@ -4,42 +4,42 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ExcelService {
|
||||
async readEmailsFromExcel(file: Express.Multer.File) {
|
||||
|
||||
// async readEmailsFromExcel(file: Express.Multer.File) {
|
||||
|
||||
|
||||
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer); // 🔹 `await` para leer el archivo correctamente
|
||||
|
||||
const worksheet = workbook.getWorksheet(1); // Obtiene la primera hoja
|
||||
if (!worksheet) {
|
||||
throw new Error(' No se encontró ninguna hoja en el archivo.');
|
||||
}
|
||||
|
||||
const jsonData: any[] = [];
|
||||
|
||||
// 🔹 Iterar sobre las filas y extraer datos
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber === 1) return; // Ignorar encabezado
|
||||
// const workbook = new ExcelJS.Workbook();
|
||||
// await workbook.xlsx.load(file.buffer); // 🔹 `await` para leer el archivo correctamente
|
||||
|
||||
const rowData = {
|
||||
Correo: row.getCell(1).value, // Ajusta el índice según tu archivo
|
||||
};
|
||||
// const worksheet = workbook.getWorksheet(1); // Obtiene la primera hoja
|
||||
// if (!worksheet) {
|
||||
// throw new Error(' No se encontró ninguna hoja en el archivo.');
|
||||
// }
|
||||
|
||||
jsonData.push(rowData);
|
||||
});
|
||||
// const jsonData: any[] = [];
|
||||
|
||||
console.log(` Datos extraídos:`, jsonData);
|
||||
// // 🔹 Iterar sobre las filas y extraer datos
|
||||
// worksheet.eachRow((row, rowNumber) => {
|
||||
// if (rowNumber === 1) return; // Ignorar encabezado
|
||||
|
||||
// 🔹 Extraer correos de la columna "Correo"
|
||||
const emails = jsonData
|
||||
.map(row => row.Correo)
|
||||
.filter(email => typeof email === 'string');
|
||||
// const rowData = {
|
||||
// Correo: row.getCell(1).value, // Ajusta el índice según tu archivo
|
||||
// };
|
||||
|
||||
console.log(` Correos extraídos:`, emails);
|
||||
// jsonData.push(rowData);
|
||||
// });
|
||||
|
||||
return emails;
|
||||
}
|
||||
// console.log(` Datos extraídos:`, jsonData);
|
||||
|
||||
// // 🔹 Extraer correos de la columna "Correo"
|
||||
// const emails = jsonData
|
||||
// .map(row => row.Correo)
|
||||
// .filter(email => typeof email === 'string');
|
||||
|
||||
// console.log(` Correos extraídos:`, emails);
|
||||
|
||||
// return emails;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
+61
-58
@@ -2,98 +2,101 @@ import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UploadedFile,
|
||||
Headers,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { MailService } from './mail.service';
|
||||
import { ExcelService } from 'src/excel/excel.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiSendMail, ApiSendMailExcel} from './mail.decorators';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Sistema } from 'src/typeorm/votacionesPrueba.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ApiSendMail } from './mail.decorators';
|
||||
import { SistemaService } from 'src/sistema/sistema.service';
|
||||
import { SendCorreoDto } from 'src/mail/dto/send-email.dto';
|
||||
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,
|
||||
) {}
|
||||
|
||||
// Endpoint para enviar correos a través de una solicitud POST
|
||||
|
||||
|
||||
@Post('send')
|
||||
@ApiSendMail()
|
||||
async sendMail(
|
||||
@Headers() headers:{password:string},
|
||||
@Body() body: { to: string; subject: string; text: string; fecha_recibido: Date, html:string, adjuntos:any }) {
|
||||
|
||||
/*
|
||||
|
||||
// esta parte hay que rehacerla, se tiene que obtener la contraseña del sistema en base a la clave del sistema.
|
||||
// debe de existir un modulo para crear contraseñas para las apis y administrar esas contraseñas
|
||||
|
||||
|
||||
if (!headers.password) {
|
||||
throw new UnauthorizedException('Header "Password" es requerido');
|
||||
@Headers() headers: { token: string },
|
||||
@Body() body: SendCorreoDto,
|
||||
) {
|
||||
if (!headers.token) {
|
||||
throw new UnauthorizedException('Header "token" es requerido');
|
||||
}
|
||||
|
||||
const sistem = await this.sistemaRepository.findOne({ where: { password: headers.password } });
|
||||
const result = await this.mailService
|
||||
.sendMail(body, headers.token)
|
||||
.then((value) => {
|
||||
console.log('ya acabe el correo', value);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if (!sistem) {
|
||||
throw new UnauthorizedException('Password incorrecto');
|
||||
} */
|
||||
|
||||
|
||||
|
||||
console.log("estos son los archivos adjuntos en el controller",body);
|
||||
|
||||
|
||||
|
||||
const { to, subject, text, html, fecha_recibido, adjuntos } = body;
|
||||
|
||||
const result = await this.mailService.sendMail(to, subject, text,html, fecha_recibido,1,adjuntos); // sistem
|
||||
// sistem
|
||||
return result;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Post('masivo')
|
||||
async enviarMasivo(
|
||||
@Body() data: SendCorreoDto[],
|
||||
@Headers() headers: { token: string },
|
||||
) {
|
||||
return await this.mailService.sendBulkMail(data, headers.token);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@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: { password: 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);
|
||||
}
|
||||
}
|
||||
|
||||
return { message: 'Correos enviados (si no hubo errores)' };
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
+44
-59
@@ -1,78 +1,63 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiBody, ApiConsumes, ApiHeader, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiBody, ApiHeader } from '@nestjs/swagger';
|
||||
|
||||
export const ApiSendMail = () => {
|
||||
return applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Enviar un correo manualmente',
|
||||
description: `Este endpoint permite enviar un correo individualmente. Se requiere proporcionar:
|
||||
- **to**: Dirección de correo del destinatario.
|
||||
- **subject**: Asunto del correo.
|
||||
- **text**: Cuerpo del mensaje.
|
||||
- **fecha_recibido**: Fecha en que se registró la solicitud.
|
||||
- **sistema**: contraseña del sistema.`,
|
||||
summary: 'Enviar un correo electrónico desde el sistema autenticado',
|
||||
description: `Este endpoint permite a un sistema autenticado enviar un correo electrónico utilizando su configuración registrada.
|
||||
Debe incluirse un token JWT válido en los encabezados (headers).
|
||||
El sistema debe proporcionar el destinatario, asunto, contenido del mensaje y credenciales de su correo.`,
|
||||
}),
|
||||
ApiHeader({
|
||||
name: 'password',
|
||||
description: 'Contraseña del sistema para autenticación',
|
||||
name: 'token',
|
||||
description: 'Token JWT del sistema para autenticación',
|
||||
required: true,
|
||||
example: 'mi-password-segura'
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos del correo a enviar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
to: { type: 'string', example: 'usuario@example.com' },
|
||||
subject: { type: 'string', example: 'Asunto del correo' },
|
||||
text: { type: 'string', example: 'Cuerpo del mensaje' },
|
||||
fecha_recibido: { type: 'string', format: 'date-time', example: '2025-03-10T12:00:00Z' }
|
||||
to: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'destinatario@example.com',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
example: 'Informe Semanal',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
example: 'Aquí está el informe solicitado...',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
example: '<p>Este es un <b>mensaje</b> en HTML</p>',
|
||||
},
|
||||
fecha_recibido: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
example: '2025-05-01T15:00:00Z',
|
||||
},
|
||||
adjuntos: {
|
||||
type: 'array',
|
||||
description: 'Archivos adjuntos en base64',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
filename: { type: 'string', example: 'reporte.pdf' },
|
||||
content: { type: 'string', example: 'base64string...' },
|
||||
encoding: { type: 'string', example: 'base64' },
|
||||
cid: { type: 'string', example: 'file123' }
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
required: ['to', 'fecha_recibido'],
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const ApiSendMailExcel = () => {
|
||||
return applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Enviar correos desde un archivo Excel',
|
||||
description: `Este endpoint permite enviar correos a partir de una lista contenida en un archivo Excel.
|
||||
El archivo debe contener una columna con direcciones de correo válidas.`,
|
||||
}),
|
||||
ApiConsumes('multipart/form-data'),
|
||||
ApiBody({
|
||||
description: 'Sube un archivo Excel con correos',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Correos enviados exitosamente',
|
||||
schema: {
|
||||
example: {
|
||||
message: 'Correos enviados correctamente',
|
||||
emails: ['usuario1@example.com', 'usuario2@example.com'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 400,
|
||||
description: 'Error al procesar el archivo',
|
||||
schema: {
|
||||
example: {
|
||||
statusCode: 400,
|
||||
message: 'El archivo no es un Excel válido',
|
||||
error: 'Bad Request',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,12 +7,16 @@ 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,
|
||||
TypeOrmModule.forFeature([
|
||||
Status,
|
||||
Correo,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+268
-98
@@ -1,52 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Correo, Status } from '../typeorm/votacionesPrueba.entity';
|
||||
|
||||
dotenv.config();
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
private transporter;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Correo) private readonly correoRepository: Repository<Correo>,
|
||||
@InjectRepository(Status) private readonly statusRepository: Repository<Status>,
|
||||
|
||||
) {
|
||||
|
||||
|
||||
|
||||
/* {
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.USER_GMAIL,
|
||||
pass: process.env.PASS_GMAIL,
|
||||
},
|
||||
} */
|
||||
|
||||
|
||||
this.transporter = nodemailer.createTransport(
|
||||
{
|
||||
host: 'smtp.gmail.com', // o smtp‑relay.gmail.com (ver punto 2)
|
||||
port: 587,
|
||||
secure: false,
|
||||
requireTLS: true, // TLS STARTTLS
|
||||
auth: { user: process.env.USER_GMAIL, pass: process.env.PASS_GMAIL },
|
||||
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
|
||||
}
|
||||
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 sistemaRepositosistemaRepositoryry: Repository<Sistema>,
|
||||
) {
|
||||
/* this.transporter = nodemailer.createTransport({
|
||||
host: 'smtp-relay.gmail.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: {
|
||||
user: process.env.USER_GMAIL,
|
||||
pass: process.env.PASS_GMAIL,
|
||||
},
|
||||
}); */
|
||||
/*
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: 'smtp.gmail.com', // o smtp‑relay.gmail.com (ver punto 2)
|
||||
port: 587,
|
||||
secure: false,
|
||||
requireTLS: true, // TLS STARTTLS
|
||||
auth: { user: process.env.USER_GMAIL, pass: process.env.PASS_GMAIL },
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
/*
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: 'localhost',
|
||||
@@ -56,81 +55,252 @@ export class MailService {
|
||||
*/
|
||||
}
|
||||
|
||||
// Función para generar el reporte de correos enviados y fallidos
|
||||
|
||||
async findAllBySistema(TOKEN: string, nombreSistema: string) {
|
||||
if (!TOKEN) {
|
||||
throw new UnauthorizedException('Header "Password" es requerido');
|
||||
}
|
||||
|
||||
const sistem = await this.sistemaService.acceso(TOKEN);
|
||||
if (!sistem) {
|
||||
throw new UnauthorizedException('Password incorrecto');
|
||||
}
|
||||
|
||||
const result = await this.correoRepository.find({
|
||||
where: {
|
||||
id_sistema: { nombre_sistema: nombreSistema },
|
||||
},
|
||||
relations: {
|
||||
id_sistema: true,
|
||||
id_status: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendMail(sendEmail: SendCorreoDto, token: string) {
|
||||
let payload;
|
||||
|
||||
payload = await this.authService.verificarToken(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 decryptedPassword = decrypt(
|
||||
sistem.email_password_encrypted,
|
||||
sistem.email_password_iv,
|
||||
);
|
||||
|
||||
const decryptedToken = decrypt(
|
||||
sistem.refreshToken_encrypted,
|
||||
sistem.refreshToken_iv,
|
||||
);
|
||||
|
||||
console.log('sistem', {
|
||||
type: 'OAuth2',
|
||||
user: sistem.email,
|
||||
clientId: sistem.clientId,
|
||||
clientSecret: decryptedPassword,
|
||||
refreshToken: decryptedToken,
|
||||
//accessToken: sistem.accessToken, // opcional
|
||||
});
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
type: 'OAuth2',
|
||||
user: sistem.email,
|
||||
clientId: sistem.clientId,
|
||||
clientSecret: decryptedPassword,
|
||||
refreshToken: decryptedToken,
|
||||
//accessToken: sistem.accessToken, // opcional
|
||||
},
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: 100,
|
||||
rateDelta: 2000,
|
||||
rateLimit: 1,
|
||||
});
|
||||
|
||||
async sendMail(to: string, subject: string, text: string, html: string, fecha_recibido: Date, sistema, adjuntos?: any[] ) {
|
||||
|
||||
console.log("estos son los archivos adjuntos en el service",adjuntos);
|
||||
|
||||
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.USER_GMAIL,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
|
||||
// OJO: nodemailer usa 'attachments', no 'adjuntos'
|
||||
attachments: (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
|
||||
if (base64Clean.startsWith('data:image/')) {
|
||||
base64Clean = base64Clean.split('base64,')[1];
|
||||
}
|
||||
|
||||
return {
|
||||
filename: adj.filename || 'qr.png',
|
||||
content: Buffer.from(base64Clean, adj.encoding || 'base64'),
|
||||
cid: adj.cid || 'qrCode', // el mismo que uses en <img src="cid:..."/>
|
||||
contentType: 'image/png' // recomendable para que sepa que es PNG
|
||||
};
|
||||
})
|
||||
from: sistem.email,
|
||||
to: sendEmail.to,
|
||||
subject: sendEmail.subject,
|
||||
text: sendEmail.text,
|
||||
html: sendEmail.html,
|
||||
|
||||
// OJO: nodemailer usa 'attachments', no 'adjuntos'
|
||||
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
|
||||
if (base64Clean.startsWith('data:image/')) {
|
||||
base64Clean = base64Clean.split('base64,')[1];
|
||||
}
|
||||
|
||||
return {
|
||||
filename: adj.filename || 'qr.png',
|
||||
content: Buffer.from(base64Clean, adj.encoding || 'base64'),
|
||||
cid: adj.cid || 'qrCode', // el mismo que uses en <img src="cid:..."/>
|
||||
contentType: 'image/png', // recomendable para que sepa que es PNG
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
let resMail = await transporter.sendMail(mailOptions);
|
||||
if (!resMail) throw new Error('');
|
||||
|
||||
console.log(mailOptions);
|
||||
|
||||
|
||||
|
||||
|
||||
let resMail = await this.transporter.sendMail(mailOptions);
|
||||
const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido";
|
||||
|
||||
/*
|
||||
let status = await this.statusRepository.findOne({ where: { status: statusTexto } });
|
||||
const statusTexto = resMail.accepted.length > 0 ? 'Enviado' : 'Fallido';
|
||||
|
||||
let status = await this.statusRepository.findOne({
|
||||
where: { status: statusTexto },
|
||||
});
|
||||
|
||||
if (!status) {
|
||||
status = this.statusRepository.create({ status: statusTexto });
|
||||
await this.statusRepository.save(status);
|
||||
}
|
||||
|
||||
const info= this.correoRepository.create({
|
||||
|
||||
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,
|
||||
remitente: process.env.USER_GMAIL,
|
||||
destinatario: sendEmail.to,
|
||||
remitente: sistem.email,
|
||||
});
|
||||
|
||||
}); */
|
||||
|
||||
|
||||
if(!resMail){
|
||||
throw new Error("fallo")
|
||||
if (!resMail) {
|
||||
throw new Error('fallo');
|
||||
}
|
||||
//this.correoRepository.save(info);
|
||||
|
||||
this.correoRepository.save(info);
|
||||
|
||||
|
||||
return (statusTexto);
|
||||
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,5 @@
|
||||
// comprobar-sistema.dto.ts
|
||||
export class ComprobarSistemaDto {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CreateSistemaDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
Nombre:string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
ip:string;
|
||||
|
||||
//Password del sistema
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
//clientSecret
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email_password:string
|
||||
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
clientId:string
|
||||
|
||||
//TU_REFRESH_TOKEN
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken:string;
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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,
|
||||
ApiUpdateSistema,
|
||||
} from './sistema.decorators';
|
||||
import { UpdateSistemaDto } from './dto/sistema-update';
|
||||
|
||||
@Controller('sistema')
|
||||
export class SistemaController {
|
||||
constructor(private readonly sistemaService: SistemaService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.sistemaService.findAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiCreateSistema()
|
||||
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()
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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({
|
||||
summary: 'Registrar un nuevo sistema',
|
||||
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 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', 'email', 'email_password', 'password'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const ApiGenerarToken = () => {
|
||||
return applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Generar token JWT para un sistema',
|
||||
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 Notificaciones',
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Contraseña del sistema para verificar identidad',
|
||||
schema: {
|
||||
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...',
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
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/entidades';
|
||||
import { AuthModule } from 'src/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
controllers: [SistemaController],
|
||||
providers: [SistemaService],
|
||||
imports:[
|
||||
TypeOrmModule.forFeature([
|
||||
Sistema]),
|
||||
AuthModule
|
||||
],
|
||||
exports:[SistemaModule,SistemaService]
|
||||
})
|
||||
export class SistemaModule {}
|
||||
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { CreateSistemaDto } from './dto/create-sistema.dto';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
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 {
|
||||
constructor(
|
||||
@InjectRepository(Sistema)
|
||||
private readonly sistemaRepository: Repository<Sistema>,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
async findAll() {
|
||||
const sistemas = await this.sistemaRepository.find();
|
||||
if (!sistemas || sistemas.length === 0) {
|
||||
throw new NotFoundException('No hay sistemas registrados');
|
||||
}
|
||||
return sistemas;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const token = this.authService.generarToken({
|
||||
sistemaId: sistema.id_sistema,
|
||||
nombre: sistema.nombre_sistema,
|
||||
ip: sistema.ip,
|
||||
});
|
||||
return token;
|
||||
}
|
||||
|
||||
async acceso(token: string) {
|
||||
const decoded = await this.authService.verificarToken(token);
|
||||
if (!decoded) {
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
|
||||
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 erróneo');
|
||||
}
|
||||
|
||||
const { encryptedData: encryptedData_token, iv: encryptedData_iv } =
|
||||
encrypt(createSistemaDto.refreshToken);
|
||||
|
||||
const { encryptedData: encrypted_password, iv: iv_password } = encrypt(
|
||||
createSistemaDto.email_password,
|
||||
);
|
||||
|
||||
const sistema = this.sistemaRepository.create({
|
||||
nombre_sistema: createSistemaDto.Nombre,
|
||||
ip: createSistemaDto.ip,
|
||||
email: createSistemaDto.email,
|
||||
email_password_encrypted: encrypted_password,
|
||||
email_password_iv: iv_password,
|
||||
clientId: createSistemaDto.clientId,
|
||||
refreshToken_encrypted: encryptedData_token,
|
||||
refreshToken_iv: encryptedData_iv,
|
||||
});
|
||||
await this.sistemaRepository.save(sistema);
|
||||
|
||||
return this.Generar_Token(
|
||||
createSistemaDto.Nombre,
|
||||
createSistemaDto.password,
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
if (!sistem) {
|
||||
throw new UnauthorizedException('Password erróneo');
|
||||
}
|
||||
return sistem;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'class-validator';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@@ -5,12 +6,34 @@ export class Sistema {
|
||||
@PrimaryGeneratedColumn()
|
||||
id_sistema: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
password: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
nombre_sistema: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50,nullable: false })
|
||||
ip:string;
|
||||
|
||||
@Column({nullable: false})
|
||||
email: string;
|
||||
|
||||
//clientSecret encriptada
|
||||
@Column({nullable: false})
|
||||
email_password_encrypted: string;
|
||||
|
||||
|
||||
@Column({nullable: false})
|
||||
email_password_iv: string;
|
||||
|
||||
@Column({nullable: false})
|
||||
clientId:string
|
||||
|
||||
//TU_REFRESH_TOKEN
|
||||
@Column({nullable: false})
|
||||
refreshToken_encrypted:string;
|
||||
|
||||
@Column({nullable: false})
|
||||
refreshToken_iv: string;
|
||||
|
||||
|
||||
@OneToMany(() => Correo, (correo) => correo.id_sistema)
|
||||
correos: Correo[];
|
||||
}
|
||||
Reference in New Issue
Block a user