feat: add safe user directory query

This commit is contained in:
Bastian Wagner
2026-07-31 22:12:53 +02:00
parent bcb4207233
commit c382234746
6 changed files with 506 additions and 0 deletions

View File

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