68 Commits

Author SHA1 Message Date
jorgemike 43b9387a9f cambios 2025-03-02 21:01:47 -06:00
jalvarado 0658c1f80b Cambiso lineas, proyectos datos 2025-02-28 15:02:28 -06:00
jalvarado 1d8f959eb4 cambios 2025-02-27 16:01:41 -06:00
jorgemike f9c72ced6d profesores 2025-02-26 23:13:28 -06:00
jorgemike 098c01884e comienzo de modificaciones de mike 2025-02-26 21:14:07 -06:00
TheManus88 08aade7dcd paginated 2024-10-24 20:26:40 -06:00
TheManus88 f04f01a39c Merge remote-tracking branch 'origin/Lino' into development 2024-10-14 17:48:25 -06:00
IO 5322aefcdb fixing update auto id 2024-08-18 23:03:28 -06:00
IO 53bd76c78d new limit and image fixed 2024-08-18 22:09:32 -06:00
TheManus88 82550eefcc Merge remote-tracking branch 'origin/Lino' into development 2024-07-30 22:22:32 -06:00
IO bf22d644b4 logger 2024-07-29 20:34:04 -06:00
IO b952de694b :) 2024-07-29 20:32:27 -06:00
IO 604f09893a Merge branch 'Sam' of https://repositorio.acatlan.unam.mx/mcervantesag/api_directorio into Sam 2024-07-29 20:27:10 -06:00
IO 1e4e7f2d65 loger 2024-07-29 20:21:38 -06:00
IO a8794eee14 Merge branch 'Lino' of https://repositorio.acatlan.unam.mx/mcervantesag/api_directorio into Sam 2024-07-29 20:02:51 -06:00
IO 71b65ad5a5 si funciona no le muevas 2024-07-29 20:02:27 -06:00
TheManus88 c9f883de3c merge into Lino 2024-07-29 19:51:30 -06:00
IO 672a5e82fd si funciona no le muevas 2024-07-29 19:27:28 -06:00
IO 376e75c5ee si funciona no le muevas 2024-07-29 19:26:21 -06:00
TheManus88 61ffbc195b log into professors 2024-07-29 19:18:06 -06:00
IO 28aba967b1 fixed update user 2024-07-27 21:03:38 -06:00
TioSam77 41be2b547e Cambios api 3 2024-07-27 17:42:00 -06:00
TioSam77 76eb6523e7 Cambios api 3 2024-07-27 17:40:33 -06:00
IO 896631ef5e pk remove 2024-07-27 17:31:44 -06:00
TioSam77 cc39055581 Cambios api 2 2024-07-27 17:18:36 -06:00
IO e52a90b725 id 2024-07-27 17:17:58 -06:00
IO 10c415cdd4 fixed table issues 2024-07-27 00:11:39 -06:00
IO 39c37faafc fk 2024-07-26 23:25:46 -06:00
IO 63f3f7f443 Teacher Modification 2024-07-26 18:24:17 -06:00
IO 81dfbe4910 deveelopment 2024-07-25 16:41:23 -06:00
TheManus88 a23ba6da18 remove db fields and change idprofresor by no autoincremental id 2024-07-25 08:55:18 -06:00
IO f8df1e2504 fixed delete image 2024-07-17 21:27:03 -06:00
IO f419ff1703 porsi 2024-07-17 20:37:54 -06:00
IO 70882772c8 delete image 2024-07-17 20:36:33 -06:00
IO b814bdd53e funciona rememberMe 2024-07-17 13:50:33 -06:00
IO 2307169453 teachers active 2024-07-11 01:48:37 -06:00
TioSam77 9fcb69acd5 Pruba 5 2024-07-08 19:25:43 -06:00
TioSam77 867f055de9 Pruba 4 2024-07-08 17:41:08 -06:00
IO ebbf6a53f5 fixed images 2024-07-08 16:13:24 -06:00
IO f569081092 imagen fixed 2024-07-05 19:31:07 -06:00
IO 6f8cff7c2d imagen 2024-07-05 19:27:20 -06:00
IO 3c67e8e61c ya funciona todo, y pensar que solo era un import :( 2024-07-05 10:44:42 -06:00
IO 9c2d104868 espero ya no moverle 2024-07-05 08:58:20 -06:00
IO 82394b05a2 fixed name 2024-07-05 08:08:31 -06:00
IO 3be07318ea test 2024-07-03 23:46:11 -06:00
IO 97fd5b27eb users 2024-07-03 14:55:06 -06:00
IO 208715a6e1 images 2024-06-30 20:11:27 -06:00
IO 3e3925c063 fixed error 2024-06-30 17:04:42 -06:00
IO 90ddb52370 images 2024-06-30 15:58:08 -06:00
IO 8738d65382 Merge branch 'Sam' of https://repositorio.acatlan.unam.mx/mcervantesag/api_directorio into Lino 2024-06-26 16:28:40 -06:00
IO fb04abfb06 :c 2024-06-26 16:17:28 -06:00
IO a210ac3d66 hope 2024-06-26 16:07:11 -06:00
IO a7175979c3 add spec 2024-06-26 15:51:14 -06:00
IO 788c6f2a0a add get user 2024-06-26 13:46:55 -06:00
IO dd6ff0d87c add get user 2024-06-26 13:31:33 -06:00
TioSam77 87fae370be Api fix 5 2024-06-26 13:19:10 -06:00
IO e72bbbc8fe ayuda 2024-06-26 13:18:22 -06:00
IO 2bcacc5362 Merge branch 'Sam' of https://repositorio.acatlan.unam.mx/mcervantesag/api_directorio into Lino 2024-06-26 11:48:09 -06:00
IO 45368ff981 replace 2024-06-26 11:48:03 -06:00
IO 8a03c17c65 replace 2024-06-26 11:47:56 -06:00
IO bd9adfe7d0 replace 2024-06-26 11:39:41 -06:00
TioSam77 6227b1aa89 Api fix 3 2024-06-26 11:37:26 -06:00
TioSam77 7ca922dc61 Api fix 2 2024-06-25 22:28:37 -06:00
TioSam77 17bdd137e3 Api fix 2 2024-06-25 22:28:06 -06:00
TioSam77 148760cc04 Interfaces fix 4 2024-06-25 22:06:02 -06:00
TioSam77 2cc767612d Api fix 2024-06-25 17:45:44 -06:00
IO 38b7066ac0 before the disastre 2024-06-25 17:25:07 -06:00
TioSam77 30a4b0270a Fix Delete 2024-06-25 15:28:59 -06:00
92 changed files with 4997 additions and 2537 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Directorio API Dev Container",
"dockerFile": "../Dockerfile",
"appPort": ["3000:3000"],
"postCreateCommand": "npm install",
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"editor.formatOnSave": true
},
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker",
"streetsidesoftware.code-spell-checker"
],
"remoteUser": "node"
}
+2
View File
@@ -54,3 +54,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
fotografias
+18
View File
@@ -0,0 +1,18 @@
# Usa una imagen de Node.js 22 basada en Alpine
FROM node:22.10.0-alpine
# Establece el directorio de trabajo
WORKDIR /app
# Copia los archivos de dependencias y los instala
COPY package.json package-lock.json ./
RUN npm install
# Copia el resto del código fuente
COPY . .
# Expone el puerto 3000
EXPOSE 3000
# Comando por defecto para iniciar la aplicación en desarrollo
CMD ["npm", "run", "start:dev"]
+2798 -1343
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -20,13 +20,15 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/common": "^10.3.9",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-express": "^10.3.9",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/swagger": "^7.4.2",
"@nestjs/typeorm": "^10.0.2",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
@@ -38,7 +40,9 @@
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
"sharp": "^0.33.4",
"typeorm": "^0.3.20",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
@@ -47,6 +51,7 @@
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.11",
"@types/node": "^20.14.2",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
-112
View File
@@ -1,112 +0,0 @@
import { IsBoolean, IsInt, IsNotEmpty, IsNumber, IsString, IsOptional, Length} from "class-validator";
export class profesorDto{
@IsInt()
@IsNotEmpty()
id_usuario: number;
@IsString()
@IsOptional()
@Length(0,10)
num_trabajador: string;
@IsString()
@IsNotEmpty()
@Length(0,80)
nombre: string;
@IsString()
@IsOptional()
@Length(0,10)
rfc: string;
@IsString()
@IsOptional()
@Length(0,3)
homoclave: string;
@IsString()
@IsOptional()
@Length(0,13)
tel_oficina: string;
@IsString()
@IsOptional()
@Length(0,10)
extension: string;
@IsString()
@IsOptional()
@Length(0,13)
tel_personal: string;
@IsString()
@IsOptional()
@Length(0,65)
correo_pcp: string;
@IsString()
@IsOptional()
@Length(0,65)
correo_per: string;
@IsNumber()
@IsOptional()
id_adscripcion: number;
@IsNumber()
@IsOptional()
id_categoria: number;
@IsNumber()
@IsOptional()
id_edificio: number;
@IsString()
@IsOptional()
@Length(0,256)
ubicacion: string;
@IsString()
@IsOptional()
@Length(0,150)
fotografia: string;
@IsBoolean()
@IsOptional()
activo: boolean;
@IsBoolean()
@IsOptional()
mostrar: boolean;
proyectosAcademicos: ProyectoDto[];
datosAcademicos: DatosAcademicos[];
lineasInvestigacion: LineasInvestigacion[];
}
export class ProyectoDto {
@IsOptional()
@Length(0, 500)
proyecto: string;
}
export class DatosAcademicos{
@IsOptional()
@Length(0,30)
grado_maximo: string;
@IsOptional()
@Length(0,600)
grados_obtenidos: string;
}
export class LineasInvestigacion{
@IsOptional()
@Length(0,300)
lineas_inv: string;
}
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProfesorController } from './profesor.controller';
describe('ProfesorController', () => {
let controller: ProfesorController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProfesorController],
}).compile();
controller = module.get<ProfesorController>(ProfesorController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
-53
View File
@@ -1,53 +0,0 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query,UseGuards } from '@nestjs/common';
import { ProfesorService } from './profesor.service';
import { profesorDto } from './dto/profesorDto.dto';
import {Roles} from '../permissions/roles.decorator'
import {RolesGuard} from '../permissions/roles.guard'
import {Role} from '../permissions/role.enum'
@Controller('profesor')
@UseGuards(RolesGuard)
export class ProfesorController {
constructor(private profesorService: ProfesorService) {}
@Post()
@Roles(Role.Responsable,Role.Admin)
async register(@Body() data: profesorDto) {
return this.profesorService.register(data);
}
@Get()
findAll() {
return this.profesorService.findAll();
}
@Get('page')
findAllPaginated(@Query('page') page: number = 1, @Query('limit') limit: number = 10, @Query() filters: any) {
page = page < 1 ? 1 : page;
limit = limit > 10 || limit < 1 ? 10 : limit;
return this.profesorService.findAllPaginated(page, limit, filters);
}
@Put(':id_profesor')
@Roles(Role.Responsable,Role.Admin)
async modify(@Param('id_profesor', ParseIntPipe) id_profesor: number, @Body() data: profesorDto) {
return this.profesorService.modify(id_profesor, data);
}
@Delete(':id_profesor')
@Roles(Role.Responsable,Role.Admin)
async remove(@Param('id_profesor', ParseIntPipe) id_profesor: number) {
return this.profesorService.remove(id_profesor);
}
@Get(':id_profesor')
async profile(@Param('id_profesor', ParseIntPipe) id_profesor: number) {
return this.profesorService.profile(id_profesor);
}
@Post('filter')
async filter(@Body() data: profesorDto) {
return this.profesorService.filter(data);
}
}
-30
View File
@@ -1,30 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProfesorService } from './profesor.service';
import { ProfesorController } from './profesor.controller';
import { Profesor } from './entities/profesor.entity';
import { DatosAcademicosModule } from '../datos_academicos/datos_academicos.module';
import { LineasInvestigacionModule } from '../lineas_investigacion/lineas_investigacion.module';
import { ProyectosAcademicosModule } from '../proyectos_academicos/proyectos_academicos.module';
import { ProyectosAcademicos } from 'src/proyectos_academicos/entities/proyectos_academicos.entity';
import { DatosAcademicos, LineasInvestigacion } from './dto/profesorDto.dto';
import { ProyectosAcademicosService } from 'src/proyectos_academicos/proyectos_academicos.service';
import { DatosAcademicosService } from 'src/datos_academicos/datos_academicos.service';
import { LineasInvestigacionService } from 'src/lineas_investigacion/lineas_investigacion.service';
@Module({
imports: [TypeOrmModule.forFeature([Profesor,
ProyectosAcademicos,
DatosAcademicos,
LineasInvestigacion]),
DatosAcademicosModule,
LineasInvestigacionModule,
ProyectosAcademicosModule,
],
providers: [ProfesorService,
ProyectosAcademicosService,
DatosAcademicosService,
LineasInvestigacionService,],
controllers: [ProfesorController],
})
export class ProfesorModule {}
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProfesorService } from './profesor.service';
describe('ProfesorService', () => {
let service: ProfesorService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProfesorService],
}).compile();
service = module.get<ProfesorService>(ProfesorService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
-241
View File
@@ -1,241 +0,0 @@
import { HttpException, Injectable } from '@nestjs/common';
import { Profesor } from './entities/profesor.entity';
import { DatosAcademicosService } from '../datos_academicos/datos_academicos.service';
import { LineasInvestigacionService } from '../lineas_investigacion/lineas_investigacion.service';
import { ProyectosAcademicosService } from '../proyectos_academicos/proyectos_academicos.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { profesorDto } from './dto/profesorDto.dto';
import { ProyectosAcademicos } from '../proyectos_academicos/entities/proyectos_academicos.entity';
import { DatosAcademicos } from '../datos_academicos/entities/datos_academicos.entity'
import { LineasInvestigacion } from '../lineas_investigacion/entities/lineas_investigacion.entity'
@Injectable()
export class ProfesorService {
constructor(
@InjectRepository(Profesor) private profesorRepository: Repository<Profesor>,
private datosAcademicosService: DatosAcademicosService,
private lineasInvestigacionService: LineasInvestigacionService,
private proyectosAcademicosService: ProyectosAcademicosService,
@InjectRepository(ProyectosAcademicos)
private readonly proyectosAcademicosRepository: Repository<ProyectosAcademicos>,
@InjectRepository(DatosAcademicos)
private readonly datosAcademicosRepository: Repository<DatosAcademicos>,
@InjectRepository(LineasInvestigacion)
private readonly lineasInvestigacionRepository: Repository<LineasInvestigacion>,
) {}
//register teacher
async register(data: profesorDto) {
const {num_trabajador,rfc,proyectosAcademicos, datosAcademicos, lineasInvestigacion } = data;
const existrfc = await this.profesorRepository.findOne({where:{rfc}});
if(existrfc){
throw new HttpException('rfc already exist', 403);
}
const existnumber = await this.profesorRepository.findOne({where:{num_trabajador}});
if(existnumber){
throw new HttpException('number of worker already exist', 403);
}
const newProfesor = this.profesorRepository.create(data);
const savedProfesor = await this.profesorRepository.save(newProfesor);
if (proyectosAcademicos && proyectosAcademicos.length > 0) {
const proyectos = proyectosAcademicos.map(proyecto => {
const newProyecto = this.proyectosAcademicosRepository.create({
...proyecto,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newProyecto;
});
await this.proyectosAcademicosRepository.save(proyectos);
}
if (datosAcademicos && datosAcademicos.length > 0) {
const datos = datosAcademicos.map(dato => {
const newDato = this.datosAcademicosRepository.create({
...dato,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newDato;
});
await this.datosAcademicosRepository.save(datos);
}
if (lineasInvestigacion && lineasInvestigacion.length > 0) {
const lineas = lineasInvestigacion.map(linea => {
const newLinea = this.lineasInvestigacionRepository.create({
...linea,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newLinea;
});
await this.lineasInvestigacionRepository.save(lineas);
}
return savedProfesor;
}
//brings all teachers
async findAll(): Promise<Profesor[]> {
return this.profesorRepository.find({
order: {
nombre: 'ASC',
},
});
}
//modify teacher
async modify(id_profesor: number, data: profesorDto) {
const profesor = await this.profesorRepository.findOne({ where: { id_profesor } });
if (!profesor) {
throw new HttpException('Profesor not found', 404);
}
const { proyectosAcademicos, datosAcademicos, lineasInvestigacion, ...profesorData } = data;
Object.assign(profesor, profesorData);
const savedProfesor = await this.profesorRepository.save(profesor);
if (proyectosAcademicos) {
await this.proyectosAcademicosRepository.delete({ profesor: { id_profesor } });
const proyectos = proyectosAcademicos.map(proyecto => {
const newProyecto = this.proyectosAcademicosRepository.create({
...proyecto,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newProyecto;
});
await this.proyectosAcademicosRepository.save(proyectos);
}
if (datosAcademicos) {
await this.datosAcademicosRepository.delete({ profesor: { id_profesor } });
const datos = datosAcademicos.map(dato => {
const newDato = this.datosAcademicosRepository.create({
...dato,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newDato;
});
await this.datosAcademicosRepository.save(datos);
}
if (lineasInvestigacion) {
await this.lineasInvestigacionRepository.delete({ profesor: { id_profesor } });
const lineas = lineasInvestigacion.map(linea => {
const newLinea = this.lineasInvestigacionRepository.create({
...linea,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor,
});
return newLinea;
});
await this.lineasInvestigacionRepository.save(lineas);
}
return savedProfesor;
}
//remove teacher
async remove(id_profesor: number) {
const profesor = await this.profesorRepository.findOne({ where: { id_profesor }, relations: ['datosAcademicos'] });
if (!profesor) {
throw new HttpException('Profesor not found', 404);
}
// Delete related records in datos_academicos
await this.datosAcademicosService.removeByProfesorId(id_profesor);
await this.lineasInvestigacionService.removeByProfesorId(id_profesor);
await this.proyectosAcademicosService.removeByProfesorId(id_profesor);
await this.profesorRepository.remove(profesor);
return { message: 'Profesor removed successfully' };
}
//brings teachers by id
async profile(id_profesor: number) {
const profesor = await this.profesorRepository.findOne({
where: { id_profesor },
relations: ['proyectosAcademicos', 'datosAcademicos', 'lineasInvestigacion'],
});
if (!profesor) {
throw new HttpException('Profesor not found', 404);
}
return profesor;
}
//brings teachers by filter
async filter(filterDto: profesorDto): Promise<Profesor[]> {
const query = this.profesorRepository.createQueryBuilder('profesor');
if (filterDto.nombre) {
query.andWhere('profesor.nombre like :nombre', { nombre: `%${filterDto.nombre}%` });
}
if (filterDto.id_categoria !== undefined) {
query.andWhere('profesor.id_categoria = :id_categoria', { id_categoria: filterDto.id_categoria });
}
if (filterDto.extension) {
query.andWhere('profesor.extension like :extension', { extension: `%${filterDto.extension}%` });
}
if (filterDto.id_adscripcion !== undefined) {
query.andWhere('profesor.id_adscripcion = :id_adscripcion', { id_adscripcion: filterDto.id_adscripcion });
}
if (filterDto.id_edificio !== undefined) {
query.andWhere('profesor.id_edificio = :id_edificio', { id_edificio: filterDto.id_edificio });
}
if (filterDto.activo !== undefined) {
query.andWhere('profesor.activo = :activo', { activo: filterDto.activo });
}
return query.getMany();
}
async findAllPaginated(
page: number,
limit: number,
filters: any
): Promise<{ profesores: Profesor[], total: number, totalPages: number }> {
const query = this.profesorRepository.createQueryBuilder('profesor')
if (filters.nombre) {
query.andWhere('profesor.nombre LIKE :nombre', { nombre: `%${filters.nombre}%` });
}
if (filters.adscripcion) {
query.andWhere('profesor.adscripcion = :adscripcion', { adscripcion: filters.adscripcion });
}
if (filters.categoria) {
query.andWhere('profesor.categoria = :categoria', { categoria: filters.categoria });
}
if (filters.edificio) {
query.andWhere('profesor.edificio = :edificio', { edificio: filters.edificio });
}
const [profesores, total] = await query
.skip((page - 1) * limit)
.take(limit)
.orderBy('profesor.nombre', 'ASC')
.getManyAndCount();
const totalPages = Math.ceil(total / limit);
return { profesores, total, totalPages };
}
}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdscripcionController } from './adscripcion.controller'; // Asumiendo que el controlador se llama EdificioController
describe('AdscripcionController', () => {
let controller: AdscripcionController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdscripcionController],
}).compile();
controller = module.get<AdscripcionController>(AdscripcionController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Logger } from '@nestjs/common';
import { AdscripcionService } from './adscripcion.service';
@Controller('adscripcion')
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdscripcionService } from './adscripcion.service';
describe('AdscripcionService', () => {
let service: AdscripcionService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AdscripcionService],
}).compile();
service = module.get<AdscripcionService>(AdscripcionService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -1,6 +1,6 @@
import { Entity, PrimaryColumn, Column, OneToMany } from 'typeorm';
@Entity()
@Entity({ name: 'adscripcion' })
export class Adscripcion {
@PrimaryColumn({ type: 'int', nullable: false })
id_adscripcion: number;
-22
View File
@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
-1
View File
@@ -10,7 +10,6 @@ export class AppController {
@Get()
getHello(): string {
console.log("Todo bien", this.configservice.get("DATABASE_NAME"))
return this.appService.getHello();
}
}
+36 -20
View File
@@ -1,40 +1,43 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { User } from './users/entities/user.entity';
import { UsuarioModule } from './usuario/usuario.module';
import { Usuario } from './usuario/entities/usuario.entity';
import { Adscripcion } from './adscripcion/entities/adscripcion.entity';
import { Categoria } from './categoria/entities/categoria.entity';
import { DatosAcademicos } from './datos_academicos/entities/datos_academicos.entity';
import { DatosAcademicos } from './datos-academicos/entities/datos_academicos.entity';
import { Edificio } from './edificio/entities/edificio.entity';
import { LineasInvestigacion } from './lineas_investigacion/entities/lineas_investigacion.entity';
import { ProyectosAcademicos } from './proyectos_academicos/entities/proyectos_academicos.entity';
import { LineasInvestigacion } from './lineas-investigacion/entities/lineas-investigacion.entity';
import { ProyectosAcademicos } from './proyectos-academicos/entities/proyectos-academicos.entity';
import { TipoUsuario } from './tipo_usuario/entities/tipo_usuario.entity';
import { ProfesorModule } from './Profesor/profesor.module';
import { Profesor } from './Profesor/entities/profesor.entity';
import { AdscripcionController } from "./adscripcion/adscripcion.controller";
import { AdscripcionService } from "./adscripcion/adscripcion.service";
import { AdscripcionModule } from "./adscripcion/adscripcion.module";
import { EdificioModule } from "./edificio/edificio.module";
import { CategoriaModule } from "./categoria/categoria.module";
import { ProfesorModule } from './profesor/profesor.module';
import { Profesor } from './profesor/entities/profesor.entity';
import { AdscripcionModule } from './adscripcion/adscripcion.module';
import { EdificioModule } from './edificio/edificio.module';
import { CategoriaModule } from './categoria/categoria.module';
import { DatabaseModule } from './database/database.module';
import { LineasInvestigacionModule } from './lineas-investigacion/lineas-investigacion.module';
import { ProyectosAcademicosModule } from './proyectos-academicos/proyectos-academicos.module';
import { FotografiaModule } from './fotografia/fotografia.module';
@Module({
imports: [
AuthModule,
UsersModule,
UsuarioModule,
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule, AuthModule, UsersModule],
imports: [ConfigModule, AuthModule, UsuarioModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mariadb',
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
@@ -42,19 +45,32 @@ import { CategoriaModule } from "./categoria/categoria.module";
database: configService.get('DB_DATABASE'),
entities: [
Profesor,
User,
Usuario,
Adscripcion,
Categoria,
DatosAcademicos,
Edificio,
DatosAcademicos,
LineasInvestigacion,
ProyectosAcademicos,
TipoUsuario,
],
synchronize: true,
//dropSchema: true,
//synchronize: true, JAMA USAR ESTO EN PRODUCCION BORRA DATOS DE LA DB
}),
}),
ProfesorModule, AdscripcionModule, EdificioModule, CategoriaModule
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'fotografias'),
serveRoot: '/fotografias',
}),
FotografiaModule,
ProfesorModule,
AdscripcionModule,
EdificioModule,
CategoriaModule,
DatabaseModule,
LineasInvestigacionModule,
ProyectosAcademicosModule,
UsuarioModule,
],
controllers: [AppController],
providers: [AppService],
-2
View File
@@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
console.log("it's ok ")
return "hei verden";
}
}
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+31 -44
View File
@@ -9,59 +9,46 @@ import {
Put,
Request,
UseGuards,
Delete
Delete,
ParseIntPipe,
Query,
Logger,
Req,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
import { registerDto } from './dto/registerDto.dto';
import {Roles} from '../permissions/roles.decorator'
import {RolesGuard} from '../permissions/roles.guard'
import {Role} from '../permissions/role.enum'
import { registerDto, SignInDto } from './dto/sign-in.dto';
import { Roles } from '../permissions/roles.decorator';
import { RolesGuard } from '../permissions/roles.guard';
import { Role } from '../permissions/role.enum';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SignUpDto } from './dto/sign-up.dto';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
@ApiTags('Auth')
@Controller('auth')
//@UseGuards(RolesGuard)
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
async signIn(@Body() data: registerDto) {
@ApiOperation({
summary: 'Iniciar sesión',
description: 'Autentica a un usuario y devuelve un token de acceso.',
})
async signIn(@Body() data: SignInDto) {
return this.authService.signIn(data);
}
//@UseGuards()
@Post("register")
//@Roles(Role.Admin)
async register(@Body() data: registerDto){
return this.authService.register(data)
@Post('register')
@ApiOperation({
summary: 'Registro de usuario',
description: 'Registro ded administradores o responsables',
})
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@Roles(Role.Admin)
async register(@Body() data: SignUpDto) {
Logger.debug('register user');
return this.authService.register(data);
}
//@UseGuards(AuthGuard)
@Get('profile')
async getProfile(@Request() req) {
return req.user;
}
//@UseGuards(AuthGuard)
@Put('Modificacion/:id')
//@Roles(Role.Admin)
async update(@Param('id') userId: number, @Body() data: registerDto) {
return this.authService.update(userId, data);
}
//@UseGuards(AuthGuard)
@Delete('Borrado/:id')
//@Roles(Role.Admin)
async remove(@Param('id') id: number) {
await this.authService.remove(id);
return { message: 'User successfully deleted' };
}
@HttpCode(HttpStatus.OK)
@Post('validate')
async validateToken(@Body('token') token: string) {
return this.authService.validateToken(token);
}
}
}
-43
View File
@@ -1,43 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('token faltante');
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: this.configService.get('JWT_SECRET'),
}
);
request['user'] = payload;
} catch {
throw new UnauthorizedException('algo fallo');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
+14 -12
View File
@@ -1,29 +1,31 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { UsuarioModule } from '../usuario/usuario.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from 'src/permissions/jwt.strategy';
@Module({
imports: [
UsersModule,
UsuarioModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject:[ConfigService],
useFactory:async(configService:ConfigService)=>{
return{
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
global: true,
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: "1h"},
}
}
signOptions: { expiresIn: configService.get('JWT_SECRET_EXPIRES') },
};
},
}),
TypeOrmModule.forFeature([User])
TypeOrmModule.forFeature([Usuario]),
],
providers: [AuthService],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+45 -41
View File
@@ -2,52 +2,59 @@ import {
HttpException,
Inject,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { UsuarioService } from '../usuario/usuario.service';
import { JwtService } from '@nestjs/jwt';
import { registerDto } from './dto/registerDto.dto';
import { User } from 'src/users/entities/user.entity';
import { registerDto, SignInDto } from './dto/sign-in.dto';
import { Usuario } from 'src/usuario/entities/usuario.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { hash } from 'bcrypt';
import { compare } from 'bcryptjs';
import { ConfigService } from '@nestjs/config';
import { SignUpDto } from './dto/sign-up.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private usersService: UsuarioService,
private jwtService: JwtService,
private configService: ConfigService,
@InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(Usuario) private userRepository: Repository<Usuario>,
) {}
//register users
async register(data: registerDto) {
const { usuario, contraseña} = data;
async register(data: SignUpDto) {
const { usuario, password } = data;
if (await this.usersService.findOne(usuario)) {
throw new HttpException('User already exist', 403);
const searchedUser = await this.usersService.findOneByName(usuario);
if (searchedUser) {
throw new HttpException('El usuario ya existe', 409);
}
const hashedpassword = await hash(contraseña, 10);
const hashedpassword = await hash(password, 10);
data = { ...data, password: hashedpassword };
data = { ...data, contraseña: hashedpassword };
return this.userRepository.save(this.userRepository.create(data));
const createdUsuario = await this.userRepository.create(data);
const savedUsuario = await this.userRepository.save(createdUsuario);
return savedUsuario;
}
//singin
async signIn(data: registerDto) {
const { usuario, contraseña } = data;
const user = await this.usersService.findOne(usuario);
async signIn(data: SignInDto) {
const { usuario, password, rememberMe } = data;
const user = await this.usersService.findOneByName(usuario);
if (!user) {
throw new UnauthorizedException('Invalid name');
throw new UnauthorizedException('Credenciales inválidas.');
}
const checkPassword = await compare(contraseña, user.contraseña);
const checkPassword = await compare(password, user.password);
if (!checkPassword) {
throw new UnauthorizedException('Invalid password');
throw new UnauthorizedException('Credenciales inválidas.');
}
const payload = {
@@ -55,44 +62,32 @@ export class AuthService {
usuario: user.usuario,
id_tipo_usuario: user.id_tipo_usuario,
};
const token = this.jwtService.sign(payload);
const token = this.jwtService.sign(payload, {
expiresIn: rememberMe ? '31d' : '7h',
});
const expirationTime = rememberMe ? 31 * 24 * 3600 * 1000 : 7 * 3600 * 1000;
await this.usersService.updateTokenAndDates(
user.id_usuario,
token,
new Date(),
new Date(Date.now() + 3600 * 1000),
new Date(Date.now() + expirationTime),
);
return { token: token };
}
//update user
async update(userId, data:registerDto) {
const{contraseña}=data;
if(contraseña){
const hashedpassword = await hash(contraseña, 10);
data = {...data,contraseña:hashedpassword};
}
return await this.userRepository.update(userId, data);
}
async remove(userId: number): Promise<void> {
await this.userRepository.delete(userId);
return { token };
}
//validate tojen of users
async validateToken(token: string): Promise<User> {
async validateToken(token: string): Promise<Usuario> {
try {
const decoded = this.jwtService.verify(token, {
secret: this.configService.get<string>('JWT_SECRET'),
});
//verify JWT's username with DB's username
const user = await this.usersService.findOne(decoded.username);
const user = await this.usersService.findOneByName(decoded.username);
if (!user) {
throw new UnauthorizedException('Invalid token');
}
@@ -103,9 +98,18 @@ export class AuthService {
throw new UnauthorizedException('Token has expired');
}
return user;
return;
} catch (error) {
throw new UnauthorizedException('validation token failed');
}
}
getTokenUserId(token: string): number {
try {
const decoded = this.jwtService.verify(token);
return decoded['id_usuario'];
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class JwtPayloadDto {
@ApiProperty({ example: 1, description: 'ID único del usuario' })
@IsNumber()
id_usuario: number;
@ApiProperty({ example: 'juan.perez', description: 'Nombre de usuario' })
@IsString()
usuario: string;
@ApiProperty({
example: 2,
description: 'Tipo de usuario (ejemplo: Admin, Estudiante, etc.)',
})
@IsNumber()
id_tipo_usuario: number;
}
-25
View File
@@ -1,25 +0,0 @@
import {IsString, IsNotEmpty,IsNumber } from "class-validator";
export class registerDto{
@IsString()
@IsNotEmpty()
usuario: string;
@IsString()
@IsNotEmpty()
nombre: string;
@IsString()
@IsNotEmpty()
contraseña: string;
@IsNumber()
@IsNotEmpty()
id_tipo_usuario:number;
@IsString()
@IsNotEmpty()
correo: string;
}
+61
View File
@@ -0,0 +1,61 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsString,
IsNotEmpty,
IsNumber,
IsBoolean,
IsOptional,
} from 'class-validator';
export class SignInDto {
@ApiProperty({
description: 'Nombre de usuario o correo electrónico para iniciar sesión',
example: 'admin',
})
@IsString()
@IsNotEmpty()
usuario: string;
@ApiProperty({
description: 'Contraseña del usuario',
example: 'Test2024DSC',
})
@IsString()
@IsNotEmpty()
password: string;
@ApiProperty({
description: 'Indica si se debe recordar la sesión (opcional)',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
rememberMe?: boolean;
}
export class registerDto {
@IsString()
@IsNotEmpty()
usuario: string;
@IsString()
@IsNotEmpty()
nombre: string;
@IsString()
@IsNotEmpty()
contraseña: string;
@IsNumber()
@IsNotEmpty()
id_tipo_usuario: number;
@IsString()
@IsNotEmpty()
correo: string;
@IsBoolean()
@IsOptional()
rememberMe: boolean;
}
+46
View File
@@ -0,0 +1,46 @@
import {
IsEmail,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
MinLength,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SignUpDto {
@ApiProperty({ description: 'ID del tipo de usuario', example: 1 })
@IsInt()
@IsNotEmpty()
id_tipo_usuario: number;
@ApiProperty({
description: 'Nombre completo del usuario',
example: 'Juan Pérez',
})
@IsString()
@IsNotEmpty()
nombre: string;
@ApiProperty({
description: 'Correo electrónico',
example: 'juan.perez@example.com',
})
@IsEmail()
@IsNotEmpty()
correo: string;
@ApiProperty({ description: 'Nombre de usuario', example: 'juanperez' })
@IsString()
@IsNotEmpty()
usuario: string;
@ApiProperty({
description: 'Contraseña del usuario',
example: 'password',
})
@IsString()
@MinLength(2)
@IsNotEmpty()
password: string;
}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CategoriaController } from './categoria.controller';
describe('CategoriaController', () => {
let controller: CategoriaController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CategoriaController],
}).compile();
controller = module.get<CategoriaController>(CategoriaController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Logger } from '@nestjs/common';
import { CategoriaService } from './categoria.service';
@Controller('categoria')
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CategoriaService } from './categoria.service';
describe('CategoriaService', () => {
let service: CategoriaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CategoriaService],
}).compile();
service = module.get<CategoriaService>(CategoriaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+2 -2
View File
@@ -1,6 +1,6 @@
import { Entity, PrimaryColumn, Column, OneToMany } from 'typeorm';
@Entity()
@Entity({ name: 'categoria' })
export class Categoria {
@PrimaryColumn({ type: 'int', nullable: false })
id_categoria: number;
@@ -10,4 +10,4 @@ export class Categoria {
@OneToMany(() => Categoria, (categoria) => categoria.profesores)
profesores: Categoria[];
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Controller, Post } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@ApiTags('Carga de Datos')
@Controller('database')
export class DatabaseController {
constructor(private readonly databaseService: DatabaseService) {}
@Post('cargar-catalogos')
async uploadFile() {
await this.databaseService.cargarDatosDesdeExcel();
return { message: 'Datos cargados exitosamente' };
}
@Post('migrar-datos')
@ApiOperation({
summary: 'Migrar datos a la nueva base de datos',
description:
'Este endpoint ejecuta la migración desde la base de datos antigua a la nueva.',
})
async migrateDatabase() {
await this.databaseService.migrateDB();
return { message: 'Migración completada exitosamente' };
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { DatabaseController } from './database.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Adscripcion } from 'src/adscripcion/entities/adscripcion.entity';
import { Edificio } from 'src/edificio/entities/edificio.entity';
import { Categoria } from 'src/categoria/entities/categoria.entity';
import { TipoUsuario } from 'src/tipo_usuario/entities/tipo_usuario.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Adscripcion,
Edificio,
Categoria,
TipoUsuario,
Usuario,
]),
],
providers: [DatabaseService],
controllers: [DatabaseController],
})
export class DatabaseModule {}
+67
View File
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Adscripcion } from 'src/adscripcion/entities/adscripcion.entity';
import { Repository } from 'typeorm';
import * as XLSX from 'xlsx';
import * as path from 'path';
import * as fs from 'fs';
import { Edificio } from 'src/edificio/entities/edificio.entity';
import { Categoria } from 'src/categoria/entities/categoria.entity';
import { TipoUsuario } from 'src/tipo_usuario/entities/tipo_usuario.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
@Injectable()
export class DatabaseService {
constructor(
@InjectRepository(Adscripcion)
private readonly adscripcionRepository: Repository<Adscripcion>,
@InjectRepository(Edificio)
private readonly edificioRepository: Repository<Edificio>,
@InjectRepository(Categoria)
private readonly categoriaRepository: Repository<Categoria>,
@InjectRepository(TipoUsuario)
private readonly tipoUsuarioRepository: Repository<TipoUsuario>,
@InjectRepository(Usuario)
private readonly usuarioRepository: Repository<Usuario>,
) {}
async cargarDatosDesdeExcel(): Promise<void> {
const folderPath = path.join(__dirname, '../../utils');
if (!fs.existsSync(folderPath)) {
console.error('La carpeta utils no existe:', folderPath);
return;
}
const files = fs
.readdirSync(folderPath)
.filter((file) => file.endsWith('.xlsx'));
const repos = {
'adscripciones_data.xlsx': this.adscripcionRepository,
'edificios_data.xlsx': this.edificioRepository,
'categorias_data.xlsx': this.categoriaRepository,
'tipo_usuario.xlsx': this.tipoUsuarioRepository,
'usuarios.xlsx': this.usuarioRepository,
};
for (const file of files) {
const repo = repos[file];
if (!repo) continue; // Si el archivo no está en el mapeo, lo omitimos
const filePath = path.join(folderPath, file);
console.log(`Procesando: ${file}`);
const workbook = XLSX.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const data = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
// Guardar datos en paralelo para mejorar el rendimiento
await Promise.all(data.map((row) => repo.save(row)));
}
}
async migrateDB(): Promise<any> {
}
}
@@ -0,0 +1,65 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Param,
Patch,
UseGuards,
} from '@nestjs/common';
import { DatosAcademicosService } from './datos-academicos.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
import { RolesGuard } from 'src/permissions/roles.guard';
import { Roles } from 'src/permissions/roles.decorator';
import { Role } from 'src/permissions/role.enum';
@ApiBearerAuth()
@ApiTags('datos-academicos')
@Controller('datos-academicos')
export class DatosAcademicosController {
constructor(private datosAcademicosService: DatosAcademicosService) {}
@Patch(':id_profesor')
@ApiOperation({
summary: 'Actualizar datos academicos de un profesor',
description:
'Este endpoint permite actualizar las líneas de investigación en formato Markdown/HTML para un profesor específico.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
id_datos: {
type: 'number',
description: 'ID de los datos que se van a actualizar',
example: 1,
},
grados_obtenidos: {
type: 'string',
description:
'Nuevo contenido de líneas de investigación en formato Markdown o HTML',
example: '# Investigación en IA\n- Tema 1\n- Tema 2\n- Tema 3',
},
},
},
})
async updateDatosAcademicos(
@Param('id_profesor') id_profesor: number,
@Body('id_datos') id_datos: number,
@Body('grados_obtenidos') grados_obtenidos: string,
) {
if (!grados_obtenidos || grados_obtenidos.trim() === '') {
throw new HttpException(
'El contenido no puede estar vacío.',
HttpStatus.BAD_REQUEST,
);
}
return this.datosAcademicosService.updateDatosAcademicos(
id_datos,
id_profesor,
grados_obtenidos,
);
}
}
@@ -0,0 +1,19 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatosAcademicosService } from './datos-academicos.service';
import { DatosAcademicosController } from './datos-academicos.controller';
import { DatosAcademicos } from './entities/datos_academicos.entity';
import { ProfesorModule } from 'src/profesor/profesor.module';
import { UsuarioModule } from 'src/usuario/usuario.module';
@Module({
imports: [
TypeOrmModule.forFeature([DatosAcademicos]),
forwardRef(() => ProfesorModule),
UsuarioModule,
],
providers: [DatosAcademicosService],
controllers: [DatosAcademicosController],
exports: [DatosAcademicosService],
})
export class DatosAcademicosModule {}
@@ -0,0 +1,88 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DatosAcademicos } from './entities/datos_academicos.entity';
import { Profesor } from 'src/profesor/entities/profesor.entity';
import { ProfesorService } from 'src/profesor/profesor.service';
@Injectable()
export class DatosAcademicosService {
constructor(
@InjectRepository(DatosAcademicos)
private readonly datosAcademicosRepository: Repository<DatosAcademicos>,
@Inject(forwardRef(() => ProfesorService))
private readonly profesorService: ProfesorService,
) {}
async create(profesor: Profesor, grado_maximo: string) {
const datosAcademicos = this.datosAcademicosRepository.create({
profesor: profesor,
grados_obtenidos: '',
grado_maximo: grado_maximo || 'Sin Grado Maximo',
});
return await this.datosAcademicosRepository.save(datosAcademicos);
}
async updateDatosAcademicos(
id_datos: number,
id_profesor: number,
nuevoContenido: string,
) {
const profesor = await this.profesorService.findOne(id_profesor);
let datosAcademicos = await this.datosAcademicosRepository.findOne({
where: { profesor: profesor, id_datos },
});
if (!datosAcademicos) {
Logger.error(
`No se encontraron datos académicos del profesor con ID ${id_profesor} al intentar actualizar. Se procederá a crear una nueva.`,
);
// Crear nuevos datos académicos con el contenido
datosAcademicos = this.datosAcademicosRepository.create({
profesor: profesor,
grados_obtenidos: nuevoContenido, // Se asigna el nuevo contenido directamente
grado_maximo: 'Sin Grado Maximo', // Valor predeterminado
});
// Guardar los nuevos datos académicos
await this.datosAcademicosRepository.save(datosAcademicos);
} else {
// Si ya existen, actualizar su contenido
datosAcademicos.grados_obtenidos = nuevoContenido;
await this.datosAcademicosRepository.save(datosAcademicos);
}
return datosAcademicos; // Devolver los datos académicos actualizados o recién creados
}
async updateGradoMaximo(id_profesor: number, nuevoGradoMaximo: string) {
const profesor = await this.profesorService.findOne(id_profesor);
const datosAcademicos = await this.datosAcademicosRepository.findOne({
where: { profesor: profesor },
});
if (!datosAcademicos) {
Logger.error(
`No se encontraron datos académicos del profesor con ID ${id_profesor} al intentar actualizar el grado máximo \n Se procede a crear una nueva`,
);
return await this.create(profesor, nuevoGradoMaximo);
}
datosAcademicos.grado_maximo = nuevoGradoMaximo;
return await this.datosAcademicosRepository.save(datosAcademicos);
}
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.datosAcademicosRepository.delete({ profesor: { id_profesor } });
}
async findAll(): Promise<DatosAcademicos[]> {
return this.datosAcademicosRepository.find();
}
}
@@ -1,5 +1,11 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Profesor } from '../../Profesor/entities/profesor.entity';
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Profesor } from '../../profesor/entities/profesor.entity';
@Entity({ name: 'datos_academicos' })
export class DatosAcademicos {
@@ -15,7 +21,7 @@ export class DatosAcademicos {
@Column({ type: 'varchar', length: 600, nullable: true })
grados_obtenidos: string;
@ManyToOne(() => Profesor, (profesor) => profesor.datosAcademicos)
@ManyToOne(() => Profesor, (profesor) => profesor.lineasInvestigacion)
@JoinColumn({ name: 'id_profesor' })
profesor: Profesor;
}
@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { DatosAcademicosService } from './datos_academicos.service';
@Controller('datos-academicos')
export class DatosAcademicosController {
constructor(private readonly datosAcademicosService: DatosAcademicosService) {}
@Get()
findAll() {
return this.datosAcademicosService.findAll();
}
}
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatosAcademicos } from './entities/datos_academicos.entity';
import { DatosAcademicosService } from './datos_academicos.service';
@Module({
imports: [TypeOrmModule.forFeature([DatosAcademicos])],
providers: [DatosAcademicosService],
exports: [DatosAcademicosService],
})
export class DatosAcademicosModule {}
@@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DatosAcademicos } from './entities/datos_academicos.entity';
@Injectable()
export class DatosAcademicosService {
constructor(
@InjectRepository(DatosAcademicos)
private readonly datosAcademicosRepository: Repository<DatosAcademicos>,
) {}
// Add methods to manage DatosAcademicos entities as needed
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.datosAcademicosRepository.delete({ profesor: { id_profesor } });
}
async findAll(): Promise<DatosAcademicos[]> {
return this.datosAcademicosRepository.find();
}
}
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EdificioController } from './edificio.controller';
describe('EdificioController', () => {
let controller: EdificioController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EdificioController],
}).compile();
controller = module.get<EdificioController>(EdificioController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Logger } from '@nestjs/common';
import { EdificioService } from './edificio.service';
@Controller('edificio')
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EdificioService } from './edificio.service'; // Asegúrate de importar el servicio correcto
describe('EdificioService', () => {
let service: EdificioService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EdificioService], // Asegúrate de incluir el servicio correcto aquí
}).compile();
service = module.get<EdificioService>(EdificioService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+2 -2
View File
@@ -1,6 +1,6 @@
import { Entity, PrimaryColumn, Column, OneToMany } from 'typeorm';
import { Entity, PrimaryColumn, Column, OneToMany} from 'typeorm';
@Entity()
@Entity({ name: 'edificio' })
export class Edificio {
@PrimaryColumn({ type: 'int', nullable: false })
id_edificio: number;
+85
View File
@@ -0,0 +1,85 @@
import {
Controller,
Post,
Patch,
UseInterceptors,
UploadedFile,
Body,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { FotografiaService } from './fotografia.service';
import {
ApiBody,
ApiConsumes,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { Express } from 'express';
@ApiTags('fotografia')
@Controller('fotografia')
export class FotografiaController {
constructor(private readonly fotografiaService: FotografiaService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: 'Subir una imagen con un nombre personalizado' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
customName: {
type: 'string',
description: 'Nombre personalizado de la imagen',
},
file: {
type: 'string',
format: 'binary',
},
},
},
})
@ApiResponse({ status: 201, description: 'Imagen subida exitosamente' })
uploadFile(
@UploadedFile() file: Express.Multer.File,
@Body('customName') customName: string,
) {
return { message: 'Imagen subida exitosamente', fileName: file.filename };
}
@Post('update')
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({
summary: 'Actualizar una imagen existente con un nombre personalizado',
})
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
oldFileName: {
type: 'string',
description: 'Nombre de la imagen antigua',
},
customName: {
type: 'string',
description: 'Nuevo nombre personalizado de la imagen',
},
file: {
type: 'string',
format: 'binary',
},
},
},
})
@ApiResponse({ status: 200, description: 'Imagen actualizada correctamente' })
updateFile(
@UploadedFile() file: Express.Multer.File,
@Body('oldFileName') oldFileName: string,
@Body('customName') customName: string,
) {
return this.fotografiaService.updateFile(oldFileName, file, customName);
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { FotografiaController } from './fotografia.controller';
import { FotografiaService } from './fotografia.service';
import { diskStorage } from 'multer';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: (req, file, cb) => {
const uploadPath = join(__dirname, '..', '..', 'fotografias');
if (!existsSync(uploadPath)) {
mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const uniqueSuffix =
Date.now() + '-' + Math.round(Math.random() * 1e9);
const customName = req.body.customName
? req.body.customName.toLowerCase().replace(/\s+/g, '-')
: 'image';
cb(
null,
`${customName}-${uniqueSuffix}${file.originalname.substring(file.originalname.lastIndexOf('.'))}`,
);
},
}),
}),
],
controllers: [FotografiaController],
providers: [FotografiaService],
})
export class FotografiaModule {}
+58
View File
@@ -0,0 +1,58 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { renameSync, existsSync, unlinkSync, mkdirSync } from 'fs';
import { join } from 'path';
@Injectable()
export class FotografiaService {
updateFile(
oldFileName: string,
newFile: Express.Multer.File,
customName?: string,
) {
try {
const uploadPath = join(__dirname, '..', '..', 'fotografias');
const oldFilePath = join(uploadPath, oldFileName);
const tempFilePath = newFile.path; // Ruta temporal del archivo subido
// 🛠️ 1. Asegurar que la carpeta 'fotografias/' exista
if (!existsSync(uploadPath)) {
mkdirSync(uploadPath, { recursive: true });
}
// 🛠️ 2. Verificar si el archivo temporal realmente existe antes de renombrar
if (!existsSync(tempFilePath)) {
throw new BadRequestException('El archivo temporal no fue encontrado.');
}
// 🛠️ 3. Eliminar la imagen anterior si existe
if (oldFileName && existsSync(oldFilePath)) {
try {
unlinkSync(oldFilePath);
console.log(`✅ Imagen anterior eliminada: ${oldFileName}`);
} catch (error) {
console.error(`❌ Error al eliminar la imagen anterior: ${error.message}`);
}
} else {
console.log('No se encontró imagen anterior para eliminar.', oldFileName);
}
// 🛠️ 4. Generar nuevo nombre de archivo con customName y timestamp
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const fileExtension = newFile.originalname.substring(newFile.originalname.lastIndexOf('.'));
const newFileName = `${customName ? customName.toLowerCase().replace(/\s+/g, '-') : 'image'}-${uniqueSuffix}${fileExtension}`;
const newFilePath = join(uploadPath, newFileName);
// 🛠️ 5. Mover el archivo de la carpeta temporal a 'fotografias/' con su nuevo nombre
renameSync(tempFilePath, newFilePath);
console.log(`Imagen subida con éxito: ${newFileName}`);
console.log('-------------------')
return {
message: 'Imagen actualizada correctamente',
fileName: newFileName,
};
} catch (error) {
throw new BadRequestException(`Error en la actualización de la imagen: ${error.message}`);
}
}
}
@@ -0,0 +1,21 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Profesor } from '../../profesor/entities/profesor.entity';
@Entity({ name: 'lineas_investigacion' })
export class LineasInvestigacion {
@PrimaryGeneratedColumn()
id_linea_inv: number;
@Column({ type: 'varchar', length: 600, nullable: true })
lineas_inv_html: string;
@ManyToOne(() => Profesor, (profesor) => profesor.lineasInvestigacion)
@JoinColumn({ name: 'id_profesor' })
profesor: Profesor;
}
@@ -0,0 +1,65 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Param,
Patch,
UseGuards,
} from '@nestjs/common';
import { LineasInvestigacionService } from './lineas-investigacion.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
import { RolesGuard } from 'src/permissions/roles.guard';
@ApiBearerAuth()
@ApiTags('lineas-investigacion')
@Controller('lineas-investigacion')
@UseGuards(JwtAuthGuard, RolesGuard)
export class LineasInvestigacionController {
constructor(private lineasInvestigacionService: LineasInvestigacionService) {}
@Patch(':id_profesor')
@ApiOperation({
summary: 'Actualizar líneas de investigación de un profesor',
description:
'Este endpoint permite actualizar las líneas de investigación en formato Markdown/HTML para un profesor específico.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
id_linea_inv: {
type: 'number',
description:
'ID de las lineas de investigacion que se deben actualizar',
example: 1,
},
lineas_inv_html: {
type: 'string',
description:
'Nuevo contenido de líneas de investigación en formato Markdown o HTML',
example: '# Investigación en IA\n- Tema 1\n- Tema 2\n- Tema 3',
},
},
},
})
async updateLineasInvestigacion(
@Param('id_profesor') id_profesor: number,
@Body('id_linea_inv') id_linea_inv: number,
@Body('lineas_inv_html') lineasInvHtml: string,
) {
if (!lineasInvHtml || lineasInvHtml.trim() === '') {
throw new HttpException(
'El contenido no puede estar vacío.',
HttpStatus.BAD_REQUEST,
);
}
return await this.lineasInvestigacionService.updateLineasInvestigacion(
id_linea_inv,
id_profesor,
lineasInvHtml,
);
}
}
@@ -0,0 +1,19 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LineasInvestigacion } from './entities/lineas-investigacion.entity';
import { LineasInvestigacionService } from './lineas-investigacion.service';
import { ProfesorModule } from 'src/profesor/profesor.module';
import { LineasInvestigacionController } from './lineas-investigacion.controller';
import { UsuarioModule } from 'src/usuario/usuario.module';
@Module({
imports: [
TypeOrmModule.forFeature([LineasInvestigacion]),
forwardRef(() => ProfesorModule),
UsuarioModule,
],
controllers: [LineasInvestigacionController],
providers: [LineasInvestigacionService],
exports: [LineasInvestigacionService],
})
export class LineasInvestigacionModule {}
@@ -0,0 +1,69 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { LineasInvestigacion } from './entities/lineas-investigacion.entity';
import { Profesor } from 'src/profesor/entities/profesor.entity';
import { ProfesorService } from 'src/profesor/profesor.service';
@Injectable()
export class LineasInvestigacionService {
constructor(
@InjectRepository(LineasInvestigacion)
private readonly lineasInvestigacionRepository: Repository<LineasInvestigacion>,
@Inject(forwardRef(() => ProfesorService))
private readonly profesorService: ProfesorService,
) {}
async create(profesor: Profesor) {
const lineasInvestigacion = this.lineasInvestigacionRepository.create({
profesor: profesor,
lineas_inv_html: '',
});
return await this.lineasInvestigacionRepository.save(lineasInvestigacion);
}
async updateLineasInvestigacion(
id_linea_inv: number,
id_profesor: number,
nuevoContenido: string,
) {
const profesor = await this.profesorService.findOne(id_profesor);
let lineasInvestigacion = await this.lineasInvestigacionRepository.findOne({
where: { profesor: profesor, id_linea_inv },
});
if (!lineasInvestigacion) {
Logger.error(
`No se encontró la línea de investigación del profesor con ID ${id_profesor} al intentar actualizarla. Se procederá a crear una nueva.`,
);
// Crear una nueva línea de investigación con el contenido
lineasInvestigacion = this.lineasInvestigacionRepository.create({
profesor: profesor,
lineas_inv_html: nuevoContenido, // Se asigna el nuevo contenido directamente
});
// Guardar la nueva línea de investigación
await this.lineasInvestigacionRepository.save(lineasInvestigacion);
} else {
// Si ya existe, actualizar su contenido
lineasInvestigacion.lineas_inv_html = nuevoContenido;
await this.lineasInvestigacionRepository.save(lineasInvestigacion);
}
return lineasInvestigacion; // Devolver la línea de investigación actualizada o recién creada
}
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.lineasInvestigacionRepository.delete({
profesor: { id_profesor },
});
}
async findAll(): Promise<LineasInvestigacion[]> {
return this.lineasInvestigacionRepository.find();
}
}
@@ -1,22 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Profesor } from "../../Profesor/entities/profesor.entity";
@Entity({ name: 'lineas_investigacion' })
export class LineasInvestigacion {
@PrimaryGeneratedColumn()
id_linea_inv: number;
@Column({ type: 'int', nullable: false })
id_profesor: number;
@Column({ type: 'varchar', length: 300, nullable: true })
lineas_inv: string;
@Column({ type: 'varchar', length: 350, nullable: true })
lineas_inv_html: string;
@ManyToOne(() => Profesor, profesor => profesor.lineasInvestigacion)
@JoinColumn({ name: 'id_profesor' })
profesor: Profesor;
}
@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { LineasInvestigacionService } from './lineas_investigacion.service';
@Controller('lineas-investigacion')
export class LineasInvestigacionController {
constructor(private readonly lineasInvestigacionService: LineasInvestigacionService) {}
@Get()
findAll() {
return this.lineasInvestigacionService.findAll();
}
}
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LineasInvestigacion } from './entities/lineas_investigacion.entity';
import { LineasInvestigacionService } from './lineas_investigacion.service';
@Module({
imports: [TypeOrmModule.forFeature([LineasInvestigacion])],
providers: [LineasInvestigacionService],
exports: [LineasInvestigacionService],
})
export class LineasInvestigacionModule {}
@@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { LineasInvestigacion } from './entities/lineas_investigacion.entity';
@Injectable()
export class LineasInvestigacionService {
constructor(
@InjectRepository(LineasInvestigacion)
private readonly lineasInvestigacionRepository: Repository<LineasInvestigacion>,
) {}
// Add methods to manage LineasInvestigacion entities as needed
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.lineasInvestigacionRepository.delete({ profesor: { id_profesor } });
}
async findAll(): Promise<LineasInvestigacion[]> {
return this.lineasInvestigacionRepository.find();
}
}
+42 -1
View File
@@ -1,9 +1,50 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import { Logger, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 💡 Agregar validaciones globales con class-validator
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Elimina propiedades desconocidas
forbidNonWhitelisted: true, // Lanza error si hay propiedades no permitidas
transform: true, // Convierte tipos automáticamente
}),
);
if (process.env.PROD === 'false') {
Logger.log('--- Cargando Documentacion ---');
const config = new DocumentBuilder()
.setTitle('Directorio API')
.setVersion('1.0')
.addTag('Carga de Datos')
.addTag('Auth')
.addTag('Profesor')
.addTag('Usuario')
.addTag('lineas-investigacion')
.addTag('proyectos-academicos')
.addTag('datos-academicos')
.addBearerAuth()
.build();
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, documentFactory);
}
app.enableCors();
app.use(bodyParser.json({ limit: '1mb' }));
app.use(bodyParser.urlencoded({ limit: '1mb', extended: true }));
app.useStaticAssets(path.join(__dirname, '..', 'fotografias'), {
prefix: '/fotografias/',
});
await app.listen(3000);
}
bootstrap();
+53
View File
@@ -0,0 +1,53 @@
import {
Injectable,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { UsuarioService } from 'src/usuario/usuario.service';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private usuarioService: UsuarioService,
private readonly reflector: Reflector,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// ✅ Primero, ejecutar la validación del padre
const isValid = await super.canActivate(context);
if (!isValid) {
throw new UnauthorizedException('Token inválido o expirado');
}
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token no proporcionado');
}
const token = authHeader.split(' ')[1];
const payload = request.user; // Extraído desde Passport
if (!payload || !payload.id_usuario) {
throw new UnauthorizedException('Token inválido');
}
// Obtener el usuario desde la base de datos
const usuario = await this.usuarioService.findOneById(payload.id_usuario);
if (!usuario) {
throw new UnauthorizedException('Usuario no encontrado');
}
// Validar que el token en la DB coincida con el enviado
if (usuario.jwt !== token) {
throw new UnauthorizedException('Token no autorizado');
}
return true;
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { JwtPayloadDto } from 'src/auth/dto/payload.dto';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: JwtPayloadDto) {
return payload
}
}
+3 -4
View File
@@ -1,5 +1,4 @@
export enum Role {
Admin = '1',
Responsable = '2',
}
Admin = 1,
Responsable = 2,
}
+20 -23
View File
@@ -1,47 +1,44 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector, private jwtService: JwtService) {}
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// ✅ Obtener los roles requeridos en el endpoint
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // Si no hay roles requeridos, se permite el acceso
return true; // Si el endpoint no tiene restricción de roles, permitir acceso
}
const request = context.switchToHttp().getRequest();
const authHeader = request.headers['authorization'];
const user = request.user; // Se obtiene de `JwtAuthGuard`
if (!authHeader) {
throw new UnauthorizedException('No authorization header provided');
if (!user || !user.id_tipo_usuario) {
throw new ForbiddenException(
'No tienes permisos para acceder a este recurso',
);
}
const [, token] = authHeader.split(' ');
if (!token) {
throw new UnauthorizedException('No token provided');
// Verificar si el usuario tiene el rol requerido
if (!requiredRoles.includes(user.id_tipo_usuario)) {
throw new ForbiddenException(
'No tienes permisos para acceder a este recurso',
);
}
let userPayload;
try {
userPayload = this.jwtService.verify(token);
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
const userRoles: Role[] = userPayload.id_tipo_usuario;
if (!userRoles) {
return false; // Si el token no contiene roles, se niega el acceso
}
return requiredRoles.some(role => userRoles.includes(role));
return true;
}
}
+162
View File
@@ -0,0 +1,162 @@
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsString,
IsOptional,
Length,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PartialType } from '@nestjs/mapped-types';
export class CreateProfesorDto {
@ApiProperty({
example: 'Juan Pérez',
description: 'Nombre completo del profesor',
})
@IsString({ message: 'El nombre debe ser una cadena de texto' })
@IsNotEmpty({ message: 'El nombre es obligatorio' })
@Length(1, 80, { message: 'El nombre debe tener entre 1 y 80 caracteres' })
nombre: string;
@ApiPropertyOptional({
example: '123456',
description: 'Número de trabajador único',
})
@IsString({ message: 'El número de trabajador debe ser una cadena de texto' })
@IsOptional()
@Length(1, 10, {
message: 'El número de trabajador debe ser maximo de 10 caracteres',
})
num_trabajador?: string;
@ApiPropertyOptional({
example: 'ABC1234567',
description: 'RFC del profesor',
})
@IsString({ message: 'El RFC debe ser una cadena de texto' })
@IsOptional()
@Length(1, 10, { message: 'El RFC debe tener maximo 10 caracteres' })
rfc?: string;
@ApiPropertyOptional({ example: 'XYZ', description: 'Homoclave del RFC' })
@IsString({ message: 'La homoclave debe ser una cadena de texto' })
@IsOptional()
@Length(1, 3, { message: 'La homoclave debe tener maximo 3 caracteres' })
homoclave?: string;
@ApiPropertyOptional({
example: '5551234567',
description: 'Teléfono de oficina del profesor',
})
@IsString({ message: 'El teléfono debe ser una cadena de texto' })
@IsOptional()
@Length(1, 14, { message: 'El teléfono debe tener entre 1 y 14 caracteres' })
tel_oficina?: string;
@ApiPropertyOptional({
example: '1234',
description: 'Extensión del teléfono de oficina',
})
@IsString({ message: 'La extensión debe ser una cadena de texto' })
@IsOptional()
@Length(1, 10, { message: 'La extensión debe tener entre 1 y 10 caracteres' })
extension?: string;
@ApiPropertyOptional({
example: 'correo@escuela.edu.mx',
description: 'Correo institucional',
})
@IsString({ message: 'El correo debe ser una cadena de texto' })
@IsOptional()
@Length(1, 65, { message: 'El correo debe tener entre 1 y 65 caracteres' })
correo_pcp?: string;
@ApiPropertyOptional({
example: 1,
description: 'ID de la adscripción a la que pertenece el profesor',
})
@IsInt({ message: 'El ID de adscripción debe ser un número entero' })
@IsOptional()
id_adscripcion?: number;
@ApiPropertyOptional({
example: 2,
description: 'ID de la categoría del profesor',
})
@IsInt({ message: 'El ID de categoría debe ser un número entero' })
@IsOptional()
id_categoria?: number;
@ApiPropertyOptional({
example: 3,
description: 'ID del edificio donde se ubica el profesor',
})
@IsInt({ message: 'El ID de edificio debe ser un número entero' })
@IsOptional()
id_edificio?: number;
@ApiPropertyOptional({
example: 'Aula 205',
description: 'Ubicación física del profesor',
})
@IsString({ message: 'La ubicación debe ser una cadena de texto' })
@IsOptional()
ubicacion?: string;
@ApiPropertyOptional({
example: 'https://mi-url.com/foto.jpg',
description:
'URL de la fotografía del profesor. \n\n Antes de crear el registro del profesor, la imagen debe ser subida al endpoint correspondiente para almacenar imágenes. \n\n Este endpoint devolverá la URL de la imagen almacenada, la cual debe enviarse en este campo al momento de registrar al profesor.',
})
@IsString({
message: 'La URL de la fotografía debe ser una cadena de texto válida',
})
@IsOptional()
fotografia?: string;
@ApiPropertyOptional({
example: true,
description: 'Indica si el profesor está activo',
})
@IsBoolean({
message: 'El estado activo debe ser un valor booleano (true o false)',
})
@IsOptional()
activo?: boolean;
@ApiPropertyOptional({
example: false,
description: 'Indica si el profesor debe mostrarse en la lista pública',
})
@IsBoolean({
message:
'El estado de visibilidad debe ser un valor booleano (true o false)',
})
@IsOptional()
mostrar?: boolean;
@ApiPropertyOptional({
example: 'Maestro en Ciencias',
description: 'Grado máximo de estudios del profesor',
})
@IsString({ message: 'El grado máximo debe ser una cadena de texto' })
@IsOptional()
grado_maximo?: string;
}
export class UpdateProfesorDto extends PartialType(CreateProfesorDto) {}
/*
Se retiro informacion personal
@IsString()
@IsOptional()
@Length(0, 13)
tel_personal: string;
@IsString()
@IsOptional()
@Length(0, 65)
correo_per: string;
*/
@@ -1,63 +1,45 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
JoinColumn,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Edificio } from '../../edificio/entities/edificio.entity';
import { Adscripcion } from '../../adscripcion/entities/adscripcion.entity';
import { Categoria } from '../../categoria/entities/categoria.entity';
import { DatosAcademicos } from '../../datos_academicos/entities/datos_academicos.entity';
import { LineasInvestigacion } from '../../lineas_investigacion/entities/lineas_investigacion.entity';
import { ProyectosAcademicos } from '../../proyectos_academicos/entities/proyectos_academicos.entity';
import { User } from 'src/users/entities/user.entity';
import { DatosAcademicos } from '../../datos-academicos/entities/datos_academicos.entity';
import { LineasInvestigacion } from '../../lineas-investigacion/entities/lineas-investigacion.entity';
import { ProyectosAcademicos } from '../../proyectos-academicos/entities/proyectos-academicos.entity';
import { Usuario } from 'src/usuario/entities/usuario.entity';
@Entity()
@Entity({ name: 'profesor' })
export class Profesor {
@PrimaryGeneratedColumn()
@PrimaryGeneratedColumn({ type: 'int' })
id_profesor: number;
@Column({ type: 'int',nullable: false })
id_usuario: number;
@Column({ type: 'varchar', length: 10, nullable: true })
num_trabajador: string;
@Column({ type: 'varchar', length: 80, nullable: false })
nombre: string;
@Column({ type: 'varchar', length: 10, nullable: true })
num_trabajador: string;
@Column({ type: 'varchar', length: 10, nullable: true })
rfc: string;
@Column({ type: 'varchar', length: 3, nullable: true })
homoclave: string;
@Column({ type: 'varchar', length: 13, nullable: true })
@Column({ type: 'varchar', length: 14, nullable: true })
tel_oficina: string;
@Column({ type: 'varchar', length: 10, nullable: true })
extension: string;
@Column({ type: 'varchar', length: 13, nullable: true })
tel_personal: string;
@Column({ type: 'varchar', length: 65, nullable: true })
correo_pcp: string;
@Column({ type: 'varchar', length: 65, nullable: true })
correo_per: string;
@Column({ type: 'int', nullable: true })
id_adscripcion: number;
@Column({ type: 'int', nullable: true })
id_categoria: number;
@Column({ type: 'int', nullable: true })
id_edificio: number;
@Column({ type: 'varchar', length: 256, nullable: true })
ubicacion: string;
@@ -72,8 +54,8 @@ export class Profesor {
@Column({
type: 'timestamp',
nullable: false,
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
fecha_alta: Date;
@@ -85,29 +67,61 @@ export class Profesor {
})
fecha_actualizacion: Date;
@ManyToOne(() => Edificio, edificio => edificio.profesores)
@JoinColumn({ name: 'id_edificio' })
/* RELACIONES */
@Column({ type: 'int', nullable: true })
id_adscripcion: number;
@Column({ type: 'int', nullable: true })
id_categoria: number;
@Column({ type: 'int', nullable: true })
id_edificio: number;
@Column({ type: 'int', nullable: true })
id_usuario: number;
@ManyToOne(() => Edificio, (edificio) => edificio.profesores)
@JoinColumn({
name: 'id_edificio',
})
edificio: Edificio;
@ManyToOne(() => Adscripcion, adscripcion => adscripcion.profesores)
@JoinColumn({ name: 'id_adscripcion' })
@ManyToOne(() => Adscripcion, (adscripcion) => adscripcion.profesores)
@JoinColumn({
name: 'id_adscripcion',
})
adscripcion: Adscripcion;
@ManyToOne(() => Categoria, categoria => categoria.profesores)
@JoinColumn({ name: 'id_categoria' })
@ManyToOne(() => Categoria, (categoria) => categoria.profesores)
@JoinColumn({
name: 'id_categoria',
})
categoria: Categoria;
@ManyToOne(() => User, user => user.profesores)
@JoinColumn({ name: 'id_usuario' })
user: User;
@ManyToOne(() => Usuario, (usuario) => usuario.profesores)
@JoinColumn({
name: 'id_usuario',
})
usuario: Usuario;
@OneToMany(() => DatosAcademicos, datosAcademicos => datosAcademicos.profesor)
@OneToMany(
() => DatosAcademicos,
(datosAcademicos) => datosAcademicos.profesor,
)
datosAcademicos: DatosAcademicos[];
@OneToMany(() => LineasInvestigacion, linea => linea.profesor)
@OneToMany(() => LineasInvestigacion, (linea) => linea.profesor)
lineasInvestigacion: LineasInvestigacion[];
@OneToMany(() => ProyectosAcademicos, proyecto => proyecto.profesor)
@OneToMany(() => ProyectosAcademicos, (proyecto) => proyecto.profesor)
proyectosAcademicos: ProyectosAcademicos[];
}
/* Datos Personales Eliminados */
/* @Column({ type: 'varchar', length: 13, nullable: true })
tel_personal: string; */
/* @Column({ type: 'varchar', length: 65, nullable: true })
correo_per: string; */
+169
View File
@@ -0,0 +1,169 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Logger,
Param,
ParseIntPipe,
Patch,
Post,
Put,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ProfesorService } from './profesor.service';
import {
CreateProfesorDto,
UpdateProfesorDto,
} from './dto/create-profesor.dto';
import { Roles } from '../permissions/roles.decorator';
import { RolesGuard } from '../permissions/roles.guard';
import { Role } from '../permissions/role.enum';
import { Profesor } from './entities/profesor.entity';
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
import { JwtPayloadDto } from 'src/auth/dto/payload.dto';
interface RequestWithUser extends Request {
user?: JwtPayloadDto;
}
@ApiBearerAuth()
@ApiTags('Profesor')
@Controller('profesor')
export class ProfesorController {
constructor(private profesorService: ProfesorService) {}
@Post()
@ApiOperation({ summary: 'Crear un profesor' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.Responsable, Role.Admin)
async register(@Body() data: CreateProfesorDto, @Req() req: RequestWithUser) {
return this.profesorService.register(data, req.user);
}
@Patch(':id_profesor')
@ApiOperation({
summary: 'Actualizar parcialmente la informacion de un profesor',
})
@ApiBody({
description: 'Datos parciales del profesor a actualizar',
required: true,
schema: {
example: {
nombre: 'Dr. Juan Pérez',
tel_oficina: '5559876543',
correo_pcp: 'juan.perez@escuela.edu.mx',
activo: true,
},
},
})
async updateProfesor(
@Param('id_profesor') id_profesor: number,
@Body() body: UpdateProfesorDto,
): Promise<Profesor> {
return this.profesorService.update(id_profesor, body);
}
@Get('filter')
@ApiOperation({
summary: 'Obtener lista paginada de profesores con filtros opcionales',
})
@ApiQuery({
name: 'page',
required: false,
example: 1,
description: 'Número de página (por defecto 1)',
})
@ApiQuery({
name: 'limit',
required: false,
example: 12,
description: 'Cantidad de registros por página (máximo 12)',
})
@ApiQuery({
name: 'nombre',
required: false,
example: 'Juan',
description: 'Filtrar por nombre del profesor',
})
@ApiQuery({
name: 'adscripcion',
required: false,
example: 2,
description: 'Filtrar por ID de adscripción',
})
@ApiQuery({
name: 'edificio',
required: false,
example: 3,
description: 'Filtrar por ID de edificio',
})
@ApiQuery({
name: 'categoria',
required: false,
example: 1,
description: 'Filtrar por ID de categoría',
})
async findAllPaginated(
@Query('page') page: number = 1,
@Query('limit') limit: number = 12,
@Query('nombre') nombre?: string,
@Query('adscripcion') adscripcion?: number,
@Query('edificio') edificio?: number,
@Query('categoria') categoria?: number,
) {
// Validaciones básicas
page = page < 1 ? 1 : page;
limit = limit > 12 || limit < 1 ? 12 : limit;
// Filtros dinámicos
const filters: any = {};
if (nombre) filters.nombre = nombre;
if (adscripcion) filters.adscripcion = Number(adscripcion);
if (edificio) filters.edificio = Number(edificio);
if (categoria) filters.categoria = Number(categoria);
return await this.profesorService.findAllPaginated(page, limit, filters);
}
@Get(':id_profesor')
async profile(@Param('id_profesor', ParseIntPipe) id_profesor: number) {
return this.profesorService.profile(id_profesor);
}
@Get()
@ApiOperation({ summary: 'Obtiene todos los profesores' })
findAll() {
return this.profesorService.findAll();
}
/* @Put(':id_profesor')
@Roles(Role.Responsable, Role.Admin)
async modify(
@Param('id_profesor', ParseIntPipe) id_profesor: number,
@Body() data: CreateProfesorDto,
) {
return this.profesorService.modify(id_profesor, data);
} */
@Delete(':id_profesor')
@ApiOperation({
summary:
'Elimina un profesor con sus proyectos y lineas de investigacion por su id_profesor',
})
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.Responsable, Role.Admin)
async remove(@Param('id_profesor', ParseIntPipe) id_profesor: number) {
return this.profesorService.remove(id_profesor);
}
}
+23
View File
@@ -0,0 +1,23 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProfesorService } from './profesor.service';
import { ProfesorController } from './profesor.controller';
import { Profesor } from './entities/profesor.entity';
import { DatosAcademicosModule } from '../datos-academicos/datos-academicos.module';
import { LineasInvestigacionModule } from '../lineas-investigacion/lineas-investigacion.module';
import { ProyectosAcademicosModule } from '../proyectos-academicos/proyectos-academicos.module';
import { UsuarioModule } from 'src/usuario/usuario.module';
@Module({
imports: [
TypeOrmModule.forFeature([Profesor]),
forwardRef(() => LineasInvestigacionModule),
DatosAcademicosModule,
ProyectosAcademicosModule,
UsuarioModule,
],
providers: [ProfesorService],
controllers: [ProfesorController],
exports: [ProfesorService],
})
export class ProfesorModule {}
+242
View File
@@ -0,0 +1,242 @@
import {
BadRequestException,
forwardRef,
HttpException,
Inject,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { Profesor } from './entities/profesor.entity';
import { DatosAcademicosService } from '../datos-academicos/datos-academicos.service';
import { LineasInvestigacionService } from '../lineas-investigacion/lineas-investigacion.service';
import { ProyectosAcademicosService } from '../proyectos-academicos/proyectos-academicos.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
CreateProfesorDto,
UpdateProfesorDto,
} from './dto/create-profesor.dto';
import { ProyectosAcademicos } from '../proyectos-academicos/entities/proyectos-academicos.entity';
import { DatosAcademicos } from '../datos-academicos/entities/datos_academicos.entity';
import { LineasInvestigacion } from '../lineas-investigacion/entities/lineas-investigacion.entity';
import { JwtPayloadDto } from 'src/auth/dto/payload.dto';
@Injectable()
export class ProfesorService {
constructor(
@InjectRepository(Profesor)
private readonly profesorRepository: Repository<Profesor>,
private readonly datosAcademicosService: DatosAcademicosService,
private readonly proyectosAcademicosService: ProyectosAcademicosService,
@Inject(forwardRef(() => LineasInvestigacionService))
private readonly lineasInvestigacionService: LineasInvestigacionService,
) {}
async findOne(id_profesor: number): Promise<Profesor> {
const profesor = await this.profesorRepository.findOne({
where: { id_profesor },
});
if (!profesor) {
throw new NotFoundException(
`El profesor con ID ${id_profesor} no fue encontrado`,
);
}
return profesor;
}
async update(
id_profesor: number,
data: UpdateProfesorDto,
): Promise<Profesor> {
const { grado_maximo } = data;
const profesor = await this.findOne(id_profesor);
Object.assign(profesor, data);
if (grado_maximo) {
await this.datosAcademicosService.updateGradoMaximo(
profesor.id_profesor,
grado_maximo,
);
}
return await this.profesorRepository.save(profesor);
}
//register teacher
async register(
data: CreateProfesorDto,
payload: JwtPayloadDto,
): Promise<Profesor> {
const { num_trabajador, rfc, homoclave, grado_maximo, ...res } = data;
// 1.- Buscar si ya existe un profesor con ese número de trabajador
const profesorByNumTrabajador = await this.profesorRepository.findOne({
where: { num_trabajador },
});
if (profesorByNumTrabajador) {
Logger.error(
`Ya existe un profesor con el número de trabajador ${num_trabajador}.`,
);
throw new HttpException(
`Ya existe un profesor con el número de trabajador ${num_trabajador}.`,
409,
);
}
// 2.- Buscar si ya existe un profesor con ese RFC
const profesorByRFC = await this.profesorRepository.findOne({
where: { rfc },
});
if (profesorByRFC) {
Logger.error(`Ya existe un profesor con el RFC ${rfc}.`);
throw new HttpException(
`Ya existe un profesor con el RFC ${rfc}.`,
409, // Código 409 (Conflict)
);
}
// 4.- Buscar si ya existe un profesor con ese RFC
const profesorByHomoclave = await this.profesorRepository.findOne({
where: { homoclave },
});
if (profesorByHomoclave) {
Logger.error(`Ya existe un profesor con esa homoclave ${homoclave}.`);
throw new HttpException(
`Ya existe un profesor con esa homoclave ${homoclave}.`,
409, // Código 409 (Conflict)
);
}
// 5.- Guardar al profesor
const createdProfesor = this.profesorRepository.create({
id_usuario: payload.id_usuario,
...res,
num_trabajador,
rfc,
});
const savedProfesor = await this.profesorRepository.save(createdProfesor);
await this.lineasInvestigacionService.create(savedProfesor);
await this.proyectosAcademicosService.create(savedProfesor);
await this.datosAcademicosService.create(savedProfesor, grado_maximo);
return savedProfesor;
}
//brings all teachers
async findAll(): Promise<Profesor[]> {
return this.profesorRepository.find({
order: {
nombre: 'ASC',
},
});
}
//remove teacher
async remove(id_profesor: number) {
const profesor = await this.profesorRepository.findOne({
where: { id_profesor },
relations: ['datosAcademicos'],
});
if (!profesor) {
throw new HttpException('No se encontro profesor', 404);
}
await this.datosAcademicosService.removeByProfesorId(id_profesor);
await this.lineasInvestigacionService.removeByProfesorId(id_profesor);
await this.proyectosAcademicosService.removeByProfesorId(id_profesor);
await this.profesorRepository.remove(profesor);
return { message: 'Profesor eliminado con exito' };
}
//brings teachers by id
async profile(id_profesor: number) {
const profesor = await this.profesorRepository.findOne({
where: { id_profesor },
relations: [
'proyectosAcademicos',
'datosAcademicos',
'lineasInvestigacion',
],
});
if (profesor.lineasInvestigacion.length === 0) {
await this.lineasInvestigacionService.create(profesor);
}
if (profesor.proyectosAcademicos.length === 0) {
await this.proyectosAcademicosService.create(profesor);
}
if (profesor.datosAcademicos.length === 0) {
await this.datosAcademicosService.create(profesor, '');
}
if (!profesor) {
throw new HttpException('Profesor not found', 404);
}
return profesor;
}
async findAllPaginated(
page: number,
limit: number,
filters: any,
): Promise<{ profesores: Profesor[]; total: number; totalPages: number }> {
const query = this.profesorRepository.createQueryBuilder('profesor');
console.log(filters);
if (filters.nombre) {
Logger.debug('Filter by name');
query.andWhere('LOWER(profesor.nombre) LIKE LOWER(:nombre)', {
nombre: `%${filters.nombre}%`,
});
}
if (filters.adscripcion && !isNaN(filters.adscripcion)) {
Logger.debug('Filter by adscription');
query.andWhere('profesor.id_adscripcion = :adscripcion', {
adscripcion: Number(filters.adscripcion),
});
}
if (filters.categoria && !isNaN(filters.categoria)) {
Logger.debug('Filter by category');
query.andWhere('profesor.categoria = :categoria', {
categoria: Number(filters.categoria),
});
}
if (filters.edificio && !isNaN(filters.edificio)) {
Logger.debug('Filter by building');
query.andWhere('profesor.id_edificio = :edificio', {
edificio: Number(filters.edificio),
});
}
query.addOrderBy('profesor.nombre', 'ASC');
const [profesores, total] = await query
.skip((page - 1) * limit)
.take(limit)
.getManyAndCount();
const totalPages = Math.ceil(total / limit);
return { profesores, total, totalPages };
}
async updateImageURL(id_profesor: number, imageUrl: string): Promise<void> {
await this.profesorRepository.update(id_profesor, { fotografia: imageUrl });
}
}
@@ -0,0 +1,26 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Profesor } from '../../profesor/entities/profesor.entity';
@Entity({ name: 'proyectos_academicos' })
export class ProyectosAcademicos {
@PrimaryGeneratedColumn()
id_proyecto: number;
@Column({ type: 'int', nullable: false })
id_profesor: number;
@Column({ type: 'varchar', length: 700, nullable: true })
proyectos_html: string;
@ManyToOne(() => Profesor, (profesor) => profesor.proyectosAcademicos)
@JoinColumn({
name: 'id_profesor',
})
profesor: Profesor;
}
@@ -0,0 +1,66 @@
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Logger,
Param,
Patch,
UseGuards,
} from '@nestjs/common';
import { ProyectosAcademicosService } from './proyectos-academicos.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RolesGuard } from 'src/permissions/roles.guard';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
@ApiBearerAuth()
@ApiTags('proyectos-academicos')
@Controller('proyectos-academicos')
export class ProyectosAcademicosController {
constructor(private proyectosAcademicosService: ProyectosAcademicosService) {}
@Patch(':id_profesor')
@ApiOperation({
summary: 'Actualizar los proyectos academicos de un profesor',
description:
'Este endpoint permite actualizar las líneas de investigación en formato Markdown/HTML para un profesor específico.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
id_profesor: {
type: 'number',
description:
'ID del profesor al que se debe actualizar sus proyectos academicos',
example: 1,
},
proyectos_html: {
type: 'string',
description:
'Nuevo contenido de líneas de investigación en formato Markdown',
example: '# Proyectos Academicos \n- Proyecto 1\n- Proyecto 2',
},
},
},
})
async updateProyectosAcademicos(
@Param('id_profesor') id_profesor: number,
@Body('proyectos_html') proyectos_html: string,
@Body('id_proyecto') id_proyecto: number,
) {
if (!proyectos_html || proyectos_html.trim() === '') {
throw new HttpException(
'El contenido no puede estar vacío.',
HttpStatus.BAD_REQUEST,
);
}
return await this.proyectosAcademicosService.updateProyectosAcademicos(
id_proyecto,
id_profesor,
proyectos_html,
);
}
}
@@ -0,0 +1,19 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProyectosAcademicos } from './entities/proyectos-academicos.entity';
import { ProyectosAcademicosService } from './proyectos-academicos.service';
import { ProfesorModule } from 'src/profesor/profesor.module';
import { UsuarioModule } from 'src/usuario/usuario.module';
import { ProyectosAcademicosController } from './proyectos-academicos.controller';
@Module({
imports: [
TypeOrmModule.forFeature([ProyectosAcademicos]),
forwardRef(() => ProfesorModule),
UsuarioModule,
],
controllers: [ProyectosAcademicosController],
providers: [ProyectosAcademicosService],
exports: [ProyectosAcademicosService],
})
export class ProyectosAcademicosModule {}
@@ -0,0 +1,69 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProyectosAcademicos } from './entities/proyectos-academicos.entity';
import { Profesor } from 'src/profesor/entities/profesor.entity';
import { ProfesorService } from 'src/profesor/profesor.service';
@Injectable()
export class ProyectosAcademicosService {
constructor(
@InjectRepository(ProyectosAcademicos)
private readonly proyectosAcademicosRepository: Repository<ProyectosAcademicos>,
@Inject(forwardRef(() => ProfesorService))
private readonly profesorService: ProfesorService,
) {}
async create(profesor: Profesor) {
const proyectosAcademicos = this.proyectosAcademicosRepository.create({
profesor: profesor,
proyectos_html: '',
});
return await this.proyectosAcademicosRepository.save(proyectosAcademicos);
}
async updateProyectosAcademicos(
id_proyecto: number,
id_profesor: number,
nuevoContenido: string,
) {
const profesor = await this.profesorService.findOne(id_profesor);
let proyecto = await this.proyectosAcademicosRepository.findOne({
where: { profesor: profesor, id_proyecto },
});
if (!proyecto) {
Logger.error(
`No se encontraron proyectos académicos del profesor con ID ${id_profesor} al intentar actualizar. Se procederá a crear uno nuevo.`,
);
// Crear nuevo proyecto con el contenido
proyecto = this.proyectosAcademicosRepository.create({
profesor: profesor,
proyectos_html: nuevoContenido, // Se asigna el nuevo contenido directamente
});
// Guardar el nuevo proyecto
await this.proyectosAcademicosRepository.save(proyecto);
} else {
// Si ya existe, actualizar su contenido
proyecto.proyectos_html = nuevoContenido;
await this.proyectosAcademicosRepository.save(proyecto);
}
return proyecto; // Devolver el proyecto actualizado o recién creado
}
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.proyectosAcademicosRepository.delete({
profesor: { id_profesor },
});
}
async findAll(): Promise<ProyectosAcademicos[]> {
return this.proyectosAcademicosRepository.find();
}
}
@@ -1,22 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Profesor } from "../../Profesor/entities/profesor.entity";
@Entity({ name: 'proyectos_academicos' })
export class ProyectosAcademicos {
@PrimaryGeneratedColumn()
id_proyecto: number;
@Column({ type: 'int', nullable: false })
id_profesor: number;
@Column({ type: 'varchar', length: 500, nullable: true })
proyecto: string;
@Column({ type: 'varchar', length: 700, nullable: true })
proyecto_html: string;
@ManyToOne(() => Profesor, profesor => profesor.proyectosAcademicos)
@JoinColumn({ name: 'id_profesor' })
profesor: Profesor;
}
@@ -1,12 +0,0 @@
import { Controller,Get } from '@nestjs/common';
import { ProyectosAcademicosService } from './proyectos_academicos.service';
@Controller('proyectos-academicos')
export class ProyectosAcademicosController {
constructor(private readonly proyectosAcademicosService: ProyectosAcademicosService) {}
@Get()
findAll() {
return this.proyectosAcademicosService.findAll();
}
}
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProyectosAcademicos } from './entities/proyectos_academicos.entity';
import { ProyectosAcademicosService } from './proyectos_academicos.service';
@Module({
imports: [TypeOrmModule.forFeature([ProyectosAcademicos])],
providers: [ProyectosAcademicosService],
exports: [ProyectosAcademicosService],
})
export class ProyectosAcademicosModule {}
@@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProyectosAcademicos } from './entities/proyectos_academicos.entity';
@Injectable()
export class ProyectosAcademicosService {
constructor(
@InjectRepository(ProyectosAcademicos)
private readonly proyectosAcademicosRepository: Repository<ProyectosAcademicos>,
) {}
// Add methods to manage ProyectosAcademicos entities as needed
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.proyectosAcademicosRepository.delete({ profesor: { id_profesor } });
}
async findAll(): Promise<ProyectosAcademicos[]> {
return this.proyectosAcademicosRepository.find();
}
}
@@ -1,5 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Usuario } from '../../usuario/entities/usuario.entity';
@Entity({ name: 'tipo_usuario' })
export class TipoUsuario {
@@ -9,6 +9,6 @@ export class TipoUsuario {
@Column({ type: 'varchar', length: 30, nullable: false })
tipo_usuario: string;
@OneToMany(() => User, (user) => user.tipoUsuario)
users: User[];
@OneToMany(() => Usuario, (usuario) => usuario.tipoUsuario)
usuarios: Usuario[];
}
-41
View File
@@ -1,41 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
import { TipoUsuario } from "../../tipo_usuario/entities/tipo_usuario.entity";
import { Profesor } from 'src/Profesor/entities/profesor.entity';
@Entity({ name: 'usuario' })
export class User {
@PrimaryGeneratedColumn()
id_usuario: number;
@Column({ type: 'int', nullable: false })
id_tipo_usuario: number;
@Column({type: 'varchar', length: 40, nullable: false })
nombre: string;
@Column({ type: 'varchar', length: 60, nullable: false })
correo: string;
@Column({ type: 'varchar', length: 60, nullable: false })
usuario: string;
@Column({ type: 'varchar', length: 60, nullable: false })
contraseña: string;
@Column({ type: 'varchar', length: 255, nullable: true })
jwt: string;
@Column({ type: 'datetime', nullable: true })
expirationjwt: Date;
@Column({ type: 'datetime', nullable: true })
lastlog: Date;
@ManyToOne(() => TipoUsuario, tipoUsuario => tipoUsuario.users)
@JoinColumn({ name: 'id_tipo_usuario' })
tipoUsuario: TipoUsuario;
@OneToMany(() => Profesor, profesor => profesor.user)
profesores: Profesor[];
}
-11
View File
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
-29
View File
@@ -1,29 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { UserDto } from './dto/userDto.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private users: Repository<User>,
) {}
async findOne(usuario: string): Promise<User | undefined> {
return this.users.findOne({ where: { usuario } });
}
async updateTokenAndDates(
id: number,
token: string,
lastLogin: Date,
jwtExpiry: Date,
): Promise<void> {
await this.users.update(id, {
jwt: token,
lastlog: lastLogin,
expirationjwt: jwtExpiry,
});
}
}
@@ -4,17 +4,21 @@ export class UserDto{
@IsString()
@IsNotEmpty()
usuario: string;
@IsString()
@IsNotEmpty()
nombre: string;
@IsString()
@IsNotEmpty()
contraseña: string;
@IsNumber()
@IsString()
@IsNotEmpty()
id_tipo_usuario:number;
id_tipo_usuario:string;
@IsString()
@IsNotEmpty()
correo: string;
}
+22
View File
@@ -0,0 +1,22 @@
import { PartialType } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { SignUpDto } from 'src/auth/dto/sign-up.dto';
export class UsuarioResponseDto {
@Expose()
id_usuario: number;
@Expose()
usuario: string;
@Expose()
nombre: string;
@Expose()
id_tipo_usuario: string;
@Expose()
correo: string;
}
export class UpdateUsuarioDto extends PartialType(SignUpDto) {}
+49
View File
@@ -0,0 +1,49 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { TipoUsuario } from '../../tipo_usuario/entities/tipo_usuario.entity';
import { Profesor } from 'src/profesor/entities/profesor.entity';
@Entity({ name: 'usuario' })
export class Usuario {
@PrimaryGeneratedColumn()
id_usuario: number;
@Column({ type: 'int', nullable: false })
id_tipo_usuario: number;
@Column({ type: 'varchar', length: 40, nullable: false })
nombre: string;
@Column({ type: 'varchar', length: 60, nullable: false })
correo: string;
@Column({ type: 'varchar', length: 60, nullable: false })
usuario: string;
@Column({ type: 'varchar', length: 60, nullable: false }) // ALTER TABLE usuario CHANGE COLUMN contraseña password VARCHAR(60);
password: string;
@Column({ type: 'varchar', length: 255, nullable: true })
jwt: string;
@Column({ type: 'datetime', nullable: true })
expirationjwt: Date;
@Column({ type: 'datetime', nullable: true })
lastlog: Date;
@ManyToOne(() => TipoUsuario, (tipoUsuario) => tipoUsuario.usuarios)
@JoinColumn({
name: 'id_tipo_usuario',
})
tipoUsuario: TipoUsuario;
@OneToMany(() => Profesor, (profesor) => profesor.usuario)
profesores: Profesor[];
}
+66
View File
@@ -0,0 +1,66 @@
import {
Controller,
Get,
Patch,
Body,
Param,
UseGuards,
Delete,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { RolesGuard } from 'src/permissions/roles.guard';
import { UsuarioService } from './usuario.service';
import { JwtAuthGuard } from 'src/permissions/jwt.guard';
import { UpdateUsuarioDto } from './dto/usuario-response.dto';
import { DeleteResult } from 'typeorm';
@ApiBearerAuth()
@ApiTags('Usuario')
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('usuario')
export class UsuarioController {
constructor(private readonly usuarioService: UsuarioService) {}
@Get(':id_usuario')
@ApiOperation({
summary: 'Obtener un usuario por su id',
})
async getUsuario(@Param('id_usuario') id_usuario: number) {
return this.usuarioService.findOneById(id_usuario);
}
@Get()
@ApiOperation({
summary: 'Obtener todos los usuarios registrados',
})
async getUsuarios() {
return this.usuarioService.findAll();
}
@Patch(':id_usuario')
@ApiOperation({
summary: 'Actualizar un usuario por su ID',
})
async updateUsuario(
@Param('id_usuario') id_usuario: number,
@Body() updateUsuarioDto: UpdateUsuarioDto,
) {
return this.usuarioService.update(id_usuario, updateUsuarioDto);
}
@Delete(':id_usuario')
@ApiOperation({
summary: 'Eliminar un usuario',
description:
'Elimina un usuario y desvincula a los profesores relacionados.',
})
async deleteUsuario(@Param('id_usuario') id_usuario: number) {
return this.usuarioService.remove(id_usuario);
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsuarioService } from './usuario.service';
import { Usuario } from './entities/usuario.entity';
import { UsuarioController } from './usuario.controller';
@Module({
imports: [TypeOrmModule.forFeature([Usuario])],
providers: [UsuarioService],
exports: [UsuarioService],
controllers: [UsuarioController],
})
export class UsuarioModule {}
+109
View File
@@ -0,0 +1,109 @@
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Not, Repository } from 'typeorm';
import { Usuario } from './entities/usuario.entity';
import { plainToInstance } from 'class-transformer';
import {
UpdateUsuarioDto,
UsuarioResponseDto,
} from './dto/usuario-response.dto';
import { Profesor } from 'src/profesor/entities/profesor.entity';
@Injectable()
export class UsuarioService {
constructor(
@InjectRepository(Usuario)
private usuarioRepository: Repository<Usuario>,
) {}
async findOneByName(usuario: string): Promise<Usuario> {
return this.usuarioRepository.findOne({ where: { usuario } });
}
async findOneById(id_usuario: number): Promise<Usuario> {
return this.usuarioRepository.findOne({ where: { id_usuario } });
}
async findAll(): Promise<UsuarioResponseDto[]> {
const usuarios = await this.usuarioRepository.find();
return plainToInstance(UsuarioResponseDto, usuarios, {
excludeExtraneousValues: true,
});
}
async update(
id_usuario: number,
updateUsuarioDto: UpdateUsuarioDto,
): Promise<Usuario> {
const usuario = await this.usuarioRepository.findOne({
where: { id_usuario },
});
if (!usuario) {
throw new NotFoundException(`Usuario con ID ${id_usuario} no encontrado`);
}
if (updateUsuarioDto.nombre) {
const usuarioExistente = await this.usuarioRepository.findOne({
where: {
usuario: updateUsuarioDto.usuario,
id_usuario: Not(id_usuario),
}, // Excluye al usuario actual
});
if (usuarioExistente) {
throw new BadRequestException(
`El nombre de usuario "${updateUsuarioDto.nombre}" ya está en uso`,
);
}
}
// Si el usuario envía una nueva contraseña, la encriptamos antes de guardarla
if (updateUsuarioDto.password) {
const salt = await bcrypt.genSalt(10);
updateUsuarioDto.password = await bcrypt.hash(
updateUsuarioDto.password,
salt,
);
}
Object.assign(usuario, updateUsuarioDto);
return this.usuarioRepository.save(usuario);
}
async remove(id_usuario: number): Promise<DeleteResult> {
return await this.usuarioRepository.manager.transaction(
async (transactionalEntityManager) => {
// Paso 1: Desvincular el usuario de la tabla profesor
await transactionalEntityManager.update(
Profesor,
{ id_usuario },
{ id_usuario: null },
);
// Paso 2: Intentar eliminar el usuario
return await transactionalEntityManager.delete(Usuario, { id_usuario });
},
);
}
async updateTokenAndDates(
id: number,
token: string,
lastLogin: Date,
jwtExpiry: Date,
): Promise<void> {
Logger.debug('update token and date user');
await this.usuarioRepository.update(id, {
jwt: token,
lastlog: lastLogin,
expirationjwt: jwtExpiry,
});
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.