mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add staff user list endpoint
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { EUserStatus, EUserType } from '@tria-plc/api-common/utils/enums/user.enum';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsIn, IsOptional } from 'class-validator';
|
||||
|
||||
import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
|
||||
|
||||
/** Query-string booleans arrive as strings; implicit conversion is off app-wide. */
|
||||
const toOptionalBoolean = ({ value }: TransformFnParams): boolean | undefined =>
|
||||
value === undefined || value === null || value === ''
|
||||
? undefined
|
||||
: value === true || value === 'true';
|
||||
|
||||
export class ListUsersQueryDto extends PaginationQueryDto {
|
||||
@ApiPropertyOptional({ enum: EUserType })
|
||||
@IsOptional()
|
||||
@IsEnum(EUserType)
|
||||
userType?: EUserType;
|
||||
|
||||
@ApiPropertyOptional({ enum: EUserStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(EUserStatus)
|
||||
userStatus?: EUserStatus;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by active flag.' })
|
||||
@IsOptional()
|
||||
@Transform(toOptionalBoolean)
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: ['username', 'email', 'createdAt'],
|
||||
default: 'username',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(['username', 'email', 'createdAt'])
|
||||
sortBy?: string;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import { ForgotPasswordController } from './forgot-password.controller';
|
||||
import { ForgotPasswordService } from './forgot-password.service';
|
||||
import { FreightMeController } from './freight-me.controller';
|
||||
import { FreightMeService } from './freight-me.service';
|
||||
import { ListUsersController } from './list-users.controller';
|
||||
import { ListUsersService } from './list-users.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -39,8 +41,10 @@ import { FreightMeService } from './freight-me.service';
|
||||
CheckAvailabilityController,
|
||||
ForgotPasswordController,
|
||||
CustomerResetController,
|
||||
ListUsersController,
|
||||
],
|
||||
providers: [
|
||||
ListUsersService,
|
||||
FreightMeService,
|
||||
AccountService,
|
||||
CheckAvailabilityService,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { ListUsersQueryDto } from './dto/list-users-query.dto';
|
||||
import { ListUsersService } from './list-users.service';
|
||||
import { StaffReference } from '../../common/booking-guards';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('staff/users')
|
||||
@ApiBearerAuth()
|
||||
export class ListUsersController {
|
||||
constructor(private readonly service: ListUsersService) {}
|
||||
|
||||
@Get()
|
||||
@StaffReference()
|
||||
@ApiOperation({
|
||||
summary: 'List IAM users (paginated) for backoffice pickers',
|
||||
})
|
||||
findAll(@Query() query: ListUsersQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
}
|
||||
}
|
||||
69
apps/edr-freight-api/src/modules/auth/list-users.service.ts
Normal file
69
apps/edr-freight-api/src/modules/auth/list-users.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { PaginatedResponse } from '@edr/types';
|
||||
import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ListUsersQueryDto } from './dto/list-users-query.dto';
|
||||
import { paginateQuery } from '../../common/utils/pagination.util';
|
||||
|
||||
/**
|
||||
* Read-only listing of `iam.users` for backoffice pickers.
|
||||
*
|
||||
* Exists because `@tria-plc/iamapi-common@1.0.0`'s `GET /users/filter` pairs a
|
||||
* `@QueryParams()` pagination DTO with a plain `@Query()` DTO that does not
|
||||
* declare `skip`/`take`/`orderBy`; the global whitelist pipe then 400s on the
|
||||
* very params the route's own paginator reads. Drop this once IAM ships a fix.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ListUsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private readonly users: Repository<User>,
|
||||
) {}
|
||||
|
||||
findAll(query: ListUsersQueryDto): Promise<PaginatedResponse<User>> {
|
||||
const sortBy = query.sortBy ?? 'username';
|
||||
const qb = this.users
|
||||
.createQueryBuilder('user')
|
||||
// Explicit select: never widen this to `user` — the entity's lazy
|
||||
// relations include credentials and sessions.
|
||||
.select([
|
||||
'user.id',
|
||||
'user.name',
|
||||
'user.username',
|
||||
'user.email',
|
||||
'user.phoneNumber',
|
||||
'user.userType',
|
||||
'user.status',
|
||||
'user.isActive',
|
||||
'user.createdAt',
|
||||
])
|
||||
.orderBy(`user.${sortBy}`, query.sortOrder ?? 'ASC');
|
||||
|
||||
if (query.userType) {
|
||||
qb.andWhere('user.userType = :userType', { userType: query.userType });
|
||||
}
|
||||
if (query.userStatus) {
|
||||
qb.andWhere('user.status = :userStatus', { userStatus: query.userStatus });
|
||||
}
|
||||
if (query.isActive !== undefined) {
|
||||
qb.andWhere('user.isActive = :isActive', { isActive: query.isActive });
|
||||
}
|
||||
if (query.search) {
|
||||
// `name` is localized jsonb ({ en, am, … }), not a string — match its
|
||||
// values rather than casting the whole object to text.
|
||||
qb.andWhere(
|
||||
`(user.username ILIKE :search
|
||||
OR user.email ILIKE :search
|
||||
OR user.phone_number ILIKE :search
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM jsonb_each_text(user.name) AS n(k, v)
|
||||
WHERE n.v ILIKE :search
|
||||
))`,
|
||||
{ search: `%${query.search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
return paginateQuery(qb, query);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user