Se agregaron funciones y cambios en los dto

This commit is contained in:
2025-10-16 14:04:30 -06:00
parent cdf3b761e4
commit 09fda8af12
16 changed files with 1470 additions and 296 deletions
+66 -10
View File
@@ -18,6 +18,7 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/typeorm": "^11.0.0",
"argon2": "^0.44.0",
"axios": "^1.12.2",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
@@ -794,6 +795,12 @@
"tslib": "^2.4.0"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -2576,12 +2583,12 @@
}
},
"node_modules/@nestjs/jwt": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz",
"integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz",
"integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.7",
"@types/jsonwebtoken": "9.0.10",
"jsonwebtoken": "9.0.2"
},
"peerDependencies": {
@@ -2774,6 +2781,15 @@
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -3410,11 +3426,12 @@
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
"integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
@@ -3432,6 +3449,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
@@ -5167,6 +5190,22 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/argon2": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
"integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@phc/format": "^1.0.0",
"cross-env": "^10.0.0",
"node-addon-api": "^8.5.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -6165,6 +6204,23 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -9928,9 +9984,9 @@
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz",
"integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
+1
View File
@@ -29,6 +29,7 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/typeorm": "^11.0.0",
"argon2": "^0.44.0",
"axios": "^1.12.2",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
+6 -5
View File
@@ -14,6 +14,7 @@ import { gmail } from 'src/helpers.services/gmail.service';
import { Servicio } from 'src/servicio/entities/servicio.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
import { Like, Not, Repository } from 'typeorm';
import * as argon2 from 'argon2';
@Injectable()
export class AuthService {
@@ -80,15 +81,15 @@ export class AuthService {
}
async comparar(password, dbPassword) {
if (!bcrypt.compareSync(password, dbPassword)) {
return false;
} else {
if (await argon2.verify(password, dbPassword)) {
return true;
} else {
return false;
}
}
async encriptar(password) {
return bcrypt.hashSync(password, Number(process.env.SALT_ROUNDS));
return await argon2.hash(password);
}
async generarPassword() {
@@ -107,7 +108,7 @@ export class AuthService {
async login(usuario: string, password: string) {
const user = await this.userRepo.findOne({ where: { usuario } });
if (!user) throw new UnauthorizedException('No existe este usuario.');
const match = await bcrypt.compare(password, user.password);
const match = await this.comparar(password, user.password);
if (!match) throw new UnauthorizedException('Credenciales inválidas');
if (!user.activo) throw new Error('Este usuario no esta activo.');
const token = this.jwtCreate(
@@ -1,73 +1,72 @@
import {
IsEmail,
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
Length,
IsDateString,
IsNumber,
IsIn,
IsInt,
Matches,
} from 'class-validator';
export class CreateCasoEspecialDto {
@IsNumber()
idUsuario: number;
@IsNumber()
@IsInt()
idCarrera: number;
@IsNumber()
@Length(1, 11)
idStatus: number;
@IsString()
@Length(9, 9)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
numeroCuenta: string;
@IsEmail()
correo: string;
@IsString()
@Length(1, 3)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
creditos : string;
@IsString()
@Length(1, 15)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
telefono: string;
@IsDateString()
fechaInicio: Date;
@IsDateString()
fechaFin: Date;
@IsDateString()
fechaNacimiento: Date;
@IsString()
@Length(1, 200)
direccion: string;
@IsOptional()
@IsString()
@Length(1, 200)
institucion?: string;
@IsOptional()
@IsString()
@Length(1, 200)
dependencia?: string;
@IsOptional()
@IsNumber()
@Length(1, 1)
motivo?: string;
}
IsEmail,
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
Length,
IsDateString,
IsNumber,
IsIn,
IsInt,
Matches,
} from 'class-validator';
export class CreateCasoEspecialDto {
@IsNumber()
idUsuario: number;
@IsNumber()
@IsInt()
idCarrera: number;
@IsNumber()
@Length(1, 11)
idStatus: number;
@IsString()
@Length(9, 9)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
numeroCuenta: string;
@IsEmail()
correo: string;
@IsString()
@Length(1, 3)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
creditos: string;
@IsString()
@Length(1, 15)
@Matches(/^\d+$/, { message: 'solo puede contener números' })
telefono: string;
@IsDateString()
fechaInicio: Date;
@IsDateString()
fechaFin: Date;
@IsDateString()
fechaNacimiento: Date;
@IsString()
@Length(1, 200)
direccion: string;
@IsOptional()
@IsString()
@Length(1, 200)
institucion?: string;
@IsOptional()
@IsString()
@Length(1, 200)
dependencia?: string;
@IsOptional() // Permite que no se envíe
@IsString() // Debe ser string si se envía
@Length(1, 1)
motivo?: string;
}
@@ -1,51 +1,51 @@
import {
IsOptional,
IsString,
IsEmail,
Length,
IsDateString,
IsNumber,
} from 'class-validator';
export class UpdateCasoEspecialDto {
@IsOptional()
@IsEmail()
correo?: string;
@IsOptional()
@IsDateString()
fechaInicio?: Date;
@IsOptional()
@IsDateString()
fechaFin?: Date;
@IsOptional()
@IsDateString()
fechaNacimiento?: Date;
@IsOptional()
@IsString()
@Length(1, 200)
direccion?: string;
@IsOptional()
@IsString()
@Length(1, 15)
telefono?: string;
@IsOptional()
@IsString()
@Length(1, 200)
institucion?: string;
@IsOptional()
@IsString()
@Length(1, 200)
dependencia?: string;
@IsOptional()
@IsNumber()
motivo?: number;
}
IsOptional,
IsString,
IsEmail,
Length,
IsDateString,
IsNumber,
} from 'class-validator';
export class UpdateCasoEspecialDto {
@IsOptional()
@IsEmail()
correo?: string;
@IsOptional()
@IsDateString()
fechaInicio?: Date;
@IsOptional()
@IsDateString()
fechaFin?: Date;
@IsOptional()
@IsDateString()
fechaNacimiento?: Date;
@IsOptional()
@IsString()
@Length(1, 200)
direccion?: string;
@IsOptional()
@IsString()
@Length(1, 15)
telefono?: string;
@IsOptional()
@IsString()
@Length(1, 200)
institucion?: string;
@IsOptional()
@IsString()
@Length(1, 200)
dependencia?: string;
@IsOptional() // Permite que no se envíe
@IsString() // Debe ser string si se envía
@Length(1, 1)
motivo?: string;
}
@@ -8,7 +8,7 @@ import { CuestionarioAlumno2 } from './entities/cuestionario-alumno2.entity';
import { messageByStatus } from 'src/messageByStatus';
import { ArchivoService } from 'src/helpers.services/archivo.service';
import { ValidacionService } from 'src/helpers.services/validacion.service';
const { convertArrayToCSV } = require('convert-array-to-csv');
import { convertArrayToCSV } from 'convert-array-to-csv';
@Injectable()
export class CuestionarioAlumno2Service {
@@ -8,7 +8,7 @@ import { ArchivoService } from 'src/helpers.services/archivo.service';
import { ValidacionService } from 'src/helpers.services/validacion.service';
import { Servicio } from 'src/servicio/entities/servicio.entity';
import { messageByStatus } from 'src/messageByStatus';
const { convertArrayToCSV } = require('convert-array-to-csv');
import { convertArrayToCSV } from 'convert-array-to-csv';
@Injectable()
export class CuestionarioPrograma2Service {
+33 -14
View File
@@ -1,27 +1,46 @@
import { IsString, IsEmail, IsDate, IsOptional } from 'class-validator';
import {
IsString,
IsArray,
IsOptional,
IsISO8601,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
export class SendCorreoDto {
@IsEmail()
class AdjuntoDto {
@IsString()
filename: string;
@IsString()
content: string;
@IsString()
encoding: string;
@IsOptional()
@IsString()
cid?: string;
}
export class SendMailDto {
@IsString()
to: string;
@IsString()
@IsOptional()
subject?: string;
subject: string;
@IsString()
@IsOptional()
text?: string;
text: string;
@IsString()
@IsOptional()
html?: string;
html: string;
@IsString()
@IsOptional()
fileName?: string;
@IsISO8601()
fecha_recibido: string;
@IsString()
@IsOptional()
filePath?: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdjuntoDto)
adjuntos?: AdjuntoDto[];
}
+27 -86
View File
@@ -1,93 +1,34 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { SendCorreoDto } from './dto/send-email.dto';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import axios from 'axios';
import { SendMailDto } from './dto/send-email.dto';
@Injectable()
export class gmail {
private readonly baseUrl = process.env.MAIL_URL;
private readonly token = process.env.MAIL_TOKEN; // idealmente: process.env.MAIL_TOKEN
async sendMail(sendEmail: SendCorreoDto) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: process.env.EMAIL_USER, // tu correo de Gmail
clientId: process.env.CLIENT_ID, // tu Client ID de OAuth2
clientSecret: process.env.CLIENT_SECRET, // tu Client Secret de OAuth2
refreshToken: process.env.REFRESH_TOKEN, // tu Refresh Token de OAuth2
//accessToken: sistem.accessToken, // opcional
},
pool: true,
maxConnections: 1,
maxMessages: 100,
rateDelta: 2000,
rateLimit: 1
});
const mailOptions = {
from: process.env.EMAIL_USER, // tu correo de Gmail
to: sendEmail.to,
subject: sendEmail.subject,
text: sendEmail.text,
html: sendEmail.html,
attachments: [
{
filename: '',
path: '', // ruta local
async enviarCorreo(data: SendMailDto): Promise<any> {
try {
if (!this.baseUrl || !this.token) {
throw new HttpException(
'Configuración de correo no encontrada',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const response = await axios.post(this.baseUrl, data, {
headers: {
'Content-Type': 'application/json',
accept: '*/*',
token: this.token,
},
{
filename: '',
path: '', // adjuntar desde URL
},
{
filename: '',
content: '', // adjunto generado en memoria
},
],
});
};
console.log(mailOptions);
let resMail = await transporter.sendMail(mailOptions);
if (!resMail) throw new Error("")
const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido";
if (!resMail) {
throw new Error("fallo")
}
return (statusTexto);
return response.data;
} catch (error: any) {
throw new HttpException(
error.response?.data || 'Error al enviar correo',
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
}
+105 -20
View File
@@ -1,34 +1,119 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Post,
Put,
Get,
Body,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
BadRequestException,
Param,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import * as fs from 'fs';
import { AuthGuard } from '@nestjs/passport';
import { ProgramaService } from './programa.service';
import { CreateProgramaDto } from './dto/create-programa.dto';
import { UpdateProgramaDto } from './dto/update-programa.dto';
import { IsEmail, isEmail } from 'class-validator';
class Email {
@IsEmail()
correo: string;
}
@Controller('programa')
export class ProgramaController {
constructor(private readonly programaService: ProgramaService) {}
@Post()
create(@Body() createProgramaDto: CreateProgramaDto) {
return this.programaService.create(createProgramaDto);
// ==================== POST /carga_masiva ====================
@Post('carga_masiva')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(
FileInterceptor('csv', {
storage: diskStorage({
destination: './server/uploads',
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}${extname(file.originalname)}`;
cb(null, uniqueName);
},
}),
}),
)
async cargaMasiva(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException(
'No se envio un archivo csv para la carga masiva.',
);
}
try {
const data = await this.programaService.cargaMasiva(file.filename);
return {
statusCode: 201,
message: 'Carga masiva realizada con éxito',
data,
};
} catch (err) {
// Eliminar archivo si ocurre error
if (file && file.filename) {
fs.unlinkSync(`./server/uploads/${file.filename}`);
}
throw new BadRequestException(err.message);
}
}
@Get()
findAll() {
return this.programaService.findAll();
// ==================== PUT /reasignar_programas ====================
@Put('reasignar_programas/:idUsuario')
@UseGuards(AuthGuard('jwt'))
async reasignarProgramas(
@Body() email: Email,
@Param('idUsuario') idUsuario: number,
) {
try {
const data = await this.programaService.reasignarProgramas(
idUsuario,
email.correo,
);
return {
statusCode: 200,
message: 'Programas reasignados con éxito',
data,
};
} catch (err) {
throw new BadRequestException(err.message);
}
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.programaService.findOne(+id);
// ==================== GET /programas_admin ====================
@Get('programas_admin/:idUsuario')
@UseGuards(AuthGuard('jwt'))
async programasAdmin(@Param('idUsuario') query: number) {
try {
const data = await this.programaService.programasAdmin(query);
return {
statusCode: 200,
data,
};
} catch (err) {
throw new BadRequestException(err.message);
}
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateProgramaDto: UpdateProgramaDto) {
return this.programaService.update(+id, updateProgramaDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.programaService.remove(+id);
// ==================== GET /programas_responsable ====================
@Get('programas_responsable/:idUsuario')
@UseGuards(AuthGuard('jwt'))
async programasResponsable(@Param(`idUsuario`) idUsuario: number) {
try {
const data = await this.programaService.programasResponsable(idUsuario);
return {
statusCode: 200,
data,
};
} catch (err) {
throw new BadRequestException(err.message);
}
}
}
+4 -4
View File
@@ -179,9 +179,9 @@ export class ProgramaService {
};
}
async programasAdmin(body: { idUsuario: number }) {
async programasAdmin(idUser: number) {
const idUsuario = this.validacionService.validarNumeroEntero(
body.idUsuario,
idUser,
'id usuario',
);
@@ -201,9 +201,9 @@ export class ProgramaService {
});
}
async programasResponsable(body: { idUsuario: number }) {
async programasResponsable(idUser: number) {
const idUsuario = this.validacionService.validarNumeroEntero(
body.idUsuario,
idUser,
'id usuario',
);
+42 -1
View File
@@ -1 +1,42 @@
export class CreateServicioDto {}
import {
IsNumber,
IsOptional,
IsString,
IsDateString,
IsEmail,
IsDate,
} from 'class-validator';
export class CrearServicioDto {
@IsNumber()
idUsuario: number;
@IsNumber()
idPrograma: number;
@IsNumber()
idCarrera: number;
@IsString()
numeroCuenta: string;
@IsNumber()
creditos: number;
@IsEmail()
correo: string;
@IsDate()
fechaInicio: Date;
@IsDate()
fechaFin: Date;
@IsOptional()
@IsString()
programaInterno?: string;
@IsOptional()
@IsString()
profesor?: string;
}
+26
View File
@@ -0,0 +1,26 @@
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
export class RegistroValidadoDto {
@IsNumber()
@IsNotEmpty()
idServicio: number;
@IsOptional()
fechaNacimiento?: Date;
@IsOptional()
@IsString()
@MaxLength(15)
telefono?: string;
@IsOptional()
@IsString()
@MaxLength(200)
direccion?: string;
}
+19
View File
@@ -0,0 +1,19 @@
import { IsOptional, IsNumber, IsString } from 'class-validator';
export class ServiciosAdminDto {
@IsOptional()
@IsNumber()
pagina?: number;
@IsOptional()
@IsNumber()
idStatus?: number;
@IsOptional()
@IsString()
nombre?: string;
@IsOptional()
@IsString()
numeroCuenta?: string;
}
+15 -1
View File
@@ -1,9 +1,23 @@
import { Module } from '@nestjs/common';
import { ServicioService } from './servicio.service';
import { ServicioController } from './servicio.controller';
import { DriveService } from 'src/drive/drive.service';
import { ValidacionService } from 'src/helpers.services/validacion.service';
import { gmail } from 'src/helpers.services/gmail.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Servicio } from './entities/servicio.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
import { ArchivoService } from 'src/helpers.services/archivo.service';
@Module({
imports: [TypeOrmModule.forFeature([Servicio, Usuario])],
controllers: [ServicioController],
providers: [ServicioService],
providers: [
ServicioService,
DriveService,
ValidacionService,
gmail,
ArchivoService,
],
})
export class ServicioModule {}
File diff suppressed because it is too large Load Diff