10 Commits

Author SHA1 Message Date
Your Name 0d0077cbcb smtp new account 2025-05-02 16:02:59 -06:00
Your Name c4666718a4 log 2025-05-02 15:41:20 -06:00
Your Name 11fe86a111 name stmp 2025-05-02 15:35:20 -06:00
Your Name 59a5ff8c35 smtp rellay 2025-05-02 15:10:08 -06:00
Your Name 58b49ba93d tls true 2025-04-22 10:09:22 -06:00
Your Name b2e992dd04 tls true 2025-04-22 10:05:20 -06:00
Your Name 2df592ee89 email pool 2025-04-22 09:48:21 -06:00
Your Name ee622197c1 comentar token y sistema 2025-04-21 13:54:37 -06:00
Your Name 6c9d003cb9 smtp relay host 2025-04-21 12:59:59 -06:00
evenegas 1895e29cf2 Se agregó módulo de sistemas 2025-04-09 11:10:28 -06:00
14 changed files with 255 additions and 25 deletions
+1
View File
@@ -1,6 +1,7 @@
# Gmail # Gmail
USER_GMAIL= USER_GMAIL=
PASS_GMAIL= PASS_GMAIL=
ADMIN_PASSWORD=12345
# MariaDB database settings # MariaDB database settings
db_type=mysql db_type=mysql
+33
View File
@@ -15,6 +15,7 @@
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.0.5", "@nestjs/swagger": "^11.0.5",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"class-validator": "^0.14.1",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
@@ -3169,6 +3170,12 @@
"@types/serve-static": "*" "@types/serve-static": "*"
} }
}, },
"node_modules/@types/validator": {
"version": "13.12.3",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.3.tgz",
"integrity": "sha512-2ipwZ2NydGQJImne+FhNdhgRM37e9lCev99KnqkbFHd94Xn/mErARWI1RSLem1QA19ch5kOhzIZd7e8CA2FI8g==",
"license": "MIT"
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.33", "version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -4643,6 +4650,17 @@
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
"dev": true "dev": true
}, },
"node_modules/class-validator": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
"license": "MIT",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.10.53",
"validator": "^13.9.0"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -7929,6 +7947,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.12.6",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz",
"integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==",
"license": "MIT"
},
"node_modules/lie": { "node_modules/lie": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -11099,6 +11123,15 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/validator": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz",
"integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+1
View File
@@ -26,6 +26,7 @@
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.0.5", "@nestjs/swagger": "^11.0.5",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"class-validator": "^0.14.1",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
+2
View File
@@ -5,6 +5,7 @@ import { MailModule } from './mail/mail.module';
import {ExcelModule} from './excel/excel.module' import {ExcelModule} from './excel/excel.module'
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { SistemaModule } from './sistema/sistema.module';
@@ -28,6 +29,7 @@ import { ConfigModule } from '@nestjs/config';
MailModule, MailModule,
ExcelModule, ExcelModule,
SistemaModule,
], ],
+7 -7
View File
@@ -30,24 +30,24 @@ export class MailController {
@Post('send') @Post('send')
@ApiSendMail() @ApiSendMail()
async sendMail( async sendMail(
@Headers() headers:{password:string}, @Headers() headers:{token:string},
@Body() body: { to: string; subject: string; text: string; fecha_recibido: Date, html:string, adjuntos:any }) { @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. // 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 // debe de existir un modulo para crear contraseñas para las apis y administrar esas contraseñas
if (!headers.password) { /* if (!headers.token) {
throw new UnauthorizedException('Header "Password" es requerido'); throw new UnauthorizedException('Header "Password" es requerido');
} } */
const sistem = await this.sistemaRepository.findOne({ where: { password: headers.password } }); /* const sistem = await this.sistemaRepository.findOne({ where: { token: headers.token } });
if (!sistem) { if (!sistem) {
throw new UnauthorizedException('Password incorrecto'); throw new UnauthorizedException('Password incorrecto');
} */ } */
@@ -73,7 +73,7 @@ export class MailController {
throw new UnauthorizedException('Header "Password" es requerido'); throw new UnauthorizedException('Header "Password" es requerido');
} }
const sistem = await this.sistemaRepository.findOne({ where: { password: headers.password } }); const sistem = await this.sistemaRepository.findOne({ where: { token: headers.password } });
if (!sistem) { if (!sistem) {
throw new UnauthorizedException('Password incorrecto'); throw new UnauthorizedException('Password incorrecto');
+12 -2
View File
@@ -10,8 +10,18 @@ export const ApiSendMail = () => {
- **subject**: Asunto del correo. - **subject**: Asunto del correo.
- **text**: Cuerpo del mensaje. - **text**: Cuerpo del mensaje.
- **fecha_recibido**: Fecha en que se registró la solicitud. - **fecha_recibido**: Fecha en que se registró la solicitud.
- **sistema**: contraseña del sistema.`, - **sistema**: contraseña del sistema.
}), - **html**: se puede mandar html
- **adjuntos**:mandar qr
(Se requiere una contraseña de aplicación
No caducan automáticamente.
Pero se invalidan si:
Cambias la contraseña de tu cuenta.
Desactivas la verificación en dos pasos.
Revocas el acceso desde https://myaccount.google.com/apppasswords.
Google detecta actividad sospechosa o problemas de seguridad.))`,
}),
ApiHeader({ ApiHeader({
name: 'password', name: 'password',
description: 'Contraseña del sistema para autenticación', description: 'Contraseña del sistema para autenticación',
+2
View File
@@ -8,11 +8,13 @@ import {
Correo, Correo,
Sistema, Sistema,
}from 'src/typeorm/votacionesPrueba.entity' }from 'src/typeorm/votacionesPrueba.entity'
import { SistemaModule } from 'src/sistema/sistema.module';
@Module({ @Module({
providers: [MailService], providers: [MailService],
imports: [ imports: [
ExcelModule, ExcelModule,
SistemaModule,
TypeOrmModule.forFeature([ TypeOrmModule.forFeature([
Status, Status,
Correo, Correo,
+34 -14
View File
@@ -18,18 +18,19 @@ export class MailService {
) { ) {
/* {
service: 'gmail',
auth: {
user: process.env.USER_GMAIL,
pass: process.env.PASS_GMAIL,
},
} */
this.transporter = nodemailer.createTransport( /* 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 smtprelay.gmail.com (ver punto 2) host: 'smtp.gmail.com', // o smtprelay.gmail.com (ver punto 2)
port: 587, port: 587,
secure: false, secure: false,
@@ -40,9 +41,24 @@ export class MailService {
maxMessages: 100, // reabrir después de 100 envíos maxMessages: 100, // reabrir después de 100 envíos
rateDelta: 2000, // ventana 1 s rateDelta: 2000, // ventana 1 s
rateLimit: 1 // máx. 5 mensajes/seg rateLimit: 1 // máx. 5 mensajes/seg
} });
);
/* this.transporter = nodemailer.createTransport({
host: 'smtp-relay.gmail.com', // o smtprelay.gmail.com (ver punto 2)
port: 587,
secure: false,
requireTLS: true, // TLS STARTTLS
//auth: { user: process.env.USER_GMAIL, pass: process.env.PASS_GMAIL },
name:"acatlan.unam.mx",
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
}); */
@@ -99,6 +115,10 @@ export class MailService {
let resMail = await this.transporter.sendMail(mailOptions); let resMail = await this.transporter.sendMail(mailOptions);
console.log("este es el resultado del envio",resMail);
//console.log("este es el resultado del envio",resMail.accepted.length);
const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido"; const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido";
/* /*
+16
View File
@@ -0,0 +1,16 @@
import { IsString, IsOptional, IsBoolean, IsNotEmpty } from 'class-validator';
export class CreateSistemaDto {
@IsNotEmpty()
@IsString()
Nombre:string;
@IsNotEmpty()
@IsString()
ip:string;
@IsNotEmpty()
@IsString()
password: string;
}
+22
View File
@@ -0,0 +1,22 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { SistemaService } from './sistema.service';
import { CreateSistemaDto } from './dto/create-sistema.dto';
import { ApiCreateSistema, ApiGenerarToken } from './sistema.decorators';
@Controller('sistema')
export class SistemaController {
constructor(private readonly sistemaService: SistemaService) {}
@Post()
@ApiCreateSistema()
create(@Body() createSistemaDto: CreateSistemaDto, password:string) {
return this.sistemaService.create(createSistemaDto);
}
@Patch(':nombre_sistema')
@ApiGenerarToken()
update(@Param('nombre_sistema') nombre_sistema: string, @Body() password: string) {
return this.sistemaService.Generar_Token(nombre_sistema, password);
}
}
+50
View File
@@ -0,0 +1,50 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam } 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.`,
}),
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' },
},
required: ['Nombre', 'ip', 'password'],
},
})
);
};
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.`,
}),
ApiParam({
name: 'nombre_sistema',
required: true,
description: 'Nombre exacto del sistema registrado',
example: 'Sistema de Reportes',
}),
ApiBody({
description: 'Contraseña del sistema',
schema: {
type: 'string',
example: 'clave-segura-123',
},
})
);
};
+16
View File
@@ -0,0 +1,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';
@Module({
controllers: [SistemaController],
providers: [SistemaService],
imports:[
TypeOrmModule.forFeature([
Sistema])
],
exports:[SistemaModule]
})
export class SistemaModule {}
+53
View File
@@ -0,0 +1,53 @@
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 { Repository } from 'typeorm';
import { randomBytes } from 'crypto';
@Injectable()
export class SistemaService {
constructor(
@InjectRepository(Sistema) private readonly sistemaRepository: Repository<Sistema>
){}
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');
}
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;
}
async create(createSistemaDto: CreateSistemaDto) {
if(createSistemaDto.password!==process.env.ADMIN_PASSWORD){
throw new UnauthorizedException('Password erroneo');
}
const sistem = this.sistemaRepository.create({
nombre_sistema:createSistemaDto.Nombre,
ip:createSistemaDto.ip,
});
await this.sistemaRepository.save(sistem)
return this.Generar_Token(createSistemaDto.Nombre,createSistemaDto.password);
}
}
+6 -2
View File
@@ -1,3 +1,4 @@
import { isEmpty } from 'class-validator';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
@Entity() @Entity()
@@ -5,12 +6,15 @@ export class Sistema {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id_sistema: number; id_sistema: number;
@Column({ type: 'varchar', length: 50 }) @Column({ type: 'varchar', length: 255, nullable: true })
password: string; token: string;
@Column({ type: 'varchar', length: 50 }) @Column({ type: 'varchar', length: 50 })
nombre_sistema: string; nombre_sistema: string;
@Column({ type: 'varchar', length: 50 })
ip:string
@OneToMany(() => Correo, (correo) => correo.id_sistema) @OneToMany(() => Correo, (correo) => correo.id_sistema)
correos: Correo[]; correos: Correo[];
} }