19 Commits

Author SHA1 Message Date
santiago 4a156beab6 Agregar funcionalidad para subir imágenes desde la computadora con Multer y guardarlas en /uploads 2025-05-01 08:18:37 -06:00
Your Name 14d191790f login de admin y endpoints de admin validados, falta validar lo de qr 2025-04-21 12:32:45 -06:00
eithan f496c2d8be Merge pull request 'develop' (#26) from develop into master
Reviewed-on: #26
2025-04-09 16:34:53 -06:00
eithan 8a837eda8d Merge branch 'master' into develop 2025-04-09 16:34:44 -06:00
eithan aae1e5ede3 Actualizar 'src/app.module.ts'
false drop y sync
2025-04-09 16:34:11 -06:00
eithan 0c6f5336a9 Actualizar 'src/app.module.ts' 2025-04-09 16:33:17 -06:00
eithan 2a807acb87 Merge pull request 'Actualizar 'src/app.module.ts'' (#25) from develop into master
Reviewed-on: #25
2025-04-09 16:29:09 -06:00
eithan a8645d28e8 Merge branch 'master' into develop 2025-04-09 16:29:04 -06:00
eithan f7f6d8296c Actualizar 'src/app.module.ts'
sync y drop true
2025-04-09 16:28:25 -06:00
eithan f747e146e8 Merge pull request 'Actualizar 'src/cuestionario_respondido/cuestionario_respondido.service.ts'' (#24) from develop into master
Reviewed-on: #24
2025-04-09 09:02:01 -06:00
eithan 10ba9642bd Merge branch 'master' into develop 2025-04-09 09:01:52 -06:00
eithan a2e4f62135 Actualizar 'src/cuestionario_respondido/cuestionario_respondido.service.ts'
nuevo mensaje correo
2025-04-09 09:01:31 -06:00
eithan 283b8df276 Merge pull request 'Actualizar 'src/app.module.ts'' (#23) from develop into master
Reviewed-on: #23
2025-04-07 19:41:09 -06:00
eithan a48f2cb41c Merge branch 'master' into develop 2025-04-07 19:41:03 -06:00
eithan 564cbd4f69 Actualizar 'src/app.module.ts'
synchronize: false,
      dropSchema: false,
2025-04-07 19:40:37 -06:00
eithan ed7cadd6e0 Merge pull request 'develop' (#22) from develop into master
Reviewed-on: #22
2025-04-07 15:25:08 -06:00
eithan d436ca6bbc Merge branch 'master' into develop 2025-04-07 15:24:58 -06:00
eithan 489965681f Merge pull request 'reporte csv' (#21) from eithan into develop
Reviewed-on: #21
2025-04-07 15:07:14 -06:00
eithan daca1aa0b0 Merge pull request 'develop' (#20) from develop into master
Reviewed-on: #20
2025-04-07 11:36:13 -06:00
155 changed files with 1814 additions and 236 deletions
+2
View File
@@ -5,6 +5,8 @@ WORKDIR /app
COPY package*.json ./
RUN npm install
# Instalar explícitamente las dependencias de autenticación
RUN npm install bcryptjs @types/bcryptjs @nestjs/jwt @nestjs/passport passport passport-jwt @types/passport-jwt
COPY . .
+523 -168
View File
File diff suppressed because it is too large Load Diff
+17 -1
View File
@@ -23,19 +23,29 @@
"@nestjs/common": "^11.0.12",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.1.0",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^2.4.6",
"@types/passport-jwt": "^4.0.1",
"axios": "^1.8.4",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"multer": "^1.4.5-lts.2",
"mysql2": "^3.14.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.21"
"typeorm": "^0.3.22"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -47,6 +57,7 @@
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2",
@@ -81,5 +92,10 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"compilerOptions": {
"outDir": "./dist"
}
}
+41 -26
View File
@@ -1,57 +1,72 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { AdministradorService } from './administrador.service';
import { Administrador } from './administrador.entity';
import { CreateAdministradorDto } from './dto/create-administrador.dto';
import { UpdateAdministradorDto } from './dto/update.administrador.dto';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { LoginAdministradorDto } from './dto/login-administrador.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
import { ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AdministradorApiDocumentation } from './administrador.documentation';
@ApiTags('Administradores') // Agrupa los endpoints en Swagger
@Controller('administrador')
@ApiBearerAuth()
@AdministradorApiDocumentation.ApiController
export class AdministradorController {
constructor(private administradorService: AdministradorService) {}
@Get()
@ApiOperation({ summary: 'Obtener todos los administradores' })
@ApiResponse({ status: 200, description: 'Lista de administradores obtenida correctamente.' })
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiGetAll
getAdministradores(): Promise<Administrador[]> {
return this.administradorService.getAdministradores();
}
@Get(':id')
@ApiOperation({ summary: 'Obtener un administrador por ID' })
@ApiParam({ name: 'id', description: 'ID del administrador', example: 1 })
@ApiResponse({ status: 200, description: 'Administrador obtenido correctamente.' })
@ApiResponse({ status: 404, description: 'Administrador no encontrado.' })
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiGetOne
getAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.getAdministrador(id)
return this.administradorService.getAdministrador(id);
}
@Post()
@ApiOperation({ summary: 'Registrar un nuevo administrador' })
@ApiBody({
description: 'Datos del administrador a registrar',
schema: {
type: 'object',
properties: {
id_tipo_user: { type: 'integer', example: 1 }
}
}
})
@ApiResponse({ status: 201, description: 'Administrador registrado exitosamente.' })
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
@AdministradorApiDocumentation.ApiCreate
createAdministrador(@Body() newAdministrador: CreateAdministradorDto) {
return this.administradorService.createAdministrador(newAdministrador)
return this.administradorService.createAdministrador(newAdministrador);
}
@Post('login')
@AdministradorApiDocumentation.ApiLogin
login(@Body() loginData: LoginAdministradorDto) {
return this.administradorService.login(loginData.correo, loginData.password);
}
@Patch(':id/change-password')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiChangePassword
changePassword(
@Param('id', ParseIntPipe) id: number,
@Body() changePasswordDto: ChangePasswordDto
) {
return this.administradorService.changePassword(
id,
changePasswordDto.currentPassword,
changePasswordDto.newPassword
);
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiRemove
deleteAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.deleteAdministrador(id)
return this.administradorService.deleteAdministrador(id);
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiUpdate
updateAdministrador(@Param('id', ParseIntPipe) id: number, @Body() administrador: UpdateAdministradorDto) {
return this.administradorService.updateAdministrador(id, administrador)
return this.administradorService.updateAdministrador(id, administrador);
}
}
@@ -0,0 +1,314 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class AdministradorApiDocumentation {
// Decorador para toda la clase del controlador
static ApiController = ApiTags('Administradores');
// Documentación para crear un administrador
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Registrar un nuevo administrador',
description: 'Crea un nuevo administrador con correo, contraseña y tipo de administrador'
}),
ApiBody({
description: 'Datos del administrador a registrar',
schema: {
type: 'object',
required: ['correo', 'password', 'id_tipo_user'],
properties: {
correo: {
type: 'string',
format: 'email',
example: 'admin@ejemplo.com',
description: 'Correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña del administrador (mínimo 6 caracteres)'
},
id_tipo_user: {
type: 'integer',
example: 1,
description: 'ID del tipo de usuario administrador'
}
}
}
}),
ApiResponse({
status: 201,
description: 'Administrador registrado exitosamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos del administrador inválidos' }),
ApiResponse({ status: 409, description: 'El correo ya está registrado' })
);
// Documentación para iniciar sesión
static ApiLogin = applyDecorators(
ApiOperation({
summary: 'Iniciar sesión como administrador',
description: 'Autentica un administrador con correo y contraseña, y devuelve un token JWT'
}),
ApiBody({
description: 'Credenciales de inicio de sesión',
schema: {
type: 'object',
required: ['correo', 'password'],
properties: {
correo: {
type: 'string',
format: 'email',
example: 'admin@ejemplo.com',
description: 'Correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña del administrador'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Inicio de sesión exitoso',
schema: {
type: 'object',
properties: {
access_token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
description: 'Token JWT para autenticación'
},
id_administrador: {
type: 'number',
example: 1,
description: 'ID del administrador autenticado'
},
correo: {
type: 'string',
example: 'admin@ejemplo.com',
description: 'Correo del administrador autenticado'
},
tipo_user: {
type: 'number',
example: 1,
description: 'Tipo de usuario del administrador'
}
}
}
}),
ApiResponse({ status: 401, description: 'Credenciales inválidas' })
);
// Documentación para obtener todos los administradores
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todos los administradores',
description: 'Retorna una lista de todos los administradores registrados (requiere autenticación)'
}),
ApiResponse({
status: 200,
description: 'Lista de administradores obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipoUser: {
type: 'array',
items: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Administrador General' }
}
}
}
}
}
}
}),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para obtener un administrador por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener un administrador por ID',
description: 'Retorna los datos de un administrador específico según su ID (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Administrador obtenido correctamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipoUser: {
type: 'array',
items: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Administrador General' }
}
}
}
}
}
}),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para actualizar un administrador
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar datos de un administrador',
description: 'Actualiza la información de un administrador existente (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador a actualizar',
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar del administrador',
schema: {
type: 'object',
properties: {
correo: {
type: 'string',
format: 'email',
example: 'nuevo_admin@ejemplo.com',
description: 'Nuevo correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'NuevaPassword123',
description: 'Nueva contraseña del administrador (mínimo 6 caracteres)'
},
id_tipo_user: {
type: 'integer',
example: 2,
description: 'Nuevo tipo de usuario administrador'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Administrador actualizado correctamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'nuevo_admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 2 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para cambiar contraseña
static ApiChangePassword = applyDecorators(
ApiOperation({
summary: 'Cambiar contraseña de un administrador',
description: 'Permite a un administrador cambiar su contraseña verificando primero la contraseña actual (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador',
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos para cambio de contraseña',
schema: {
type: 'object',
required: ['currentPassword', 'newPassword'],
properties: {
currentPassword: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña actual del administrador'
},
newPassword: {
type: 'string',
format: 'password',
example: 'NuevaPassword123',
description: 'Nueva contraseña del administrador (mínimo 6 caracteres)'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Contraseña actualizada correctamente',
schema: {
type: 'object',
properties: {
message: { type: 'string', example: 'Contraseña actualizada correctamente' }
}
}
}),
ApiResponse({ status: 400, description: 'Contraseña actual incorrecta' }),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para eliminar un administrador
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar un administrador',
description: 'Elimina permanentemente un administrador por su ID (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador a eliminar',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Administrador eliminado correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
}
@@ -6,6 +6,12 @@ export class Administrador {
@PrimaryGeneratedColumn()
id_admnistrador: number
@Column({ unique: true })
correo: string
@Column()
password: string
/*
@ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administradores)
@JoinColumn({ name: "id_tipo_user" })
+5 -1
View File
@@ -3,9 +3,13 @@ import { AdministradorService } from './administrador.service';
import { AdministradorController } from './administrador.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Administrador } from './administrador.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([Administrador])],
imports: [
TypeOrmModule.forFeature([Administrador]),
AuthModule
],
controllers: [AdministradorController],
providers: [AdministradorService],
exports: [AdministradorService],
+115 -23
View File
@@ -4,36 +4,115 @@ import { Administrador } from './administrador.entity';
import { Repository } from 'typeorm';
import { CreateAdministradorDto } from './dto/create-administrador.dto';
import { UpdateAdministradorDto } from './dto/update.administrador.dto';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AdministradorService {
constructor(
@InjectRepository(Administrador) private administradorRepository: Repository<Administrador>
@InjectRepository(Administrador) private administradorRepository: Repository<Administrador>,
private jwtService: JwtService
) {}
async createAdministrador(administrador: CreateAdministradorDto) {
//revisar el where
// Verificar si ya existe un administrador con el mismo correo
const administradorFound = await this.administradorRepository.findOne({
where: {
id_admnistrador: administrador.id_tipo_user
correo: administrador.correo
}
})
});
if (administradorFound) {
return new HttpException('Administrador already exists', HttpStatus.CONFLICT)
throw new HttpException('El correo ya está registrado', HttpStatus.CONFLICT);
}
// Encriptar la contraseña
const hashedPassword = await this.hashPassword(administrador.password);
// Crear y guardar el nuevo administrador
const newAdministrador = this.administradorRepository.create({
correo: administrador.correo,
password: hashedPassword,
id_tipo_user: administrador.id_tipo_user
});
//Falta regresar el return
//return this.administradorRepository.save(administradorFound)
const savedAdministrador = await this.administradorRepository.save(newAdministrador);
// Excluir la contraseña de la respuesta
const { password, ...result } = savedAdministrador;
return result;
}
async login(correo: string, password: string) {
// Buscar administrador por correo
const administrador = await this.administradorRepository.findOne({
where: { correo },
relations: ['tipoUser']
});
if (!administrador) {
throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED);
}
// Verificar contraseña
const isPasswordValid = await bcrypt.compare(password, administrador.password);
if (!isPasswordValid) {
throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED);
}
// Generar token JWT
const payload = {
sub: administrador.id_admnistrador,
correo: administrador.correo,
id_tipo_user: administrador.id_tipo_user
};
return {
access_token: this.jwtService.sign(payload),
id_administrador: administrador.id_admnistrador,
correo: administrador.correo,
tipo_user: administrador.id_tipo_user
};
}
async changePassword(id_admnistrador: number, currentPassword: string, newPassword: string) {
// Buscar administrador
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador }
});
if (!administrador) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
}
// Verificar contraseña actual
const isPasswordValid = await bcrypt.compare(currentPassword, administrador.password);
if (!isPasswordValid) {
throw new HttpException('Contraseña actual incorrecta', HttpStatus.BAD_REQUEST);
}
// Hashear y actualizar nueva contraseña
administrador.password = await this.hashPassword(newPassword);
await this.administradorRepository.save(administrador);
return { message: 'Contraseña actualizada correctamente' };
}
private async hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt();
return bcrypt.hash(password, salt);
}
getAdministradores() {
return this.administradorRepository.find({
relations: ['tipoUser']
})
relations: ['tipoUser'],
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
}
async getAdministrador(id_admnistrador: number) {
@@ -41,24 +120,29 @@ export class AdministradorService {
where: {
id_admnistrador
},
relations: ['tipoUser']
})
relations: ['tipoUser'],
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
if (!Administrador) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
if (!administradorFound) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
}
return administradorFound
return administradorFound;
}
async deleteAdministrador(id_admnistrador: number) {
const result = await this.administradorRepository.delete({ id_admnistrador })
const result = await this.administradorRepository.delete({ id_admnistrador });
if (result.affected === 0) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
}
return result
return result;
}
async updateAdministrador(id_admnistrador: number, administrador: UpdateAdministradorDto) {
@@ -66,14 +150,22 @@ export class AdministradorService {
where: {
id_admnistrador
}
})
});
if (!administradorFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND)
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
}
const updateAdministrador = Object.assign(administradorFound, administrador)
return this.administradorRepository.save(updateAdministrador)
}
// Si se incluye una nueva contraseña, hashearla
if (administrador.password) {
administrador.password = await this.hashPassword(administrador.password);
}
const updateAdministrador = Object.assign(administradorFound, administrador);
const savedAdministrador = await this.administradorRepository.save(updateAdministrador);
// Excluir la contraseña de la respuesta
const { password, ...result } = savedAdministrador;
return result;
}
}
@@ -0,0 +1,13 @@
import { IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ChangePasswordDto {
@ApiProperty({ description: 'Contraseña actual del administrador' })
@IsNotEmpty({ message: 'La contraseña actual es requerida' })
currentPassword: string;
@ApiProperty({ description: 'Nueva contraseña del administrador' })
@MinLength(6, { message: 'La nueva contraseña debe tener al menos 6 caracteres' })
@IsNotEmpty({ message: 'La nueva contraseña es requerida' })
newPassword: string;
}
@@ -1,3 +1,19 @@
import { IsEmail, IsNotEmpty, IsNumber, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateAdministradorDto {
id_tipo_user: number
@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: 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;
}
@@ -0,0 +1,13 @@
import { IsEmail, IsNotEmpty, MinLength } 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: 'Password123', description: 'Contraseña del administrador' })
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
}
@@ -1,3 +1,19 @@
import { IsEmail, IsNumber, IsOptional, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateAdministradorDto {
id_tipo_user?: number
@ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador', required: false })
@IsEmail({}, { message: 'El correo no es válido' })
@IsOptional()
correo?: string;
@ApiProperty({ example: 'Password123', description: 'Contraseña del administrador', required: false })
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
@IsOptional()
password?: string;
@ApiProperty({ example: 1, description: 'ID del tipo de usuario', required: false })
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
@IsOptional()
id_tipo_user?: number;
}
+6 -1
View File
@@ -3,12 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AlumnosController } from './alumnos.controller';
import { AlumnosService } from './alumnos.service';
import { RegistroAlumno } from './entities/registro-alumno.entity';
import { ConfigService } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
ConfigModule.forRoot({
isGlobal: true, // importante si querés que esté disponible en todos lados
envFilePath: '.env', // opcional si el archivo ya es '.env'
}),
TypeOrmModule.forRootAsync({
name: 'alumnosConnection',
@@ -24,6 +28,7 @@ import { ConfigService } from '@nestjs/config';
retryAttempts: 10,
retryDelay: 3000,
connectTimeout: 30000,
}),
inject: [ConfigService],
}),
+33 -8
View File
@@ -15,7 +15,7 @@ import { RespuestaParticipanteCerradaModule } from './respuesta_participante_cer
import { CuestionarioRespondidoModule } from './cuestionario_respondido/cuestionario_respondido.module';
import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventoModule } from './evento/evento.module';
import { TipoUserModule } from './tipo_user/tipo_user.module';
import { ParticipanteModule } from './participante/participante.module';
@@ -25,6 +25,10 @@ import { AsistenciaModule } from './asistencia/asistencia.module';
import { ParticipanteEventoModule } from './participante_evento/participante_evento.module';
import { ValidacionesModule } from './validaciones/validaciones.module';
import { AlumnosModule } from './alumnos/alumnos.module';
import { AuthModule } from './auth/auth.module';
import { RegistroAlumno } from './alumnos/entities/registro-alumno.entity';
import { ImagenController } from './imagen/imagen.controller';
import { ImagenModule } from './imagen/imagen.module';
@Module({
@@ -32,22 +36,40 @@ import { AlumnosModule } from './alumnos/alumnos.module';
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRoot({
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: config.get<'mysql'>('DB_TYPE'),
host: config.get<string>('DB_HOST'),
port: +(config.get<number>('DB_PORT') || 3306),
username: config.get<string>('DB_USERNAME'),
password: config.get<string>('DB_PASSWORD'),
database: config.get<string>('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
//dropSchema: true, // elimina la base de datos
}),
/*
type: 'mysql',
host: process.env.db_host, //process.env.db_host
username: process.env.db_username,
database: process.env.db_database,
password: process.env.db_password,
port: Number(process.env.db_port),
synchronize: true,
dropSchema: true, // elimina la base de datos
*/
//synchronize: true,
//dropSchema: true, // elimina la base de datos
// logging: true, // Habilita los logs para depuración
autoLoadEntities: true, // Carga automáticamente las entidades
//autoLoadEntities: true, // Carga automáticamente las entidades
//esta es mi base de datos local
/*
/*
type: 'mysql',
host: 'localhost',
port: 3306, //3306
@@ -60,9 +82,11 @@ import { AlumnosModule } from './alumnos/alumnos.module';
//extra
retryAttempts: 10, // Intentos para reconectar
retryDelay: 3000, // Tiempo entre reintentos
//retryAttempts: 10, // Intentos para reconectar
//retryDelay: 3000, // Tiempo entre reintentos
}),
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
AuthModule,
CuestionarioModule,
TipoCuestionarioModule,
CuestionarioSeccionModule,
@@ -83,6 +107,7 @@ import { AlumnosModule } from './alumnos/alumnos.module';
ParticipanteEventoModule,
ValidacionesModule,
AlumnosModule,
ImagenModule,
],
controllers: [AppController],
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Administrador } from '../administrador/administrador.entity';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'tu_clave_secreta',
signOptions: {
expiresIn: '24h', // Tokens expiran en 24 horas
},
}),
TypeOrmModule.forFeature([Administrador]),
],
providers: [JwtStrategy],
exports: [PassportModule, JwtModule],
})
export class AuthModule {}
+5
View File
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
+39
View File
@@ -0,0 +1,39 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Administrador } from '../administrador/administrador.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(Administrador)
private administradorRepository: Repository<Administrador>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'tu_clave_secreta',
});
}
async validate(payload: any) {
const { sub: id_admnistrador } = payload;
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador },
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
if (!administrador) {
throw new UnauthorizedException('Token inválido');
}
return administrador;
}
}
@@ -214,6 +214,8 @@ export class CuestionarioRespondidoService {
html: `
<h1>¡Gracias por registrarte!</h1>
<p>Has completado exitosamente el formulario: ${cuestionario.nombre_form}</p>
<p>Deberás presentar este QR (celular o impreso) en cualquier stand para que valide tu asistencia y
recibas tu constancia.</p>
<p>Este es tu QR para registrar asistencia:</p>
<img src="cid:qrCode" alt="Código QR" />
<p>Tus datos de registro:</p>
+6
View File
@@ -1,10 +1,16 @@
import { IsString } from "class-validator";
export class CreateEventoDto {
@IsString()
tipo_evento: string;
@IsString()
nombre_evento: string;
@IsString()
fecha_inicio: Date;
@IsString()
fecha_fin: Date;
//agregar el id del administrador
+3
View File
@@ -1,3 +1,4 @@
import { Imagen } from "src/imagen/imagen.entity";
import { Participante } from "src/participante/participante.entity";
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@@ -38,4 +39,6 @@ export class Evento {
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento)
participanteEventos: ParticipanteEvento[];
@OneToMany(() => Imagen, (imagen) => imagen.evento)
imagenes: Imagen[]
}
+4 -3
View File
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { EventoService } from './evento.service';
import { EventoController } from './evento.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Evento } from './evento.entity';
import { ImagenModule } from 'src/imagen/imagen.module';
@Module({
imports: [TypeOrmModule.forFeature([Evento])],
imports: [TypeOrmModule.forFeature([Evento]), forwardRef(() => ImagenModule)],
controllers: [EventoController],
providers: [EventoService],
exports: [EventoService]
exports: [EventoService, TypeOrmModule]
})
export class EventoModule {}
+2 -2
View File
@@ -29,7 +29,7 @@ export class EventoService {
getEventos() {
return this.eventoRepository.find({
relations: ['participantes']
relations: ['participanteEventos', 'imagenes'] //participante
})
}
@@ -38,7 +38,7 @@ export class EventoService {
where: {
id_evento
},
relations: ['participantes']
relations: ['participanteEventos', 'imagenes'],
})
if (!eventoFound)
+47
View File
@@ -0,0 +1,47 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
@ApiSchema({ name: 'CreateImagenRequest' })
class CreateCatDto {}
export class CreateImagenDto {
/*
url: string
orden: number
tipo: string
evento: {
id_evento: number
} // si usas relaciones
evento: number
*/
@IsOptional()
@IsString()
url: string;
@Type(() => Number)
@IsInt()
orden: number;
@IsString()
tipo: string;
@Type(() => Number)
@IsInt() // tiene que ser el id del evento
evento: number;
// Campo extra para controlar lógica, no se guarda
@IsOptional()
@IsBoolean()
@ApiProperty({
type: Boolean
})
@Transform(({ value }) => value === 'true' || value === true)
force_orden?: boolean;
}
+17
View File
@@ -0,0 +1,17 @@
import { Type } from "class-transformer"
import { IsInt, IsOptional, IsString } from "class-validator"
export class UpdateImagenDto {
@IsString()
@IsOptional()
url?: string
@Type(() => Number)
@IsOptional()
@IsInt()
orden?: number
@IsOptional()
@IsString()
tipo?: string
}
+108
View File
@@ -0,0 +1,108 @@
import { BadRequestException, Body, Controller, Delete, Get, HttpException, HttpStatus, Param, ParseIntPipe, Patch, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
import { ImagenService, imageStorage } from './imagen.service';
import { UpdateImagenDto } from './dto/update.imagen.dto';
import { FileInterceptor } from '@nestjs/platform-express';
import { extname, join } from 'path';
import { diskStorage } from 'multer';
import { CreateImagenDto } from './dto/create-imagen.dto';
import { existsSync } from 'fs';
import { Response } from 'express';
import { ImagenApiDocumentation } from './imagen.documentation'; // Importar la documentación
@ImagenApiDocumentation.ApiController
@Controller('imagen')
export class ImagenController {
constructor(private imagenService: ImagenService) {}
@Get()
getImagenes() {
return this.imagenService.getImagenes()
}
@Get('evento/:id_evento')
@ImagenApiDocumentation.ApiGetImagenEvento // Aplicando la documentación al método
getImagenEvento(@Param('id_evento', ParseIntPipe) id_evento: number) {
return this.imagenService.getImagenEvento(id_evento)
}
@Get(':id')
@ImagenApiDocumentation.ApiGetOne
getImagen(@Param('id', ParseIntPipe) id_imagen: number) {
return this.imagenService.getImagen(id_imagen)
}
@Get('visual/:id')
@ImagenApiDocumentation.ApiGetVisual // Aplicando la documentación al método
async getVisualImagen(
@Param('id') id_imagen: number,
@Res() res: Response
) {
const url = await this.imagenService.getVisualImagen(id_imagen);
const rutaImagen = join(process.cwd(), url);
if (!existsSync(rutaImagen)) {
throw new HttpException('Imagen no encontrada', HttpStatus.NOT_FOUND);
}
return res.sendFile(rutaImagen);
}
@Post()
@ImagenApiDocumentation.ApiCreate
@UseInterceptors(FileInterceptor('imagen', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1e9);
const extension = extname(file.originalname);
cb(null, `${uniqueName}${extension}`);
},
}),
}))
async subirImagen(@UploadedFile() file: Express.Multer.File, @Body() imagen: CreateImagenDto,) {
if (!file) {
throw new BadRequestException('No se ha subido ningún archivo');
}
if (!imagen.evento) {
throw new BadRequestException('Falta el id_evento en los datos');
}
const url = `/uploads/${file.filename}`;
return this.imagenService.crearImagen({
...imagen,
url,
evento: imagen.evento,
});
}
@Delete(':id')
@ImagenApiDocumentation.ApiRemove
deleteImagen(@Param('id', ParseIntPipe) id_imagen: number) {
return this.imagenService.deleteImagen(id_imagen)
}
@Patch(':id')
@ImagenApiDocumentation.ApiUpdate
@UseInterceptors(FileInterceptor('imagen', { storage: imageStorage })) // Interceptor para manejar la subida de archivo
async updateImagen(
@Param('id') id: number,
@UploadedFile() file: Express.Multer.File, // Recibe el archivo cargado
@Body() imagen: UpdateImagenDto,
) {
if (file) {
// Si se sube un nuevo archivo, actualiza la URL
imagen.url = `/uploads/${file.filename}`;
}
const newImagen = await this.imagenService.updateImagen(id, imagen);
return newImagen;
}
}
+210
View File
@@ -0,0 +1,210 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators, Get, HttpException, HttpStatus, Param, ParseIntPipe, Res } from '@nestjs/common';
import { join } from 'path';
import { existsSync } from 'fs';
export class ImagenApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Imágenes');
// Documentación para obtener una imagen visual por ID
static ApiGetVisual = applyDecorators(
ApiOperation({
summary: 'Obtener imagen visual por ID',
description: 'Retorna una imagen visual específica dada su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen visual',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen visual encontrada correctamente.',
content: {
'image/png': {
schema: { type: 'string', format: 'binary' }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para obtener las imágenes asociadas a un evento
static ApiGetImagenEvento = applyDecorators(
ApiOperation({
summary: 'Obtener imágenes por evento',
description: 'Retorna una lista de imágenes asociadas a un evento específico.'
}),
ApiParam({
name: 'id_evento',
description: 'ID del evento',
required: true,
type: 'number',
example: 10
}),
ApiResponse({
status: 200,
description: 'Imágenes del evento obtenidas correctamente.',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/imagen1.png' },
evento_id: { type: 'number', example: 10 }
}
}
}
}),
ApiResponse({ status: 404, description: 'Evento no encontrado.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para crear una nueva imagen
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear una nueva imagen',
description: 'Crea una nueva imagen asociada a un evento específico.'
}),
ApiBody({
description: 'Datos para crear una nueva imagen',
schema: {
type: 'object',
required: ['orden', 'tipo', 'evento'],
properties: {
url: { type: 'string', example: '/uploads/imagen1.png' },
orden: { type: 'number', example: 1 },
tipo: { type: 'string', example: 'cabezera' },
evento: { type: 'number', example: 5 }
}
}
}),
ApiResponse({
status: 201,
description: 'Imagen creada correctamente.',
schema: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/imagen1.png' },
orden: { type: 'number', example: 1 },
tipo: { type: 'string', example: 'cabezera' },
evento: {
type: 'object',
properties: {
id_evento: { type: 'number', example: 1 }
}
}
}
}
}),
ApiResponse({ status: 400, description: 'Datos inválidos para crear la imagen.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para actualizar una imagen
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar una imagen',
description: 'Permite actualizar los datos de una imagen existente'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar de la imagen',
schema: {
type: 'object',
properties: {
url: { type: 'string', example: '/uploads/1745549896986-789608318_updated.png' },
descripcion: { type: 'string', example: 'Nueva imagen de ejemplo para actualización' }
}
}
}),
ApiResponse({
status: 200,
description: 'Imagen actualizada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos inválidos para la actualización de la imagen.' }),
ApiResponse({ status: 404, description: 'Imagen no encontrada.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener una imagen por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener una imagen por ID',
description: 'Permite obtener los detalles de una imagen a partir de su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen encontrada correctamente',
schema: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/1745549896986-789608318.png' },
orden: { type: 'number', example: '1'},
tipo: { type: 'string', example: 'Cabezera'},
//descripcion: { type: 'string', example: 'Imagen de muestra para la documentación' }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar una imagen
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar una imagen',
description: 'Permite eliminar una imagen a partir de su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen eliminada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
+25
View File
@@ -0,0 +1,25 @@
import { Type } from "class-transformer";
import { IsInt } from "class-validator";
import { Evento } from "src/evento/evento.entity";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Imagen {
@PrimaryGeneratedColumn()
id_imagen: number
@Column()
url?: string
@Column()
@Type(() => Number)
orden: number
@Column()
tipo: string
@ManyToOne(() => Evento, (evento) => evento.imagenes, { onDelete: 'CASCADE' })
@IsInt()
@Type(() => Number)
evento: Evento;
}
+14
View File
@@ -0,0 +1,14 @@
import { forwardRef, Module } from '@nestjs/common';
import { ImagenService } from './imagen.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Imagen } from './imagen.entity';
import { EventoModule } from 'src/evento/evento.module';
import { ImagenController } from './imagen.controller';
@Module({
imports: [TypeOrmModule.forFeature([Imagen]), forwardRef(() => EventoModule)], //forwardRef(() => EventoModule
controllers: [ImagenController],
providers: [ImagenService],
exports: [ImagenService]
})
export class ImagenModule {}
+169
View File
@@ -0,0 +1,169 @@
import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Imagen } from './imagen.entity';
import { Repository } from 'typeorm';
import { Evento } from 'src/evento/evento.entity';
import { CreateImagenDto } from './dto/create-imagen.dto';
import { UpdateImagenDto } from './dto/update.imagen.dto';
import { extname } from 'path';
import { diskStorage } from 'multer';
@Injectable()
export class ImagenService {
constructor(
@InjectRepository(Imagen) private imagenRepository: Repository<Imagen>,
@InjectRepository(Evento) private eventoRepository: Repository<Evento>
) {}
async getUltimoOrden(id_evento: number): Promise<number> {
const [ultimo] = await this.imagenRepository.find({
where: {
evento: { id_evento }, // Filtrar por evento
},
order: {
orden: 'DESC',
},
take: 1,
});
return ultimo?.orden ?? 0;
}
async crearImagen(imagen: CreateImagenDto): Promise<Imagen> {
const evento = await this.eventoRepository.findOneBy({ id_evento: imagen.evento });
if (!evento) {
throw new NotFoundException('Evento no encontrado');
}
const ordenExiste = await this.imagenRepository.findOneBy({
orden: imagen.orden,
evento: { id_evento: imagen.evento },
});
if (ordenExiste) {
if (imagen.force_orden === false || imagen.force_orden === undefined) {
throw new HttpException('Este orden ya existe', HttpStatus.CONFLICT)
}
}
if (imagen.force_orden === true) {
const imagenExistente = await this.imagenRepository.findOne({
where: {
orden: imagen.orden,
evento: { id_evento: imagen.evento },
},
relations: ['evento'],
});
if (imagenExistente) {
const ultimoOrden = await this.getUltimoOrden(imagen.evento);
imagenExistente.orden = ultimoOrden + 1;
await this.imagenRepository.save(imagenExistente); // Guardar imagen existente con nuevo orden
}
}
// Crear y guardar nueva imagen
const nuevaImagen = this.imagenRepository.create({
...imagen,
evento: { id_evento: imagen.evento }, // para la relación ManyToOne
});
return this.imagenRepository.save(nuevaImagen);
}
getImagenes(): Promise<Imagen[]> {
return this.imagenRepository.find()
}
async getImagen(id_imagen: number) {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
}
})
if (!imagenFound) {
return new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return imagenFound
}
async getImagenEvento(id_evento: number) {
const imagenEventoFound = await this.eventoRepository.findOne({
where: {
id_evento
}
})
if (!imagenEventoFound) {
return new HttpException('Evento not found', HttpStatus.NOT_FOUND);
}
return this.imagenRepository.find({
where: {
evento: {
id_evento
}
}
})
}
async getVisualImagen(id_imagen: number): Promise<string> {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
},
});
if (!imagenFound || !imagenFound.url) {
throw new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return imagenFound.url;
}
async deleteImagen(id_imagen: number) {
const result = await this.imagenRepository.delete({ id_imagen })
if (result.affected === 0) {
return new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return result
}
async updateImagen(id_imagen: number, imagen: UpdateImagenDto) {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
}
});
if (!imagenFound) {
throw new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
if (imagen.url) {
imagenFound.url = imagen.url;
}
const updatedImagen = Object.assign(imagenFound, imagen);
return this.imagenRepository.save(updatedImagen);
}
}
export const imageStorage = diskStorage({
destination: './uploads', // Carpeta donde se almacenarán las imágenes
filename: (req, file, callback) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); // Genera un nombre único
callback(null, uniqueSuffix + extname(file.originalname)); // Usa la extensión original del archivo
},
});
+19 -1
View File
@@ -5,9 +5,14 @@ import { DocsModule } from './docs/docs.module';
import { DataSource } from 'typeorm';
import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join, resolve } from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Configuración de validación global
app.useGlobalPipes(
@@ -15,6 +20,8 @@ async function bootstrap() {
whitelist: true, // Elimina propiedades no decoradas
forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas
transform: true, // Transforma los datos recibidos al tipo definido en el DTO
})
);
@@ -23,8 +30,19 @@ async function bootstrap() {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
// Verifica que la carpeta uploads esté correctamente ubicada
const uploadsPath = resolve(__dirname, '..', 'uploads');
// Configurar los archivos estáticos para que se sirvan desde /uploads
app.useStaticAssets(uploadsPath, {
prefix: '/uploads', // Esto asegura que los archivos sean accesibles en http://localhost:4200/uploads
});
// Swagger setup
DocsModule.setupSwagger(app);
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Some files were not shown because too many files have changed in this diff Show More