From 65f3d733f41cef118cd0805869b07b01d5199f6b Mon Sep 17 00:00:00 2001 From: konstantinabl Date: Wed, 30 Jun 2021 16:24:10 +0300 Subject: [PATCH 1/4] Add user filters --- src/users/api/users-filters.dto.ts | 24 ++++++++++++++ src/users/api/users.controller.ts | 23 ++++++++----- src/users/entities/users.repository.ts | 36 ++++++++++++++++++++- src/violations/api/violations.controller.ts | 27 +++++++++------- 4 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 src/users/api/users-filters.dto.ts diff --git a/src/users/api/users-filters.dto.ts b/src/users/api/users-filters.dto.ts new file mode 100644 index 00000000..52de9ecd --- /dev/null +++ b/src/users/api/users-filters.dto.ts @@ -0,0 +1,24 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional } from 'class-validator'; +import { IsOrganizationExists } from 'src/users/api/organization-exists.constraint'; +import { PageDTO } from 'src/utils/page.dto'; + +export class UsersFilters extends PageDTO { + @IsOptional() + firstName: string; + + @IsOptional() + lastName: string; + + @IsOptional() + email: string; + + @IsOptional() + @IsInt() + @Type(() => Number) + @IsOrganizationExists() + organization: number; + + @IsOptional() + role: string; +} diff --git a/src/users/api/users.controller.ts b/src/users/api/users.controller.ts index 977d1c88..07125763 100644 --- a/src/users/api/users.controller.ts +++ b/src/users/api/users.controller.ts @@ -30,6 +30,7 @@ import { User } from '../entities'; import { UsersRepository } from '../entities/users.repository'; import RegistrationService, { RegistrationError } from './registration.service'; import { UserDto } from './user.dto'; +import { UsersFilters } from './users-filters.dto'; @Controller('users') export class UsersController { @@ -91,15 +92,21 @@ export class UsersController { @UseGuards(PoliciesGuard) @CheckPolicies((ability: Ability) => ability.can(Action.Manage, User)) @UsePipes(new ValidationPipe({ transform: true })) - async index(@Query() query: PageDTO): Promise> { - const pagination = await paginate(this.repo.getRepo(), { - page: query.page, - limit: 20, - route: '/users', - }); - pagination.items.map((user: User) => UserDto.fromEntity(user)); + async index(@Query() query: UsersFilters): Promise> { + const pagination = await paginate( + this.repo.queryBuilderWithFilters(query), + { + page: query.page, + limit: 20, + route: '/users', + }, + ); + + const items = pagination.items.map((user: User) => + UserDto.fromEntity(user, [UserDto.ADMIN_READ]), + ); - return pagination; + return new Pagination(items, pagination.meta, pagination.links); } @Patch(':id') diff --git a/src/users/entities/users.repository.ts b/src/users/entities/users.repository.ts index 074dd22f..7ae7e6c9 100644 --- a/src/users/entities/users.repository.ts +++ b/src/users/entities/users.repository.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { User } from './user.entity'; +import { UsersFilters } from '../api/users-filters.dto'; @Injectable() export class UsersRepository { @@ -40,6 +41,39 @@ export class UsersRepository { return this.repo.find(); } + queryBuilderWithFilters(filters: UsersFilters): SelectQueryBuilder { + const qb = this.repo.createQueryBuilder('people'); + + qb.innerJoinAndSelect('people.organization', 'organization'); + const { firstName, lastName, email, organization, role } = filters; + + if (firstName) { + qb.andWhere('people.firstName LIKE :firstName', { + firstName: `${firstName}%`, + }); + } + + if (lastName) { + qb.andWhere('people.lastName LIKE :lastName', { + lastName: `${lastName}%`, + }); + } + + if (email) { + qb.andWhere('people.email LIKE :email', { email: `${email}%` }); + } + + if (organization) { + qb.andWhere('organization.id = :organization', { organization }); + } + + if (role) { + qb.andWhere('people.roles = :role', { role }); + } + + return qb; + } + async save(user: User): Promise { return await this.repo.save(user); } diff --git a/src/violations/api/violations.controller.ts b/src/violations/api/violations.controller.ts index 02cffd51..892f2363 100644 --- a/src/violations/api/violations.controller.ts +++ b/src/violations/api/violations.controller.ts @@ -48,18 +48,23 @@ export class ViolationsController { { page: query.page, limit: 100, route: '/violations' }, ); + const processViolation = async (violation: Violation) => { + const dto = ViolationDto.fromEntity(violation, [ + 'violation.process', + UserDto.AUTHOR_READ, + ]); + this.updatePicturesUrl(dto); + + return dto; + }; + + const promises: Promise[] = pagination.items.map( + processViolation, + ); + const violationDtoItems = await Promise.all(promises); + return new Pagination( - await Promise.all( - pagination.items.map(async (violation: Violation) => { - const dto = ViolationDto.fromEntity(violation, [ - 'violation.process', - UserDto.AUTHOR_READ, - ]); - this.updatePicturesUrl(dto); - - return dto; - }), - ), + violationDtoItems, pagination.meta, pagination.links, ); From 5e7c0284766dd20a3ede645647bb8aaefe940c49 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Wed, 30 Jun 2021 23:10:43 +0300 Subject: [PATCH 2/4] Allow full partial matching on text fields in user filters --- src/users/entities/users.repository.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/users/entities/users.repository.ts b/src/users/entities/users.repository.ts index 7ae7e6c9..95b53650 100644 --- a/src/users/entities/users.repository.ts +++ b/src/users/entities/users.repository.ts @@ -49,18 +49,18 @@ export class UsersRepository { if (firstName) { qb.andWhere('people.firstName LIKE :firstName', { - firstName: `${firstName}%`, + firstName: `%${firstName}%`, }); } if (lastName) { qb.andWhere('people.lastName LIKE :lastName', { - lastName: `${lastName}%`, + lastName: `%${lastName}%`, }); } if (email) { - qb.andWhere('people.email LIKE :email', { email: `${email}%` }); + qb.andWhere('people.email LIKE :email', { email: `%${email}%` }); } if (organization) { From f8aabd60ab76dbf683e29e6016fb7aa4169f919e Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Wed, 30 Jun 2021 23:14:41 +0300 Subject: [PATCH 3/4] Fix role matching in admin users filter --- src/users/entities/users.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/users/entities/users.repository.ts b/src/users/entities/users.repository.ts index 95b53650..a776d4ca 100644 --- a/src/users/entities/users.repository.ts +++ b/src/users/entities/users.repository.ts @@ -68,7 +68,7 @@ export class UsersRepository { } if (role) { - qb.andWhere('people.roles = :role', { role }); + qb.andWhere('people.roles::jsonb ? :role', { role }); } return qb; From abf2b18910dbd402d0839948c89936333b8c8374 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Wed, 30 Jun 2021 23:18:38 +0300 Subject: [PATCH 4/4] Add more validation on admin users filters --- src/users/api/users-filters.dto.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/users/api/users-filters.dto.ts b/src/users/api/users-filters.dto.ts index 52de9ecd..cf0face1 100644 --- a/src/users/api/users-filters.dto.ts +++ b/src/users/api/users-filters.dto.ts @@ -1,24 +1,41 @@ import { Type } from 'class-transformer'; -import { IsInt, IsOptional } from 'class-validator'; +import { + IsIn, + IsInt, + IsOptional, + IsString, + Length, + Min, +} from 'class-validator'; +import { Role } from 'src/casl/role.enum'; import { IsOrganizationExists } from 'src/users/api/organization-exists.constraint'; import { PageDTO } from 'src/utils/page.dto'; export class UsersFilters extends PageDTO { @IsOptional() + @IsString() + @Length(1, 255) firstName: string; @IsOptional() + @IsString() + @Length(1, 255) lastName: string; @IsOptional() + @IsString() + @Length(1, 255) email: string; @IsOptional() @IsInt() @Type(() => Number) + @Min(1) @IsOrganizationExists() organization: number; @IsOptional() + @IsString() + @IsIn(Object.values(Role)) role: string; }