5 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
100 changed files with 4520 additions and 3130 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"
}
+1 -1
View File
@@ -55,4 +55,4 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
imagenes
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"]
+2405 -1536
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -28,6 +28,7 @@
"@nestjs/passport": "^10.0.3",
"@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",
@@ -40,7 +41,8 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.33.4",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
-129
View File
@@ -1,129 +0,0 @@
import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsNotEmpty, IsNumber, IsString, IsOptional, Length, ValidateNested} from "class-validator";
export class profesorDto{
@IsInt()
id_profesor: number;
@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;
@ValidateNested({ each: true })
@Type(() => ProyectosAcademicos)
proyectosAcademicos: ProyectosAcademicos[];
@ValidateNested({ each: true })
@Type(() => DatosAcademicos)
datosAcademicos: DatosAcademicos[];
@ValidateNested({ each: true })
@Type(() => LineasInvestigacion)
lineasInvestigacion: LineasInvestigacion[];
}
export class ProyectosAcademicos {
@IsOptional()
@Length(0, 500)
proyecto_html: 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_html: string;
}
export class UpdateProfesorImageDto {
@IsString()
fotografia: 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();
});
});
-96
View File
@@ -1,96 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Logger,
Param,
ParseIntPipe,
Post,
Put,
Query,
UseGuards
} from "@nestjs/common";
import { ProfesorService } from "./profesor.service";
import { profesorDto, UpdateProfesorImageDto } from "./dto/profesorDto.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";
@Controller("profesor")
@UseGuards(RolesGuard)
export class ProfesorController {
constructor(private profesorService: ProfesorService) {
}
@Post()
@Roles(Role.Responsable, Role.Admin)
async register(@Body() data: profesorDto) {
Logger.debug("register profesor");
return this.profesorService.register(data);
}
@Put(':id_profesor/image')
async updateImage(
@Param('id_profesor') id_profesor: number,
@Body() updateProfesorImageDto: UpdateProfesorImageDto,
): Promise<Profesor> {
const { fotografia } = updateProfesorImageDto;
const profesor = await this.profesorService.findOne(id_profesor);
if (!profesor) {
throw new HttpException('Profesor not found', 404);
}
profesor.fotografia = fotografia;
return this.profesorService.update(profesor);
}
@Get()
findAll() {
Logger.debug("findAllProfesors");
return this.profesorService.findAll();
}
@Get("page")
findAllPaginated(@Query("page") page: number = 1, @Query("limit") limit: number = 12, @Query() filters: any) {
try {
Logger.debug("Page Profesors");
page = page < 1 ? 1 : page;
limit = limit > 12 || limit < 1 ? 12 : limit;
return this.profesorService.findAllPaginated(page, limit, filters);
} catch (err) {
Logger.error("profesor controller ", err.error);
return [];
}
}
@Put(":id_profesor")
@Roles(Role.Responsable, Role.Admin)
async modify(@Param("id_profesor", ParseIntPipe) id_profesor: number, @Body() data: profesorDto) {
Logger.debug("put Profesors by id");
return this.profesorService.modify(id_profesor, data);
}
@Delete(":id_profesor")
@Roles(Role.Responsable, Role.Admin)
async remove(@Param("id_profesor", ParseIntPipe) id_profesor: number) {
Logger.debug("delete Profesors by id");
return this.profesorService.remove(id_profesor);
}
@Get(":id_profesor")
async profile(@Param("id_profesor", ParseIntPipe) id_profesor: number) {
Logger.debug("get Profesors by id");
return this.profesorService.profile(id_profesor);
}
}
-31
View File
@@ -1,31 +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 } from 'src/datos_academicos/entities/datos_academicos.entity'
import { LineasInvestigacion } from 'src/lineas_investigacion/entities/lineas_investigacion.entity'
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
TypeOrmModule.forFeature([
Profesor,
ProyectosAcademicos,
DatosAcademicos,
LineasInvestigacion
]),
DatosAcademicosModule,
LineasInvestigacionModule,
ProyectosAcademicosModule,
JwtModule,
],
providers: [ProfesorService],
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();
});
});
-255
View File
@@ -1,255 +0,0 @@
import { HttpException, Injectable, Logger } 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 readonly profesorRepository: Repository<Profesor>,
private readonly datosAcademicosService: DatosAcademicosService,
private readonly lineasInvestigacionService: LineasInvestigacionService,
private readonly proyectosAcademicosService: ProyectosAcademicosService,
@InjectRepository(DatosAcademicos)
private readonly datosAcademicosRepository: Repository<DatosAcademicos>,
@InjectRepository(LineasInvestigacion)
private readonly lineasInvestigacionRepository: Repository<LineasInvestigacion>,
@InjectRepository(ProyectosAcademicos)
private readonly proyectosAcademicosRepository: Repository<ProyectosAcademicos>
) {
}
async findOne(id_profesor: number): Promise<Profesor | undefined> {
return this.profesorRepository.findOne({ where: { id_profesor } });
}
async update(profesor: Profesor): Promise<Profesor> {
return this.profesorRepository.save(profesor);
}
private async findAvailableId(): Promise<number> {
let id = 1;
let exists = true;
while (exists) {
if (!await this.profesorRepository.findOne({ where: { id_profesor: id } })) {
Logger.debug(`id_profesor = ${id}`);
exists = false;
return id;
}
id++;
}
}
//register teacher
async register(data: profesorDto): Promise<Profesor | undefined> {
const { num_trabajador, rfc, proyectosAcademicos, datosAcademicos, lineasInvestigacion } = data;
if (await this.profesorRepository.findOne({ where: { rfc } })) {
throw new HttpException("rfc already exist", 403);
}
if (await this.profesorRepository.findOne({ where: { num_trabajador } })) {
throw new HttpException("number of worker already exist", 403);
}
const availableId = await this.findAvailableId();
data.id_profesor = availableId;
const savedProfesor = await this.profesorRepository.save(data);
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 (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);
}
if (datosAcademicos && datosAcademicos.length > 0) {
const datos = datosAcademicos.map(datosAcademicos => {
const newDato = this.datosAcademicosRepository.create({
...datosAcademicos,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor
});
return newDato;
});
await this.datosAcademicosRepository.save(datos);
}
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 (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);
}
if (datosAcademicos) {
await this.datosAcademicosRepository.delete({ profesor: { id_profesor } });
const datos = datosAcademicos.map(datosAcademicos => {
const newDato = this.datosAcademicosRepository.create({
...datosAcademicos,
profesor: savedProfesor,
id_profesor: savedProfesor.id_profesor
});
return newDato;
});
await this.datosAcademicosRepository.save(datos);
}
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);
}
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;
}
async findAllPaginated(
page: number,
limit: number,
filters: any
): Promise<{ profesores: Profesor[], total: number, totalPages: number }> {
const query = this.profesorRepository.createQueryBuilder("profesor");
if (filters.nombre) {
Logger.debug('fiter by name');
query.andWhere("profesor.nombre LIKE :nombre", { nombre: `%${filters.nombre}%` });
}
if (filters.adscripcion) {
Logger.debug("fiter by adscription");
query.andWhere("profesor.adscripcion = :adscripcion", { adscripcion: filters.adscripcion });
}
if (filters.categoria) {
Logger.debug("fiter by category");
query.andWhere("profesor.categoria = :categoria", { categoria: filters.categoria });
}
if (filters.edificio) {
Logger.debug("fiter by building");
query.andWhere("profesor.edificio = :edificio", { edificio: filters.edificio });
}
if (typeof filters.activo !== "undefined") {
Logger.debug("fiter by is active");
query.andWhere("profesor.activo = :activo", { activo: filters.activo });
} else {
query.orderBy("profesor.activo", "DESC");
}
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 });
}
}
@@ -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();
});
});
@@ -7,7 +7,6 @@ export class AdscripcionController {
@Get()
findAll() {
Logger.debug('find all adscripcion');
return this.adscripcionService.findAll();
}
}
@@ -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 -5
View File
@@ -11,10 +11,6 @@ export class AdscripcionService {
) {}
async findAll(): Promise<Adscripcion[]> {
return await this.adscripcionRepository.find({
order: {
adscripcion: 'ASC',
}
});
return this.adscripcionRepository.find();
}
}
-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();
}
}
+23 -17
View File
@@ -6,35 +6,35 @@ 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 { 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 { DatosAcademicosModule } from './datos_academicos/datos_academicos.module';
import { LineasInvestigacionModule } from './lineas_investigacion/lineas_investigacion.module';
import { ProyectosAcademicosModule } from './proyectos_academicos/proyectos_academicos.module';
import { ImagenModule } from './images/images.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: 'mysql',
@@ -45,7 +45,7 @@ import { ImagenModule } from './images/images.module';
database: configService.get('DB_DATABASE'),
entities: [
Profesor,
User,
Usuario,
Adscripcion,
Categoria,
Edificio,
@@ -54,17 +54,23 @@ import { ImagenModule } from './images/images.module';
ProyectosAcademicos,
TipoUsuario,
],
//dropSchema: true,
//synchronize: true, JAMA USAR ESTO EN PRODUCCION BORRA DATOS DE LA DB
}),
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'imagenes'),
serveRoot: '/imagenes',
rootPath: join(__dirname, '..', 'fotografias'),
serveRoot: '/fotografias',
}),
ImagenModule,
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 IO")
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();
});
});
+19 -65
View File
@@ -15,86 +15,40 @@ import {
Logger,
Req,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
import { registerDto } from './dto/registerDto.dto';
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) {
Logger.debug('signIn');
const { rememberMe } = data;
return this.authService.signIn(data, rememberMe);
@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')
@ApiOperation({
summary: 'Registro de usuario',
description: 'Registro ded administradores o responsables',
})
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@Roles(Role.Admin)
async register(@Body() data: registerDto) {
async register(@Body() data: SignUpDto) {
Logger.debug('register user');
return this.authService.register(data);
}
@Get()
findAll() {
Logger.debug('find all users');
return this.authService.findAll();
}
@Get('page')
findAllPaginated(
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
@Query() filters: any,
) {
Logger.debug('find all users max 10');
page = page < 1 ? 1 : page;
limit = limit > 10 || limit < 1 ? 10 : limit;
return this.authService.findAllPaginated(page, limit, filters);
}
@Get(':id_usuario')
async profile(@Param('id_usuario', ParseIntPipe) id_usuario: number) {
Logger.debug('find one user');
return this.authService.profile(id_usuario);
}
//@UseGuards(AuthGuard)
@Put(':id_usuario')
@Roles(Role.Admin)
async update(
@Param('id_usuario') id_usuario: number,
@Body() data: registerDto,
@Req() req: any
) {
const authHeader = req.headers['authorization'];
const [, token] = authHeader.split(' ');
return this.authService.update(id_usuario, data, token);
}
//@UseGuards(AuthGuard)
@Delete(':id_usuario')
@Roles(Role.Admin)
async remove(@Param('id_usuario') id_usuario: number) {
Logger.debug('delete user');
await this.authService.remove(id_usuario);
return { message: 'User successfully deleted' };
}
@HttpCode(HttpStatus.OK)
@Post('validate')
async validateToken(@Body('token') token: string) {
Logger.debug('validate user');
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;
}
}
+9 -6
View File
@@ -1,28 +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 {
global: true,
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '120h' },
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();
});
});
+26 -103
View File
@@ -5,50 +5,56 @@ import {
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, rememberMe: boolean) {
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 = {
@@ -73,49 +79,15 @@ export class AuthService {
return { token };
}
//update user
async update(
id_usuario: number,
data: registerDto,
token: string,
): Promise<any> {
const { contraseña, id_tipo_usuario } = data;
const tokenUserId = this.getTokenUserId(token);
if (tokenUserId == id_usuario && id_tipo_usuario !== undefined) {
Logger.debug('Cannot modify id_tipo_usuario')
delete data.id_tipo_usuario;
}
const user = await this.userRepository.findOne({ where: { id_usuario } });
if (!user) {
throw new HttpException('User not found', 404);
}
if (contraseña) {
const hashedpassword = await hash(contraseña, 10);
data = { ...data, contraseña: hashedpassword };
} else {
data = { ...data, contraseña: user.contraseña };
}
return await this.userRepository.update(id_usuario, data);
}
async remove(id_usuario: number): Promise<void> {
await this.userRepository.delete(id_usuario);
}
//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');
}
@@ -132,55 +104,6 @@ export class AuthService {
}
}
async findAll(): Promise<User[]> {
return this.userRepository.find();
}
async findAllPaginated(
page: number,
limit: number,
filters: any,
): Promise<{ usuarios: User[]; total: number; totalPages: number }> {
const query = this.userRepository.createQueryBuilder('usuario');
if (filters.nombre) {
Logger.debug('fiter by name user');
query.andWhere('usuario.nombre LIKE :nombre', {
nombre: `%${filters.nombre}%`,
});
}
if (filters.id_tipo_usuario) {
Logger.debug('fiter by id user');
query.andWhere('usuario.id_tipo_usuario = :id_tipo_usuario', {
id_tipo_usuario: filters.id_tipo_usuario,
});
}
const [usuarios, total] = await query
.skip((page - 1) * limit)
.take(limit)
.orderBy('usuario.nombre', 'ASC')
.getManyAndCount();
const totalPages = Math.ceil(total / limit);
return { usuarios, total, totalPages };
}
async profile(id_usuario: number) {
const user = await this.userRepository.findOne({
where: { id_usuario },
});
if (!user) {
Logger.debug('user not found');
throw new HttpException('user not found', 404);
}
return user;
}
getTokenUserId(token: string): number {
try {
const decoded = this.jwtService.verify(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;
}
-29
View File
@@ -1,29 +0,0 @@
import {IsString, IsNotEmpty,IsNumber, IsBoolean, IsOptional } 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;
@IsBoolean()
@IsOptional()
rememberMe: boolean;
}
+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
View File
@@ -7,7 +7,6 @@ export class CategoriaController {
@Get()
findAll() {
Logger.debug('find all categoria');
return this.categoriaService.findAll();
}
}
-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();
});
});
+1 -5
View File
@@ -11,10 +11,6 @@ export class CategoriaService {
) {}
async findAll(): Promise<Categoria[]> {
return this.categoriaRepository.find({
order: {
categoria: 'ASC',
},
});
return this.categoriaRepository.find();
}
}
+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)
@JoinColumn({ name: 'id_profesor', referencedColumnName: 'id_profesor', foreignKeyConstraintName: 'FK_datos_academicos' })
@ManyToOne(() => Profesor, (profesor) => profesor.lineasInvestigacion)
@JoinColumn({ name: 'id_profesor' })
profesor: Profesor;
}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatosAcademicosController } from './datos_academicos.controller';
describe('DatosAcademicosController', () => {
let controller: DatosAcademicosController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DatosAcademicosController],
}).compile();
controller = module.get<DatosAcademicosController>(DatosAcademicosController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -1,15 +0,0 @@
import { Controller, Get, Logger } from '@nestjs/common';
import { DatosAcademicosService } from './datos_academicos.service';
@Controller('datos-academicos')
export class DatosAcademicosController {
constructor(
private readonly datosAcademicosService: DatosAcademicosService,
) {}
@Get()
findAll() {
Logger.debug('find all datos academicos');
return this.datosAcademicosService.findAll();
}
}
@@ -1,13 +0,0 @@
import { 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';
@Module({
imports: [TypeOrmModule.forFeature([DatosAcademicos])],
providers: [DatosAcademicosService],
controllers: [DatosAcademicosController],
exports: [DatosAcademicosService],
})
export class DatosAcademicosModule {}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatosAcademicosService } from './datos_academicos.service';
describe('DatosAcademicosService', () => {
let service: DatosAcademicosService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatosAcademicosService],
}).compile();
service = module.get<DatosAcademicosService>(DatosAcademicosService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -1,20 +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>,
) {}
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
View File
@@ -7,7 +7,6 @@ export class EdificioController {
@Get()
findAll() {
Logger.debug('find all edificio');
return this.edificioService.findAll();
}
}
-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();
});
});
+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}`);
}
}
}
-27
View File
@@ -1,27 +0,0 @@
import { Controller, Post, Body, Put, Logger } from '@nestjs/common';
import { ImagenService } from './images.service';
@Controller('imagen')
export class ImagenController {
constructor(private readonly imagenService: ImagenService) {}
@Post()
async upload(@Body() body) {
Logger.debug('upload image');
const { fotografia, nombre } = body;
const imageUrl = await this.imagenService.saveImage(fotografia, nombre);
return { message: 'Image uploaded successfully', imageUrl };
}
@Put()
async modify(@Body() body) {
Logger.debug('modify image');
const { fotografia, nombre } = body;
const imageUrl = await this.imagenService.modify(fotografia, nombre);
return { message: 'Image uploaded successfully', imageUrl };
}
}
-10
View File
@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { ImagenController } from './images.controller';
import { ImagenService } from './images.service';
@Module({
controllers: [ImagenController],
providers: [ImagenService],
})
export class ImagenModule {}
-90
View File
@@ -1,90 +0,0 @@
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
@Injectable()
export class ImagenService {
async saveImage(fotografia: string, id: string): Promise<string> {
if (!fotografia) {
throw new BadRequestException('No image provided');
}
// Extract the image extension
const matches = fotografia.match(/^data:image\/([a-zA-Z]+);base64,/);
if (!matches) {
throw new BadRequestException('Invalid image format');
}
const ext = matches[1];
// Decode base64 string and save the image
const buffer = Buffer.from(fotografia.split(',')[1], 'base64');
const imageName = `${id}.${ext}`;
// Save image to the 'imagenes' directory in the root of the project
const imagePath = path.join(process.cwd(), 'imagenes', imageName);
// Ensure the directory exists
const dirPath = path.join(process.cwd(), 'imagenes');
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(imagePath, buffer);
return imageName;
}
async deleteImage(id: string): Promise<void> {
Logger.debug('delete image');
const imageDir = path.join(process.cwd(), 'imagenes');
const imagePath = path.join(imageDir, id);
const extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'avif'];
let fileFound = false;
for (const ext of extensions) {
const fullImagePath = `${imagePath}.${ext}`;
if (fs.existsSync(fullImagePath)) {
fs.unlinkSync(fullImagePath);
fileFound = true;
break;
}
}
if (!fileFound) {
console.log('Image not found, but continuing to save the new image.');
}
}
async modify(fotografia: string, id: string): Promise<string> {
if (!fotografia) {
throw new BadRequestException('No image provided');
}
await this.deleteImage(id);
// Extract the image extension
const matches = fotografia.match(/^data:image\/([a-zA-Z]+);base64,/);
if (!matches) {
throw new BadRequestException('Invalid image format');
}
const ext = matches[1];
// Decode base64 string and save the image
const buffer = Buffer.from(fotografia.split(',')[1], 'base64');
const imageName = `${id}.${ext}`;
// Save image to the 'imagenes' directory in the root of the project
const imagePath = path.join(process.cwd(), 'imagenes', imageName);
// Ensure the directory exists
const dirPath = path.join(process.cwd(), 'imagenes');
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(imagePath, buffer);
return imageName;
}
}
@@ -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,19 +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: 350, nullable: true })
lineas_inv_html: string;
@ManyToOne(() => Profesor, profesor => profesor.lineasInvestigacion)
@JoinColumn({ name: 'id_profesor', referencedColumnName: 'id_profesor', foreignKeyConstraintName: 'FK_lineas_investigacion' })
profesor: Profesor;
}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LineasInvestigacionController } from './lineas_investigacion.controller';
describe('LineasInvestigacionController', () => {
let controller: LineasInvestigacionController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LineasInvestigacionController],
}).compile();
controller = module.get<LineasInvestigacionController>(LineasInvestigacionController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -1,13 +0,0 @@
import { Controller, Get, Logger } from '@nestjs/common';
import { LineasInvestigacionService } from './lineas_investigacion.service';
@Controller('lineas-investigacion')
export class LineasInvestigacionController {
constructor(private readonly lineasInvestigacionService: LineasInvestigacionService) {}
@Get()
findAll() {
Logger.debug('find all lineas de investigacion');
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,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LineasInvestigacionService } from './lineas_investigacion.service';
describe('LineasInvestigacionService', () => {
let service: LineasInvestigacionService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LineasInvestigacionService],
}).compile();
service = module.get<LineasInvestigacionService>(LineasInvestigacionService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -1,20 +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>,
) {}
async removeByProfesorId(id_profesor: number): Promise<void> {
await this.lineasInvestigacionRepository.delete({ profesor: { id_profesor } });
}
async findAll(): Promise<LineasInvestigacion[]> {
return this.lineasInvestigacionRepository.find();
}
}
+32 -2
View File
@@ -3,16 +3,46 @@ 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<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, '..', 'imagenes'), {
prefix: '/imagenes/',
app.useStaticAssets(path.join(__dirname, '..', 'fotografias'), {
prefix: '/fotografias/',
});
await app.listen(3000);
+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,
}
+16 -35
View File
@@ -2,62 +2,43 @@ import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
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';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService,
private configService: ConfigService,
) {}
constructor(private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
Logger.debug('verify roles');
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;
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',
);
}
try {
const userPayload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('JWT_SECRET'),
});
const userRole: Role = userPayload.id_tipo_usuario;
const userId: number = userPayload.id_usuario;
const paramId = parseInt(request.params.id_usuario, 10);
if (requiredRoles.includes(userRole) || userId === paramId) {
return true;
}
return false;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
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,
PrimaryColumn,
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({ name: 'profesor' })
export class Profesor {
@PrimaryColumn({ type: 'int', nullable: false })
@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', referencedColumnName: 'id_edificio', foreignKeyConstraintName: 'FK_edificio_fk' })
/* 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', referencedColumnName: 'id_adscripcion', foreignKeyConstraintName: 'FK_adscripcion_fk' })
@ManyToOne(() => Adscripcion, (adscripcion) => adscripcion.profesores)
@JoinColumn({
name: 'id_adscripcion',
})
adscripcion: Adscripcion;
@ManyToOne(() => Categoria, categoria => categoria.profesores)
@JoinColumn({ name: 'id_categoria', referencedColumnName: 'id_categoria', foreignKeyConstraintName: 'FK_categoria_fk' })
@ManyToOne(() => Categoria, (categoria) => categoria.profesores)
@JoinColumn({
name: 'id_categoria',
})
categoria: Categoria;
@ManyToOne(() => User, user => user.profesores)
@JoinColumn({ name: 'id_usuario', referencedColumnName: 'id_usuario', foreignKeyConstraintName: 'FK_usuario_fk' })
user: User;
@ManyToOne(() => Usuario, (usuario) => usuario.profesores)
@JoinColumn({
name: 'id_usuario',
})
usuario: Usuario;
@OneToMany(() => DatosAcademicos, datosAcademicos => datosAcademicos.profesor,{ cascade: true })
@OneToMany(
() => DatosAcademicos,
(datosAcademicos) => datosAcademicos.profesor,
)
datosAcademicos: DatosAcademicos[];
@OneToMany(() => LineasInvestigacion, linea => linea.profesor,{ cascade: true })
@OneToMany(() => LineasInvestigacion, (linea) => linea.profesor)
lineasInvestigacion: LineasInvestigacion[];
@OneToMany(() => ProyectosAcademicos, proyecto => proyecto.profesor,{ cascade: true })
@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,19 +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: 700, nullable: true })
proyecto_html: string;
@ManyToOne(() => Profesor, profesor => profesor.proyectosAcademicos)
@JoinColumn({ name: 'id_profesor', referencedColumnName: 'id_profesor', foreignKeyConstraintName: 'FK_proyectos_academicos' })
profesor: Profesor;
}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProyectosAcademicosController } from './proyectos_academicos.controller';
describe('ProyectosAcademicosController', () => {
let controller: ProyectosAcademicosController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProyectosAcademicosController],
}).compile();
controller = module.get<ProyectosAcademicosController>(ProyectosAcademicosController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -1,13 +0,0 @@
import { Controller,Get, Logger } from '@nestjs/common';
import { ProyectosAcademicosService } from './proyectos_academicos.service';
@Controller('proyectos-academicos')
export class ProyectosAcademicosController {
constructor(private readonly proyectosAcademicosService: ProyectosAcademicosService) {}
@Get()
findAll() {
Logger.debug('find all proyectos academicos');
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,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProyectosAcademicosService } from './proyectos_academicos.service';
describe('ProyectosAcademicosService', () => {
let service: ProyectosAcademicosService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProyectosAcademicosService],
}).compile();
service = module.get<ProyectosAcademicosService>(ProyectosAcademicosService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -1,20 +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>,
) {}
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', referencedColumnName: 'id_tipo_usuario', foreignKeyConstraintName: 'FK_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();
});
});
-31
View File
@@ -1,31 +0,0 @@
import { Injectable, Logger } 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> {
Logger.debug('update token and date user');
await this.users.update(id, {
jwt: token,
lastlog: lastLogin,
expirationjwt: jwtExpiry,
});
}
}
+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.