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 <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 06:12:29 +07:00
parent 811417d77d
commit d77c14e549
5 changed files with 28 additions and 28 deletions

View File

@@ -212,6 +212,8 @@ export class PrismaAdminQueryRepository implements IAdminQueryRepository {
},
listings: {
select: { id: true, status: true },
take: 100,
orderBy: { createdAt: 'desc' },
},
},
});

View File

@@ -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<UserListResult> {
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,
),
);
}

View File

@@ -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()

View File

@@ -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 } },

View File

@@ -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 })