feat: add safe user directory query
This commit is contained in:
@@ -4,8 +4,18 @@ import { Player } from 'src/players/entities/player.entity';
|
||||
import { EntityCondition } from 'src/utils/types/entity-condition.type';
|
||||
import { IPaginationOptions } from 'src/utils/types/pagination-options';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RoleEnum } from '../roles/roles.enum';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserDirectoryQueryDto } from './dto/user-directory-query.dto';
|
||||
import {
|
||||
AdminUserDirectorySummaryDto,
|
||||
UserDirectoryAssignmentDto,
|
||||
UserDirectoryPageDto,
|
||||
UserDirectoryReferenceDto,
|
||||
UserDirectorySummaryDto,
|
||||
UserDirectoryTeamDto,
|
||||
} from './dto/user-directory-response.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -30,6 +40,66 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
async findDirectory(
|
||||
requester: Pick<User, 'id' | 'role'>,
|
||||
query: UserDirectoryQueryDto,
|
||||
): Promise<UserDirectoryPageDto> {
|
||||
const [users, players] = await Promise.all([
|
||||
this.usersRepository.find({ order: { id: 'ASC' } }),
|
||||
this.playersRepository.find({
|
||||
relations: ['user', 'team', 'teamRole'],
|
||||
order: { id: 'ASC' },
|
||||
}),
|
||||
]);
|
||||
const isAdmin = requester.role?.id === RoleEnum.admin;
|
||||
const sharedTeamIds = new Set(
|
||||
players
|
||||
.filter((player) => player.user?.id === requester.id && player.active)
|
||||
.map((player) => player.team.id),
|
||||
);
|
||||
const assignmentsByUserId = new Map<number, Player[]>();
|
||||
|
||||
for (const player of players) {
|
||||
if (!player.user || (!isAdmin && !sharedTeamIds.has(player.team.id))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignments = assignmentsByUserId.get(player.user.id) ?? [];
|
||||
assignments.push(player);
|
||||
assignmentsByUserId.set(player.user.id, assignments);
|
||||
}
|
||||
|
||||
const visibleUsers = users.filter(
|
||||
(user) => isAdmin || assignmentsByUserId.has(user.id),
|
||||
);
|
||||
const searchedUsers = this.filterDirectorySearch(
|
||||
visibleUsers,
|
||||
assignmentsByUserId,
|
||||
query.search,
|
||||
isAdmin,
|
||||
);
|
||||
const total = searchedUsers.length;
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const data = searchedUsers
|
||||
.slice((page - 1) * limit, page * limit)
|
||||
.map((user) =>
|
||||
this.mapDirectoryUser(
|
||||
user,
|
||||
assignmentsByUserId.get(user.id) ?? [],
|
||||
isAdmin,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
hasNextPage: page * limit < total,
|
||||
};
|
||||
}
|
||||
|
||||
findOne(fields: EntityCondition<User>) {
|
||||
return this.usersRepository.findOne({
|
||||
where: fields,
|
||||
@@ -77,4 +147,89 @@ export class UsersService {
|
||||
return resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
private filterDirectorySearch(
|
||||
users: User[],
|
||||
assignmentsByUserId: Map<number, Player[]>,
|
||||
search: string | undefined,
|
||||
includeEmail: boolean,
|
||||
): User[] {
|
||||
const term = search?.trim().toLocaleLowerCase();
|
||||
if (!term) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.filter((user) => {
|
||||
const assignments = assignmentsByUserId.get(user.id) ?? [];
|
||||
const values = [
|
||||
user.firstName,
|
||||
user.lastName,
|
||||
...(includeEmail ? [user.email] : []),
|
||||
...assignments.flatMap((assignment) => [
|
||||
assignment.firstName,
|
||||
assignment.lastName,
|
||||
]),
|
||||
];
|
||||
|
||||
return values.some((value) => value?.toLocaleLowerCase().includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
private mapDirectoryUser(
|
||||
user: User,
|
||||
assignments: Player[],
|
||||
includeAdminFields: boolean,
|
||||
): UserDirectorySummaryDto | AdminUserDirectorySummaryDto {
|
||||
const summary: UserDirectorySummaryDto = {
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
status: this.mapDirectoryReference(user.status),
|
||||
assignments: assignments
|
||||
.sort((left, right) => left.id - right.id)
|
||||
.map((assignment) => this.mapDirectoryAssignment(assignment)),
|
||||
};
|
||||
|
||||
if (!includeAdminFields) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
return {
|
||||
...summary,
|
||||
email: user.email,
|
||||
role: this.mapDirectoryReference(user.role),
|
||||
};
|
||||
}
|
||||
|
||||
private mapDirectoryAssignment(player: Player): UserDirectoryAssignmentDto {
|
||||
return {
|
||||
id: player.id,
|
||||
firstName: player.firstName,
|
||||
lastName: player.lastName,
|
||||
active: player.active,
|
||||
team: this.mapDirectoryTeam(player.team),
|
||||
teamRole: this.mapDirectoryReference(player.teamRole),
|
||||
};
|
||||
}
|
||||
|
||||
private mapDirectoryTeam(team: Player['team']): UserDirectoryTeamDto {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
alias: team.alias,
|
||||
};
|
||||
}
|
||||
|
||||
private mapDirectoryReference(
|
||||
reference: { id: number; name?: string } | null | undefined,
|
||||
): UserDirectoryReferenceDto | null {
|
||||
if (!reference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: reference.id,
|
||||
name: reference.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user