From 307e8abbf898253d97b6affbe54a2bdcd232fc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:17:23 -0600 Subject: [PATCH 01/18] fix: ensure .DS_Store and .claude/ are ignored in the project --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9bfffd4..fc29fd3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ node_modules/ # Yarn Integrity file .yarn-integrity -.DS_Store \ No newline at end of file +.DS_Store + +# Claude +.claude/ \ No newline at end of file From e2e73e79f25baebd98ad794b9adf44648f3d2aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:17:33 -0600 Subject: [PATCH 02/18] feat: add LoginDto for user authentication --- src/modules/auth/dto/login.dto.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/modules/auth/dto/login.dto.ts diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..ec4080c --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,10 @@ +import { IsString, IsEmail, IsNotEmpty } from "class-validator"; + +export class LoginDto { + @IsEmail({}, { message: "Please provide a valid email address" }) + email: string; + + @IsString({ message: "Password must be a string" }) + @IsNotEmpty({ message: "Password is required" }) + password: string; +} From 43cefb0ed01d85b1385e70fbee118baeba650a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:17:43 -0600 Subject: [PATCH 03/18] feat: add RegisterDto for user registration --- src/modules/auth/dto/register.dto.ts | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/modules/auth/dto/register.dto.ts diff --git a/src/modules/auth/dto/register.dto.ts b/src/modules/auth/dto/register.dto.ts new file mode 100644 index 0000000..0e7dbf2 --- /dev/null +++ b/src/modules/auth/dto/register.dto.ts @@ -0,0 +1,32 @@ +import { + IsString, + IsEmail, + MinLength, + MaxLength, + IsOptional, +} from "class-validator"; + +export class RegisterDto { + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(100, { message: "Name cannot exceed 100 characters" }) + name: string; + + @IsEmail({}, { message: "Please provide a valid email address" }) + email: string; + + @IsString({ message: "Password must be a string" }) + @MinLength(8, { message: "Password must be at least 8 characters long" }) + @MaxLength(128, { message: "Password cannot exceed 128 characters" }) + password: string; + + @IsOptional() + @IsString({ message: "Wallet address must be a string" }) + @MinLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @MaxLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + walletAddress?: string; +} From 1dfb9b6641ffb1de1bec364e7d4b7514b96e5808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:18:01 -0600 Subject: [PATCH 04/18] fix: add custom error message for email validation in ResendVerificationDTO --- src/modules/auth/dto/resendVerificationDTO.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/dto/resendVerificationDTO.ts b/src/modules/auth/dto/resendVerificationDTO.ts index cbe5b4a..27b7d98 100644 --- a/src/modules/auth/dto/resendVerificationDTO.ts +++ b/src/modules/auth/dto/resendVerificationDTO.ts @@ -1,6 +1,6 @@ import { IsEmail } from "class-validator"; export class ResendVerificationDTO { - @IsEmail() + @IsEmail({}, { message: "Please provide a valid email address" }) email: string; } From ce2db79b57420b59f6afa4b4faebad749dece01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:18:32 -0600 Subject: [PATCH 05/18] fix: add custom error messages for token validation in VerifyEmailDTO --- src/modules/auth/dto/verifyEmailDTO.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/dto/verifyEmailDTO.ts b/src/modules/auth/dto/verifyEmailDTO.ts index 3d81e80..ff74d77 100644 --- a/src/modules/auth/dto/verifyEmailDTO.ts +++ b/src/modules/auth/dto/verifyEmailDTO.ts @@ -1,7 +1,7 @@ import { IsString, IsNotEmpty } from "class-validator"; export class VerifyEmailDTO { - @IsString() - @IsNotEmpty() + @IsString({ message: "Token must be a string" }) + @IsNotEmpty({ message: "Token is required" }) token: string; } From f871bbb75ac9b337b0ea5c5dd47c19d7bda9ea56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:18:41 -0600 Subject: [PATCH 06/18] feat: add wallet validation DTOs for Stellar wallet address format --- src/modules/auth/dto/wallet-validation.dto.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/modules/auth/dto/wallet-validation.dto.ts diff --git a/src/modules/auth/dto/wallet-validation.dto.ts b/src/modules/auth/dto/wallet-validation.dto.ts new file mode 100644 index 0000000..3caf8cb --- /dev/null +++ b/src/modules/auth/dto/wallet-validation.dto.ts @@ -0,0 +1,35 @@ +import { IsString, MinLength, MaxLength, Matches } from "class-validator"; + +export class ValidateWalletFormatDto { + @IsString({ message: "Wallet address must be a string" }) + @MinLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @MaxLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @Matches(/^G[A-Z2-7]{55}$/, { + message: "Invalid Stellar wallet address format", + }) + walletAddress: string; +} + +export class VerifyWalletDto { + @IsString({ message: "Wallet address must be a string" }) + @MinLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @MaxLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @Matches(/^G[A-Z2-7]{55}$/, { + message: "Invalid Stellar wallet address format", + }) + walletAddress: string; + + @IsString({ message: "Signature must be a string" }) + signature: string; + + @IsString({ message: "Message must be a string" }) + message: string; +} From 8839f82a487f95745f95962c5b4721418d5ce1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:19:07 -0600 Subject: [PATCH 07/18] fix: add custom validation messages for SendMessageDto and MarkAsReadDto --- src/modules/messaging/dto/message.dto.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/messaging/dto/message.dto.ts b/src/modules/messaging/dto/message.dto.ts index f8adec6..43df2e3 100644 --- a/src/modules/messaging/dto/message.dto.ts +++ b/src/modules/messaging/dto/message.dto.ts @@ -1,22 +1,22 @@ import { IsString, IsNotEmpty, IsUUID } from "class-validator"; export class SendMessageDto { - @IsString() - @IsNotEmpty() + @IsString({ message: "Content must be a string" }) + @IsNotEmpty({ message: "Content is required" }) content: string; - @IsUUID() - @IsNotEmpty() + @IsUUID(4, { message: "Receiver ID must be a valid UUID" }) + @IsNotEmpty({ message: "Receiver ID is required" }) receiverId: string; - @IsUUID() - @IsNotEmpty() + @IsUUID(4, { message: "Volunteer ID must be a valid UUID" }) + @IsNotEmpty({ message: "Volunteer ID is required" }) volunteerId: string; } export class MarkAsReadDto { - @IsUUID() - @IsNotEmpty() + @IsUUID(4, { message: "Message ID must be a valid UUID" }) + @IsNotEmpty({ message: "Message ID is required" }) messageId: string; } From b6259038606a4ee9d73d2a51cf2fc86566adeebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:19:16 -0600 Subject: [PATCH 08/18] feat: implement CreateNFTDto with validation rules for userId, organizationId, and description --- src/modules/nft/dto/create-nft.dto.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/nft/dto/create-nft.dto.ts b/src/modules/nft/dto/create-nft.dto.ts index dd7cad3..57c235b 100644 --- a/src/modules/nft/dto/create-nft.dto.ts +++ b/src/modules/nft/dto/create-nft.dto.ts @@ -1,5 +1,14 @@ -export interface CreateNFTDto { +import { IsString, IsUUID, MinLength, MaxLength } from "class-validator"; + +export class CreateNFTDto { + @IsUUID(4, { message: "User ID must be a valid UUID" }) userId: string; + + @IsUUID(4, { message: "Organization ID must be a valid UUID" }) organizationId: string; + + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(1000, { message: "Description cannot exceed 1000 characters" }) description: string; } From 3e03567f5f16ced50769c1c685f3905546a4573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:22:24 -0600 Subject: [PATCH 09/18] feat: enhance UpdateUserDto with validation rules for name, last name, email, password, and Stellar wallet address --- .../dto/update-organization.dto.ts | 34 ++++++------- src/modules/project/dto/UpdateProjectDto.ts | 25 +++++++++- src/modules/user/dto/UpdateUserDto.ts | 49 ++++++++++++++++--- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/modules/organization/presentation/dto/update-organization.dto.ts b/src/modules/organization/presentation/dto/update-organization.dto.ts index cd12f3d..6dd037a 100644 --- a/src/modules/organization/presentation/dto/update-organization.dto.ts +++ b/src/modules/organization/presentation/dto/update-organization.dto.ts @@ -10,46 +10,46 @@ import { export class UpdateOrganizationDto { @IsOptional() - @IsString() - @MinLength(2) - @MaxLength(100) + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(100, { message: "Name cannot exceed 100 characters" }) name?: string; @IsOptional() - @IsEmail() + @IsEmail({}, { message: "Please provide a valid email address" }) email?: string; @IsOptional() - @IsString() - @MinLength(10) - @MaxLength(500) + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(500, { message: "Description cannot exceed 500 characters" }) description?: string; @IsOptional() - @IsString() - @MaxLength(50) + @IsString({ message: "Category must be a string" }) + @MaxLength(50, { message: "Category cannot exceed 50 characters" }) category?: string; @IsOptional() - @IsUrl() + @IsUrl({}, { message: "Please provide a valid website URL" }) website?: string; @IsOptional() - @IsString() - @MaxLength(200) + @IsString({ message: "Address must be a string" }) + @MaxLength(200, { message: "Address cannot exceed 200 characters" }) address?: string; @IsOptional() - @IsString() - @MaxLength(20) + @IsString({ message: "Phone must be a string" }) + @MaxLength(20, { message: "Phone cannot exceed 20 characters" }) phone?: string; @IsOptional() - @IsBoolean() + @IsBoolean({ message: "isVerified must be a boolean" }) isVerified?: boolean; @IsOptional() - @IsString() - @MaxLength(1000) + @IsString({ message: "Logo URL must be a string" }) + @MaxLength(1000, { message: "Logo URL cannot exceed 1000 characters" }) logoUrl?: string; } diff --git a/src/modules/project/dto/UpdateProjectDto.ts b/src/modules/project/dto/UpdateProjectDto.ts index 6d1a5bf..27c225a 100644 --- a/src/modules/project/dto/UpdateProjectDto.ts +++ b/src/modules/project/dto/UpdateProjectDto.ts @@ -1,8 +1,31 @@ +import { + IsString, + IsUUID, + IsOptional, + MinLength, + MaxLength, + IsEnum, +} from "class-validator"; import { ProjectStatus } from "../domain/Project"; -export interface UpdateProjectDto { +export class UpdateProjectDto { + @IsOptional() + @IsString({ message: "Title must be a string" }) + @MinLength(3, { message: "Title must be at least 3 characters long" }) + @MaxLength(200, { message: "Title cannot exceed 200 characters" }) title?: string; + + @IsOptional() + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(2000, { message: "Description cannot exceed 2000 characters" }) description?: string; + + @IsOptional() + @IsUUID(4, { message: "Organization ID must be a valid UUID" }) organizationId?: string; + + @IsOptional() + @IsEnum(ProjectStatus, { message: "Status must be a valid project status" }) status?: ProjectStatus; } diff --git a/src/modules/user/dto/UpdateUserDto.ts b/src/modules/user/dto/UpdateUserDto.ts index 69f338e..8687e70 100644 --- a/src/modules/user/dto/UpdateUserDto.ts +++ b/src/modules/user/dto/UpdateUserDto.ts @@ -1,8 +1,45 @@ +import { + IsString, + IsEmail, + MinLength, + MaxLength, + IsOptional, + Matches, +} from "class-validator"; + export class UpdateUserDto { - id: string; - name: string; - lastName: string; - email: string; - password: string; - wallet: string; + @IsOptional() + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(50, { message: "Name cannot exceed 50 characters" }) + name?: string; + + @IsOptional() + @IsString({ message: "Last name must be a string" }) + @MinLength(2, { message: "Last name must be at least 2 characters long" }) + @MaxLength(50, { message: "Last name cannot exceed 50 characters" }) + lastName?: string; + + @IsOptional() + @IsEmail({}, { message: "Please provide a valid email address" }) + email?: string; + + @IsOptional() + @IsString({ message: "Password must be a string" }) + @MinLength(8, { message: "Password must be at least 8 characters long" }) + @MaxLength(128, { message: "Password cannot exceed 128 characters" }) + password?: string; + + @IsOptional() + @IsString({ message: "Wallet address must be a string" }) + @MinLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @MaxLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @Matches(/^G[A-Z2-7]{55}$/, { + message: "Invalid Stellar wallet address format", + }) + wallet?: string; } From 45d1743541a2a7018f1551332d72c04960063382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:22:35 -0600 Subject: [PATCH 10/18] feat: add validation rules and messages for CreateUserDto fields including wallet address format --- .../dto/create-organization.dto.ts | 32 ++++++++--------- src/modules/project/dto/CreateProjectDto.ts | 22 +++++++++++- src/modules/user/dto/CreateUserDto.ts | 36 ++++++++++++++++++- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/modules/organization/presentation/dto/create-organization.dto.ts b/src/modules/organization/presentation/dto/create-organization.dto.ts index 1515d69..904b7ea 100644 --- a/src/modules/organization/presentation/dto/create-organization.dto.ts +++ b/src/modules/organization/presentation/dto/create-organization.dto.ts @@ -8,39 +8,39 @@ import { } from "class-validator"; export class CreateOrganizationDto { - @IsString() - @MinLength(2) - @MaxLength(100) + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(100, { message: "Name cannot exceed 100 characters" }) name: string; - @IsEmail() + @IsEmail({}, { message: "Please provide a valid email address" }) email: string; - @IsString() - @MinLength(8) + @IsString({ message: "Password must be a string" }) + @MinLength(8, { message: "Password must be at least 8 characters long" }) password: string; - @IsString() - @MinLength(10) - @MaxLength(500) + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(500, { message: "Description cannot exceed 500 characters" }) description: string; @IsOptional() - @IsString() - @MaxLength(50) + @IsString({ message: "Category must be a string" }) + @MaxLength(50, { message: "Category cannot exceed 50 characters" }) category?: string; @IsOptional() - @IsUrl() + @IsUrl({}, { message: "Please provide a valid website URL" }) website?: string; @IsOptional() - @IsString() - @MaxLength(200) + @IsString({ message: "Address must be a string" }) + @MaxLength(200, { message: "Address cannot exceed 200 characters" }) address?: string; @IsOptional() - @IsString() - @MaxLength(20) + @IsString({ message: "Phone must be a string" }) + @MaxLength(20, { message: "Phone cannot exceed 20 characters" }) phone?: string; } diff --git a/src/modules/project/dto/CreateProjectDto.ts b/src/modules/project/dto/CreateProjectDto.ts index bb21b59..b371123 100644 --- a/src/modules/project/dto/CreateProjectDto.ts +++ b/src/modules/project/dto/CreateProjectDto.ts @@ -1,8 +1,28 @@ +import { + IsString, + IsUUID, + IsOptional, + MinLength, + MaxLength, + IsEnum, +} from "class-validator"; import { ProjectStatus } from "../domain/Project"; -export interface CreateProjectDto { +export class CreateProjectDto { + @IsString({ message: "Title must be a string" }) + @MinLength(3, { message: "Title must be at least 3 characters long" }) + @MaxLength(200, { message: "Title cannot exceed 200 characters" }) title: string; + + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(2000, { message: "Description cannot exceed 2000 characters" }) description: string; + + @IsUUID(4, { message: "Organization ID must be a valid UUID" }) organizationId: string; + + @IsOptional() + @IsEnum(ProjectStatus, { message: "Status must be a valid project status" }) status?: ProjectStatus; } diff --git a/src/modules/user/dto/CreateUserDto.ts b/src/modules/user/dto/CreateUserDto.ts index 8304423..6706c42 100644 --- a/src/modules/user/dto/CreateUserDto.ts +++ b/src/modules/user/dto/CreateUserDto.ts @@ -1,7 +1,41 @@ +import { + IsString, + IsEmail, + MinLength, + MaxLength, + IsOptional, + Matches, +} from "class-validator"; + export class CreateUserDto { + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(50, { message: "Name cannot exceed 50 characters" }) name: string; + + @IsString({ message: "Last name must be a string" }) + @MinLength(2, { message: "Last name must be at least 2 characters long" }) + @MaxLength(50, { message: "Last name cannot exceed 50 characters" }) lastName: string; + + @IsEmail({}, { message: "Please provide a valid email address" }) email: string; + + @IsString({ message: "Password must be a string" }) + @MinLength(8, { message: "Password must be at least 8 characters long" }) + @MaxLength(128, { message: "Password cannot exceed 128 characters" }) password: string; - wallet: string; + + @IsOptional() + @IsString({ message: "Wallet address must be a string" }) + @MinLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @MaxLength(56, { + message: "Stellar wallet address must be 56 characters long", + }) + @Matches(/^G[A-Z2-7]{55}$/, { + message: "Invalid Stellar wallet address format", + }) + wallet?: string; } From 9aa3162bccb54ea2056ec43c2ad7a1f6d757bc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:22:43 -0600 Subject: [PATCH 11/18] feat: refactor CreateVolunteerDTO and UpdateVolunteerDTO to use classes with validation rules --- src/modules/volunteer/dto/volunteer.dto.ts | 51 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/modules/volunteer/dto/volunteer.dto.ts b/src/modules/volunteer/dto/volunteer.dto.ts index 3a23f18..59bed04 100644 --- a/src/modules/volunteer/dto/volunteer.dto.ts +++ b/src/modules/volunteer/dto/volunteer.dto.ts @@ -1,15 +1,62 @@ -export interface CreateVolunteerDTO { +import { + IsString, + IsUUID, + IsOptional, + MinLength, + MaxLength, +} from "class-validator"; + +export class CreateVolunteerDTO { + @IsString({ message: "Name must be a string" }) + @MinLength(3, { message: "Name must be at least 3 characters long" }) + @MaxLength(200, { message: "Name cannot exceed 200 characters" }) name: string; + + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(2000, { message: "Description cannot exceed 2000 characters" }) description: string; + + @IsString({ message: "Requirements must be a string" }) + @MinLength(10, { + message: "Requirements must be at least 10 characters long", + }) + @MaxLength(1000, { message: "Requirements cannot exceed 1000 characters" }) requirements: string; + + @IsOptional() + @IsString({ message: "Incentive must be a string" }) + @MaxLength(500, { message: "Incentive cannot exceed 500 characters" }) incentive?: string; + + @IsUUID(4, { message: "Project ID must be a valid UUID" }) projectId: string; } -export interface UpdateVolunteerDTO { +export class UpdateVolunteerDTO { + @IsOptional() + @IsString({ message: "Name must be a string" }) + @MinLength(3, { message: "Name must be at least 3 characters long" }) + @MaxLength(200, { message: "Name cannot exceed 200 characters" }) name?: string; + + @IsOptional() + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(2000, { message: "Description cannot exceed 2000 characters" }) description?: string; + + @IsOptional() + @IsString({ message: "Requirements must be a string" }) + @MinLength(10, { + message: "Requirements must be at least 10 characters long", + }) + @MaxLength(1000, { message: "Requirements cannot exceed 1000 characters" }) requirements?: string; + + @IsOptional() + @IsString({ message: "Incentive must be a string" }) + @MaxLength(500, { message: "Incentive cannot exceed 500 characters" }) incentive?: string; } From f2c75e9e1efee283536e7ea977de6cde92088a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:22:58 -0600 Subject: [PATCH 12/18] feat: replace inline validation with CreateNFTDto for NFT creation route --- src/routes/nftRoutes.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/routes/nftRoutes.ts b/src/routes/nftRoutes.ts index 469ae2a..279cf05 100644 --- a/src/routes/nftRoutes.ts +++ b/src/routes/nftRoutes.ts @@ -1,21 +1,32 @@ import { Router } from "express"; import NFTController from "../modules/nft/presentation/controllers/NFTController.stub"; -import { body } from "express-validator"; +import { + validateDto, + validateParamsDto, +} from "../shared/middleware/validation.middleware"; +import { CreateNFTDto } from "../modules/nft/dto/create-nft.dto"; +import { UuidParamsDto } from "../shared/dto/base.dto"; const router = Router(); -router.post( - "/nfts", - [ - body("userId").isUUID(), - body("organizationId").isUUID(), - body("description").isString().notEmpty(), - ], - NFTController.createNFT +router.post("/nfts", validateDto(CreateNFTDto), NFTController.createNFT); + +router.get( + "/nfts/:id", + validateParamsDto(UuidParamsDto), + NFTController.getNFTById +); + +router.get( + "/users/:userId/nfts", + validateParamsDto(UuidParamsDto), + NFTController.getNFTsByUserId ); -router.get("/nfts/:id", NFTController.getNFTById); -router.get("/users/:userId/nfts", NFTController.getNFTsByUserId); -router.delete("/nfts/:id", NFTController.deleteNFT); +router.delete( + "/nfts/:id", + validateParamsDto(UuidParamsDto), + NFTController.deleteNFT +); export default router; From c67d2de18e2d2d89de226feea0d6e585614f12da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:23:17 -0600 Subject: [PATCH 13/18] feat: add authentication routes with validation middleware --- src/routes/v2/auth.routes.ts | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/routes/v2/auth.routes.ts diff --git a/src/routes/v2/auth.routes.ts b/src/routes/v2/auth.routes.ts new file mode 100644 index 0000000..61ffa43 --- /dev/null +++ b/src/routes/v2/auth.routes.ts @@ -0,0 +1,59 @@ +import { Router } from "express"; +import { validateDto } from "../../shared/middleware/validation.middleware"; +import { RegisterDto } from "../../modules/auth/dto/register.dto"; +import { LoginDto } from "../../modules/auth/dto/login.dto"; +import { ResendVerificationDTO } from "../../modules/auth/dto/resendVerificationDTO"; +import { VerifyEmailDTO } from "../../modules/auth/dto/verifyEmailDTO"; +import { + ValidateWalletFormatDto, + VerifyWalletDto, +} from "../../modules/auth/dto/wallet-validation.dto"; + +const router = Router(); + +// Note: This is an example of how to properly integrate validation middleware +// The controller would need to be properly instantiated with dependencies + +// POST /auth/register - User registration +router.post( + "/register", + validateDto(RegisterDto) + // authController.register +); + +// POST /auth/login - User login +router.post( + "/login", + validateDto(LoginDto) + // authController.login +); + +// POST /auth/resend-verification - Resend email verification +router.post( + "/resend-verification", + validateDto(ResendVerificationDTO) + // authController.resendVerificationEmail +); + +// POST /auth/verify-email - Verify email with token +router.post( + "/verify-email", + validateDto(VerifyEmailDTO) + // authController.verifyEmail +); + +// POST /auth/validate-wallet-format - Validate wallet address format +router.post( + "/validate-wallet-format", + validateDto(ValidateWalletFormatDto) + // authController.validateWalletFormat +); + +// POST /auth/verify-wallet - Verify wallet ownership +router.post( + "/verify-wallet", + validateDto(VerifyWalletDto) + // authController.verifyWallet +); + +export default router; From 22e514b6ce209305f2a91ab9df6b3de161f71cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:24:01 -0600 Subject: [PATCH 14/18] feat: add comprehensive DTO-based validation implementation guide --- docs/dto-validation-guide.md | 256 +++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/dto-validation-guide.md diff --git a/docs/dto-validation-guide.md b/docs/dto-validation-guide.md new file mode 100644 index 0000000..516bb77 --- /dev/null +++ b/docs/dto-validation-guide.md @@ -0,0 +1,256 @@ +# DTO-Based Validation Implementation Guide + +## Overview + +This guide documents the comprehensive DTO-based validation system implemented using `class-validator` and `class-transformer`. This system replaces the previous express-validator approach with a more robust, type-safe validation mechanism that aligns with our Domain-Driven Design (DDD) architecture. + +## Architecture + +### Core Components + +1. **Validation Middleware** (`src/shared/middleware/validation.middleware.ts`) + + - `validateDto()` - Validates request body + - `validateQueryDto()` - Validates query parameters + - `validateParamsDto()` - Validates route parameters + +2. **Base DTOs** (`src/shared/dto/base.dto.ts`) + + - `UuidParamsDto` - For UUID route parameters + - `PaginationQueryDto` - For pagination query parameters + - `BaseResponseDto` - Base response structure + - `ErrorResponseDto` - Error response structure + +3. **Module-Specific DTOs** + - Auth: `RegisterDto`, `LoginDto`, `VerifyEmailDTO`, `ResendVerificationDTO` + - Organization: `CreateOrganizationDto`, `UpdateOrganizationDto` + - User: `CreateUserDto`, `UpdateUserDto` + - Project: `CreateProjectDto`, `UpdateProjectDto` + - NFT: `CreateNFTDto` + - Messaging: `SendMessageDto`, `MarkAsReadDto` + - Volunteer: `CreateVolunteerDTO`, `UpdateVolunteerDTO` + +## Usage Examples + +### 1. Route-Level Validation + +```typescript +import { Router } from "express"; +import { + validateDto, + validateParamsDto, + validateQueryDto, +} from "../shared/middleware/validation.middleware"; +import { CreateOrganizationDto } from "../modules/organization/presentation/dto/create-organization.dto"; +import { UuidParamsDto, PaginationQueryDto } from "../shared/dto/base.dto"; + +const router = Router(); + +// POST with body validation +router.post( + "/organizations", + validateDto(CreateOrganizationDto), + organizationController.create +); + +// GET with parameter validation +router.get( + "/organizations/:id", + validateParamsDto(UuidParamsDto), + organizationController.getById +); + +// GET with query validation +router.get( + "/organizations", + validateQueryDto(PaginationQueryDto), + organizationController.getAll +); +``` + +### 2. Controller Type Safety + +```typescript +import { Request, Response } from "express"; +import { CreateOrganizationDto } from "../dto/create-organization.dto"; +import { UuidParamsDto, PaginationQueryDto } from "../../shared/dto/base.dto"; + +export class OrganizationController { + createOrganization = async ( + req: Request<{}, {}, CreateOrganizationDto>, + res: Response + ): Promise => { + // req.body is now typed as CreateOrganizationDto + const organization = await this.createUseCase.execute(req.body); + res.status(201).json({ success: true, data: organization }); + }; + + getById = async ( + req: Request, + res: Response + ): Promise => { + // req.params.id is validated as UUID + const organization = await this.getUseCase.execute(req.params.id); + res.json({ success: true, data: organization }); + }; + + getAll = async ( + req: Request<{}, {}, {}, PaginationQueryDto>, + res: Response + ): Promise => { + // req.query is typed and validated + const { page, limit, search } = req.query; + const organizations = await this.getAllUseCase.execute({ + page, + limit, + search, + }); + res.json({ success: true, data: organizations }); + }; +} +``` + +### 3. Creating Custom DTOs + +```typescript +import { + IsString, + IsEmail, + MinLength, + MaxLength, + IsOptional, + IsUUID, +} from "class-validator"; + +export class CreateOrganizationDto { + @IsString({ message: "Name must be a string" }) + @MinLength(2, { message: "Name must be at least 2 characters long" }) + @MaxLength(100, { message: "Name cannot exceed 100 characters" }) + name: string; + + @IsEmail({}, { message: "Please provide a valid email address" }) + email: string; + + @IsString({ message: "Password must be a string" }) + @MinLength(8, { message: "Password must be at least 8 characters long" }) + password: string; + + @IsOptional() + @IsString({ message: "Description must be a string" }) + @MinLength(10, { message: "Description must be at least 10 characters long" }) + @MaxLength(500, { message: "Description cannot exceed 500 characters" }) + description?: string; +} +``` + +## Validation Rules + +### Common Validation Decorators + +- `@IsString()` - Validates string type +- `@IsEmail()` - Validates email format +- `@IsUUID(4)` - Validates UUID v4 format +- `@IsInt()`, `@IsNumber()` - Validates numeric types +- `@IsBoolean()` - Validates boolean type +- `@IsOptional()` - Makes field optional +- `@MinLength(n)`, `@MaxLength(n)` - String length validation +- `@Min(n)`, `@Max(n)` - Numeric range validation +- `@Matches(regex)` - Regular expression validation +- `@IsEnum(enum)` - Enum validation +- `@IsUrl()` - URL validation + +### Transform Decorators + +```typescript +import { Transform } from "class-transformer"; + +export class PaginationQueryDto { + @Transform(({ value }) => parseInt(value, 10)) + @IsInt({ message: "Page must be an integer" }) + @Min(1, { message: "Page must be at least 1" }) + page: number; +} +``` + +## Error Response Format + +When validation fails, the middleware returns a standardized error response: + +```json +{ + "success": false, + "error": "Validation failed", + "details": [ + { + "property": "email", + "value": "invalid-email", + "constraints": ["Please provide a valid email address"] + }, + { + "property": "name", + "value": "A", + "constraints": ["Name must be at least 2 characters long"] + } + ] +} +``` + +## Migration from express-validator + +### Before (express-validator) + +```typescript +import { body, param, validationResult } from "express-validator"; + +router.post( + "/nfts", + [ + body("userId").isUUID(), + body("organizationId").isUUID(), + body("description").isString().notEmpty(), + ], + NFTController.createNFT +); +``` + +### After (class-validator) + +```typescript +import { validateDto } from "../shared/middleware/validation.middleware"; +import { CreateNFTDto } from "../modules/nft/dto/create-nft.dto"; + +router.post("/nfts", validateDto(CreateNFTDto), NFTController.createNFT); +``` + +## Benefits + +1. **Type Safety** - Full TypeScript support with typed request objects +2. **Centralized Validation** - All validation rules defined in DTO classes +3. **Reusability** - DTOs can be reused across different endpoints +4. **Consistency** - Standardized error responses +5. **Maintainability** - Easy to update validation rules in one place +6. **DDD Alignment** - Fits well with Domain-Driven Design principles +7. **Auto-transformation** - Automatic type conversion with class-transformer + +## Testing + +Test files are available in `src/shared/middleware/__tests__/validation.middleware.test.ts` demonstrating proper testing of validation middleware. + +## Migration Checklist + +- [ ] Replace express-validator imports with class-validator DTOs +- [ ] Update route handlers to use validation middleware +- [ ] Update controller method signatures with proper typing +- [ ] Test all endpoints with both valid and invalid data +- [ ] Update API documentation with new validation rules +- [ ] Remove unused express-validator dependencies (optional) + +## Best Practices + +1. Always provide descriptive error messages in validation decorators +2. Use appropriate validation decorators for each field type +3. Group related validations in the same DTO class +4. Use base DTOs for common patterns (UUID params, pagination) +5. Keep DTOs focused and specific to their use case +6. Test validation logic thoroughly +7. Document custom validation rules From 60d5c3dc971a6e45e1fc362aaed6375d0995de3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:24:09 -0600 Subject: [PATCH 15/18] feat: implement organization routes with validation middleware --- src/routes/v2/organization.routes.ts | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/routes/v2/organization.routes.ts diff --git a/src/routes/v2/organization.routes.ts b/src/routes/v2/organization.routes.ts new file mode 100644 index 0000000..b58e338 --- /dev/null +++ b/src/routes/v2/organization.routes.ts @@ -0,0 +1,55 @@ +import { Router } from "express"; +import { + validateDto, + validateParamsDto, + validateQueryDto, +} from "../../shared/middleware/validation.middleware"; +import { CreateOrganizationDto } from "../../modules/organization/presentation/dto/create-organization.dto"; +import { UpdateOrganizationDto } from "../../modules/organization/presentation/dto/update-organization.dto"; +import { UuidParamsDto, PaginationQueryDto } from "../../shared/dto/base.dto"; +import auth from "../../middleware/authMiddleware"; + +const router = Router(); + +// Note: This is an example of how to properly integrate validation middleware +// The controller would need to be properly instantiated with dependencies + +// POST /organizations - Create organization +router.post( + "/", + validateDto(CreateOrganizationDto) + // organizationController.createOrganization +); + +// GET /organizations - Get all organizations with pagination +router.get( + "/", + validateQueryDto(PaginationQueryDto) + // organizationController.getAllOrganizations +); + +// GET /organizations/:id - Get organization by ID +router.get( + "/:id", + validateParamsDto(UuidParamsDto) + // organizationController.getOrganizationById +); + +// PUT /organizations/:id - Update organization (protected) +router.put( + "/:id", + auth.authMiddleware, + validateParamsDto(UuidParamsDto), + validateDto(UpdateOrganizationDto) + // organizationController.updateOrganization +); + +// DELETE /organizations/:id - Delete organization (protected) +router.delete( + "/:id", + auth.authMiddleware, + validateParamsDto(UuidParamsDto) + // organizationController.deleteOrganization +); + +export default router; From a86d80ba43b81468377b89bacbc44f58bfadb57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:27:41 -0600 Subject: [PATCH 16/18] feat: enhance organization controller with DTOs for request validation --- .../controllers/organization.controller.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/modules/organization/presentation/controllers/organization.controller.ts b/src/modules/organization/presentation/controllers/organization.controller.ts index 70bee98..e306815 100644 --- a/src/modules/organization/presentation/controllers/organization.controller.ts +++ b/src/modules/organization/presentation/controllers/organization.controller.ts @@ -8,6 +8,10 @@ import { GetAllOrganizationsUseCase } from "../../application/use-cases/get-all- import { CreateOrganizationDto } from "../dto/create-organization.dto"; import { UpdateOrganizationDto } from "../dto/update-organization.dto"; import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; +import { + UuidParamsDto, + PaginationQueryDto, +} from "../../../shared/dto/base.dto"; export class OrganizationController { constructor( @@ -19,11 +23,12 @@ export class OrganizationController { ) {} createOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const createOrganizationDto = req.body as CreateOrganizationDto; - + async ( + req: Request, + res: Response + ): Promise => { const organization = await this.createOrganizationUseCase.execute( - createOrganizationDto + req.body ); res.status(201).json({ @@ -35,7 +40,7 @@ export class OrganizationController { ); getOrganizationById = asyncHandler( - async (req: Request, res: Response): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; try { @@ -59,14 +64,16 @@ export class OrganizationController { ); updateOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { + async ( + req: Request, + res: Response + ): Promise => { const { id } = req.params; - const updateOrganizationDto = req.body as UpdateOrganizationDto; try { const organization = await this.updateOrganizationUseCase.execute( id, - updateOrganizationDto + req.body ); res.status(200).json({ @@ -88,7 +95,7 @@ export class OrganizationController { ); deleteOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; try { @@ -109,21 +116,24 @@ export class OrganizationController { ); getAllOrganizations = asyncHandler( - async (req: Request, res: Response): Promise => { - const { page = 1, limit = 10, search } = req.query; + async ( + req: Request, + res: Response + ): Promise => { + const { page, limit, search } = req.query; const organizations = await this.getAllOrganizationsUseCase.execute({ - page: Number(page), - limit: Number(limit), - search: search as string, + page: page || 1, + limit: limit || 10, + search, }); res.status(200).json({ success: true, data: organizations, pagination: { - page: Number(page), - limit: Number(limit), + page: page || 1, + limit: limit || 10, total: organizations.length, }, }); From e98e676a698701f4a107507c376c262583da019d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:28:28 -0600 Subject: [PATCH 17/18] feat: add base DTOs for UUID parameters, pagination queries, and response handling --- src/shared/dto/base.dto.ts | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/shared/dto/base.dto.ts diff --git a/src/shared/dto/base.dto.ts b/src/shared/dto/base.dto.ts new file mode 100644 index 0000000..1f71a27 --- /dev/null +++ b/src/shared/dto/base.dto.ts @@ -0,0 +1,40 @@ +import { IsUUID, IsOptional, IsInt, Min, IsString } from "class-validator"; +import { Transform } from "class-transformer"; + +export class UuidParamsDto { + @IsUUID(4, { message: "ID must be a valid UUID" }) + id: string; +} + +export class PaginationQueryDto { + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsInt({ message: "Page must be an integer" }) + @Min(1, { message: "Page must be at least 1" }) + page?: number = 1; + + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsInt({ message: "Limit must be an integer" }) + @Min(1, { message: "Limit must be at least 1" }) + limit?: number = 10; + + @IsOptional() + @IsString({ message: "Search must be a string" }) + search?: string; +} + +export class BaseResponseDto { + success: boolean; + message?: string; +} + +export class ErrorResponseDto extends BaseResponseDto { + success: false; + error: string; + details?: Array<{ + property: string; + value: unknown; + constraints: string[]; + }>; +} From aa074d7bf11aba590474b9153e6b6ddd02eb3ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Brenes?= Date: Tue, 5 Aug 2025 16:29:20 -0600 Subject: [PATCH 18/18] feat: implement validation middleware with DTO validation for requests, queries, and parameters --- .../__tests__/validation.middleware.test.ts | 151 ++++++++++++++++++ .../middleware/validation.middleware.ts | 127 +++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 src/shared/middleware/__tests__/validation.middleware.test.ts create mode 100644 src/shared/middleware/validation.middleware.ts diff --git a/src/shared/middleware/__tests__/validation.middleware.test.ts b/src/shared/middleware/__tests__/validation.middleware.test.ts new file mode 100644 index 0000000..99117c2 --- /dev/null +++ b/src/shared/middleware/__tests__/validation.middleware.test.ts @@ -0,0 +1,151 @@ +import { Request, Response } from "express"; +import { + validateDto, + validateQueryDto, + validateParamsDto, +} from "../validation.middleware"; +import { CreateOrganizationDto } from "../../../modules/organization/presentation/dto/create-organization.dto"; +import { UuidParamsDto, PaginationQueryDto } from "../../dto/base.dto"; +import "reflect-metadata"; + +describe("Validation Middleware", () => { + let mockRequest: Partial; + let mockResponse: Partial; + let nextFunction: jest.Mock; + + beforeEach(() => { + mockRequest = {}; + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + nextFunction = jest.fn(); + }); + + describe("validateDto", () => { + it("should pass validation with valid CreateOrganizationDto", async () => { + const validData = { + name: "Test Organization", + email: "test@example.com", + password: "password123", + description: "A test organization for unit testing purposes", + }; + + mockRequest.body = validData; + + const middleware = validateDto(CreateOrganizationDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + }); + + it("should fail validation with invalid CreateOrganizationDto", async () => { + const invalidData = { + name: "A", // Too short + email: "invalid-email", // Invalid email + password: "123", // Too short + description: "Short", // Too short + }; + + mockRequest.body = invalidData; + + const middleware = validateDto(CreateOrganizationDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).not.toHaveBeenCalled(); + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: "Validation failed", + details: expect.arrayContaining([ + expect.objectContaining({ + property: expect.any(String), + constraints: expect.any(Array), + }), + ]), + }) + ); + }); + }); + + describe("validateParamsDto", () => { + it("should pass validation with valid UUID", async () => { + mockRequest.params = { + id: "550e8400-e29b-41d4-a716-446655440000", + }; + + const middleware = validateParamsDto(UuidParamsDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + }); + + it("should fail validation with invalid UUID", async () => { + mockRequest.params = { + id: "invalid-uuid", + }; + + const middleware = validateParamsDto(UuidParamsDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).not.toHaveBeenCalled(); + expect(mockResponse.status).toHaveBeenCalledWith(400); + }); + }); + + describe("validateQueryDto", () => { + it("should pass validation with valid pagination query", async () => { + mockRequest.query = { + page: "1", + limit: "10", + search: "test", + }; + + const middleware = validateQueryDto(PaginationQueryDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + }); + + it("should fail validation with invalid pagination query", async () => { + mockRequest.query = { + page: "invalid", + limit: "-5", + }; + + const middleware = validateQueryDto(PaginationQueryDto); + await middleware( + mockRequest as Request, + mockResponse as Response, + nextFunction + ); + + expect(nextFunction).not.toHaveBeenCalled(); + expect(mockResponse.status).toHaveBeenCalledWith(400); + }); + }); +}); diff --git a/src/shared/middleware/validation.middleware.ts b/src/shared/middleware/validation.middleware.ts new file mode 100644 index 0000000..54fb7cc --- /dev/null +++ b/src/shared/middleware/validation.middleware.ts @@ -0,0 +1,127 @@ +import { Request, Response, NextFunction } from "express"; +import { validate, ValidationError } from "class-validator"; +import { plainToClass } from "class-transformer"; + +export interface ValidationErrorResponse { + success: false; + error: string; + details: Array<{ + property: string; + value: unknown; + constraints: string[]; + }>; +} + +export function validateDto(dtoClass: new () => T) { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const dto = plainToClass(dtoClass, req.body); + const errors = await validate(dto); + + if (errors.length > 0) { + const errorResponse: ValidationErrorResponse = { + success: false, + error: "Validation failed", + details: errors.map((error: ValidationError) => ({ + property: error.property, + value: error.value, + constraints: error.constraints + ? Object.values(error.constraints) + : [], + })), + }; + + res.status(400).json(errorResponse); + return; + } + + req.body = dto; + next(); + } catch { + res.status(500).json({ + success: false, + error: "Internal server error during validation", + }); + } + }; +} + +export function validateQueryDto(dtoClass: new () => T) { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const dto = plainToClass(dtoClass, req.query); + const errors = await validate(dto); + + if (errors.length > 0) { + const errorResponse: ValidationErrorResponse = { + success: false, + error: "Query validation failed", + details: errors.map((error: ValidationError) => ({ + property: error.property, + value: error.value, + constraints: error.constraints + ? Object.values(error.constraints) + : [], + })), + }; + + res.status(400).json(errorResponse); + return; + } + + req.query = dto as Record; + next(); + } catch { + res.status(500).json({ + success: false, + error: "Internal server error during query validation", + }); + } + }; +} + +export function validateParamsDto(dtoClass: new () => T) { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const dto = plainToClass(dtoClass, req.params); + const errors = await validate(dto); + + if (errors.length > 0) { + const errorResponse: ValidationErrorResponse = { + success: false, + error: "Parameters validation failed", + details: errors.map((error: ValidationError) => ({ + property: error.property, + value: error.value, + constraints: error.constraints + ? Object.values(error.constraints) + : [], + })), + }; + + res.status(400).json(errorResponse); + return; + } + + req.params = dto as Record; + next(); + } catch { + res.status(500).json({ + success: false, + error: "Internal server error during parameter validation", + }); + } + }; +}