diff --git a/eslint.config.mjs b/eslint.config.mjs index 32465cc..47f45d6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,35 +1,24 @@ -// @ts-check -import eslint from '@eslint/js'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { - ignores: ['eslint.config.mjs'], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - eslintPluginPrettierRecommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - ecmaVersion: 5, - sourceType: 'module', - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn' - }, - }, -); \ No newline at end of file +module.exports = { + // parser: '@typescript-eslint/parser', + // parserOptions: { + // project: 'tsconfig.json', + // sourceType: 'module', + // }, + // plugins: ['@typescript-eslint/eslint-plugin'], + // extends: [ + // 'plugin:@typescript-eslint/recommended', + // 'plugin:prettier/recommended', + // ], + // root: true, + // env: { + // node: true, + // jest: true, + // }, + // ignorePatterns: ['.eslintrc.js'], + // rules: { + // '@typescript-eslint/interface-name-prefix': 'off', + // '@typescript-eslint/explicit-function-return-type': 'off', + // '@typescript-eslint/explicit-module-boundary-types': 'off', + // '@typescript-eslint/no-explicit-any': 'off', + // }, +}; diff --git a/init-scripts/init-tipo-eventos.sql b/init-scripts/init-tipo-eventos.sql new file mode 100644 index 0000000..32e903d --- /dev/null +++ b/init-scripts/init-tipo-eventos.sql @@ -0,0 +1,14 @@ +INSERT INTO tipo_evento (tipo_evento) VALUES +('Académico'), +('Taller / Capacitación'), +('Conferencia / Charla'), +('Panel / Mesa redonda'), +('Feria / Expo'), +('Networking / Vinculación'), +('Entrevista / Sesión 1:1'), +('Cultural / Artístico'), +('Deportivo / Recreativo'), +('Institucional / Ceremonial'), +('Social / Comunitario'), +('Aniversario'), +('Otro'); diff --git a/src/administrador/administrador.controller.ts b/src/administrador/administrador.controller.ts index bffcfe1..7c25e0d 100644 --- a/src/administrador/administrador.controller.ts +++ b/src/administrador/administrador.controller.ts @@ -15,9 +15,12 @@ import { CreateAdministradorDto } from './dto/create-administrador.dto'; import { UpdateAdministradorDto } from './dto/update.administrador.dto'; import { LoginAdministradorDto } from './dto/login-administrador.dto'; import { ChangePasswordDto } from './dto/change-password.dto'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; -import { AdministradorApiDocumentation } from './administrador.documentation'; +import { AdministradorApiDocumentation } from './docs/administrador.documentation'; +import { JwtValidationGuard } from 'src/auth/guards/jwt-validation.guard'; +import { Roles } from 'src/auth/decorators/roles.decorator'; +import { RolesGuard } from 'src/auth/guards/roles.guard'; @AdministradorApiDocumentation.ApiController @ApiBearerAuth() @@ -26,20 +29,25 @@ export class AdministradorController { constructor(private administradorService: AdministradorService) {} @Get() - @UseGuards(JwtAuthGuard) + @UseGuards(RolesGuard) + @UseGuards(JwtValidationGuard) + @Roles('administrador') @AdministradorApiDocumentation.ApiGetAll getAdministradores(): Promise { return this.administradorService.getAdministradores(); } @Get(':id') - @UseGuards(JwtAuthGuard) + @UseGuards(RolesGuard) + @UseGuards(JwtValidationGuard) + @Roles('administrador') @AdministradorApiDocumentation.ApiGetOne getAdministrador(@Param('id', ParseIntPipe) id: number) { - return this.administradorService.getAdministrador(id); + return this.administradorService.getAdminByIdOrFail(id); } @Post() + @Roles('administrador') @AdministradorApiDocumentation.ApiCreate createAdministrador(@Body() newAdministrador: CreateAdministradorDto) { return this.administradorService.createAdministrador(newAdministrador); @@ -49,13 +57,15 @@ export class AdministradorController { @AdministradorApiDocumentation.ApiLogin login(@Body() loginData: LoginAdministradorDto) { return this.administradorService.login( - loginData.correo, + loginData.nombre_usuario, loginData.password, ); } @Patch(':id/change-password') - @UseGuards(JwtAuthGuard) + @UseGuards(RolesGuard) + @UseGuards(JwtValidationGuard) + @Roles('administrador') @AdministradorApiDocumentation.ApiChangePassword changePassword( @Param('id', ParseIntPipe) id: number, @@ -69,14 +79,18 @@ export class AdministradorController { } @Delete(':id') - @UseGuards(JwtAuthGuard) + @UseGuards(RolesGuard) + @UseGuards(JwtValidationGuard) + @Roles('administrador') @AdministradorApiDocumentation.ApiRemove deleteAdministrador(@Param('id', ParseIntPipe) id: number) { return this.administradorService.deleteAdministrador(id); } @Patch(':id') - @UseGuards(JwtAuthGuard) + @UseGuards(RolesGuard) + @UseGuards(JwtValidationGuard) + @Roles('administrador') @AdministradorApiDocumentation.ApiUpdate updateAdministrador( @Param('id', ParseIntPipe) id: number, diff --git a/src/administrador/administrador.service.ts b/src/administrador/administrador.service.ts index 818188e..2f6f703 100644 --- a/src/administrador/administrador.service.ts +++ b/src/administrador/administrador.service.ts @@ -35,9 +35,10 @@ export class AdministradorService { // Crear y guardar el nuevo administrador const newAdministrador = this.administradorRepository.create({ + nombre_usuario: administrador.nombre_usuario, correo: administrador.correo, password: hashedPassword, - id_tipo_user: administrador.id_tipo_user, + tipoUser: { id_tipo_user: administrador.id_tipo_user }, // Asignar el tipo de usuario }); const savedAdministrador = @@ -48,10 +49,10 @@ export class AdministradorService { return result; } - async login(correo: string, password: string) { - // Buscar administrador por correo + async login(nombre_usuario: string, password: string) { + // Buscar administrador por nombre de usuario const administrador = await this.administradorRepository.findOne({ - where: { correo }, + where: { nombre_usuario }, relations: ['tipoUser'], }); @@ -76,24 +77,24 @@ export class AdministradorService { // Generar token JWT const payload = { - sub: administrador.id_admnistrador, + sub: administrador.id_administrador, correo: administrador.correo, - id_tipo_user: administrador.id_tipo_user, }; return { token: this.jwtService.sign(payload), + tipo_usuario: administrador.tipoUser.tipo, }; } async changePassword( - id_admnistrador: number, + id_administrador: number, currentPassword: string, newPassword: string, ) { // Buscar administrador const administrador = await this.administradorRepository.findOne({ - where: { id_admnistrador }, + where: { id_administrador }, }); if (!administrador) { @@ -131,23 +132,17 @@ export class AdministradorService { return this.administradorRepository.find({ relations: ['tipoUser'], select: { - id_admnistrador: true, + nombre_usuario: true, + id_administrador: true, correo: true, - id_tipo_user: true, }, }); } - async getAdministrador(id_admnistrador: number) { + async getAdminByIdOrFail(id_administrador: number) { const administradorFound = await this.administradorRepository.findOne({ where: { - id_admnistrador, - }, - relations: ['tipoUser'], - select: { - id_admnistrador: true, - correo: true, - id_tipo_user: true, + id_administrador, }, }); @@ -161,9 +156,20 @@ export class AdministradorService { return administradorFound; } - async deleteAdministrador(id_admnistrador: number) { + async getAdminById(id_administrador: number) { + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_administrador, + }, + relations: ['tipoUser'], + }); + + return administradorFound; + } + + async deleteAdministrador(id_administrador: number) { const result = await this.administradorRepository.delete({ - id_admnistrador, + id_administrador, }); if (result.affected === 0) { @@ -177,12 +183,12 @@ export class AdministradorService { } async updateAdministrador( - id_admnistrador: number, + id_administrador: number, administrador: UpdateAdministradorDto, ) { const administradorFound = await this.administradorRepository.findOne({ where: { - id_admnistrador, + id_administrador, }, }); diff --git a/src/administrador/administrador.documentation.ts b/src/administrador/docs/administrador.documentation.ts similarity index 97% rename from src/administrador/administrador.documentation.ts rename to src/administrador/docs/administrador.documentation.ts index 207ade5..cae5844 100644 --- a/src/administrador/administrador.documentation.ts +++ b/src/administrador/docs/administrador.documentation.ts @@ -10,9 +10,7 @@ import { applyDecorators } from '@nestjs/common'; export class AdministradorApiDocumentation { // Decorador para toda la clase del controlador - static ApiController = applyDecorators( - ApiTags('Administrador'), - ); + static ApiController = applyDecorators(ApiTags('Administrador')); // Documentación para crear un administrador static ApiCreate = applyDecorators( ApiOperation({ @@ -26,6 +24,11 @@ export class AdministradorApiDocumentation { type: 'object', required: ['correo', 'password', 'id_tipo_user'], properties: { + nombre_usuario: { + type: 'string', + example: 'mike', + description: 'Nombre de usuario del administrador', + }, correo: { type: 'string', format: 'email', diff --git a/src/administrador/dto/create-administrador.dto.ts b/src/administrador/dto/create-administrador.dto.ts index 9efa3d5..c970b27 100644 --- a/src/administrador/dto/create-administrador.dto.ts +++ b/src/administrador/dto/create-administrador.dto.ts @@ -2,18 +2,31 @@ import { IsEmail, IsNotEmpty, IsNumber, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class CreateAdministradorDto { - @ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador' }) - @IsEmail({}, { message: 'El correo no es válido' }) - @IsNotEmpty({ message: 'El correo es requerido' }) - correo: string; + @ApiProperty({ + example: 'admin@example.com', + description: 'Correo del administrador', + }) + @IsEmail({}, { message: 'El correo no es válido' }) + @IsNotEmpty({ message: 'El correo es requerido' }) + correo: string; - @ApiProperty({ example: 'Password123', description: 'Contraseña del administrador' }) - @MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' }) - @IsNotEmpty({ message: 'La contraseña es requerida' }) - password: string; + @ApiProperty({ + example: 'mike', + description: 'Nombre de usuario del administrador', + }) + @IsNotEmpty({ message: 'El nombre de usuario es requerido' }) + nombre_usuario: string; - @ApiProperty({ example: 1, description: 'ID del tipo de usuario' }) - @IsNumber({}, { message: 'El tipo de usuario debe ser un número' }) - @IsNotEmpty({ message: 'El tipo de usuario es requerido' }) - id_tipo_user: number; -} \ No newline at end of file + @ApiProperty({ + example: 'Password123', + description: 'Contraseña del administrador', + }) + @MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' }) + @IsNotEmpty({ message: 'La contraseña es requerida' }) + password: string; + + @ApiProperty({ example: 1, description: 'ID del tipo de usuario' }) + @IsNumber({}, { message: 'El tipo de usuario debe ser un número' }) + @IsNotEmpty({ message: 'El tipo de usuario es requerido' }) + id_tipo_user: number; +} diff --git a/src/administrador/dto/login-administrador.dto.ts b/src/administrador/dto/login-administrador.dto.ts index 79de46f..e57530a 100644 --- a/src/administrador/dto/login-administrador.dto.ts +++ b/src/administrador/dto/login-administrador.dto.ts @@ -1,13 +1,18 @@ -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class LoginAdministradorDto { - @ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador' }) - @IsEmail({}, { message: 'El correo no es válido' }) - @IsNotEmpty({ message: 'El correo es requerido' }) - correo: string; + @ApiProperty({ + example: 'mike', + description: 'Nombre de usuario del administrador', + }) + @IsNotEmpty({ message: 'El nombre de usuario es requerido' }) + nombre_usuario: string; - @ApiProperty({ example: 'Password123', description: 'Contraseña del administrador' }) - @IsNotEmpty({ message: 'La contraseña es requerida' }) - password: string; -} \ No newline at end of file + @ApiProperty({ + example: 'Password123', + description: 'Contraseña del administrador', + }) + @IsNotEmpty({ message: 'La contraseña es requerida' }) + password: string; +} diff --git a/src/administrador/entities/administrador.entity.ts b/src/administrador/entities/administrador.entity.ts index eb639a0..fe81988 100644 --- a/src/administrador/entities/administrador.entity.ts +++ b/src/administrador/entities/administrador.entity.ts @@ -1,10 +1,19 @@ import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity'; -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; @Entity('administrador') export class Administrador { @PrimaryGeneratedColumn() - id_admnistrador: number; + id_administrador: number; + + @Column({ unique: true }) + nombre_usuario: string; @Column({ unique: true }) correo: string; @@ -12,9 +21,10 @@ export class Administrador { @Column() password: string; - @Column() + @Column({ type: 'int' }) id_tipo_user: number; @ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador) - tipoUser: TipoUser[]; + @JoinColumn({ name: 'id_tipo_user' }) + tipoUser: TipoUser; } diff --git a/src/app.module.ts b/src/app.module.ts index 649d94d..471d82b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -28,6 +28,7 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve import { ValidacionesModule } from './validaciones/validaciones.module'; import { AlumnosModule } from './alumnos/alumnos.module'; import { TrabajadoresModule } from './trabajadores/trabajadores.module'; +import { TipoEventoModule } from './tipo_evento/tipo_evento.module'; @Module({ imports: [ @@ -62,6 +63,7 @@ import { TrabajadoresModule } from './trabajadores/trabajadores.module'; SeccionModule, SeccionPreguntaModule, TipoCuestionarioModule, + TipoEventoModule, TipoPreguntaModule, TipoUserModule, ValidacionesModule, diff --git a/src/auth/decorators/roles.decorator.ts b/src/auth/decorators/roles.decorator.ts new file mode 100644 index 0000000..26c0987 --- /dev/null +++ b/src/auth/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: ('administrador' | 'staff')[]) => + SetMetadata(ROLES_KEY, roles); diff --git a/src/auth/guards/jwt-validation.guard.ts b/src/auth/guards/jwt-validation.guard.ts new file mode 100644 index 0000000..0b4e6bb --- /dev/null +++ b/src/auth/guards/jwt-validation.guard.ts @@ -0,0 +1,52 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AdministradorService } from 'src/administrador/administrador.service'; + +@Injectable() +export class JwtValidationGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private administradorService: AdministradorService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader) { + throw new UnauthorizedException('Token no proporcionado'); + } + + const token = authHeader.split(' ')[1]; + + try { + const decodedToken = this.jwtService.verify(token, { + secret: process.env.JWT_SECRET, + }); + + const administrador = await this.administradorService.getAdminById( + decodedToken.sub, + ); + + if (!administrador) { + throw new UnauthorizedException('Usuario no encontrado'); + } + + request.user = administrador; + } catch (error) { + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException( + `La sesión ha expirado, inicia sesión nuevamente.`, + ); + } + throw new UnauthorizedException('Token inválido'); + } + + return true; + } +} diff --git a/src/auth/guards/roles.guard.ts b/src/auth/guards/roles.guard.ts new file mode 100644 index 0000000..e7f6450 --- /dev/null +++ b/src/auth/guards/roles.guard.ts @@ -0,0 +1,46 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.get( + ROLES_KEY, + context.getHandler(), + ); + if (!requiredRoles) return true; // Si no hay roles definidos, permitir acceso. + + const request = context.switchToHttp().getRequest(); + const user = request.user; // Usuario ya validado en JwtValidationGuard + + if (!user) { + throw new ForbiddenException('Usuario no autenticado'); + } + + // Obtener el tipo de usuario + const tipo_usuario = user.tipoUser.tipo; + + if (!tipo_usuario) { + throw new ForbiddenException('No se pudo determinar el tipo de usuario'); + } + + // Validar si el usuario tiene uno de los roles requeridos + const hasRole = requiredRoles.includes(tipo_usuario); + + if (!hasRole) { + throw new ForbiddenException( + 'No tienes permisos para acceder a este recurso', + ); + } + + return true; + } +} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 20fb513..7d3658d 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -19,15 +19,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } async validate(payload: any) { - const { sub: id_admnistrador } = payload; - + const { sub: id_administrador } = payload; + const administrador = await this.administradorRepository.findOne({ - where: { id_admnistrador }, + where: { id_administrador }, select: { - id_admnistrador: true, + id_administrador: true, correo: true, - id_tipo_user: true - } + }, }); if (!administrador) { @@ -36,4 +35,4 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return administrador; } -} \ No newline at end of file +} diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index f4134c4..a4ff6c8 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -67,6 +67,7 @@ export class CuestionarioService { cuestionario.fecha_fin = new Date(createCuestionarioDto.fecha_fin); cuestionario.id_tipo_cuestionario = createCuestionarioDto.id_tipo_cuestionario; + cuestionario.id_tipo_evento = createCuestionarioDto.id_tipo_evento; cuestionario.contador_secciones = createCuestionarioDto.secciones?.length || 0; cuestionario.editable = true; @@ -227,6 +228,7 @@ export class CuestionarioService { fecha_inicio: new Date(cuestionario.fecha_inicio), fecha_fin: new Date(cuestionario.fecha_fin), id_tipo_cuestionario: cuestionario.id_tipo_cuestionario, + id_tipo_evento: cuestionario.id_tipo_evento, contador_secciones: cuestionario.secciones?.length || 0, editable: true, id_evento: eventoExistente.id_evento, // Asociar el evento creado diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index f90ee44..0cb2f34 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -63,6 +63,14 @@ export class CreateCuestionarioDto { @IsNumber() id_tipo_cuestionario: number; + @ApiProperty({ + description: 'ID del tipo de evento', + example: 1, + }) + @IsOptional() + @IsNumber() + id_tipo_evento: number; + @ApiProperty({ description: 'Cupo máximo de participantes', example: 100, diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index 4521691..c4e569e 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -11,6 +11,7 @@ import { Evento } from 'src/evento/entities/evento.entity'; import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity'; import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; +import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity'; @Entity('cuestionario') export class Cuestionario { @@ -44,14 +45,13 @@ export class Cuestionario { @Column({ type: 'int' }) id_tipo_cuestionario: number; + @Column({ type: 'int', nullable: true }) + id_tipo_evento: number; + @Column({ type: 'int' }) id_evento: number; // Relaciones - @ManyToOne(() => Cuestionario, { nullable: true }) - @JoinColumn({ name: 'id_cuestionario_original' }) - cuestionarioOriginal?: Cuestionario; - @ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true }) @JoinColumn({ name: 'id_evento' }) evento?: Evento; @@ -62,6 +62,12 @@ export class Cuestionario { @JoinColumn({ name: 'id_tipo_cuestionario' }) tipoCuestionario: TipoCuestionario; + @ManyToOne(() => TipoEvento, (tipo) => tipo.cuestionarios, { + eager: true, + }) + @JoinColumn({ name: 'id_tipo_evento' }) + tipoEvento: TipoEvento; + @OneToMany( () => CuestionarioSeccion, (cuestionarioSeccion) => cuestionarioSeccion.cuestionario, diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index b0ba224..e006f18 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; import { CuestionarioModule } from '../cuestionario/cuestionario.module'; import { ParticipanteModule } from '../participante/participante.module'; +import { QrModule } from '../qr/qr.module'; import { Participante } from '../participante/entities/participante.entity'; import { Cuestionario } from '../cuestionario/entities/cuestionario.entity'; import { Pregunta } from '../pregunta/entities/pregunta.entity'; @@ -29,6 +30,7 @@ import { ValidacionesModule } from '../validaciones/validaciones.module'; CuestionarioModule, ParticipanteModule, ValidacionesModule, + QrModule, ], controllers: [CuestionarioRespondidoController], providers: [CuestionarioRespondidoService], diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index fdc1a1e..9f3f426 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -17,6 +17,7 @@ import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/ import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; import { Opcion } from '../opcion/entities/opcion.entity'; import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service'; +import { QrTokenService } from '../qr/qr-token.service'; import axios from 'axios'; import * as QRCode from 'qrcode'; import { CuestionarioService } from 'src/cuestionario/cuestionario.service'; @@ -39,6 +40,7 @@ export class CuestionarioRespondidoService { private dataSource: DataSource, private validadorRespuestasService: ValidadorRespuestasService, private cuestionarioService: CuestionarioService, + private qrTokenService: QrTokenService, ) {} create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) { @@ -99,22 +101,25 @@ export class CuestionarioRespondidoService { relations: ['cuestionario', 'participante'], }, ); + if (cuestionarioRespondidoExistente) { await queryRunner.rollbackTransaction(); + let qrBuffer: Buffer | undefined; + let qrToken: string | undefined; + // Aunque ya haya respondido, enviamos el correo nuevamente if (cuestionario && cuestionario.evento) { try { - // Generar datos para el QR - const datosQR = { - id_participante: participante.id_participante, - id_evento: cuestionario.evento.id_evento, - id_cuestionario: cuestionario.id_cuestionario, - }; + // Generar token para el QR + qrToken = this.qrTokenService.generateQrToken( + participante.id_participante, + cuestionario.evento.id_evento, + cuestionario.id_cuestionario, + ); - // Generar código QR - const qrJsonData = JSON.stringify(datosQR); - const qrBuffer = await QRCode.toBuffer(qrJsonData); + // Generar código QR con el token + qrBuffer = await QRCode.toBuffer(qrToken); // Enviar correo con el QR const urlApiCorreos = process.env.url_api_correos; @@ -167,14 +172,10 @@ export class CuestionarioRespondidoService { return { success: true, + registrado: true, message: `El participante con correo ${submitDto.correo} ya había respondido el cuestionario ${cuestionario.nombre_form}. Se ha reenviado el correo con el código QR.`, - datos_qr: cuestionario.evento - ? { - id_participante: participante.id_participante, - id_evento: cuestionario.evento.id_evento, - id_cuestionario: cuestionario.id_cuestionario, - } - : undefined, + qr_token: qrToken, + qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined, }; } @@ -286,6 +287,9 @@ export class CuestionarioRespondidoService { id_cuestionario: number; } | null = null; + let qrBuffer: Buffer | undefined; + let qrToken: string | undefined; + // Capturamos los datos para el QR if (cuestionario.evento && cuestionario.evento.id_evento) { datosQR = { @@ -331,9 +335,14 @@ export class CuestionarioRespondidoService { ); } - // Generar código QR - const qrJsonData = JSON.stringify(datosQR); - const qrBuffer = await QRCode.toBuffer(qrJsonData); + // Generar token para el QR + qrToken = this.qrTokenService.generateQrToken( + participante.id_participante, + cuestionario.evento.id_evento, + cuestionario.id_cuestionario, + ); + + qrBuffer = await QRCode.toBuffer(qrToken); // Enviar correo con el QR const urlApiCorreos = process.env.url_api_correos; @@ -388,9 +397,12 @@ export class CuestionarioRespondidoService { // Preparar respuesta const response: any = { success: true, + registrado: false, message: 'Respuestas guardadas correctamente', cuestionarioRespondidoId: savedCuestionarioRespondido.idCuestionarioRespondido, + qr_token: qrToken, + qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined, }; // Incluir IDs para generar QR si hay evento asociado diff --git a/src/db/typeorm.config.ts b/src/db/typeorm.config.ts index 1ae25ac..c50a53e 100644 --- a/src/db/typeorm.config.ts +++ b/src/db/typeorm.config.ts @@ -20,6 +20,7 @@ import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta. import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity'; +import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity'; export const createMainDbConfig = async ( configService: ConfigService, @@ -47,6 +48,7 @@ export const createMainDbConfig = async ( Seccion, SeccionPregunta, TipoCuestionario, + TipoEvento, TipoPregunta, TipoUser, ], @@ -54,7 +56,7 @@ export const createMainDbConfig = async ( retryDelay: 3000, connectTimeout: 30000, - synchronize: configService.get('SYNCHRONIZE') === 'true', + synchronize: configService.get('SYNCHRONIZE') === 'false', dropSchema: configService.get('DROP_SCHEMA') === 'true', }); diff --git a/src/evento/dto/outpus.dto.ts b/src/evento/dto/outpus.dto.ts index de1e392..a7d3bf8 100644 --- a/src/evento/dto/outpus.dto.ts +++ b/src/evento/dto/outpus.dto.ts @@ -1,5 +1,6 @@ import { Expose, Type } from 'class-transformer'; import { TipoCuestionarioOutputDto } from 'src/tipo_cuestionario/dto/outputs.dto'; +import { TipoEventoOutputDto } from 'src/tipo_evento/dto/outputs.dto'; export class CuestionarioConCupoDto { @Expose() @@ -35,6 +36,10 @@ export class CuestionarioConCupoDto { @Expose() @Type(() => TipoCuestionarioOutputDto) tipoCuestionario: TipoCuestionarioOutputDto; + + @Expose() + @Type(() => TipoEventoOutputDto) + tipoEvento: TipoEventoOutputDto; } export class EventoConCuestionariosDto { diff --git a/src/evento/entities/evento.entity.ts b/src/evento/entities/evento.entity.ts index d297f7e..f6195a0 100644 --- a/src/evento/entities/evento.entity.ts +++ b/src/evento/entities/evento.entity.ts @@ -1,5 +1,13 @@ import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; @Entity() export class Evento { @@ -29,4 +37,10 @@ export class Evento { @OneToMany(() => Cuestionario, (cuestionario) => cuestionario.evento) cuestionarios: Cuestionario[]; + + @ManyToOne(() => TipoEvento, (tipo) => tipo.eventos, { + eager: true, + }) + @JoinColumn({ name: 'id_tipo_evento' }) + tipoEvento: TipoEvento; } diff --git a/src/participante_evento/dto/registrar-asistencia-qr.dto.ts b/src/participante_evento/dto/registrar-asistencia-qr.dto.ts new file mode 100644 index 0000000..dcf6033 --- /dev/null +++ b/src/participante_evento/dto/registrar-asistencia-qr.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RegistrarAsistenciaQrDto { + @ApiProperty({ + description: 'Token JWT del código QR escaneado', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }) + @IsString() + @IsNotEmpty() + qr_token: string; +} diff --git a/src/participante_evento/participante_evento.controller.ts b/src/participante_evento/participante_evento.controller.ts index 5e6b26c..8a7681a 100644 --- a/src/participante_evento/participante_evento.controller.ts +++ b/src/participante_evento/participante_evento.controller.ts @@ -12,6 +12,7 @@ import { ParticipanteEventoService } from './participante_evento.service'; import { ParticipanteEvento } from './entities/participante_evento.entity'; import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto'; import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto'; +import { RegistrarAsistenciaQrDto } from './dto/registrar-asistencia-qr.dto'; import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @ApiTags('Participantes-Eventos') @@ -20,7 +21,11 @@ export class ParticipanteEventoController { constructor(private participanteEventoService: ParticipanteEventoService) {} @ApiOperation({ summary: 'Obtener participantes por evento' }) - @ApiParam({ name: 'id_cuestionario', description: 'ID del evento', type: 'number' }) + @ApiParam({ + name: 'id_cuestionario', + description: 'ID del evento', + type: 'number', + }) @ApiResponse({ status: 200, description: 'Lista de participantes que están registrados en el evento', @@ -220,6 +225,44 @@ export class ParticipanteEventoController { ); } + @ApiOperation({ + summary: 'Registrar asistencia usando token QR', + }) + @ApiResponse({ + status: 200, + description: 'Asistencia registrada correctamente mediante token QR', + schema: { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' }, + data: { + type: 'object', + properties: { + participante: { type: 'string' }, + fecha_asistencia: { type: 'string', format: 'date-time' }, + id_evento: { type: 'number' }, + id_cuestionario: { type: 'number' }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: 400, + description: 'Token QR inválido o expirado', + }) + @ApiResponse({ + status: 404, + description: 'Participante no encontrado o no registrado en el evento', + }) + @Post('asistencia-qr') + registrarAsistenciaConToken(@Body() body: { qr_token: string }) { + return this.participanteEventoService.registrarAsistenciaConToken( + body.qr_token, + ); + } + @ApiOperation({ summary: 'Eliminar un registro participante-evento' }) @ApiParam({ name: 'id', description: 'ID del participante', type: 'number' }) @ApiResponse({ diff --git a/src/participante_evento/participante_evento.module.ts b/src/participante_evento/participante_evento.module.ts index 3a957f4..d87fdb7 100644 --- a/src/participante_evento/participante_evento.module.ts +++ b/src/participante_evento/participante_evento.module.ts @@ -6,11 +6,13 @@ import { ParticipanteEvento } from './entities/participante_evento.entity'; import { Evento } from 'src/evento/entities/evento.entity'; import { Participante } from 'src/participante/entities/participante.entity'; import { CuestionarioModule } from 'src/cuestionario/cuestionario.module'; +import { QrModule } from 'src/qr/qr.module'; @Module({ imports: [ TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]), CuestionarioModule, + QrModule, ], controllers: [ParticipanteEventoController], providers: [ParticipanteEventoService], diff --git a/src/participante_evento/participante_evento.service.ts b/src/participante_evento/participante_evento.service.ts index 2b1d71e..7b570aa 100644 --- a/src/participante_evento/participante_evento.service.ts +++ b/src/participante_evento/participante_evento.service.ts @@ -7,6 +7,7 @@ import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dt import { Evento } from 'src/evento/entities/evento.entity'; import { Participante } from 'src/participante/entities/participante.entity'; import { CuestionarioService } from 'src/cuestionario/cuestionario.service'; +import { QrTokenService } from 'src/qr/qr-token.service'; @Injectable() export class ParticipanteEventoService { @@ -17,6 +18,7 @@ export class ParticipanteEventoService { @InjectRepository(Participante) private participanteRepository: Repository, private readonly cuestionarioService: CuestionarioService, + private readonly qrTokenService: QrTokenService, ) {} async createParticipanteEvento( @@ -175,4 +177,83 @@ export class ParticipanteEventoService { participante_eventoFound.asistio = true; return this.participanteEventoRepository.save(participante_eventoFound); } + + /** + * Registra la asistencia usando un token QR + * @param qrToken Token JWT del código QR + * @returns Resultado del registro de asistencia + */ + async registrarAsistenciaConToken(qrToken: string) { + // Validar el token QR + const tokenData = this.qrTokenService.validateQrToken(qrToken); + + if (!tokenData) { + throw new HttpException( + 'Token QR inválido o expirado', + HttpStatus.BAD_REQUEST, + ); + } + + const { id_participante, id_evento, id_cuestionario } = tokenData; + + // Verificar que el participante existe + const participante = await this.participanteRepository.findOne({ + where: { id_participante }, + }); + + if (!participante) { + throw new HttpException( + `Participante con ID ${id_participante} no encontrado`, + HttpStatus.NOT_FOUND, + ); + } + + // Buscar el registro participante-evento + const participanteEvento = await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_cuestionario, + }, + relations: ['participante'], + }); + + if (!participanteEvento) { + throw new HttpException( + 'El participante no está registrado en este evento', + HttpStatus.NOT_FOUND, + ); + } + + // Verificar si ya había registrado asistencia + if (participanteEvento.asistio && participanteEvento.fecha_asistencia) { + return { + success: true, + message: + 'El participante ya había registrado su asistencia previamente', + data: { + participante: participante.correo, + fecha_asistencia_previa: participanteEvento.fecha_asistencia, + fecha_actual: new Date(), + }, + }; + } + + // Registrar la asistencia + participanteEvento.fecha_asistencia = new Date(); + participanteEvento.asistio = true; + + const resultado = + await this.participanteEventoRepository.save(participanteEvento); + + return { + success: true, + message: 'Asistencia registrada exitosamente', + data: { + participante: participante.correo, + fecha_asistencia: resultado.fecha_asistencia, + id_evento, + id_cuestionario, + }, + }; + } } diff --git a/src/qr/dto/validate-qr-token.dto.ts b/src/qr/dto/validate-qr-token.dto.ts new file mode 100644 index 0000000..e0c2a04 --- /dev/null +++ b/src/qr/dto/validate-qr-token.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ValidateQrTokenDto { + @ApiProperty({ + description: 'Token JWT del código QR', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }) + @IsString() + @IsNotEmpty() + token: string; +} diff --git a/src/qr/qr-token.service.ts b/src/qr/qr-token.service.ts new file mode 100644 index 0000000..f0792e1 --- /dev/null +++ b/src/qr/qr-token.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class QrTokenService { + constructor( + private jwtService: JwtService, + private configService: ConfigService, + ) {} + + /** + * Genera un token JWT específico para el QR de asistencia + * @param id_participante ID del participante + * @param id_evento ID del evento + * @param id_cuestionario ID del cuestionario + * @returns Token JWT codificado + */ + generateQrToken( + id_participante: number, + id_evento: number, + id_cuestionario: number, + ): string { + const payload = { + id_participante, + id_evento, + id_cuestionario, + type: 'qr_asistencia', + iat: Math.floor(Date.now() / 1000), + }; + + // Usar una clave secreta específica para QR o la misma que JWT general + const secret = + this.configService.get('QR_JWT_SECRET') || + this.configService.get('JWT_SECRET', 'tu_clave_secreta'); + + return this.jwtService.sign(payload, { + secret, + expiresIn: '30d', // El QR puede ser válido por 30 días + }); + } + + /** + * Valida y decodifica un token de QR + * @param token Token JWT a validar + * @returns Datos decodificados del token o null si es inválido + */ + validateQrToken(token: string): { + id_participante: number; + id_evento: number; + id_cuestionario: number; + type: string; + iat: number; + } | null { + try { + const secret = + this.configService.get('QR_JWT_SECRET') || + this.configService.get('JWT_SECRET', 'tu_clave_secreta'); + + const decoded = this.jwtService.verify(token, { secret }); + + // Verificar que sea un token de tipo QR de asistencia + if (decoded.type !== 'qr_asistencia') { + return null; + } + + return { + id_participante: decoded.id_participante, + id_evento: decoded.id_evento, + id_cuestionario: decoded.id_cuestionario, + type: decoded.type, + iat: decoded.iat, + }; + } catch (error) { + console.error('Error validando token QR:', error.message); + return null; + } + } + + /** + * Verifica si un token QR es válido y no ha expirado + * @param token Token a verificar + * @returns true si es válido, false si no + */ + isTokenValid(token: string): boolean { + const decoded = this.validateQrToken(token); + return decoded !== null; + } +} diff --git a/src/qr/qr.controller.ts b/src/qr/qr.controller.ts index 67b4ff9..bb6246c 100644 --- a/src/qr/qr.controller.ts +++ b/src/qr/qr.controller.ts @@ -10,14 +10,20 @@ import { Query, } from '@nestjs/common'; import { QrService } from './qr.service'; +import { QrTokenService } from './qr-token.service'; import { Qr } from './qr.entity'; import { CreateQrDto } from './dto/create-qr.dto'; import { UpdateQrDto } from './dto/update.qr.dto'; -import { ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { ValidateQrTokenDto } from './dto/validate-qr-token.dto'; +import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +@ApiTags('QR') @Controller('qr') export class QrController { - constructor(private qrService: QrService) {} + constructor( + private qrService: QrService, + private qrTokenService: QrTokenService, + ) {} @Get('generate') @ApiOperation({ summary: 'Genera un código QR a partir de un texto' }) @@ -31,16 +37,64 @@ export class QrController { } @Get('asistencia/:idParticipante/:idEvento') - @ApiOperation({ summary: 'Genera un código QR para asistencia con IDs de participante y evento' }) + @ApiOperation({ + summary: + 'Genera un código QR para asistencia con IDs de participante y evento', + }) @ApiParam({ name: 'idParticipante', description: 'ID del participante' }) @ApiParam({ name: 'idEvento', description: 'ID del evento' }) async generateAsistenciaQR( @Param('idParticipante', ParseIntPipe) idParticipante: number, - @Param('idEvento', ParseIntPipe) idEvento: number + @Param('idEvento', ParseIntPipe) idEvento: number, ): Promise { return this.qrService.generateAsistenciaQR(idParticipante, idEvento); } + @Post('validate-token') + @ApiOperation({ summary: 'Valida un token de QR para asistencia' }) + async validateQrToken(@Body() validateDto: ValidateQrTokenDto) { + const tokenData = this.qrTokenService.validateQrToken(validateDto.token); + + if (!tokenData) { + return { + valid: false, + message: 'Token inválido o expirado', + }; + } + + return { + valid: true, + data: tokenData, + message: 'Token válido', + }; + } + + @Get('generate-token/:idParticipante/:idEvento/:idCuestionario') + @ApiOperation({ summary: 'Genera un token JWT para QR de asistencia' }) + @ApiParam({ name: 'idParticipante', description: 'ID del participante' }) + @ApiParam({ name: 'idEvento', description: 'ID del evento' }) + @ApiParam({ name: 'idCuestionario', description: 'ID del cuestionario' }) + async generateQrToken( + @Param('idParticipante', ParseIntPipe) idParticipante: number, + @Param('idEvento', ParseIntPipe) idEvento: number, + @Param('idCuestionario', ParseIntPipe) idCuestionario: number, + ) { + const token = this.qrTokenService.generateQrToken( + idParticipante, + idEvento, + idCuestionario, + ); + + return { + token, + qr_data: { + id_participante: idParticipante, + id_evento: idEvento, + id_cuestionario: idCuestionario, + }, + }; + } + @Get() getQrs(): Promise { return this.qrService.getQrs(); diff --git a/src/qr/qr.module.ts b/src/qr/qr.module.ts index 1a6040e..e7471b9 100644 --- a/src/qr/qr.module.ts +++ b/src/qr/qr.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; import { QrService } from './qr.service'; import { QrController } from './qr.controller'; +import { QrTokenService } from './qr-token.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Qr } from './qr.entity'; +import { AuthModule } from '../auth/auth.module'; +import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([Qr])], + imports: [TypeOrmModule.forFeature([Qr]), AuthModule, ConfigModule], controllers: [QrController], - providers: [QrService] + providers: [QrService, QrTokenService], + exports: [QrTokenService], }) export class QrModule {} diff --git a/src/tipo_cuestionario/dto/outputs.dto.ts b/src/tipo_cuestionario/dto/outputs.dto.ts index 1f225ee..c21eabf 100644 --- a/src/tipo_cuestionario/dto/outputs.dto.ts +++ b/src/tipo_cuestionario/dto/outputs.dto.ts @@ -1,9 +1,9 @@ import { Expose } from 'class-transformer'; export class TipoCuestionarioOutputDto { - @Expose() - id_tipo_cuestionario: number; + @Expose() + id_tipo_cuestionario: number; - @Expose() - tipo_cuestionario: string; -} \ No newline at end of file + @Expose() + tipo_cuestionario: string; +} diff --git a/src/tipo_evento/dto/create-tipo_evento.dto.ts b/src/tipo_evento/dto/create-tipo_evento.dto.ts new file mode 100644 index 0000000..83453f6 --- /dev/null +++ b/src/tipo_evento/dto/create-tipo_evento.dto.ts @@ -0,0 +1,8 @@ +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { TipoEventoEnum } from '../entities/tipo_evento.entity'; + +export class CreateTipoEventoDto { + @IsNotEmpty() + @IsEnum(TipoEventoEnum) + tipo_evento: TipoEventoEnum; +} diff --git a/src/tipo_evento/dto/outputs.dto.ts b/src/tipo_evento/dto/outputs.dto.ts new file mode 100644 index 0000000..23c2bec --- /dev/null +++ b/src/tipo_evento/dto/outputs.dto.ts @@ -0,0 +1,9 @@ +import { Expose } from 'class-transformer'; + +export class TipoEventoOutputDto { + @Expose() + id_tipo_evento: number; + + @Expose() + tipo_evento: string; +} diff --git a/src/tipo_evento/dto/update-tipo_evento.dto.ts b/src/tipo_evento/dto/update-tipo_evento.dto.ts new file mode 100644 index 0000000..403e1b8 --- /dev/null +++ b/src/tipo_evento/dto/update-tipo_evento.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateTipoEventoDto } from './create-tipo_evento.dto'; + +export class UpdateTipoEventoDto extends PartialType(CreateTipoEventoDto) {} diff --git a/src/tipo_evento/entities/tipo_evento.entity.ts b/src/tipo_evento/entities/tipo_evento.entity.ts new file mode 100644 index 0000000..ff3ce17 --- /dev/null +++ b/src/tipo_evento/entities/tipo_evento.entity.ts @@ -0,0 +1,34 @@ +import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; +import { Evento } from 'src/evento/entities/evento.entity'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; + +export enum TipoEventoEnum { + ACADEMICO = 'Académico', + TALLER_CAPACITACION = 'Taller / Capacitación', + CONFERENCIA_CHARLA = 'Conferencia / Charla', + PANEL_MESA_REDONDA = 'Panel / Mesa redonda', + FERIA_EXPO = 'Feria / Expo', + NETWORKING = 'Networking / Vinculación', + ENTREVISTA = 'Entrevista / Sesión 1:1', + CULTURAL_ARTISTICO = 'Cultural / Artístico', + DEPORTIVO = 'Deportivo / Recreativo', + INSTITUCIONAL = 'Institucional / Ceremonial', + SOCIAL_COMUNITARIO = 'Social / Comunitario', + ANIVERSARIO = 'Aniversario', + OTRO = 'Otro', +} + +@Entity('tipo_evento') +export class TipoEvento { + @PrimaryGeneratedColumn() + id_tipo_evento: number; + + @Column({ type: 'enum', enum: TipoEventoEnum }) + tipo_evento: TipoEventoEnum; + + @OneToMany(() => Cuestionario, (cuestionario) => cuestionario.tipoEvento) + cuestionarios: Cuestionario[]; + + @OneToMany(() => Evento, (evento) => evento.tipoEvento) + eventos: Evento[]; +} diff --git a/src/tipo_evento/tipo_evento.controller.ts b/src/tipo_evento/tipo_evento.controller.ts new file mode 100644 index 0000000..ad09009 --- /dev/null +++ b/src/tipo_evento/tipo_evento.controller.ts @@ -0,0 +1,46 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + ParseIntPipe, +} from '@nestjs/common'; +import { TipoEventoService } from './tipo_evento.service'; +import { CreateTipoEventoDto } from './dto/create-tipo_evento.dto'; +import { UpdateTipoEventoDto } from './dto/update-tipo_evento.dto'; + +@Controller('tipo-evento') +export class TipoEventoController { + constructor(private readonly tipoEventoService: TipoEventoService) {} + + @Post() + create(@Body() createTipoEventoDto: CreateTipoEventoDto) { + return this.tipoEventoService.create(createTipoEventoDto); + } + + @Get() + findAll() { + return this.tipoEventoService.findAll(); + } + + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.tipoEventoService.findOne(id); + } + + @Patch(':id') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateTipoEventoDto: UpdateTipoEventoDto, + ) { + return this.tipoEventoService.update(id, updateTipoEventoDto); + } + + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.tipoEventoService.remove(id); + } +} diff --git a/src/tipo_evento/tipo_evento.module.ts b/src/tipo_evento/tipo_evento.module.ts new file mode 100644 index 0000000..2c1e164 --- /dev/null +++ b/src/tipo_evento/tipo_evento.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TipoEvento } from './entities/tipo_evento.entity'; +import { TipoEventoController } from './tipo_evento.controller'; +import { TipoEventoService } from './tipo_evento.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([TipoEvento])], + controllers: [TipoEventoController], + providers: [TipoEventoService], + exports: [TipoEventoService], +}) +export class TipoEventoModule {} diff --git a/src/tipo_evento/tipo_evento.service.ts b/src/tipo_evento/tipo_evento.service.ts new file mode 100644 index 0000000..c142100 --- /dev/null +++ b/src/tipo_evento/tipo_evento.service.ts @@ -0,0 +1,98 @@ +import { + Injectable, + NotFoundException, + ConflictException, + OnModuleInit, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TipoEvento, TipoEventoEnum } from './entities/tipo_evento.entity'; +import { CreateTipoEventoDto } from './dto/create-tipo_evento.dto'; +import { UpdateTipoEventoDto } from './dto/update-tipo_evento.dto'; + +@Injectable() +export class TipoEventoService implements OnModuleInit { + constructor( + @InjectRepository(TipoEvento) + private tipoEventoRepository: Repository, + ) {} + + async onModuleInit() { + await this.seedTipoEventos(); + } + + private async seedTipoEventos() { + const enumValues = Object.values(TipoEventoEnum); + + for (const tipoEvento of enumValues) { + const existingTipo = await this.tipoEventoRepository.findOne({ + where: { tipo_evento: tipoEvento }, + }); + + if (!existingTipo) { + await this.tipoEventoRepository.save({ + tipo_evento: tipoEvento, + }); + } + } + } + + async create(createTipoEventoDto: CreateTipoEventoDto): Promise { + const existingTipo = await this.tipoEventoRepository.findOne({ + where: { tipo_evento: createTipoEventoDto.tipo_evento }, + }); + + if (existingTipo) { + throw new ConflictException('Este tipo de evento ya existe'); + } + + const tipoEvento = this.tipoEventoRepository.create(createTipoEventoDto); + return this.tipoEventoRepository.save(tipoEvento); + } + + async findAll(): Promise { + return this.tipoEventoRepository.find({ + order: { id_tipo_evento: 'ASC' }, + }); + } + + async findOne(id: number): Promise { + const tipoEvento = await this.tipoEventoRepository.findOne({ + where: { id_tipo_evento: id }, + }); + + if (!tipoEvento) { + throw new NotFoundException(`Tipo de evento con ID ${id} no encontrado`); + } + + return tipoEvento; + } + + async update( + id: number, + updateTipoEventoDto: UpdateTipoEventoDto, + ): Promise { + const tipoEvento = await this.findOne(id); + + if (updateTipoEventoDto.tipo_evento) { + const existingTipo = await this.tipoEventoRepository.findOne({ + where: { tipo_evento: updateTipoEventoDto.tipo_evento }, + }); + + if (existingTipo && existingTipo.id_tipo_evento !== id) { + throw new ConflictException('Este tipo de evento ya existe'); + } + } + + Object.assign(tipoEvento, updateTipoEventoDto); + return this.tipoEventoRepository.save(tipoEvento); + } + + async remove(id: number): Promise<{ message: string }> { + const tipoEvento = await this.findOne(id); + + await this.tipoEventoRepository.remove(tipoEvento); + + return { message: 'Tipo de evento eliminado exitosamente' }; + } +} diff --git a/src/tipo_user/entities/tipo_user.entity.ts b/src/tipo_user/entities/tipo_user.entity.ts index 53a3ed0..3f14f3c 100644 --- a/src/tipo_user/entities/tipo_user.entity.ts +++ b/src/tipo_user/entities/tipo_user.entity.ts @@ -2,6 +2,11 @@ import { Administrador } from 'src/administrador/entities/administrador.entity'; import { Participante } from 'src/participante/entities/participante.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +export enum TipoUsuarioEnum { + ADMINISTRADOR = 'administrador', + STAFF = 'staff', +} + @Entity() export class TipoUser { @PrimaryGeneratedColumn() diff --git a/src/tipo_user/tipo_user.service.ts b/src/tipo_user/tipo_user.service.ts index 8b372e8..c026e30 100644 --- a/src/tipo_user/tipo_user.service.ts +++ b/src/tipo_user/tipo_user.service.ts @@ -5,7 +5,7 @@ import { Injectable, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { TipoUser } from './entities/tipo_user.entity'; +import { TipoUser, TipoUsuarioEnum } from './entities/tipo_user.entity'; import { Repository } from 'typeorm'; import { CreateTipoUserDto } from './dto/create-tipo-user.dto'; import { UpdateTipoUserDto } from './dto/update.tipo_user.dto'; @@ -23,6 +23,25 @@ export class TipoUserService { private administradorRepository: Repository, ) {} + async onModuleInit() { + await this.seedTipoEventos(); + } + + private async seedTipoEventos() { + const enumValues = Object.values(TipoUsuarioEnum); + + for (const tipoUsuario of enumValues) { + const existingTipo = await this.tipoUserRepository.findOne({ + where: { tipo: tipoUsuario }, + }); + + if (!existingTipo) { + await this.tipoUserRepository.save({ + tipo: tipoUsuario, + }); + } + } + } /* //Funcion para registrar usuario async registrarUsuario(dto: CreateTipoUserDto) {