From 2bbf07405b33cae9c3f312715f95525aada95388 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 3 Feb 2026 17:31:56 -0600 Subject: [PATCH] =?UTF-8?q?feat(validators):=20agregar=20validadores=20con?= =?UTF-8?q?=20mensajes=20en=20espa=C3=B1ol=20a=2012=20DTOs=20adicionales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se completaron validadores en los siguientes DTOs: - storage: GetUploadUrlDto, ConfirmUploadDto, ListFilesDto, UpdateFileDto - billing: CreatePaymentMethodDto, CreateCheckoutSessionDto, CreateBillingPortalSessionDto - email: EmailAddressDto, AttachmentDto, SendEmailDto, SendTemplateEmailDto, BulkSendEmailDto - rbac: CreateRoleDto, UpdateRoleDto, AssignRoleDto - sales: CreateActivityDto, UpdateActivityDto, ActivityListQueryDto - sales: CreatePipelineStageDto, UpdatePipelineStageDto, ReorderStagesDto - commissions: CreateAssignmentDto, UpdateAssignmentDto, AssignmentListQueryDto - commissions: CreatePeriodDto, UpdatePeriodDto, ClosePeriodDto, MarkPaidDto, PeriodListQueryDto - portfolio: CreateCategoryDto, UpdateCategoryDto, CategoryListQueryDto - ai: ChatMessageDto, ChatRequestDto - whatsapp: CreateWhatsAppConfigDto, UpdateWhatsAppConfigDto, TestConnectionDto Validadores agregados: - @IsNotEmpty() con mensajes descriptivos en español - @MaxLength() en campos de texto - @Min()/@Max() en campos numéricos y porcentajes - @IsUUID() en campos de identificadores - @IsEmail() en campos de email - @IsUrl() en campos de URLs - @IsEnum() para valores fijos - @Matches() para patrones específicos (colores hex, códigos de moneda, slugs) - @ArrayMaxSize() en arreglos Build y lint exitosos. Co-Authored-By: Claude Opus 4.5 --- src/modules/ai/dto/chat.dto.ts | 37 +++--- .../billing/dto/create-payment-method.dto.ts | 24 ++-- src/modules/billing/dto/stripe-webhook.dto.ts | 72 +++++++---- src/modules/commissions/dto/assignment.dto.ts | 40 +++--- src/modules/commissions/dto/period.dto.ts | 44 ++++--- src/modules/email/dto/send-email.dto.ts | 58 ++++++--- src/modules/portfolio/dto/category.dto.ts | 104 +++++++++------ src/modules/rbac/dto/create-role.dto.ts | 40 +++--- src/modules/sales/dto/activity.dto.ts | 119 ++++++++++-------- src/modules/sales/dto/pipeline.dto.ts | 51 +++++--- src/modules/storage/dto/storage.dto.ts | 49 +++++--- .../whatsapp/dto/whatsapp-config.dto.ts | 49 +++++--- 12 files changed, 427 insertions(+), 260 deletions(-) diff --git a/src/modules/ai/dto/chat.dto.ts b/src/modules/ai/dto/chat.dto.ts index bcb400f..92d6631 100644 --- a/src/modules/ai/dto/chat.dto.ts +++ b/src/modules/ai/dto/chat.dto.ts @@ -4,8 +4,12 @@ import { IsArray, IsOptional, IsNumber, + IsBoolean, + IsNotEmpty, Min, Max, + MaxLength, + ArrayMaxSize, ValidateNested, IsIn, } from 'class-validator'; @@ -13,50 +17,55 @@ import { Type } from 'class-transformer'; export class ChatMessageDto { @ApiProperty({ description: 'Message role', enum: ['system', 'user', 'assistant'] }) - @IsString() - @IsIn(['system', 'user', 'assistant']) + @IsString({ message: 'El rol debe ser texto' }) + @IsIn(['system', 'user', 'assistant'], { message: 'El rol debe ser system, user o assistant' }) role: 'system' | 'user' | 'assistant'; @ApiProperty({ description: 'Message content' }) - @IsString() + @IsString({ message: 'El contenido del mensaje debe ser texto' }) + @IsNotEmpty({ message: 'El contenido del mensaje es requerido' }) + @MaxLength(100000, { message: 'El contenido del mensaje no debe exceder 100,000 caracteres' }) content: string; } export class ChatRequestDto { @ApiProperty({ description: 'Array of chat messages', type: [ChatMessageDto] }) - @IsArray() + @IsArray({ message: 'Los mensajes deben ser un arreglo' }) + @ArrayMaxSize(100, { message: 'No se pueden enviar más de 100 mensajes a la vez' }) @ValidateNested({ each: true }) @Type(() => ChatMessageDto) messages: ChatMessageDto[]; @ApiPropertyOptional({ description: 'Model to use (e.g., anthropic/claude-3-haiku)' }) @IsOptional() - @IsString() + @IsString({ message: 'El modelo debe ser texto' }) + @MaxLength(100, { message: 'El nombre del modelo no debe exceder 100 caracteres' }) model?: string; @ApiPropertyOptional({ description: 'Temperature (0-2)', default: 0.7 }) @IsOptional() - @IsNumber() - @Min(0) - @Max(2) + @IsNumber({}, { message: 'La temperatura debe ser un número' }) + @Min(0, { message: 'La temperatura debe ser mayor o igual a 0' }) + @Max(2, { message: 'La temperatura no debe exceder 2' }) temperature?: number; @ApiPropertyOptional({ description: 'Maximum tokens to generate', default: 2048 }) @IsOptional() - @IsNumber() - @Min(1) - @Max(32000) + @IsNumber({}, { message: 'El máximo de tokens debe ser un número' }) + @Min(1, { message: 'El máximo de tokens debe ser mayor o igual a 1' }) + @Max(32000, { message: 'El máximo de tokens no debe exceder 32,000' }) max_tokens?: number; @ApiPropertyOptional({ description: 'Top P sampling (0-1)', default: 1.0 }) @IsOptional() - @IsNumber() - @Min(0) - @Max(1) + @IsNumber({}, { message: 'Top P debe ser un número' }) + @Min(0, { message: 'Top P debe ser mayor o igual a 0' }) + @Max(1, { message: 'Top P no debe exceder 1' }) top_p?: number; @ApiPropertyOptional({ description: 'Stream response', default: false }) @IsOptional() + @IsBoolean({ message: 'El indicador de streaming debe ser verdadero o falso' }) stream?: boolean; } diff --git a/src/modules/billing/dto/create-payment-method.dto.ts b/src/modules/billing/dto/create-payment-method.dto.ts index 5354003..2e0ad35 100644 --- a/src/modules/billing/dto/create-payment-method.dto.ts +++ b/src/modules/billing/dto/create-payment-method.dto.ts @@ -1,43 +1,53 @@ -import { IsEnum, IsOptional, IsString, IsBoolean } from 'class-validator'; +import { IsEnum, IsOptional, IsString, IsBoolean, IsNumber, Min, Max, MaxLength, Length } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { PaymentMethodType } from '../entities/payment-method.entity'; export class CreatePaymentMethodDto { @ApiProperty({ description: 'Payment method type', enum: PaymentMethodType }) - @IsEnum(PaymentMethodType) + @IsEnum(PaymentMethodType, { message: 'El tipo de método de pago debe ser válido' }) type: PaymentMethodType; @ApiPropertyOptional({ description: 'Set as default payment method' }) @IsOptional() - @IsBoolean() + @IsBoolean({ message: 'El campo por defecto debe ser verdadero o falso' }) is_default?: boolean; @ApiPropertyOptional({ description: 'External payment method ID from provider' }) @IsOptional() - @IsString() + @IsString({ message: 'El ID externo del método de pago debe ser texto' }) + @MaxLength(255, { message: 'El ID externo no debe exceder 255 caracteres' }) external_payment_method_id?: string; @ApiPropertyOptional({ description: 'Payment provider (stripe, conekta, etc)' }) @IsOptional() - @IsString() + @IsString({ message: 'El proveedor de pago debe ser texto' }) + @MaxLength(50, { message: 'El proveedor de pago no debe exceder 50 caracteres' }) payment_provider?: string; // Card details (when adding a card) @ApiPropertyOptional({ description: 'Last 4 digits of card' }) @IsOptional() - @IsString() + @IsString({ message: 'Los últimos 4 dígitos deben ser texto' }) + @Length(4, 4, { message: 'Los últimos 4 dígitos de la tarjeta deben ser exactamente 4 caracteres' }) card_last_four?: string; @ApiPropertyOptional({ description: 'Card brand (visa, mastercard, etc)' }) @IsOptional() - @IsString() + @IsString({ message: 'La marca de la tarjeta debe ser texto' }) + @MaxLength(30, { message: 'La marca de la tarjeta no debe exceder 30 caracteres' }) card_brand?: string; @ApiPropertyOptional({ description: 'Card expiry month' }) @IsOptional() + @IsNumber({}, { message: 'El mes de expiración debe ser un número' }) + @Min(1, { message: 'El mes de expiración debe ser entre 1 y 12' }) + @Max(12, { message: 'El mes de expiración debe ser entre 1 y 12' }) card_exp_month?: number; @ApiPropertyOptional({ description: 'Card expiry year' }) @IsOptional() + @IsNumber({}, { message: 'El año de expiración debe ser un número' }) + @Min(2024, { message: 'El año de expiración debe ser válido' }) + @Max(2100, { message: 'El año de expiración debe ser válido' }) card_exp_year?: number; } diff --git a/src/modules/billing/dto/stripe-webhook.dto.ts b/src/modules/billing/dto/stripe-webhook.dto.ts index dacf4a4..b0b2f5b 100644 --- a/src/modules/billing/dto/stripe-webhook.dto.ts +++ b/src/modules/billing/dto/stripe-webhook.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsObject, IsNumber, IsEnum, IsBoolean } from 'class-validator'; +import { IsString, IsOptional, IsObject, IsNumber, IsEnum, IsBoolean, IsNotEmpty, IsUrl, IsEmail, Min, Max, MaxLength } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export enum StripeWebhookEventType { @@ -35,104 +35,132 @@ export enum StripeWebhookEventType { export class StripeWebhookDto { @ApiProperty({ description: 'Webhook event ID from Stripe' }) - @IsString() + @IsString({ message: 'El ID del evento debe ser texto' }) + @IsNotEmpty({ message: 'El ID del evento de Stripe es requerido' }) id: string; @ApiProperty({ description: 'Event type', enum: StripeWebhookEventType }) - @IsString() + @IsString({ message: 'El tipo de evento debe ser texto' }) + @IsNotEmpty({ message: 'El tipo de evento es requerido' }) type: string; @ApiProperty({ description: 'Event data object' }) - @IsObject() + @IsObject({ message: 'Los datos del evento deben ser un objeto' }) data: { object: Record; previous_attributes?: Record; }; @ApiProperty({ description: 'API version used' }) - @IsString() + @IsString({ message: 'La versión de API debe ser texto' }) api_version: string; @ApiProperty({ description: 'Unix timestamp of event creation' }) - @IsNumber() + @IsNumber({}, { message: 'El timestamp de creación debe ser un número' }) + @Min(0, { message: 'El timestamp debe ser mayor o igual a 0' }) created: number; @ApiPropertyOptional({ description: 'Whether this is a live mode event' }) @IsOptional() - @IsBoolean() + @IsBoolean({ message: 'El modo live debe ser verdadero o falso' }) livemode?: boolean; } export class CreateStripeCustomerDto { @ApiProperty({ description: 'Tenant ID to link customer' }) - @IsString() + @IsString({ message: 'El ID del tenant debe ser texto' }) + @IsNotEmpty({ message: 'El ID del tenant es requerido' }) + @MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' }) tenant_id: string; @ApiProperty({ description: 'Customer email' }) - @IsString() + @IsEmail({}, { message: 'El email del cliente debe tener un formato válido' }) + @IsNotEmpty({ message: 'El email del cliente es requerido' }) + @MaxLength(255, { message: 'El email no debe exceder 255 caracteres' }) email: string; @ApiPropertyOptional({ description: 'Customer name' }) @IsOptional() - @IsString() + @IsString({ message: 'El nombre del cliente debe ser texto' }) + @MaxLength(255, { message: 'El nombre no debe exceder 255 caracteres' }) name?: string; @ApiPropertyOptional({ description: 'Additional metadata' }) @IsOptional() - @IsObject() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } export class CreateStripeSubscriptionDto { @ApiProperty({ description: 'Stripe customer ID' }) - @IsString() + @IsString({ message: 'El ID del cliente de Stripe debe ser texto' }) + @IsNotEmpty({ message: 'El ID del cliente de Stripe es requerido' }) + @MaxLength(100, { message: 'El ID del cliente no debe exceder 100 caracteres' }) customer_id: string; @ApiProperty({ description: 'Stripe price ID' }) - @IsString() + @IsString({ message: 'El ID del precio de Stripe debe ser texto' }) + @IsNotEmpty({ message: 'El ID del precio de Stripe es requerido' }) + @MaxLength(100, { message: 'El ID del precio no debe exceder 100 caracteres' }) price_id: string; @ApiPropertyOptional({ description: 'Trial period in days' }) @IsOptional() - @IsNumber() + @IsNumber({}, { message: 'Los días de prueba deben ser un número' }) + @Min(0, { message: 'Los días de prueba deben ser mayor o igual a 0' }) + @Max(365, { message: 'Los días de prueba no deben exceder 365' }) trial_period_days?: number; @ApiPropertyOptional({ description: 'Additional metadata' }) @IsOptional() - @IsObject() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } export class CreateCheckoutSessionDto { @ApiProperty({ description: 'Tenant ID' }) - @IsString() + @IsString({ message: 'El ID del tenant debe ser texto' }) + @IsNotEmpty({ message: 'El ID del tenant es requerido' }) + @MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' }) tenant_id: string; @ApiProperty({ description: 'Stripe price ID' }) - @IsString() + @IsString({ message: 'El ID del precio de Stripe debe ser texto' }) + @IsNotEmpty({ message: 'El ID del precio de Stripe es requerido' }) + @MaxLength(100, { message: 'El ID del precio no debe exceder 100 caracteres' }) price_id: string; @ApiProperty({ description: 'Success redirect URL' }) - @IsString() + @IsUrl({}, { message: 'La URL de éxito debe ser una URL válida' }) + @IsNotEmpty({ message: 'La URL de éxito es requerida' }) + @MaxLength(2000, { message: 'La URL de éxito no debe exceder 2000 caracteres' }) success_url: string; @ApiProperty({ description: 'Cancel redirect URL' }) - @IsString() + @IsUrl({}, { message: 'La URL de cancelación debe ser una URL válida' }) + @IsNotEmpty({ message: 'La URL de cancelación es requerida' }) + @MaxLength(2000, { message: 'La URL de cancelación no debe exceder 2000 caracteres' }) cancel_url: string; @ApiPropertyOptional({ description: 'Trial period in days' }) @IsOptional() - @IsNumber() + @IsNumber({}, { message: 'Los días de prueba deben ser un número' }) + @Min(0, { message: 'Los días de prueba deben ser mayor o igual a 0' }) + @Max(365, { message: 'Los días de prueba no deben exceder 365' }) trial_period_days?: number; } export class CreateBillingPortalSessionDto { @ApiProperty({ description: 'Tenant ID' }) - @IsString() + @IsString({ message: 'El ID del tenant debe ser texto' }) + @IsNotEmpty({ message: 'El ID del tenant es requerido' }) + @MaxLength(100, { message: 'El ID del tenant no debe exceder 100 caracteres' }) tenant_id: string; @ApiProperty({ description: 'Return URL after portal session' }) - @IsString() + @IsUrl({}, { message: 'La URL de retorno debe ser una URL válida' }) + @IsNotEmpty({ message: 'La URL de retorno es requerida' }) + @MaxLength(2000, { message: 'La URL de retorno no debe exceder 2000 caracteres' }) return_url: string; } diff --git a/src/modules/commissions/dto/assignment.dto.ts b/src/modules/commissions/dto/assignment.dto.ts index d72097d..8ab5681 100644 --- a/src/modules/commissions/dto/assignment.dto.ts +++ b/src/modules/commissions/dto/assignment.dto.ts @@ -4,52 +4,53 @@ import { IsNumber, IsBoolean, IsDateString, + IsInt, Min, Max, } from 'class-validator'; export class CreateAssignmentDto { - @IsUUID() + @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' }) userId: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de esquema debe ser un UUID válido' }) schemeId: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' }) @IsOptional() startsAt?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' }) @IsOptional() endsAt?: string; - @IsNumber() - @Min(0) - @Max(100) + @IsNumber({}, { message: 'La tasa personalizada debe ser un número' }) + @Min(0, { message: 'La tasa personalizada debe ser mayor o igual a 0%' }) + @Max(100, { message: 'La tasa personalizada no debe exceder 100%' }) @IsOptional() customRate?: number; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; } export class UpdateAssignmentDto { - @IsDateString() + @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' }) @IsOptional() startsAt?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' }) @IsOptional() endsAt?: string; - @IsNumber() - @Min(0) - @Max(100) + @IsNumber({}, { message: 'La tasa personalizada debe ser un número' }) + @Min(0, { message: 'La tasa personalizada debe ser mayor o igual a 0%' }) + @Max(100, { message: 'La tasa personalizada no debe exceder 100%' }) @IsOptional() customRate?: number; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; } @@ -70,21 +71,26 @@ export class AssignmentResponseDto { } export class AssignmentListQueryDto { - @IsUUID() + @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' }) @IsOptional() userId?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de esquema debe ser un UUID válido' }) @IsOptional() schemeId?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; + @IsInt({ message: 'La página debe ser un número entero' }) + @Min(1, { message: 'La página debe ser mayor o igual a 1' }) @IsOptional() page?: number; + @IsInt({ message: 'El límite debe ser un número entero' }) + @Min(1, { message: 'El límite debe ser mayor o igual a 1' }) + @Max(100, { message: 'El límite no debe exceder 100 elementos' }) @IsOptional() limit?: number; } diff --git a/src/modules/commissions/dto/period.dto.ts b/src/modules/commissions/dto/period.dto.ts index 2b37684..79762c1 100644 --- a/src/modules/commissions/dto/period.dto.ts +++ b/src/modules/commissions/dto/period.dto.ts @@ -3,55 +3,64 @@ import { IsOptional, IsEnum, IsDateString, + IsInt, + Min, + Max, MaxLength, + IsNotEmpty, + Matches, } from 'class-validator'; import { PeriodStatus } from '../entities'; export class CreatePeriodDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @IsNotEmpty({ message: 'El nombre del período es requerido' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) name: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' }) startsAt: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' }) endsAt: string; - @IsString() - @MaxLength(3) + @IsString({ message: 'La moneda debe ser texto' }) + @MaxLength(3, { message: 'El código de moneda debe ser de 3 caracteres (ej: USD, MXN)' }) + @Matches(/^[A-Z]{3}$/, { message: 'El código de moneda debe ser de 3 letras mayúsculas (ej: USD, MXN)' }) @IsOptional() currency?: string; } export class UpdatePeriodDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) @IsOptional() name?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de inicio debe ser una fecha válida ISO 8601' }) @IsOptional() startsAt?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de fin debe ser una fecha válida ISO 8601' }) @IsOptional() endsAt?: string; } export class ClosePeriodDto { - @IsString() + @IsString({ message: 'Las notas deben ser texto' }) + @MaxLength(1000, { message: 'Las notas no deben exceder 1000 caracteres' }) @IsOptional() notes?: string; } export class MarkPaidDto { - @IsString() - @MaxLength(255) + @IsString({ message: 'La referencia de pago debe ser texto' }) + @MaxLength(255, { message: 'La referencia de pago no debe exceder 255 caracteres' }) @IsOptional() paymentReference?: string; - @IsString() + @IsString({ message: 'Las notas de pago deben ser texto' }) + @MaxLength(1000, { message: 'Las notas de pago no deben exceder 1000 caracteres' }) @IsOptional() paymentNotes?: string; } @@ -77,13 +86,18 @@ export class PeriodResponseDto { } export class PeriodListQueryDto { - @IsEnum(PeriodStatus) + @IsEnum(PeriodStatus, { message: 'El estado del período debe ser válido' }) @IsOptional() status?: PeriodStatus; + @IsInt({ message: 'La página debe ser un número entero' }) + @Min(1, { message: 'La página debe ser mayor o igual a 1' }) @IsOptional() page?: number; + @IsInt({ message: 'El límite debe ser un número entero' }) + @Min(1, { message: 'El límite debe ser mayor o igual a 1' }) + @Max(100, { message: 'El límite no debe exceder 100 elementos' }) @IsOptional() limit?: number; } diff --git a/src/modules/email/dto/send-email.dto.ts b/src/modules/email/dto/send-email.dto.ts index 3827879..63eef54 100644 --- a/src/modules/email/dto/send-email.dto.ts +++ b/src/modules/email/dto/send-email.dto.ts @@ -1,24 +1,31 @@ -import { IsString, IsEmail, IsOptional, IsArray, ValidateNested, IsObject } from 'class-validator'; +import { IsString, IsEmail, IsOptional, IsArray, ValidateNested, IsObject, IsNotEmpty, MaxLength, ArrayMaxSize } from 'class-validator'; import { Type } from 'class-transformer'; export class EmailAddressDto { - @IsEmail() + @IsEmail({}, { message: 'El email debe tener un formato válido' }) + @IsNotEmpty({ message: 'El email es requerido' }) + @MaxLength(255, { message: 'El email no debe exceder 255 caracteres' }) email: string; @IsOptional() - @IsString() + @IsString({ message: 'El nombre debe ser texto' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) name?: string; } export class AttachmentDto { - @IsString() + @IsString({ message: 'El nombre del archivo debe ser texto' }) + @IsNotEmpty({ message: 'El nombre del archivo es requerido' }) + @MaxLength(255, { message: 'El nombre del archivo no debe exceder 255 caracteres' }) filename: string; - @IsString() + @IsString({ message: 'El contenido debe ser texto (Base64)' }) + @IsNotEmpty({ message: 'El contenido del archivo es requerido' }) content: string; // Base64 encoded @IsOptional() - @IsString() + @IsString({ message: 'El tipo de contenido debe ser texto' }) + @MaxLength(100, { message: 'El tipo de contenido no debe exceder 100 caracteres' }) contentType?: string; } @@ -28,36 +35,41 @@ export class SendEmailDto { to: EmailAddressDto; @IsOptional() - @IsArray() + @IsArray({ message: 'CC debe ser un arreglo de direcciones' }) + @ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en CC' }) @ValidateNested({ each: true }) @Type(() => EmailAddressDto) cc?: EmailAddressDto[]; @IsOptional() - @IsArray() + @IsArray({ message: 'BCC debe ser un arreglo de direcciones' }) + @ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en BCC' }) @ValidateNested({ each: true }) @Type(() => EmailAddressDto) bcc?: EmailAddressDto[]; - @IsString() + @IsString({ message: 'El asunto debe ser texto' }) + @IsNotEmpty({ message: 'El asunto es requerido' }) + @MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' }) subject: string; @IsOptional() - @IsString() + @IsString({ message: 'El texto debe ser una cadena' }) text?: string; @IsOptional() - @IsString() + @IsString({ message: 'El HTML debe ser una cadena' }) html?: string; @IsOptional() - @IsArray() + @IsArray({ message: 'Los adjuntos deben ser un arreglo' }) + @ArrayMaxSize(10, { message: 'No se pueden incluir más de 10 archivos adjuntos' }) @ValidateNested({ each: true }) @Type(() => AttachmentDto) attachments?: AttachmentDto[]; @IsOptional() - @IsObject() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } @@ -67,37 +79,43 @@ export class SendTemplateEmailDto { to: EmailAddressDto; @IsOptional() - @IsArray() + @IsArray({ message: 'CC debe ser un arreglo de direcciones' }) + @ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en CC' }) @ValidateNested({ each: true }) @Type(() => EmailAddressDto) cc?: EmailAddressDto[]; @IsOptional() - @IsArray() + @IsArray({ message: 'BCC debe ser un arreglo de direcciones' }) + @ArrayMaxSize(50, { message: 'No se pueden incluir más de 50 destinatarios en BCC' }) @ValidateNested({ each: true }) @Type(() => EmailAddressDto) bcc?: EmailAddressDto[]; - @IsString() + @IsString({ message: 'La clave de plantilla debe ser texto' }) + @IsNotEmpty({ message: 'La clave de plantilla es requerida' }) + @MaxLength(100, { message: 'La clave de plantilla no debe exceder 100 caracteres' }) templateKey: string; @IsOptional() - @IsObject() + @IsObject({ message: 'Las variables deben ser un objeto' }) variables?: Record; @IsOptional() - @IsArray() + @IsArray({ message: 'Los adjuntos deben ser un arreglo' }) + @ArrayMaxSize(10, { message: 'No se pueden incluir más de 10 archivos adjuntos' }) @ValidateNested({ each: true }) @Type(() => AttachmentDto) attachments?: AttachmentDto[]; @IsOptional() - @IsObject() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } export class BulkSendEmailDto { - @IsArray() + @IsArray({ message: 'Los emails deben ser un arreglo' }) + @ArrayMaxSize(100, { message: 'No se pueden enviar más de 100 emails a la vez' }) @ValidateNested({ each: true }) @Type(() => SendEmailDto) emails: SendEmailDto[]; diff --git a/src/modules/portfolio/dto/category.dto.ts b/src/modules/portfolio/dto/category.dto.ts index adddce6..a062843 100644 --- a/src/modules/portfolio/dto/category.dto.ts +++ b/src/modules/portfolio/dto/category.dto.ts @@ -7,116 +7,132 @@ import { IsObject, MaxLength, Min, + Max, + IsNotEmpty, + Matches, + IsUrl, } from 'class-validator'; export class CreateCategoryDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @IsNotEmpty({ message: 'El nombre de la categoría es requerido' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) name: string; - @IsString() - @MaxLength(120) + @IsString({ message: 'El slug debe ser texto' }) + @IsNotEmpty({ message: 'El slug de la categoría es requerido' }) + @MaxLength(120, { message: 'El slug no debe exceder 120 caracteres' }) + @Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: 'El slug solo puede contener letras minúsculas, números y guiones' }) slug: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' }) @IsOptional() parentId?: string; - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) + @MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' }) @IsOptional() description?: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La posición debe ser un número entero' }) + @Min(0, { message: 'La posición debe ser mayor o igual a 0' }) + @Max(1000, { message: 'La posición no debe exceder 1000' }) @IsOptional() position?: number; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de imagen debe ser texto' }) + @MaxLength(500, { message: 'La URL de imagen no debe exceder 500 caracteres' }) @IsOptional() imageUrl?: string; - @IsString() - @MaxLength(7) + @IsString({ message: 'El color debe ser texto' }) + @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' }) + @Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' }) @IsOptional() color?: string; - @IsString() - @MaxLength(50) + @IsString({ message: 'El icono debe ser texto' }) + @MaxLength(50, { message: 'El icono no debe exceder 50 caracteres' }) @IsOptional() icon?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; - @IsString() - @MaxLength(200) + @IsString({ message: 'El meta título debe ser texto' }) + @MaxLength(200, { message: 'El meta título no debe exceder 200 caracteres' }) @IsOptional() metaTitle?: string; - @IsString() + @IsString({ message: 'La meta descripción debe ser texto' }) + @MaxLength(500, { message: 'La meta descripción no debe exceder 500 caracteres' }) @IsOptional() metaDescription?: string; - @IsObject() + @IsObject({ message: 'Los campos personalizados deben ser un objeto' }) @IsOptional() customFields?: Record; } export class UpdateCategoryDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) @IsOptional() name?: string; - @IsString() - @MaxLength(120) + @IsString({ message: 'El slug debe ser texto' }) + @MaxLength(120, { message: 'El slug no debe exceder 120 caracteres' }) + @Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: 'El slug solo puede contener letras minúsculas, números y guiones' }) @IsOptional() slug?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' }) @IsOptional() parentId?: string | null; - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) + @MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' }) @IsOptional() description?: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La posición debe ser un número entero' }) + @Min(0, { message: 'La posición debe ser mayor o igual a 0' }) + @Max(1000, { message: 'La posición no debe exceder 1000' }) @IsOptional() position?: number; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de imagen debe ser texto' }) + @MaxLength(500, { message: 'La URL de imagen no debe exceder 500 caracteres' }) @IsOptional() imageUrl?: string; - @IsString() - @MaxLength(7) + @IsString({ message: 'El color debe ser texto' }) + @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' }) + @Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' }) @IsOptional() color?: string; - @IsString() - @MaxLength(50) + @IsString({ message: 'El icono debe ser texto' }) + @MaxLength(50, { message: 'El icono no debe exceder 50 caracteres' }) @IsOptional() icon?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; - @IsString() - @MaxLength(200) + @IsString({ message: 'El meta título debe ser texto' }) + @MaxLength(200, { message: 'El meta título no debe exceder 200 caracteres' }) @IsOptional() metaTitle?: string; - @IsString() + @IsString({ message: 'La meta descripción debe ser texto' }) + @MaxLength(500, { message: 'La meta descripción no debe exceder 500 caracteres' }) @IsOptional() metaDescription?: string; - @IsObject() + @IsObject({ message: 'Los campos personalizados deben ser un objeto' }) @IsOptional() customFields?: Record; } @@ -149,21 +165,27 @@ export class CategoryTreeNodeDto extends CategoryResponseDto { } export class CategoryListQueryDto { - @IsUUID() + @IsUUID('4', { message: 'El ID de categoría padre debe ser un UUID válido' }) @IsOptional() parentId?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; - @IsString() + @IsString({ message: 'El término de búsqueda debe ser texto' }) + @MaxLength(100, { message: 'El término de búsqueda no debe exceder 100 caracteres' }) @IsOptional() search?: string; + @IsInt({ message: 'La página debe ser un número entero' }) + @Min(1, { message: 'La página debe ser mayor o igual a 1' }) @IsOptional() page?: number; + @IsInt({ message: 'El límite debe ser un número entero' }) + @Min(1, { message: 'El límite debe ser mayor o igual a 1' }) + @Max(100, { message: 'El límite no debe exceder 100 elementos' }) @IsOptional() limit?: number; } diff --git a/src/modules/rbac/dto/create-role.dto.ts b/src/modules/rbac/dto/create-role.dto.ts index 75673a1..e009833 100644 --- a/src/modules/rbac/dto/create-role.dto.ts +++ b/src/modules/rbac/dto/create-role.dto.ts @@ -1,55 +1,63 @@ -import { IsString, IsNotEmpty, IsOptional, IsArray, IsUUID } from 'class-validator'; +import { IsString, IsNotEmpty, IsOptional, IsArray, IsUUID, MaxLength, Matches, ArrayMaxSize } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateRoleDto { @ApiProperty({ example: 'Manager' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El nombre debe ser texto' }) + @IsNotEmpty({ message: 'El nombre del rol es requerido' }) + @MaxLength(100, { message: 'El nombre del rol no debe exceder 100 caracteres' }) name: string; @ApiProperty({ example: 'manager' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El código debe ser texto' }) + @IsNotEmpty({ message: 'El código del rol es requerido' }) + @MaxLength(50, { message: 'El código del rol no debe exceder 50 caracteres' }) + @Matches(/^[a-z][a-z0-9_-]*$/, { message: 'El código debe comenzar con letra minúscula y solo contener letras, números, guiones y guiones bajos' }) code: string; @ApiPropertyOptional({ example: 'Can manage team members' }) @IsOptional() - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) + @MaxLength(500, { message: 'La descripción no debe exceder 500 caracteres' }) description?: string; @ApiPropertyOptional({ type: [String], example: ['users:read', 'users:write'] }) @IsOptional() - @IsArray() - @IsString({ each: true }) + @IsArray({ message: 'Los permisos deben ser un arreglo' }) + @ArrayMaxSize(100, { message: 'No se pueden asignar más de 100 permisos a un rol' }) + @IsString({ each: true, message: 'Cada permiso debe ser texto' }) permissions?: string[]; } export class UpdateRoleDto { @ApiPropertyOptional({ example: 'Manager' }) @IsOptional() - @IsString() + @IsString({ message: 'El nombre debe ser texto' }) + @MaxLength(100, { message: 'El nombre del rol no debe exceder 100 caracteres' }) name?: string; @ApiPropertyOptional({ example: 'Can manage team members' }) @IsOptional() - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) + @MaxLength(500, { message: 'La descripción no debe exceder 500 caracteres' }) description?: string; @ApiPropertyOptional({ type: [String] }) @IsOptional() - @IsArray() - @IsString({ each: true }) + @IsArray({ message: 'Los permisos deben ser un arreglo' }) + @ArrayMaxSize(100, { message: 'No se pueden asignar más de 100 permisos a un rol' }) + @IsString({ each: true, message: 'Cada permiso debe ser texto' }) permissions?: string[]; } export class AssignRoleDto { @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' }) - @IsUUID() - @IsNotEmpty() + @IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' }) + @IsNotEmpty({ message: 'El ID de usuario es requerido' }) userId: string; @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440001' }) - @IsUUID() - @IsNotEmpty() + @IsUUID('4', { message: 'El ID de rol debe ser un UUID válido' }) + @IsNotEmpty({ message: 'El ID de rol es requerido' }) roleId: string; } diff --git a/src/modules/sales/dto/activity.dto.ts b/src/modules/sales/dto/activity.dto.ts index 5b4e227..80779ec 100644 --- a/src/modules/sales/dto/activity.dto.ts +++ b/src/modules/sales/dto/activity.dto.ts @@ -8,159 +8,171 @@ import { IsArray, MaxLength, Min, + Max, IsBoolean, IsDateString, + IsNotEmpty, + IsUrl, } from 'class-validator'; import { ActivityType, ActivityStatus } from '../entities'; export class CreateActivityDto { - @IsEnum(ActivityType) + @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' }) type: ActivityType; - @IsString() - @MaxLength(255) + @IsString({ message: 'El asunto debe ser texto' }) + @IsNotEmpty({ message: 'El asunto de la actividad es requerido' }) + @MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' }) subject: string; - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) @IsOptional() + @MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' }) description?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID del lead debe ser un UUID válido' }) @IsOptional() leadId?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de la oportunidad debe ser un UUID válido' }) @IsOptional() opportunityId?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de vencimiento debe ser una fecha válida ISO 8601' }) @IsOptional() dueDate?: string; - @IsString() + @IsString({ message: 'La hora de vencimiento debe ser texto' }) @IsOptional() + @MaxLength(10, { message: 'La hora de vencimiento no debe exceder 10 caracteres' }) dueTime?: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La duración debe ser un número entero' }) + @Min(0, { message: 'La duración debe ser mayor o igual a 0 minutos' }) + @Max(1440, { message: 'La duración no debe exceder 1440 minutos (24 horas)' }) @IsOptional() durationMinutes?: number; - @IsUUID() + @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' }) @IsOptional() assignedTo?: string; - @IsString() - @MaxLength(10) + @IsString({ message: 'La dirección de llamada debe ser texto' }) + @MaxLength(10, { message: 'La dirección de llamada no debe exceder 10 caracteres' }) @IsOptional() callDirection?: string; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de grabación debe ser texto' }) + @MaxLength(500, { message: 'La URL de grabación no debe exceder 500 caracteres' }) @IsOptional() callRecordingUrl?: string; - @IsString() - @MaxLength(255) + @IsString({ message: 'La ubicación debe ser texto' }) + @MaxLength(255, { message: 'La ubicación no debe exceder 255 caracteres' }) @IsOptional() location?: string; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de la reunión debe ser texto' }) + @MaxLength(500, { message: 'La URL de la reunión no debe exceder 500 caracteres' }) @IsOptional() meetingUrl?: string; - @IsArray() + @IsArray({ message: 'Los asistentes deben ser un arreglo' }) @IsOptional() attendees?: any[]; - @IsDateString() + @IsDateString({}, { message: 'La fecha del recordatorio debe ser una fecha válida ISO 8601' }) @IsOptional() reminderAt?: string; - @IsObject() + @IsObject({ message: 'Los campos personalizados deben ser un objeto' }) @IsOptional() customFields?: Record; } export class UpdateActivityDto { - @IsEnum(ActivityType) + @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' }) @IsOptional() type?: ActivityType; - @IsEnum(ActivityStatus) + @IsEnum(ActivityStatus, { message: 'El estado de la actividad debe ser válido' }) @IsOptional() status?: ActivityStatus; - @IsString() - @MaxLength(255) + @IsString({ message: 'El asunto debe ser texto' }) + @MaxLength(255, { message: 'El asunto no debe exceder 255 caracteres' }) @IsOptional() subject?: string; - @IsString() + @IsString({ message: 'La descripción debe ser texto' }) + @MaxLength(2000, { message: 'La descripción no debe exceder 2000 caracteres' }) @IsOptional() description?: string; - @IsDateString() + @IsDateString({}, { message: 'La fecha de vencimiento debe ser una fecha válida ISO 8601' }) @IsOptional() dueDate?: string; - @IsString() + @IsString({ message: 'La hora de vencimiento debe ser texto' }) + @MaxLength(10, { message: 'La hora de vencimiento no debe exceder 10 caracteres' }) @IsOptional() dueTime?: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La duración debe ser un número entero' }) + @Min(0, { message: 'La duración debe ser mayor o igual a 0 minutos' }) + @Max(1440, { message: 'La duración no debe exceder 1440 minutos (24 horas)' }) @IsOptional() durationMinutes?: number; - @IsString() + @IsString({ message: 'El resultado debe ser texto' }) + @MaxLength(500, { message: 'El resultado no debe exceder 500 caracteres' }) @IsOptional() outcome?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' }) @IsOptional() assignedTo?: string; - @IsString() - @MaxLength(10) + @IsString({ message: 'La dirección de llamada debe ser texto' }) + @MaxLength(10, { message: 'La dirección de llamada no debe exceder 10 caracteres' }) @IsOptional() callDirection?: string; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de grabación debe ser texto' }) + @MaxLength(500, { message: 'La URL de grabación no debe exceder 500 caracteres' }) @IsOptional() callRecordingUrl?: string; - @IsString() - @MaxLength(255) + @IsString({ message: 'La ubicación debe ser texto' }) + @MaxLength(255, { message: 'La ubicación no debe exceder 255 caracteres' }) @IsOptional() location?: string; - @IsString() - @MaxLength(500) + @IsString({ message: 'La URL de la reunión debe ser texto' }) + @MaxLength(500, { message: 'La URL de la reunión no debe exceder 500 caracteres' }) @IsOptional() meetingUrl?: string; - @IsArray() + @IsArray({ message: 'Los asistentes deben ser un arreglo' }) @IsOptional() attendees?: any[]; - @IsDateString() + @IsDateString({}, { message: 'La fecha del recordatorio debe ser una fecha válida ISO 8601' }) @IsOptional() reminderAt?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de recordatorio enviado debe ser verdadero o falso' }) @IsOptional() reminderSent?: boolean; - @IsObject() + @IsObject({ message: 'Los campos personalizados deben ser un objeto' }) @IsOptional() customFields?: Record; } export class CompleteActivityDto { - @IsString() + @IsString({ message: 'El resultado debe ser texto' }) + @MaxLength(500, { message: 'El resultado no debe exceder 500 caracteres' }) @IsOptional() outcome?: string; } @@ -196,29 +208,34 @@ export class ActivityResponseDto { } export class ActivityListQueryDto { - @IsEnum(ActivityType) + @IsEnum(ActivityType, { message: 'El tipo de actividad debe ser válido' }) @IsOptional() type?: ActivityType; - @IsEnum(ActivityStatus) + @IsEnum(ActivityStatus, { message: 'El estado de la actividad debe ser válido' }) @IsOptional() status?: ActivityStatus; - @IsUUID() + @IsUUID('4', { message: 'El ID del lead debe ser un UUID válido' }) @IsOptional() leadId?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID de la oportunidad debe ser un UUID válido' }) @IsOptional() opportunityId?: string; - @IsUUID() + @IsUUID('4', { message: 'El ID del usuario asignado debe ser un UUID válido' }) @IsOptional() assignedTo?: string; + @IsInt({ message: 'La página debe ser un número entero' }) + @Min(1, { message: 'La página debe ser mayor o igual a 1' }) @IsOptional() page?: number; + @IsInt({ message: 'El límite debe ser un número entero' }) + @Min(1, { message: 'El límite debe ser mayor o igual a 1' }) + @Max(100, { message: 'El límite no debe exceder 100 elementos' }) @IsOptional() limit?: number; } diff --git a/src/modules/sales/dto/pipeline.dto.ts b/src/modules/sales/dto/pipeline.dto.ts index a5bed30..5557b3e 100644 --- a/src/modules/sales/dto/pipeline.dto.ts +++ b/src/modules/sales/dto/pipeline.dto.ts @@ -5,67 +5,80 @@ import { IsBoolean, MaxLength, Min, + Max, + IsNotEmpty, + IsUUID, + IsArray, + Matches, + ArrayMaxSize, } from 'class-validator'; export class CreatePipelineStageDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @IsNotEmpty({ message: 'El nombre de la etapa es requerido' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) name: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La posición debe ser un número entero' }) + @Min(0, { message: 'La posición debe ser mayor o igual a 0' }) + @Max(100, { message: 'La posición no debe exceder 100' }) @IsOptional() position?: number; - @IsString() - @MaxLength(7) + @IsString({ message: 'El color debe ser texto' }) + @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' }) + @Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' }) @IsOptional() color?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de ganado debe ser verdadero o falso' }) @IsOptional() isWon?: boolean; - @IsBoolean() + @IsBoolean({ message: 'El indicador de perdido debe ser verdadero o falso' }) @IsOptional() isLost?: boolean; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; } export class UpdatePipelineStageDto { - @IsString() - @MaxLength(100) + @IsString({ message: 'El nombre debe ser texto' }) + @MaxLength(100, { message: 'El nombre no debe exceder 100 caracteres' }) @IsOptional() name?: string; - @IsInt() - @Min(0) + @IsInt({ message: 'La posición debe ser un número entero' }) + @Min(0, { message: 'La posición debe ser mayor o igual a 0' }) + @Max(100, { message: 'La posición no debe exceder 100' }) @IsOptional() position?: number; - @IsString() - @MaxLength(7) + @IsString({ message: 'El color debe ser texto' }) + @MaxLength(7, { message: 'El color no debe exceder 7 caracteres' }) + @Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'El color debe ser un código hexadecimal válido (ej: #FF5733)' }) @IsOptional() color?: string; - @IsBoolean() + @IsBoolean({ message: 'El indicador de ganado debe ser verdadero o falso' }) @IsOptional() isWon?: boolean; - @IsBoolean() + @IsBoolean({ message: 'El indicador de perdido debe ser verdadero o falso' }) @IsOptional() isLost?: boolean; - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) @IsOptional() isActive?: boolean; } export class ReorderStagesDto { - @IsString({ each: true }) + @IsArray({ message: 'Los IDs de etapas deben ser un arreglo' }) + @ArrayMaxSize(100, { message: 'No se pueden reordenar más de 100 etapas a la vez' }) + @IsUUID('4', { each: true, message: 'Cada ID de etapa debe ser un UUID válido' }) stageIds: string[]; } diff --git a/src/modules/storage/dto/storage.dto.ts b/src/modules/storage/dto/storage.dto.ts index 19f8bdc..f2dc2a8 100644 --- a/src/modules/storage/dto/storage.dto.ts +++ b/src/modules/storage/dto/storage.dto.ts @@ -1,72 +1,83 @@ -import { IsString, IsNumber, IsOptional, IsEnum, IsUUID, Min, Max } from 'class-validator'; +import { IsString, IsNumber, IsOptional, IsEnum, IsUUID, IsObject, IsNotEmpty, Min, Max, MaxLength } from 'class-validator'; import { FileVisibility } from '../entities/file.entity'; // ==================== Request DTOs ==================== export class GetUploadUrlDto { - @IsString() + @IsString({ message: 'El nombre del archivo debe ser texto' }) + @IsNotEmpty({ message: 'El nombre del archivo es requerido' }) + @MaxLength(255, { message: 'El nombre del archivo no debe exceder 255 caracteres' }) filename: string; - @IsString() + @IsString({ message: 'El tipo MIME debe ser texto' }) + @IsNotEmpty({ message: 'El tipo MIME es requerido' }) + @MaxLength(100, { message: 'El tipo MIME no debe exceder 100 caracteres' }) mimeType: string; - @IsNumber() - @Min(1) - @Max(524288000) // 500 MB max + @IsNumber({}, { message: 'El tamaño debe ser un número' }) + @Min(1, { message: 'El tamaño del archivo debe ser mayor a 0 bytes' }) + @Max(524288000, { message: 'El tamaño del archivo no debe exceder 500 MB' }) sizeBytes: number; @IsOptional() - @IsString() + @IsString({ message: 'La carpeta debe ser texto' }) + @MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' }) folder?: string; @IsOptional() - @IsEnum(FileVisibility) + @IsEnum(FileVisibility, { message: 'La visibilidad debe ser un valor válido' }) visibility?: FileVisibility; } export class ConfirmUploadDto { - @IsUUID() + @IsUUID('4', { message: 'El ID de carga debe ser un UUID válido' }) uploadId: string; @IsOptional() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } export class ListFilesDto { @IsOptional() - @IsNumber() - @Min(1) + @IsNumber({}, { message: 'La página debe ser un número' }) + @Min(1, { message: 'La página debe ser mayor o igual a 1' }) page?: number = 1; @IsOptional() - @IsNumber() - @Min(1) - @Max(100) + @IsNumber({}, { message: 'El límite debe ser un número' }) + @Min(1, { message: 'El límite debe ser mayor o igual a 1' }) + @Max(100, { message: 'El límite no debe exceder 100 elementos' }) limit?: number = 20; @IsOptional() - @IsString() + @IsString({ message: 'La carpeta debe ser texto' }) + @MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' }) folder?: string; @IsOptional() - @IsString() + @IsString({ message: 'El tipo MIME debe ser texto' }) + @MaxLength(100, { message: 'El tipo MIME no debe exceder 100 caracteres' }) mimeType?: string; @IsOptional() - @IsString() + @IsString({ message: 'El término de búsqueda debe ser texto' }) + @MaxLength(100, { message: 'El término de búsqueda no debe exceder 100 caracteres' }) search?: string; } export class UpdateFileDto { @IsOptional() - @IsString() + @IsString({ message: 'La carpeta debe ser texto' }) + @MaxLength(500, { message: 'La ruta de carpeta no debe exceder 500 caracteres' }) folder?: string; @IsOptional() - @IsEnum(FileVisibility) + @IsEnum(FileVisibility, { message: 'La visibilidad debe ser un valor válido' }) visibility?: FileVisibility; @IsOptional() + @IsObject({ message: 'Los metadatos deben ser un objeto' }) metadata?: Record; } diff --git a/src/modules/whatsapp/dto/whatsapp-config.dto.ts b/src/modules/whatsapp/dto/whatsapp-config.dto.ts index aa0cb95..a390567 100644 --- a/src/modules/whatsapp/dto/whatsapp-config.dto.ts +++ b/src/modules/whatsapp/dto/whatsapp-config.dto.ts @@ -1,64 +1,74 @@ -import { IsString, IsNotEmpty, IsOptional, IsBoolean, IsNumber, Min } from 'class-validator'; +import { IsString, IsNotEmpty, IsOptional, IsBoolean, IsNumber, Min, Max, MaxLength, Matches } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateWhatsAppConfigDto { @ApiProperty({ description: 'Meta Phone Number ID' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El ID del número de teléfono debe ser texto' }) + @IsNotEmpty({ message: 'El ID del número de teléfono de Meta es requerido' }) + @MaxLength(50, { message: 'El ID del número de teléfono no debe exceder 50 caracteres' }) phoneNumberId: string; @ApiProperty({ description: 'Meta Business Account ID' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El ID de la cuenta de negocio debe ser texto' }) + @IsNotEmpty({ message: 'El ID de la cuenta de negocio de Meta es requerido' }) + @MaxLength(50, { message: 'El ID de la cuenta de negocio no debe exceder 50 caracteres' }) businessAccountId: string; @ApiProperty({ description: 'Meta Cloud API Access Token' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El token de acceso debe ser texto' }) + @IsNotEmpty({ message: 'El token de acceso de la API de Meta es requerido' }) + @MaxLength(500, { message: 'El token de acceso no debe exceder 500 caracteres' }) accessToken: string; @ApiPropertyOptional({ description: 'Webhook verify token' }) @IsOptional() - @IsString() + @IsString({ message: 'El token de verificación debe ser texto' }) + @MaxLength(100, { message: 'El token de verificación no debe exceder 100 caracteres' }) webhookVerifyToken?: string; @ApiPropertyOptional({ description: 'Daily message limit', default: 1000 }) @IsOptional() - @IsNumber() - @Min(1) + @IsNumber({}, { message: 'El límite diario debe ser un número' }) + @Min(1, { message: 'El límite diario de mensajes debe ser mayor o igual a 1' }) + @Max(100000, { message: 'El límite diario de mensajes no debe exceder 100,000' }) dailyMessageLimit?: number; } export class UpdateWhatsAppConfigDto { @ApiPropertyOptional({ description: 'Meta Phone Number ID' }) @IsOptional() - @IsString() + @IsString({ message: 'El ID del número de teléfono debe ser texto' }) + @MaxLength(50, { message: 'El ID del número de teléfono no debe exceder 50 caracteres' }) phoneNumberId?: string; @ApiPropertyOptional({ description: 'Meta Business Account ID' }) @IsOptional() - @IsString() + @IsString({ message: 'El ID de la cuenta de negocio debe ser texto' }) + @MaxLength(50, { message: 'El ID de la cuenta de negocio no debe exceder 50 caracteres' }) businessAccountId?: string; @ApiPropertyOptional({ description: 'Meta Cloud API Access Token' }) @IsOptional() - @IsString() + @IsString({ message: 'El token de acceso debe ser texto' }) + @MaxLength(500, { message: 'El token de acceso no debe exceder 500 caracteres' }) accessToken?: string; @ApiPropertyOptional({ description: 'Webhook verify token' }) @IsOptional() - @IsString() + @IsString({ message: 'El token de verificación debe ser texto' }) + @MaxLength(100, { message: 'El token de verificación no debe exceder 100 caracteres' }) webhookVerifyToken?: string; @ApiPropertyOptional({ description: 'Daily message limit' }) @IsOptional() - @IsNumber() - @Min(1) + @IsNumber({}, { message: 'El límite diario debe ser un número' }) + @Min(1, { message: 'El límite diario de mensajes debe ser mayor o igual a 1' }) + @Max(100000, { message: 'El límite diario de mensajes no debe exceder 100,000' }) dailyMessageLimit?: number; @ApiPropertyOptional({ description: 'Enable/disable WhatsApp integration' }) @IsOptional() - @IsBoolean() + @IsBoolean({ message: 'El indicador de activo debe ser verdadero o falso' }) isActive?: boolean; } @@ -102,7 +112,8 @@ export class WhatsAppConfigResponseDto { export class TestConnectionDto { @ApiProperty({ description: 'Phone number to send test message to' }) - @IsString() - @IsNotEmpty() + @IsString({ message: 'El número de teléfono debe ser texto' }) + @IsNotEmpty({ message: 'El número de teléfono de prueba es requerido' }) + @Matches(/^\+[1-9]\d{1,14}$/, { message: 'El número de teléfono debe estar en formato E.164 (ej: +521234567890)' }) testPhoneNumber: string; }