mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
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>,
|
|
private readonly config: ConfigService,
|
|
) {}
|
|
|
|
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');
|
|
|
|
// Restrict to one IAM organization when configured. The org key differs per
|
|
// environment (dev seeds `edr_freight`, production uses the registered
|
|
// company key), so this is config rather than a constant. An unset key
|
|
// means no restriction; a key matching no organization matches no user —
|
|
// failing closed rather than silently widening to every org.
|
|
const orgKey = this.config.get<string>('FREIGHT_ORG_KEY');
|
|
if (orgKey) {
|
|
// EXISTS, not a join: a user with several employee rows would otherwise
|
|
// be returned once per row, duplicating them in the list and inflating
|
|
// `getManyAndCount`'s total.
|
|
qb.andWhere(
|
|
`EXISTS (
|
|
SELECT 1
|
|
FROM iam.employees emp
|
|
JOIN iam.organizations org ON org.id = emp.organization_id
|
|
WHERE emp.user_id = "user".id
|
|
AND org.key = :orgKey
|
|
AND org.deleted_at IS NULL
|
|
)`,
|
|
{ orgKey },
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|