From d77c14e54909e45a9924dfd265a00e4f7b6bfa78 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 8 Apr 2026 06:12:29 +0700 Subject: [PATCH] fix: add take limits on media includes and enforce pagination validation - Add take: 10 on unbounded media include in findByIdWithProperty - Add take: 100 + orderBy on user listings include in getUserDetail - Convert GetUsersQueryDto page/limit from string to validated integers with @Min(1) @Max(100) - Add @Max(100) to BillingHistoryParamsDto limit field - Refactor admin controller to use GetUsersQueryDto with class-validator pipeline Co-Authored-By: Paperclip --- .../prisma-admin-query.repository.ts | 2 ++ .../controllers/admin.controller.ts | 22 ++++++---------- .../presentation/dto/get-users-query.dto.ts | 25 +++++++++++-------- .../repositories/prisma-listing.repository.ts | 2 +- .../presentation/dto/billing-history.dto.ts | 5 ++-- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts b/apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts index 9023e86..9af469f 100644 --- a/apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts +++ b/apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts @@ -212,6 +212,8 @@ export class PrismaAdminQueryRepository implements IAdminQueryRepository { }, listings: { select: { id: true, status: true }, + take: 100, + orderBy: { createdAt: 'desc' }, }, }, }); diff --git a/apps/api/src/modules/admin/presentation/controllers/admin.controller.ts b/apps/api/src/modules/admin/presentation/controllers/admin.controller.ts index 2868295..9606483 100644 --- a/apps/api/src/modules/admin/presentation/controllers/admin.controller.ts +++ b/apps/api/src/modules/admin/presentation/controllers/admin.controller.ts @@ -40,6 +40,7 @@ import { UpdateUserStatusDto } from '../dto/update-user-status.dto'; import { ApproveKycDto } from '../dto/approve-kyc.dto'; import { RejectKycDto } from '../dto/reject-kyc.dto'; import { BulkModerateDto } from '../dto/bulk-moderate.dto'; +import { GetUsersQueryDto } from '../dto/get-users-query.dto'; import { type ApproveListingResult } from '../../application/commands/approve-listing/approve-listing.handler'; import { type RejectListingResult } from '../../application/commands/reject-listing/reject-listing.handler'; @@ -136,28 +137,19 @@ export class AdminController { @Get('users') @ApiOperation({ summary: 'List users with optional filters' }) - @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default 1)' }) - @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default 20)' }) - @ApiQuery({ name: 'role', required: false, type: String, description: 'Filter by role (BUYER, SELLER, AGENT, ADMIN)' }) - @ApiQuery({ name: 'isActive', required: false, type: String, description: 'Filter by active status (true/false)' }) - @ApiQuery({ name: 'search', required: false, type: String, description: 'Search by name or email' }) @ApiResponse({ status: 200, description: 'User list retrieved successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized – missing or invalid JWT' }) @ApiResponse({ status: 403, description: 'Forbidden – requires ADMIN role' }) async getUsers( - @Query('page') page?: string, - @Query('limit') limit?: string, - @Query('role') role?: string, - @Query('isActive') isActive?: string, - @Query('search') search?: string, + @Query() query: GetUsersQueryDto, ): Promise { return this.queryBus.execute( new GetUsersQuery( - page ? parseInt(page, 10) : 1, - limit ? parseInt(limit, 10) : 20, - role, - isActive !== undefined ? isActive === 'true' : undefined, - search, + query.page ?? 1, + query.limit ?? 20, + query.role, + query.isActive, + query.search, ), ); } diff --git a/apps/api/src/modules/admin/presentation/dto/get-users-query.dto.ts b/apps/api/src/modules/admin/presentation/dto/get-users-query.dto.ts index 45d1600..3528e92 100644 --- a/apps/api/src/modules/admin/presentation/dto/get-users-query.dto.ts +++ b/apps/api/src/modules/admin/presentation/dto/get-users-query.dto.ts @@ -1,17 +1,22 @@ -import { IsOptional, IsString, IsIn } from 'class-validator'; -import { Transform } from 'class-transformer'; +import { IsOptional, IsString, IsIn, IsBoolean, IsInt, Min, Max } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; import { ApiPropertyOptional } from '@nestjs/swagger'; export class GetUsersQueryDto { - @ApiPropertyOptional({ description: 'Page number', example: '1' }) + @ApiPropertyOptional({ description: 'Page number', example: 1, minimum: 1 }) @IsOptional() - @IsString() - page?: string; + @Type(() => Number) + @IsInt() + @Min(1) + page?: number; - @ApiPropertyOptional({ description: 'Items per page', example: '20' }) + @ApiPropertyOptional({ description: 'Items per page', example: 20, minimum: 1, maximum: 100 }) @IsOptional() - @IsString() - limit?: string; + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number; @ApiPropertyOptional({ description: 'Filter by user role', enum: ['BUYER', 'SELLER', 'AGENT', 'ADMIN'] }) @IsOptional() @@ -20,9 +25,9 @@ export class GetUsersQueryDto { @ApiPropertyOptional({ description: 'Filter by active status (true/false)', example: 'true' }) @IsOptional() - @IsString() @Transform(({ value }) => value === 'true' ? true : value === 'false' ? false : undefined) - isActive?: string; + @IsBoolean() + isActive?: boolean; @ApiPropertyOptional({ description: 'Search by name or email', example: 'john' }) @IsOptional() diff --git a/apps/api/src/modules/listings/infrastructure/repositories/prisma-listing.repository.ts b/apps/api/src/modules/listings/infrastructure/repositories/prisma-listing.repository.ts index 1667705..6d4dd51 100644 --- a/apps/api/src/modules/listings/infrastructure/repositories/prisma-listing.repository.ts +++ b/apps/api/src/modules/listings/infrastructure/repositories/prisma-listing.repository.ts @@ -20,7 +20,7 @@ export class PrismaListingRepository implements IListingRepository { include: { property: { include: { - media: { orderBy: { order: 'asc' } }, + media: { orderBy: { order: 'asc' }, take: 10 }, }, }, seller: { select: { id: true, fullName: true, phone: true } }, diff --git a/apps/api/src/modules/subscriptions/presentation/dto/billing-history.dto.ts b/apps/api/src/modules/subscriptions/presentation/dto/billing-history.dto.ts index 106067c..263d87d 100644 --- a/apps/api/src/modules/subscriptions/presentation/dto/billing-history.dto.ts +++ b/apps/api/src/modules/subscriptions/presentation/dto/billing-history.dto.ts @@ -1,13 +1,14 @@ -import { IsInt, IsOptional, Min } from 'class-validator'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; export class BillingHistoryParamsDto { - @ApiPropertyOptional({ description: 'Maximum number of records to return', minimum: 1, example: 20 }) + @ApiPropertyOptional({ description: 'Maximum number of records to return', minimum: 1, maximum: 100, example: 20 }) @IsOptional() @Transform(({ value }) => parseInt(value, 10)) @IsInt() @Min(1) + @Max(100) limit?: number; @ApiPropertyOptional({ description: 'Number of records to skip', minimum: 0, example: 0 })