feat: secure penalty catalog management

This commit is contained in:
Bastian Wagner
2026-08-01 13:17:44 +02:00
parent 45658e42fa
commit 17228a52db
12 changed files with 629 additions and 98 deletions

View File

@@ -20,6 +20,9 @@ export type LOGEVENT =
| 'admin_user_status_update'
| 'admin_player_assign'
| 'admin_player_unlink'
| 'penalty_catalog_create'
| 'penalty_catalog_update'
| 'penalty_catalog_delete'
| 'team_create';
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';

View File

@@ -1,19 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
import { Team } from 'src/teams/entities/team.entity';
import { Transform } from 'class-transformer';
import {
IsInt,
IsNumber,
IsString,
Length,
Max,
Min,
} from 'class-validator';
export class CreatePenaltyDTO {
@ApiProperty({ example: 2342 })
@IsNotEmpty()
@IsInt()
@Min(1)
teamId: number;
@ApiProperty({ example: 1 })
@IsNotEmpty()
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
@Max(10000)
amount: number;
team?: Team;
@ApiProperty({ example: 'Zu spät kommen' })
@IsNotEmpty()
@Transform(({ value }) =>
typeof value === 'string' ? value.trim() : value,
)
@IsString()
@Length(1, 120)
description: string;
}

View File

@@ -0,0 +1,6 @@
export class PenaltyResponseDTO {
id: number;
description: string;
amount: number;
createdAt: Date;
}

View File

@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNumber, IsString, Length, Max, Min } from 'class-validator';
export class UpdatePenaltyDTO {
@ApiProperty({ example: 'Zu spät kommen' })
@Transform(({ value }) =>
typeof value === 'string' ? value.trim() : value,
)
@IsString()
@Length(1, 120)
description: string;
@ApiProperty({ example: 5 })
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
@Max(10000)
amount: number;
}

View File

@@ -1,45 +1,62 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
ParseIntPipe,
Patch,
Post,
Req,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { Roles } from 'src/roles/roles.decorator';
import { RolesGuard } from 'src/roles/roles.guard';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import { UpdatePenaltyDTO } from './dto/update-penalty.dto';
import { PenaltyService } from './penalty.service';
type AuthenticatedRequest = { user: { id: number } };
@ApiBearerAuth()
@Controller({
path: 'penalty',
version: '1',
})
@UseGuards(AuthGuard('jwt'))
@Controller({ path: 'penalty', version: '1' })
export class PenaltyController {
constructor(private service: PenaltyService) {}
constructor(private readonly service: PenaltyService) {}
@Roles([])
@Get()
getIt(@Req() req: any) {
const userId = req.user?.id;
return this.service.getAll(userId);
@Get(':teamId')
getTeamPenalties(
@Request() request: AuthenticatedRequest,
@Param('teamId', ParseIntPipe) teamId: number,
) {
return this.service.getTeamPenalties(request.user.id, teamId);
}
@Roles([])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Get(':id')
getTeams(@Param('id') teamId: string) {
return this.service.getTeamPenalties(teamId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Post()
createPenalty(@Req() req: any, @Body() createPenaltyDto: CreatePenaltyDTO) {
const userId = req.user?.id;
return this.service.createPenalty(createPenaltyDto, userId);
createPenalty(
@Request() request: AuthenticatedRequest,
@Body() dto: CreatePenaltyDTO,
) {
return this.service.createPenalty(dto, request.user.id);
}
@Patch(':penaltyId')
updatePenalty(
@Request() request: AuthenticatedRequest,
@Param('penaltyId', ParseIntPipe) penaltyId: number,
@Body() dto: UpdatePenaltyDTO,
) {
return this.service.updatePenalty(penaltyId, dto, request.user.id);
}
@Delete(':penaltyId')
@HttpCode(HttpStatus.NO_CONTENT)
async deletePenalty(
@Request() request: AuthenticatedRequest,
@Param('penaltyId', ParseIntPipe) penaltyId: number,
): Promise<void> {
await this.service.deletePenalty(penaltyId, request.user.id);
}
}

View File

@@ -0,0 +1,116 @@
import {
INestApplication,
UnauthorizedException,
ValidationPipe,
VersioningType,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import validationOptions from '../utils/validation-options';
import { PenaltyController } from './penalty.controller';
import { PenaltyService } from './penalty.service';
describe('penalty catalog HTTP boundary', () => {
let app: INestApplication;
const penalty = {
id: 8,
description: 'Zu spät',
amount: 5,
createdAt: '2026-01-02T00:00:00.000Z',
};
const service = {
getTeamPenalties: jest.fn(() => [penalty]),
createPenalty: jest.fn(() => penalty),
updatePenalty: jest.fn(() => penalty),
deletePenalty: jest.fn(),
};
beforeAll(async () => {
const module = await Test.createTestingModule({
controllers: [PenaltyController],
providers: [{ provide: PenaltyService, useValue: service }],
})
.overrideGuard(AuthGuard('jwt'))
.useValue({
canActivate(context) {
const httpRequest = context.switchToHttp().getRequest();
if (httpRequest.headers.authorization !== 'Bearer user') {
throw new UnauthorizedException();
}
httpRequest.user = { id: 42, role: { id: 2 } };
return true;
},
})
.compile();
app = module.createNestApplication();
app.setGlobalPrefix('api');
app.enableVersioning({ type: VersioningType.URI });
app.useGlobalPipes(new ValidationPipe(validationOptions));
await app.init();
});
afterAll(() => app.close());
beforeEach(() => jest.clearAllMocks());
it('requires authentication and passes the actor to a team read', async () => {
await request(app.getHttpServer()).get('/api/v1/penalty/5').expect(401);
await request(app.getHttpServer())
.get('/api/v1/penalty/5')
.set('Authorization', 'Bearer user')
.expect(200, [penalty]);
expect(service.getTeamPenalties).toHaveBeenCalledWith(42, 5);
});
it('validates and strips create fields before invoking the service', async () => {
await request(app.getHttpServer())
.post('/api/v1/penalty')
.set('Authorization', 'Bearer user')
.send({
teamId: 5,
description: ' Zu spät ',
amount: 5.25,
team: { id: 99 },
})
.expect(201, penalty);
expect(service.createPenalty).toHaveBeenCalledWith(
{ teamId: 5, description: 'Zu spät', amount: 5.25 },
42,
);
});
it.each([
[{ teamId: 5, description: '', amount: 5 }],
[{ teamId: 5, description: 'x'.repeat(121), amount: 5 }],
[{ teamId: 5, description: 'Test', amount: 0 }],
[{ teamId: 5, description: 'Test', amount: 10000.01 }],
[{ teamId: 5, description: 'Test', amount: 1.001 }],
[{ teamId: true, description: 'Test', amount: 1 }],
])('rejects invalid create input: %p', async (body) => {
await request(app.getHttpServer())
.post('/api/v1/penalty')
.set('Authorization', 'Bearer user')
.send(body)
.expect(422);
expect(service.createPenalty).not.toHaveBeenCalled();
});
it('exposes update and delete with numeric IDs and safe payloads', async () => {
await request(app.getHttpServer())
.patch('/api/v1/penalty/8')
.set('Authorization', 'Bearer user')
.send({ description: ' Neu ', amount: 2 })
.expect(200, penalty);
expect(service.updatePenalty).toHaveBeenCalledWith(
8,
{ description: 'Neu', amount: 2 },
42,
);
await request(app.getHttpServer())
.delete('/api/v1/penalty/8')
.set('Authorization', 'Bearer user')
.expect(204);
expect(service.deletePenalty).toHaveBeenCalledWith(8, 42);
});
});

View File

@@ -1,29 +1,18 @@
import { Module } from '@nestjs/common';
import { PenaltyController } from './penalty.controller';
import { PenaltyService } from './penalty.service';
import { LoggingModule } from 'src/database/logging/logging.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamWalletTransactionType } from 'src/team-wallet-transactions/entities/team-wallet-transaction-type.entity';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { LoggingModule } from 'src/database/logging/logging.module';
import { Team } from 'src/teams/entities/team.entity';
import { User } from 'src/users/entities/user.entity';
import { Role } from 'src/roles/entities/role.entity';
import { TeamsModule } from 'src/teams/teams.module';
import { PenaltyController } from './penalty.controller';
import { PenaltyEntity } from './entities/penalty.entity';
import { Player } from 'src/players/entities/player.entity';
import { PenaltyService } from './penalty.service';
@Module({
controllers: [PenaltyController],
providers: [PenaltyService],
imports: [
TypeOrmModule.forFeature([
User,
Role,
TeamWalletTransaction,
TeamWalletTransactionType,
Team,
PenaltyEntity,
Player,
]),
TypeOrmModule.forFeature([Team, PenaltyEntity]),
TeamsModule,
LoggingModule,
],
})

View File

@@ -0,0 +1,203 @@
import { ConflictException, NotFoundException } from '@nestjs/common';
import { PenaltyService } from './penalty.service';
describe('PenaltyService catalog management', () => {
const readRepository = { find: jest.fn(), findOne: jest.fn() };
const teamReadRepository = { findOne: jest.fn() };
const teamQuery = {
where: jest.fn().mockReturnThis(),
setLock: jest.fn().mockReturnThis(),
getOne: jest.fn(),
};
const duplicateQuery = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getCount: jest.fn(),
};
const teamRepository = { createQueryBuilder: jest.fn(() => teamQuery) };
const writeRepository = {
createQueryBuilder: jest.fn(() => duplicateQuery),
create: jest.fn((value) => value),
save: jest.fn(),
findOne: jest.fn(),
remove: jest.fn(),
};
const manager = {
getRepository: jest.fn((entity) =>
entity.name === 'Team' ? teamRepository : writeRepository,
),
};
const dataSource = { transaction: jest.fn((work) => work(manager)) };
const access = { assertMember: jest.fn(), assertManager: jest.fn() };
const logger = { info: jest.fn() };
let service: PenaltyService;
beforeEach(() => {
jest.clearAllMocks();
teamQuery.getOne.mockResolvedValue({ id: 5 });
duplicateQuery.getCount.mockResolvedValue(0);
teamReadRepository.findOne.mockResolvedValue({ id: 5 });
service = new PenaltyService(
readRepository as any,
teamReadRepository as any,
dataSource as any,
access as any,
logger as any,
);
});
it('returns not found instead of leaking an unknown team as an empty catalog', async () => {
teamReadRepository.findOne.mockResolvedValue(null);
await expect(service.getTeamPenalties(42, 999)).rejects.toBeInstanceOf(
NotFoundException,
);
expect(access.assertMember).not.toHaveBeenCalled();
});
it('authorizes team reads, sorts them, and maps only safe fields', async () => {
readRepository.find.mockResolvedValue([
{
id: 8,
description: 'Zu spät',
amount: '5.50',
createdAt: new Date('2026-01-02T00:00:00.000Z'),
team: { id: 5, secret: 'hidden' },
},
]);
await expect(service.getTeamPenalties(42, 5)).resolves.toEqual([
{
id: 8,
description: 'Zu spät',
amount: 5.5,
createdAt: new Date('2026-01-02T00:00:00.000Z'),
},
]);
expect(access.assertMember).toHaveBeenCalledWith(42, 5);
expect(readRepository.find).toHaveBeenCalledWith({
where: { team: { id: 5 } },
order: { description: 'ASC' },
});
});
it('creates a normalized entry under a team lock and audits in the transaction', async () => {
writeRepository.save.mockImplementation(async (value) => ({
id: 9,
createdAt: new Date('2026-01-03T00:00:00.000Z'),
...value,
}));
await expect(
service.createPenalty(
{ teamId: 5, description: ' Handy in der Kabine ', amount: 3.25 },
42,
),
).resolves.toMatchObject({
id: 9,
description: 'Handy in der Kabine',
amount: 3.25,
});
expect(teamQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
expect(access.assertManager).toHaveBeenCalledWith(42, 5, manager);
expect(logger.info).toHaveBeenCalledWith(
{
event: 'penalty_catalog_create',
details: 'teamId=5 penaltyId=9 action=create',
userId: 42,
},
manager,
);
});
it('rejects a normalized duplicate before writing or auditing', async () => {
duplicateQuery.getCount.mockResolvedValue(1);
await expect(
service.createPenalty(
{ teamId: 5, description: ' ZU SPÄT ', amount: 5 },
42,
),
).rejects.toBeInstanceOf(ConflictException);
expect(writeRepository.save).not.toHaveBeenCalled();
expect(logger.info).not.toHaveBeenCalled();
});
it('updates an entry in its owning team and excludes itself from duplicate detection', async () => {
readRepository.findOne.mockResolvedValue({ id: 8, team: { id: 5 } });
writeRepository.findOne.mockResolvedValue({
id: 8,
team: { id: 5 },
description: 'Alt',
amount: 1,
createdAt: new Date('2026-01-02T00:00:00.000Z'),
});
writeRepository.save.mockImplementation(async (value) => value);
await expect(
service.updatePenalty(
8,
{ description: ' Neu ', amount: 2.5 },
42,
),
).resolves.toMatchObject({ id: 8, description: 'Neu', amount: 2.5 });
expect(duplicateQuery.andWhere).toHaveBeenCalledWith(
'penalty.id != :penaltyId',
{ penaltyId: 8 },
);
expect(logger.info).toHaveBeenCalledWith(
{
event: 'penalty_catalog_update',
details: 'teamId=5 penaltyId=8 action=update',
userId: 42,
},
manager,
);
});
it('deletes an entry permanently after manager authorization and audits it', async () => {
readRepository.findOne.mockResolvedValue({ id: 8, team: { id: 5 } });
const penalty = { id: 8, team: { id: 5 } };
writeRepository.findOne.mockResolvedValue(penalty);
await service.deletePenalty(8, 42);
expect(writeRepository.remove).toHaveBeenCalledWith(penalty);
expect(logger.info).toHaveBeenCalledWith(
{
event: 'penalty_catalog_delete',
details: 'teamId=5 penaltyId=8 action=delete',
userId: 42,
},
manager,
);
});
it('returns not found when a mutation target does not exist', async () => {
readRepository.findOne.mockResolvedValue(null);
await expect(service.deletePenalty(999, 42)).rejects.toBeInstanceOf(
NotFoundException,
);
expect(dataSource.transaction).not.toHaveBeenCalled();
});
it('rejects the mutation when transactional audit persistence fails', async () => {
writeRepository.save.mockResolvedValue({
id: 9,
team: { id: 5 },
description: 'Neu',
amount: 2,
createdAt: new Date(),
});
logger.info.mockRejectedValue(new Error('audit unavailable'));
await expect(
service.createPenalty(
{ teamId: 5, description: 'Neu', amount: 2 },
42,
),
).rejects.toThrow('audit unavailable');
});
});

View File

@@ -1,61 +1,190 @@
import { Injectable } from '@nestjs/common';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LoggingService } from 'src/database/logging/logging.service';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
import { TeamAccessService } from 'src/teams/team-access.service';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import { PenaltyResponseDTO } from './dto/penalty-response.dto';
import { UpdatePenaltyDTO } from './dto/update-penalty.dto';
import { PenaltyEntity } from './entities/penalty.entity';
import { Player } from 'src/players/entities/player.entity';
@Injectable()
export class PenaltyService {
constructor(
@InjectRepository(Team)
private teamRepository: Repository<Team>,
@InjectRepository(Player)
private playerRepository: Repository<Player>,
@InjectRepository(PenaltyEntity)
private repository: Repository<PenaltyEntity>,
private readonly repository: Repository<PenaltyEntity>,
@InjectRepository(Team)
private readonly teamRepository: Repository<Team>,
private readonly dataSource: DataSource,
private readonly access: TeamAccessService,
private readonly logger: LoggingService,
) {}
async createPenalty(dto: CreatePenaltyDTO, userId: string) {
const player = await this.playerRepository.findOne({
where: { user: { id: Number(userId) }, team: { id: dto.teamId } },
async getTeamPenalties(
userId: number,
teamId: number,
): Promise<PenaltyResponseDTO[]> {
const team = await this.teamRepository.findOne({ where: { id: teamId } });
if (!team) throw new NotFoundException('Team nicht gefunden.');
await this.access.assertMember(userId, teamId);
const penalties = await this.repository.find({
where: { team: { id: teamId } },
order: { description: 'ASC' },
});
return penalties.map((penalty) => this.toResponse(penalty));
}
createPenalty(
dto: CreatePenaltyDTO,
userId: number,
): Promise<PenaltyResponseDTO> {
return this.dataSource.transaction(async (manager) => {
const team = await this.lockTeam(manager, dto.teamId);
await this.access.assertManager(userId, team.id, manager);
const repository = manager.getRepository(PenaltyEntity);
const description = dto.description.trim();
await this.assertUniqueDescription(repository, team.id, description);
const saved = await repository.save(
repository.create({ team, description, amount: dto.amount }),
);
await this.logger.info(
{
event: 'penalty_catalog_create',
details: `teamId=${team.id} penaltyId=${saved.id} action=create`,
userId,
},
manager,
);
return this.toResponse(saved);
});
}
async updatePenalty(
penaltyId: number,
dto: UpdatePenaltyDTO,
userId: number,
): Promise<PenaltyResponseDTO> {
const owner = await this.findOwner(penaltyId);
return this.dataSource.transaction(async (manager) => {
const team = await this.lockTeam(manager, owner.team.id);
await this.access.assertManager(userId, team.id, manager);
const repository = manager.getRepository(PenaltyEntity);
const penalty = await this.findTransactionalPenalty(
repository,
penaltyId,
team.id,
);
const description = dto.description.trim();
await this.assertUniqueDescription(
repository,
team.id,
description,
penaltyId,
);
penalty.description = description;
penalty.amount = dto.amount;
const saved = await repository.save(penalty);
await this.logger.info(
{
event: 'penalty_catalog_update',
details: `teamId=${team.id} penaltyId=${penaltyId} action=update`,
userId,
},
manager,
);
return this.toResponse(saved);
});
}
async deletePenalty(penaltyId: number, userId: number): Promise<void> {
const owner = await this.findOwner(penaltyId);
await this.dataSource.transaction(async (manager) => {
const team = await this.lockTeam(manager, owner.team.id);
await this.access.assertManager(userId, team.id, manager);
const repository = manager.getRepository(PenaltyEntity);
const penalty = await this.findTransactionalPenalty(
repository,
penaltyId,
team.id,
);
await repository.remove(penalty);
await this.logger.info(
{
event: 'penalty_catalog_delete',
details: `teamId=${team.id} penaltyId=${penaltyId} action=delete`,
userId,
},
manager,
);
});
}
private async findOwner(penaltyId: number): Promise<PenaltyEntity> {
const penalty = await this.repository.findOne({
where: { id: penaltyId },
relations: ['team'],
});
if (!player || !player.team) {
return;
if (!penalty?.team) throw new NotFoundException('Strafe nicht gefunden.');
return penalty;
}
dto.team = player.team;
const e = this.repository.create(dto);
return this.repository.save(e);
private async lockTeam(manager: EntityManager, teamId: number): Promise<Team> {
const team = await manager
.getRepository(Team)
.createQueryBuilder('team')
.where('team.id = :teamId', { teamId })
.setLock('pessimistic_write')
.getOne();
if (!team) throw new NotFoundException('Team nicht gefunden.');
return team;
}
getAll(userId: string) {
const id = Number(userId);
return this.repository.find({
where: {
team: { players: { user: { id } } },
},
private async findTransactionalPenalty(
repository: Repository<PenaltyEntity>,
penaltyId: number,
teamId: number,
): Promise<PenaltyEntity> {
const penalty = await repository.findOne({
where: { id: penaltyId, team: { id: teamId } },
relations: ['team'],
});
if (!penalty) throw new NotFoundException('Strafe nicht gefunden.');
return penalty;
}
async getTeamPenalties(teamId: string | number) {
teamId = Number(teamId);
private async assertUniqueDescription(
repository: Repository<PenaltyEntity>,
teamId: number,
description: string,
penaltyId?: number,
): Promise<void> {
const query = repository
.createQueryBuilder('penalty')
.where('penalty.teamId = :teamId', { teamId })
.andWhere('LOWER(TRIM(penalty.description)) = :description', {
description: description.toLocaleLowerCase('de'),
});
if (penaltyId !== undefined) {
query.andWhere('penalty.id != :penaltyId', { penaltyId });
}
if ((await query.getCount()) > 0) {
throw new ConflictException(
'Ein Eintrag mit dieser Beschreibung existiert bereits.',
);
}
}
const res = await this.repository.find({
where: {
team: { id: teamId },
},
});
return res.map((r) => {
r.amount = Number(r.amount);
return r;
});
private toResponse(penalty: PenaltyEntity): PenaltyResponseDTO {
return {
id: penalty.id,
description: penalty.description,
amount: Number(penalty.amount),
createdAt: penalty.createdAt,
};
}
}

View File

@@ -65,6 +65,30 @@ describe('TeamAccessService', () => {
await expect(service.assertManager(2, 9)).resolves.toBeUndefined();
});
it('uses repositories from the supplied transaction manager', async () => {
const transactionalUserRepository = {
findOne: jest.fn().mockResolvedValue({
id: 2,
role: { id: RoleEnum.user },
}),
};
const transactionalPlayerRepository = {
find: jest
.fn()
.mockResolvedValue([{ active: true, teamRole: { id: 3 } }]),
};
const manager = {
getRepository: jest
.fn()
.mockReturnValueOnce(transactionalUserRepository)
.mockReturnValueOnce(transactionalPlayerRepository),
};
await expect(service.assertManager(2, 9, manager as any)).resolves.toBeUndefined();
expect(userRepository.findOne).not.toHaveBeenCalled();
expect(playerRepository.find).not.toHaveBeenCalled();
});
it.each([
['a player', [{ active: true, teamRole: { id: 1 } }]],
['a second treasurer', [{ active: true, teamRole: { id: 2 } }]],

View File

@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Player } from '../players/entities/player.entity';
import { RoleEnum } from '../roles/roles.enum';
import { User } from '../users/entities/user.entity';
import { Repository } from 'typeorm';
import { EntityManager, Repository } from 'typeorm';
@Injectable()
export class TeamAccessService {
@@ -14,23 +14,35 @@ export class TeamAccessService {
private readonly playerRepository: Repository<Player>,
) {}
async assertMember(userId: number, teamId: number): Promise<void> {
await this.assertMinimumRole(userId, teamId, 1);
async assertMember(
userId: number,
teamId: number,
manager?: EntityManager,
): Promise<void> {
await this.assertMinimumRole(userId, teamId, 1, manager);
}
async assertManager(userId: number, teamId: number): Promise<void> {
await this.assertMinimumRole(userId, teamId, 3);
async assertManager(
userId: number,
teamId: number,
manager?: EntityManager,
): Promise<void> {
await this.assertMinimumRole(userId, teamId, 3, manager);
}
private async assertMinimumRole(
userId: number,
teamId: number,
minimumRole: number,
manager?: EntityManager,
): Promise<void> {
const user = await this.userRepository.findOne({ where: { id: userId } });
const userRepository = manager?.getRepository(User) ?? this.userRepository;
const playerRepository =
manager?.getRepository(Player) ?? this.playerRepository;
const user = await userRepository.findOne({ where: { id: userId } });
if (user?.role?.id === RoleEnum.admin) return;
const players = await this.playerRepository.find({
const players = await playerRepository.find({
where: { user: { id: userId }, team: { id: teamId } },
});
const highestActiveRole = players

View File

@@ -31,5 +31,6 @@ import { PenaltyEntity } from '../penalty/entities/penalty.entity';
],
controllers: [TeamsController, PublicTeamsController],
providers: [TeamsService, TeamAccessService, PublicTeamAccessService],
exports: [TeamAccessService],
})
export class TeamsModule {}