Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a156beab6 | |||
| 14d191790f | |||
| f496c2d8be | |||
| 8a837eda8d | |||
| aae1e5ede3 | |||
| 0c6f5336a9 | |||
| 2a807acb87 | |||
| a8645d28e8 | |||
| f7f6d8296c | |||
| f747e146e8 | |||
| 10ba9642bd | |||
| a2e4f62135 | |||
| 283b8df276 | |||
| a48f2cb41c | |||
| 564cbd4f69 | |||
| ed7cadd6e0 | |||
| d436ca6bbc | |||
| 489965681f | |||
| daca1aa0b0 |
@@ -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 . .
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
}),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |