berechtigungen
This commit is contained in:
@@ -16,7 +16,6 @@ import { DataSource } from 'typeorm';
|
||||
import { PlayersModule } from './players/players.module';
|
||||
import { TeamsModule } from './teams/teams.module';
|
||||
import { TransactionsModule } from './transactions/transactions.module';
|
||||
import { TeamSettingsModule } from './team-settings/team-settings.module';
|
||||
import { TeamWalletTransactionsModule } from './team-wallet-transactions/team-wallet-transactions.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
@@ -52,7 +51,6 @@ import { PenaltyModule } from './penalty/penalty.module';
|
||||
PlayersModule,
|
||||
TeamsModule,
|
||||
TransactionsModule,
|
||||
TeamSettingsModule,
|
||||
TeamWalletTransactionsModule,
|
||||
LoggingModule,
|
||||
TranslateModule,
|
||||
|
||||
@@ -108,10 +108,11 @@ export class AuthController {
|
||||
@Post('invite')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public getInvite(
|
||||
@Request() request,
|
||||
@Body()
|
||||
invite: any,
|
||||
) {
|
||||
return this.service.createTeamInvite(invite);
|
||||
return this.service.createTeamInvite(invite, Number(request.user.id));
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MailModule } from 'src/mail/mail.module';
|
||||
import { IsExist } from 'src/utils/validators/is-exists.validator';
|
||||
import { IsNotExist } from 'src/utils/validators/is-not-exists.validator';
|
||||
import { LoggingModule } from 'src/database/logging/logging.module';
|
||||
import { TeamsModule } from 'src/teams/teams.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -19,6 +20,7 @@ import { LoggingModule } from 'src/database/logging/logging.module';
|
||||
ForgotModule,
|
||||
PassportModule,
|
||||
MailModule,
|
||||
TeamsModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('AuthService inactive-user enforcement and safe logging', () => {
|
||||
mailService,
|
||||
logger,
|
||||
dataSource,
|
||||
{ assertAtLeast: jest.fn() } as any,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import { MailService } from 'src/mail/mail.service';
|
||||
import { CreateInviteDTO } from './dto/create-invite.dto';
|
||||
import { LoggingService } from 'src/database/logging/logging.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { TeamAccessService } from 'src/teams/team-access.service';
|
||||
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -35,6 +37,7 @@ export class AuthService {
|
||||
private mailService: MailService,
|
||||
private logger: LoggingService,
|
||||
private dataSource: DataSource,
|
||||
private teamAccess: TeamAccessService,
|
||||
) {}
|
||||
|
||||
async validateLogin(
|
||||
@@ -302,7 +305,14 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async createTeamInvite(object: CreateInviteDTO) {
|
||||
async createTeamInvite(object: CreateInviteDTO, actorUserId: number) {
|
||||
await this.teamAccess.assertAtLeast(
|
||||
actorUserId,
|
||||
object.teamId,
|
||||
'invite_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
|
||||
const token = await this.jwtService.sign(object, {
|
||||
expiresIn: '30d',
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export type LOGEVENT =
|
||||
| 'penalty_catalog_create'
|
||||
| 'penalty_catalog_update'
|
||||
| 'penalty_catalog_delete'
|
||||
| 'team_create';
|
||||
| 'team_create'
|
||||
| 'team_permissions_update';
|
||||
|
||||
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
const BACKFILLED_KEYS = [
|
||||
'invite_min_role',
|
||||
'member_manage_min_role',
|
||||
'penalty_manage_min_role',
|
||||
'public_access_manage_min_role',
|
||||
] as const;
|
||||
|
||||
export class AddTeamPermissionSettings1785524400000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTeamPermissionSettings1785524400000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const key of BACKFILLED_KEYS) {
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "team_setting" ("teamId", "key", "value")
|
||||
SELECT "id", '${key}', '3' FROM "team"
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM "team_setting" ts
|
||||
WHERE ts."teamId" = "team"."id" AND ts."key" = '${key}'
|
||||
)`,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "team_setting" ("teamId", "key", "value")
|
||||
SELECT "teamId", 'transaction_reverse_min_role', "value" FROM "team_setting"
|
||||
WHERE "key" = 'transaction_create_min_role'
|
||||
AND "teamId" NOT IN (
|
||||
SELECT "teamId" FROM "team_setting" WHERE "key" = 'transaction_reverse_min_role'
|
||||
)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "team_setting" WHERE "key" IN (
|
||||
'transaction_reverse_min_role',
|
||||
'invite_min_role',
|
||||
'member_manage_min_role',
|
||||
'penalty_manage_min_role',
|
||||
'public_access_manage_min_role'
|
||||
)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
describe('AddTeamPermissionSettings1785524400000', () => {
|
||||
it('backfills the new min-role settings for existing teams idempotently', async () => {
|
||||
const migrationModule = require('./1785524400000-AddTeamPermissionSettings');
|
||||
const migration =
|
||||
new migrationModule.AddTeamPermissionSettings1785524400000();
|
||||
const queryRunner = { query: jest.fn() } as any;
|
||||
|
||||
await migration.up(queryRunner);
|
||||
|
||||
const calls: string[] = queryRunner.query.mock.calls.map((c: any) => c[0]);
|
||||
expect(calls).toHaveLength(5);
|
||||
for (const key of [
|
||||
'invite_min_role',
|
||||
'member_manage_min_role',
|
||||
'penalty_manage_min_role',
|
||||
'public_access_manage_min_role',
|
||||
]) {
|
||||
expect(
|
||||
calls.some(
|
||||
(sql) => sql.includes(`'${key}'`) && sql.includes('NOT EXISTS'),
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
expect(
|
||||
calls.some(
|
||||
(sql) =>
|
||||
sql.includes("'transaction_reverse_min_role'") &&
|
||||
sql.includes("'transaction_create_min_role'"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('removes the new min-role settings again on down', async () => {
|
||||
const migrationModule = require('./1785524400000-AddTeamPermissionSettings');
|
||||
const migration =
|
||||
new migrationModule.AddTeamPermissionSettings1785524400000();
|
||||
const queryRunner = { query: jest.fn() } as any;
|
||||
|
||||
await migration.down(queryRunner);
|
||||
|
||||
expect(queryRunner.query).toHaveBeenCalledTimes(1);
|
||||
const sql = queryRunner.query.mock.calls[0][0];
|
||||
expect(sql).toContain('DELETE FROM "team_setting"');
|
||||
expect(sql).toContain('transaction_reverse_min_role');
|
||||
expect(sql).toContain('invite_min_role');
|
||||
expect(sql).toContain('member_manage_min_role');
|
||||
expect(sql).toContain('penalty_manage_min_role');
|
||||
expect(sql).toContain('public_access_manage_min_role');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { PenaltyService } from './penalty.service';
|
||||
|
||||
describe('PenaltyService catalog management', () => {
|
||||
@@ -28,7 +29,7 @@ describe('PenaltyService catalog management', () => {
|
||||
),
|
||||
};
|
||||
const dataSource = { transaction: jest.fn((work) => work(manager)) };
|
||||
const access = { assertMember: jest.fn(), assertManager: jest.fn() };
|
||||
const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() };
|
||||
const logger = { info: jest.fn() };
|
||||
let service: PenaltyService;
|
||||
|
||||
@@ -127,7 +128,13 @@ describe('PenaltyService catalog management', () => {
|
||||
});
|
||||
|
||||
expect(teamQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
|
||||
expect(access.assertManager).toHaveBeenCalledWith(42, 5, manager);
|
||||
expect(access.assertAtLeast).toHaveBeenCalledWith(
|
||||
42,
|
||||
5,
|
||||
'penalty_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
manager,
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
{
|
||||
event: 'penalty_catalog_create',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LoggingService } from 'src/database/logging/logging.service';
|
||||
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
|
||||
import { Team } from 'src/teams/entities/team.entity';
|
||||
import { TeamAccessService } from 'src/teams/team-access.service';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
@@ -55,7 +56,13 @@ export class PenaltyService {
|
||||
): Promise<PenaltyResponseDTO> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const team = await this.lockTeam(manager, dto.teamId);
|
||||
await this.access.assertManager(userId, team.id, manager);
|
||||
await this.access.assertAtLeast(
|
||||
userId,
|
||||
team.id,
|
||||
'penalty_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
manager,
|
||||
);
|
||||
const repository = manager.getRepository(PenaltyEntity);
|
||||
const description = dto.description.trim();
|
||||
await this.assertUniqueDescription(repository, team.id, description);
|
||||
@@ -82,7 +89,13 @@ export class PenaltyService {
|
||||
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);
|
||||
await this.access.assertAtLeast(
|
||||
userId,
|
||||
team.id,
|
||||
'penalty_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
manager,
|
||||
);
|
||||
const repository = manager.getRepository(PenaltyEntity);
|
||||
const penalty = await this.findTransactionalPenalty(
|
||||
repository,
|
||||
@@ -115,7 +128,13 @@ export class PenaltyService {
|
||||
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);
|
||||
await this.access.assertAtLeast(
|
||||
userId,
|
||||
team.id,
|
||||
'penalty_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
manager,
|
||||
);
|
||||
const repository = manager.getRepository(PenaltyEntity);
|
||||
const penalty = await this.findTransactionalPenalty(
|
||||
repository,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const TeamRoles = (...teamRoles: number[]) =>
|
||||
SetMetadata('teamRoles', teamRoles);
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class TeamRolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const roles = this.reflector.getAllAndOverride<number[]>('teamRoles', [
|
||||
context.getClass(),
|
||||
context.getHandler(),
|
||||
]);
|
||||
if (!roles.length) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
return roles.includes(request.user?.teamRoles?.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { TeamSettingType } from '../teams/model/team-settings-type';
|
||||
|
||||
export const TEAM_SETTING_DEFAULTS: Record<
|
||||
Exclude<TeamSettingType, ''>,
|
||||
TeamRolesEnum
|
||||
> = {
|
||||
transaction_create_min_role: TeamRolesEnum.scnd_treasurer,
|
||||
transaction_reverse_min_role: TeamRolesEnum.scnd_treasurer,
|
||||
invite_min_role: TeamRolesEnum.captain,
|
||||
member_manage_min_role: TeamRolesEnum.captain,
|
||||
penalty_manage_min_role: TeamRolesEnum.captain,
|
||||
public_access_manage_min_role: TeamRolesEnum.captain,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TeamSettingsModule])],
|
||||
})
|
||||
export class TeamSettingsModule {}
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { LoggingModule } from 'src/database/logging/logging.module';
|
||||
import { Team } from 'src/teams/entities/team.entity';
|
||||
import { TeamsModule } from 'src/teams/teams.module';
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
import { TeamWalletTransactionType } from './entities/team-wallet-transaction-type.entity';
|
||||
import { TeamWalletTransaction } from './entities/team-wallet-transaction.entity';
|
||||
@@ -17,6 +18,7 @@ import { TeamWalletTransactionsService } from './team-wallet-transactions.servic
|
||||
Team,
|
||||
]),
|
||||
LoggingModule,
|
||||
TeamsModule,
|
||||
],
|
||||
controllers: [TeamWalletTransactionsController],
|
||||
providers: [TeamWalletTransactionsService],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LoggingService } from 'src/database/logging/logging.service';
|
||||
import { RoleEnum } from 'src/roles/roles.enum';
|
||||
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
|
||||
import { TeamAccessService } from 'src/teams/team-access.service';
|
||||
import { Team } from 'src/teams/entities/team.entity';
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -21,6 +24,7 @@ export class TeamWalletTransactionsService {
|
||||
@InjectRepository(Team)
|
||||
private teamRepository: Repository<Team>,
|
||||
private logger: LoggingService,
|
||||
private access: TeamAccessService,
|
||||
) {}
|
||||
|
||||
async create(data: CreateTeamWalletTransactionDto, userId: string) {
|
||||
@@ -28,32 +32,20 @@ export class TeamWalletTransactionsService {
|
||||
where: {
|
||||
id: Number(userId),
|
||||
},
|
||||
relations: ['players', 'players.team', 'players.team.settings'],
|
||||
});
|
||||
|
||||
const teamPlayer = creatingUser.players.find(
|
||||
(p) =>
|
||||
p.team.id == data.teamId &&
|
||||
p.teamRole.id >=
|
||||
Number(
|
||||
p.team.settings.find((s) => s.key == 'transaction_create_min_role')[
|
||||
'value'
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (creatingUser.role.id != 1 && !teamPlayer) {
|
||||
return;
|
||||
if (creatingUser.role.id != RoleEnum.admin) {
|
||||
await this.access.assertAtLeast(
|
||||
Number(userId),
|
||||
data.teamId,
|
||||
'transaction_create_min_role',
|
||||
TeamRolesEnum.scnd_treasurer,
|
||||
);
|
||||
}
|
||||
|
||||
let team: Team;
|
||||
if (teamPlayer && teamPlayer.team) {
|
||||
team = teamPlayer.team;
|
||||
} else {
|
||||
team = await this.teamRepository.findOneByOrFail({
|
||||
id: data.teamId,
|
||||
});
|
||||
}
|
||||
const team = await this.teamRepository.findOneByOrFail({
|
||||
id: data.teamId,
|
||||
});
|
||||
|
||||
const transactionType =
|
||||
await this.teamWalletTransactionTypeRepository.findOne({
|
||||
|
||||
51
myteamwallet_backend/src/teams/dto/team-permissions.dto.ts
Normal file
51
myteamwallet_backend/src/teams/dto/team-permissions.dto.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IsIn, IsInt, IsOptional } from 'class-validator';
|
||||
import { TeamRolesEnum } from '../../team-roles/team-roles.enum';
|
||||
|
||||
const VALID_ROLES = [
|
||||
TeamRolesEnum.player,
|
||||
TeamRolesEnum.scnd_treasurer,
|
||||
TeamRolesEnum.captain,
|
||||
TeamRolesEnum.treasurer,
|
||||
TeamRolesEnum.coach,
|
||||
];
|
||||
|
||||
export class UpdateTeamPermissionsDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
transactionCreateMinRole?: TeamRolesEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
transactionReverseMinRole?: TeamRolesEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
inviteMinRole?: TeamRolesEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
memberManageMinRole?: TeamRolesEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
penaltyManageMinRole?: TeamRolesEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsIn(VALID_ROLES)
|
||||
publicAccessManageMinRole?: TeamRolesEnum;
|
||||
}
|
||||
|
||||
export interface TeamPermissionsDto {
|
||||
transactionCreateMinRole: number;
|
||||
transactionReverseMinRole: number;
|
||||
inviteMinRole: number;
|
||||
memberManageMinRole: number;
|
||||
penaltyManageMinRole: number;
|
||||
publicAccessManageMinRole: number;
|
||||
}
|
||||
@@ -1 +1,8 @@
|
||||
export type TeamSettingType = 'transaction_create_min_role' | '';
|
||||
export type TeamSettingType =
|
||||
| 'transaction_create_min_role'
|
||||
| 'transaction_reverse_min_role'
|
||||
| 'invite_min_role'
|
||||
| 'member_manage_min_role'
|
||||
| 'penalty_manage_min_role'
|
||||
| 'public_access_manage_min_role'
|
||||
| '';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { PublicTeamAccessService } from './public-team-access.service';
|
||||
|
||||
describe('PublicTeamAccessService', () => {
|
||||
@@ -10,7 +11,7 @@ describe('PublicTeamAccessService', () => {
|
||||
const playerRepository = { find: jest.fn(), findOne: jest.fn() };
|
||||
const transactionRepository = { find: jest.fn() };
|
||||
const penaltyRepository = { find: jest.fn() };
|
||||
const access = { assertMember: jest.fn(), assertManager: jest.fn() };
|
||||
const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() };
|
||||
let service: PublicTeamAccessService;
|
||||
|
||||
const managedTeam = {
|
||||
@@ -48,7 +49,12 @@ describe('PublicTeamAccessService', () => {
|
||||
|
||||
const status = await service.setEnabled(4, 7, true);
|
||||
|
||||
expect(access.assertManager).toHaveBeenCalledWith(4, 7);
|
||||
expect(access.assertAtLeast).toHaveBeenCalledWith(
|
||||
4,
|
||||
7,
|
||||
'public_access_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
expect(status.enabled).toBe(true);
|
||||
expect(status.token).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(teamRepository.save).toHaveBeenCalledWith(team);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { PenaltyEntity } from '../penalty/entities/penalty.entity';
|
||||
import { Player } from '../players/entities/player.entity';
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { Transaction } from '../transactions/entitites/transaction.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
@@ -42,7 +43,12 @@ export class PublicTeamAccessService {
|
||||
teamId: number,
|
||||
enabled: boolean,
|
||||
): Promise<PublicAccessStatusDto> {
|
||||
await this.access.assertManager(userId, teamId);
|
||||
await this.access.assertAtLeast(
|
||||
userId,
|
||||
teamId,
|
||||
'public_access_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
const team = await this.loadManagedTeam(teamId);
|
||||
if (enabled && !team.publicAccessToken) {
|
||||
team.publicAccessToken = this.createToken();
|
||||
@@ -53,7 +59,12 @@ export class PublicTeamAccessService {
|
||||
}
|
||||
|
||||
async rotate(userId: number, teamId: number): Promise<PublicAccessStatusDto> {
|
||||
await this.access.assertManager(userId, teamId);
|
||||
await this.access.assertAtLeast(
|
||||
userId,
|
||||
teamId,
|
||||
'public_access_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
const team = await this.loadManagedTeam(teamId);
|
||||
team.publicAccessToken = this.createToken();
|
||||
await this.teamRepository.save(team);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import { RoleEnum } from '../roles/roles.enum';
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { TeamAccessService } from './team-access.service';
|
||||
|
||||
describe('TeamAccessService', () => {
|
||||
const userRepository = { findOne: jest.fn() };
|
||||
const playerRepository = { find: jest.fn() };
|
||||
const teamSettingRepository = { findOne: jest.fn() };
|
||||
let service: TeamAccessService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -12,6 +14,7 @@ describe('TeamAccessService', () => {
|
||||
service = new TeamAccessService(
|
||||
userRepository as any,
|
||||
playerRepository as any,
|
||||
teamSettingRepository as any,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -105,4 +108,104 @@ describe('TeamAccessService', () => {
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
describe('assertAtLeast', () => {
|
||||
it('uses the configured team setting instead of the fallback', async () => {
|
||||
userRepository.findOne.mockResolvedValue({
|
||||
id: 2,
|
||||
role: { id: RoleEnum.user },
|
||||
});
|
||||
teamSettingRepository.findOne.mockResolvedValue({ value: '4' });
|
||||
playerRepository.find.mockResolvedValue([
|
||||
{ active: true, teamRole: { id: 3 } },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.assertAtLeast(2, 9, 'invite_min_role', TeamRolesEnum.captain),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('falls back to the given minimum role when no setting exists', async () => {
|
||||
userRepository.findOne.mockResolvedValue({
|
||||
id: 2,
|
||||
role: { id: RoleEnum.user },
|
||||
});
|
||||
teamSettingRepository.findOne.mockResolvedValue(null);
|
||||
playerRepository.find.mockResolvedValue([
|
||||
{ active: true, teamRole: { id: 3 } },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.assertAtLeast(2, 9, 'invite_min_role', TeamRolesEnum.captain),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to the given minimum role when the stored value is invalid', async () => {
|
||||
userRepository.findOne.mockResolvedValue({
|
||||
id: 2,
|
||||
role: { id: RoleEnum.user },
|
||||
});
|
||||
teamSettingRepository.findOne.mockResolvedValue({ value: 'not-a-role' });
|
||||
playerRepository.find.mockResolvedValue([
|
||||
{ active: true, teamRole: { id: 3 } },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.assertAtLeast(2, 9, 'invite_min_role', TeamRolesEnum.captain),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('lets a global admin bypass the check without reading settings', async () => {
|
||||
userRepository.findOne.mockResolvedValue({
|
||||
id: 1,
|
||||
role: { id: RoleEnum.admin },
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.assertAtLeast(1, 9, 'invite_min_role', TeamRolesEnum.captain),
|
||||
).resolves.toBeUndefined();
|
||||
expect(teamSettingRepository.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reads the team setting 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: 4 } }]),
|
||||
};
|
||||
const transactionalTeamSettingRepository = {
|
||||
findOne: jest.fn().mockResolvedValue({ value: '4' }),
|
||||
};
|
||||
const manager = {
|
||||
getRepository: jest
|
||||
.fn()
|
||||
.mockImplementation((entity) => {
|
||||
if (entity.name === 'TeamSetting') {
|
||||
return transactionalTeamSettingRepository;
|
||||
}
|
||||
if (entity.name === 'User') return transactionalUserRepository;
|
||||
return transactionalPlayerRepository;
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.assertAtLeast(
|
||||
2,
|
||||
9,
|
||||
'penalty_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
manager as any,
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
expect(teamSettingRepository.findOne).not.toHaveBeenCalled();
|
||||
expect(userRepository.findOne).not.toHaveBeenCalled();
|
||||
expect(playerRepository.find).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Player } from '../players/entities/player.entity';
|
||||
import { RoleEnum } from '../roles/roles.enum';
|
||||
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
|
||||
import { TeamSetting } from '../team-settings/entities/team-setting.entity';
|
||||
import { TeamSettingType } from './model/team-settings-type';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
@@ -12,6 +15,8 @@ export class TeamAccessService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Player)
|
||||
private readonly playerRepository: Repository<Player>,
|
||||
@InjectRepository(TeamSetting)
|
||||
private readonly teamSettingRepository: Repository<TeamSetting>,
|
||||
) {}
|
||||
|
||||
async assertMember(
|
||||
@@ -30,10 +35,42 @@ export class TeamAccessService {
|
||||
await this.assertMinimumRole(userId, teamId, 3, manager);
|
||||
}
|
||||
|
||||
async assertAtLeast(
|
||||
userId: number,
|
||||
teamId: number,
|
||||
settingKey: Exclude<TeamSettingType, ''>,
|
||||
fallbackMinRole: TeamRolesEnum,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
await this.assertMinimumRole(
|
||||
userId,
|
||||
teamId,
|
||||
() => this.resolveMinimumRole(teamId, settingKey, fallbackMinRole, manager),
|
||||
manager,
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveMinimumRole(
|
||||
teamId: number,
|
||||
settingKey: Exclude<TeamSettingType, ''>,
|
||||
fallbackMinRole: TeamRolesEnum,
|
||||
manager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const teamSettingRepository =
|
||||
manager?.getRepository(TeamSetting) ?? this.teamSettingRepository;
|
||||
const setting = await teamSettingRepository.findOne({
|
||||
where: { team: { id: teamId }, key: settingKey },
|
||||
});
|
||||
const parsed = setting ? Number(setting.value) : NaN;
|
||||
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 5
|
||||
? parsed
|
||||
: fallbackMinRole;
|
||||
}
|
||||
|
||||
private async assertMinimumRole(
|
||||
userId: number,
|
||||
teamId: number,
|
||||
minimumRole: number,
|
||||
minimumRole: number | (() => Promise<number>),
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const userRepository = manager?.getRepository(User) ?? this.userRepository;
|
||||
@@ -42,6 +79,9 @@ export class TeamAccessService {
|
||||
const user = await userRepository.findOne({ where: { id: userId } });
|
||||
if (user?.role?.id === RoleEnum.admin) return;
|
||||
|
||||
const resolvedMinimumRole =
|
||||
typeof minimumRole === 'function' ? await minimumRole() : minimumRole;
|
||||
|
||||
const players = await playerRepository.find({
|
||||
where: { user: { id: userId }, team: { id: teamId } },
|
||||
});
|
||||
@@ -52,7 +92,7 @@ export class TeamAccessService {
|
||||
0,
|
||||
);
|
||||
|
||||
if (highestActiveRole < minimumRole) {
|
||||
if (highestActiveRole < resolvedMinimumRole) {
|
||||
throw new ForbiddenException('Keine Berechtigung für dieses Team.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ describe('TeamMembersService', () => {
|
||||
};
|
||||
dataSource = { transaction: jest.fn((work) => work(manager)) };
|
||||
logger = { info: jest.fn() };
|
||||
access = { assertManager: jest.fn(() => Promise.resolve()) };
|
||||
access = { assertAtLeast: jest.fn(() => Promise.resolve()) };
|
||||
service = new TeamMembersService(dataSource, logger, access as any);
|
||||
});
|
||||
|
||||
it('checks the team-manager permission before touching the database', async () => {
|
||||
access.assertManager.mockRejectedValue(new Error('forbidden'));
|
||||
access.assertAtLeast.mockRejectedValue(new Error('forbidden'));
|
||||
await expect(service.setActive(5, teamId, player.id, false)).rejects.toThrow(
|
||||
'forbidden',
|
||||
);
|
||||
|
||||
@@ -26,7 +26,12 @@ export class TeamMembersService {
|
||||
playerId: number,
|
||||
active: boolean,
|
||||
): Promise<Player> {
|
||||
await this.access.assertManager(actorUserId, teamId);
|
||||
await this.access.assertAtLeast(
|
||||
actorUserId,
|
||||
teamId,
|
||||
'member_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const activeTreasurers = await this.lockActiveTreasurers(manager, teamId);
|
||||
@@ -71,7 +76,12 @@ export class TeamMembersService {
|
||||
playerId: number,
|
||||
teamRoleId: TeamRolesEnum,
|
||||
): Promise<Player> {
|
||||
await this.access.assertManager(actorUserId, teamId);
|
||||
await this.access.assertAtLeast(
|
||||
actorUserId,
|
||||
teamId,
|
||||
'member_manage_min_role',
|
||||
TeamRolesEnum.captain,
|
||||
);
|
||||
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const activeTreasurers = await this.lockActiveTreasurers(manager, teamId);
|
||||
|
||||
108
myteamwallet_backend/src/teams/team-permissions.service.spec.ts
Normal file
108
myteamwallet_backend/src/teams/team-permissions.service.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import { TeamPermissionsService } from './team-permissions.service';
|
||||
|
||||
describe('TeamPermissionsService', () => {
|
||||
const teamRepository = { findOne: jest.fn() };
|
||||
const teamSettingRepository = {
|
||||
save: jest.fn((s) => Promise.resolve(s)),
|
||||
create: jest.fn((s) => s),
|
||||
};
|
||||
const access = { assertMember: jest.fn(), assertManager: jest.fn() };
|
||||
const logger = { info: jest.fn() };
|
||||
let service: TeamPermissionsService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
teamSettingRepository.save.mockImplementation((s) => Promise.resolve(s));
|
||||
teamSettingRepository.create.mockImplementation((s) => s);
|
||||
service = new TeamPermissionsService(
|
||||
teamRepository as any,
|
||||
teamSettingRepository as any,
|
||||
access as any,
|
||||
logger as any,
|
||||
);
|
||||
});
|
||||
|
||||
const teamWithSettings = (overrides: Record<string, string> = {}) => ({
|
||||
id: 9,
|
||||
settings: [
|
||||
{ key: 'transaction_create_min_role', value: overrides.transaction_create_min_role ?? '2' },
|
||||
{ key: 'transaction_reverse_min_role', value: overrides.transaction_reverse_min_role ?? '2' },
|
||||
{ key: 'invite_min_role', value: overrides.invite_min_role ?? '3' },
|
||||
{ key: 'member_manage_min_role', value: overrides.member_manage_min_role ?? '3' },
|
||||
{ key: 'penalty_manage_min_role', value: overrides.penalty_manage_min_role ?? '3' },
|
||||
{ key: 'public_access_manage_min_role', value: overrides.public_access_manage_min_role ?? '3' },
|
||||
],
|
||||
});
|
||||
|
||||
it('requires at least team membership to read permissions', async () => {
|
||||
access.assertMember.mockRejectedValue(new ForbiddenException());
|
||||
await expect(service.getPermissions(2, 9)).rejects.toBeInstanceOf(
|
||||
ForbiddenException,
|
||||
);
|
||||
expect(teamRepository.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('maps the stored settings to the permissions DTO', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
teamRepository.findOne.mockResolvedValue(teamWithSettings());
|
||||
|
||||
await expect(service.getPermissions(2, 9)).resolves.toEqual({
|
||||
transactionCreateMinRole: 2,
|
||||
transactionReverseMinRole: 2,
|
||||
inviteMinRole: 3,
|
||||
memberManageMinRole: 3,
|
||||
penaltyManageMinRole: 3,
|
||||
publicAccessManageMinRole: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to defaults for missing settings', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
teamRepository.findOne.mockResolvedValue({ id: 9, settings: [] });
|
||||
|
||||
await expect(service.getPermissions(2, 9)).resolves.toEqual({
|
||||
transactionCreateMinRole: 2,
|
||||
transactionReverseMinRole: 2,
|
||||
inviteMinRole: 3,
|
||||
memberManageMinRole: 3,
|
||||
penaltyManageMinRole: 3,
|
||||
publicAccessManageMinRole: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('requires manager rights to update permissions', async () => {
|
||||
access.assertManager.mockRejectedValue(new ForbiddenException());
|
||||
await expect(
|
||||
service.updatePermissions(2, 9, { inviteMinRole: 4 }),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(teamSettingRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates only the fields present in the DTO and leaves the rest untouched', async () => {
|
||||
access.assertManager.mockResolvedValue(undefined);
|
||||
const team = teamWithSettings();
|
||||
teamRepository.findOne.mockResolvedValue(team);
|
||||
|
||||
await service.updatePermissions(2, 9, { inviteMinRole: 4 });
|
||||
|
||||
expect(teamSettingRepository.save).toHaveBeenCalledTimes(1);
|
||||
const saved = teamSettingRepository.save.mock.calls[0][0];
|
||||
expect(saved).toMatchObject({ key: 'invite_min_role', value: '4' });
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'team_permissions_update', userId: 2 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a new setting row if none exists yet for that key', async () => {
|
||||
access.assertManager.mockResolvedValue(undefined);
|
||||
teamRepository.findOne.mockResolvedValue({ id: 9, settings: [] });
|
||||
|
||||
await service.updatePermissions(2, 9, { memberManageMinRole: 4 });
|
||||
|
||||
expect(teamSettingRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: 'member_manage_min_role', value: '4' }),
|
||||
);
|
||||
expect(teamSettingRepository.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
104
myteamwallet_backend/src/teams/team-permissions.service.ts
Normal file
104
myteamwallet_backend/src/teams/team-permissions.service.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LoggingService } from '../database/logging/logging.service';
|
||||
import { TeamSetting } from '../team-settings/entities/team-setting.entity';
|
||||
import { TEAM_SETTING_DEFAULTS } from '../team-settings/team-setting-defaults';
|
||||
import {
|
||||
TeamPermissionsDto,
|
||||
UpdateTeamPermissionsDto,
|
||||
} from './dto/team-permissions.dto';
|
||||
import { Team } from './entities/team.entity';
|
||||
import { TeamSettingType } from './model/team-settings-type';
|
||||
import { TeamAccessService } from './team-access.service';
|
||||
|
||||
type PermissionField = keyof TeamPermissionsDto;
|
||||
|
||||
const FIELD_TO_SETTING_KEY: Record<
|
||||
PermissionField,
|
||||
Exclude<TeamSettingType, ''>
|
||||
> = {
|
||||
transactionCreateMinRole: 'transaction_create_min_role',
|
||||
transactionReverseMinRole: 'transaction_reverse_min_role',
|
||||
inviteMinRole: 'invite_min_role',
|
||||
memberManageMinRole: 'member_manage_min_role',
|
||||
penaltyManageMinRole: 'penalty_manage_min_role',
|
||||
publicAccessManageMinRole: 'public_access_manage_min_role',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class TeamPermissionsService {
|
||||
constructor(
|
||||
@InjectRepository(Team)
|
||||
private readonly teamRepository: Repository<Team>,
|
||||
@InjectRepository(TeamSetting)
|
||||
private readonly teamSettingRepository: Repository<TeamSetting>,
|
||||
private readonly access: TeamAccessService,
|
||||
private readonly logger: LoggingService,
|
||||
) {}
|
||||
|
||||
async getPermissions(
|
||||
userId: number,
|
||||
teamId: number,
|
||||
): Promise<TeamPermissionsDto> {
|
||||
await this.access.assertMember(userId, teamId);
|
||||
const team = await this.loadTeam(teamId);
|
||||
return this.toDto(team.settings ?? []);
|
||||
}
|
||||
|
||||
async updatePermissions(
|
||||
userId: number,
|
||||
teamId: number,
|
||||
dto: UpdateTeamPermissionsDto,
|
||||
): Promise<TeamPermissionsDto> {
|
||||
await this.access.assertManager(userId, teamId);
|
||||
const team = await this.loadTeam(teamId);
|
||||
|
||||
for (const field of Object.keys(FIELD_TO_SETTING_KEY) as PermissionField[]) {
|
||||
const value = dto[field];
|
||||
if (value === undefined) continue;
|
||||
const key = FIELD_TO_SETTING_KEY[field];
|
||||
const existing = (team.settings ?? []).find((s) => s.key === key);
|
||||
if (existing) {
|
||||
existing.value = String(value);
|
||||
await this.teamSettingRepository.save(existing);
|
||||
} else {
|
||||
await this.teamSettingRepository.save(
|
||||
this.teamSettingRepository.create({ team, key, value: String(value) }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.logger.info({
|
||||
event: 'team_permissions_update',
|
||||
details: `teamId=${teamId} changed=${Object.keys(dto).join(',')}`,
|
||||
userId,
|
||||
});
|
||||
|
||||
const updatedTeam = await this.loadTeam(teamId);
|
||||
return this.toDto(updatedTeam.settings ?? []);
|
||||
}
|
||||
|
||||
private async loadTeam(teamId: number): Promise<Team> {
|
||||
const team = await this.teamRepository.findOne({
|
||||
where: { id: teamId },
|
||||
relations: ['settings'],
|
||||
});
|
||||
if (!team) throw new NotFoundException('Team nicht gefunden.');
|
||||
return team;
|
||||
}
|
||||
|
||||
private toDto(settings: TeamSetting[]): TeamPermissionsDto {
|
||||
const result = {} as TeamPermissionsDto;
|
||||
for (const field of Object.keys(FIELD_TO_SETTING_KEY) as PermissionField[]) {
|
||||
const key = FIELD_TO_SETTING_KEY[field];
|
||||
const setting = settings.find((s) => s.key === key);
|
||||
const parsed = setting ? Number(setting.value) : NaN;
|
||||
result[field] =
|
||||
Number.isInteger(parsed) && parsed >= 1 && parsed <= 5
|
||||
? parsed
|
||||
: TEAM_SETTING_DEFAULTS[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import { PublicTeamAccessService } from './public-team-access.service';
|
||||
import { UpdatePublicAccessDto } from './dto/public-access.dto';
|
||||
import { UpdatePlayerProfileDto } from './dto/update-player-profile.dto';
|
||||
import { PlayerActiveDto, PlayerTeamRoleDto } from './dto/player-management.dto';
|
||||
import { UpdateTeamPermissionsDto } from './dto/team-permissions.dto';
|
||||
import { TeamMembersService } from './team-members.service';
|
||||
import { TeamPermissionsService } from './team-permissions.service';
|
||||
|
||||
@ApiTags('Teams')
|
||||
@Controller({
|
||||
@@ -35,6 +37,7 @@ export class TeamsController {
|
||||
private service: TeamsService,
|
||||
private publicAccess: PublicTeamAccessService,
|
||||
private teamMembers: TeamMembersService,
|
||||
private teamPermissions: TeamPermissionsService,
|
||||
) {}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -62,6 +65,34 @@ export class TeamsController {
|
||||
return this.publicAccess.rotate(Number(req.user.id), id);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Team-Berechtigungen abrufen',
|
||||
description:
|
||||
'Gibt die pro Team konfigurierte Mindestrolle je Aktion zurück.',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get(':id/permissions')
|
||||
getPermissions(@Req() req, @Param('id', ParseIntPipe) id: number) {
|
||||
return this.teamPermissions.getPermissions(Number(req.user.id), id);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Team-Berechtigungen ändern',
|
||||
description:
|
||||
'Setzt die Mindestrolle je Aktion für dieses Team. Nur Team-Manager (Kapitän aufwärts).',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Patch(':id/permissions')
|
||||
updatePermissions(
|
||||
@Req() req,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() body: UpdateTeamPermissionsDto,
|
||||
) {
|
||||
return this.teamPermissions.updatePermissions(Number(req.user.id), id, body);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get(':id/players/:playerId/transactions')
|
||||
@@ -82,8 +113,8 @@ export class TeamsController {
|
||||
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||
@Get(':id/overview')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.service.getOverview(id);
|
||||
findOne(@Req() req, @Param('id') id: string) {
|
||||
return this.service.getOverview(id, req.user?.id);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -121,8 +152,12 @@ export class TeamsController {
|
||||
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||
@Post(':id/players')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
createPlayer(@Param('id') id: string, @Body() playerDto: any) {
|
||||
return this.service.createNewPlayer(id, playerDto);
|
||||
createPlayer(
|
||||
@Req() req,
|
||||
@Param('id') id: string,
|
||||
@Body() playerDto: any,
|
||||
) {
|
||||
return this.service.createNewPlayer(id, playerDto, req.user?.id);
|
||||
}
|
||||
|
||||
@Roles([RoleEnum.user, RoleEnum.admin])
|
||||
@@ -133,8 +168,12 @@ export class TeamsController {
|
||||
})
|
||||
@Put(':id/players')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
updatePlayer(@Body() playerDto: UpdatePlayerProfileDto) {
|
||||
return this.service.updatePlayer(playerDto);
|
||||
updatePlayer(
|
||||
@Req() req,
|
||||
@Param('id') id: string,
|
||||
@Body() playerDto: UpdatePlayerProfileDto,
|
||||
) {
|
||||
return this.service.updatePlayer(id, playerDto, req.user?.id);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PublicTeamAccessService } from './public-team-access.service';
|
||||
import { PublicTeamsController } from './public-teams.controller';
|
||||
import { PenaltyEntity } from '../penalty/entities/penalty.entity';
|
||||
import { TeamMembersService } from './team-members.service';
|
||||
import { TeamPermissionsService } from './team-permissions.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -38,6 +39,7 @@ import { TeamMembersService } from './team-members.service';
|
||||
TeamAccessService,
|
||||
PublicTeamAccessService,
|
||||
TeamMembersService,
|
||||
TeamPermissionsService,
|
||||
],
|
||||
exports: [TeamAccessService],
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Player } from 'src/players/entities/player.entity';
|
||||
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
|
||||
import { CreateTeamSettingDTO } from 'src/team-settings/dto/create-team-setting.dto';
|
||||
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
|
||||
import { TEAM_SETTING_DEFAULTS } from 'src/team-settings/team-setting-defaults';
|
||||
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
|
||||
import { Transaction } from 'src/transactions/entitites/transaction.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -32,9 +33,11 @@ export class TeamsService {
|
||||
private access: TeamAccessService,
|
||||
) {}
|
||||
|
||||
async getOverview(teamId: string) {
|
||||
async getOverview(teamId: string, actorUserId: string) {
|
||||
const id = Number(teamId);
|
||||
|
||||
await this.access.assertMember(Number(actorUserId), id);
|
||||
|
||||
const team = await this.repository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: ['players'],
|
||||
@@ -104,7 +107,10 @@ export class TeamsService {
|
||||
async createNewPlayer(
|
||||
id: string,
|
||||
player: { firstName: string; lastName: string; teamRole: any },
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.access.assertManager(Number(actorUserId), Number(id));
|
||||
|
||||
if (typeof player.teamRole == 'string') {
|
||||
player.teamRole = await this.rolesRepository.findOneBy({
|
||||
name: player.teamRole,
|
||||
@@ -156,24 +162,26 @@ export class TeamsService {
|
||||
return team;
|
||||
}
|
||||
|
||||
private generateBasicTeamSettings(team: Team): Promise<void> {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const s: CreateTeamSettingDTO = {
|
||||
key: 'transaction_create_min_role',
|
||||
value: '2',
|
||||
team,
|
||||
};
|
||||
private async generateBasicTeamSettings(team: Team): Promise<void> {
|
||||
const settings: CreateTeamSettingDTO[] = Object.entries(
|
||||
TEAM_SETTING_DEFAULTS,
|
||||
).map(([key, value]) => ({
|
||||
key: key as CreateTeamSettingDTO['key'],
|
||||
value: String(value),
|
||||
team,
|
||||
}));
|
||||
|
||||
const setting = this.settingsRepository.create(s);
|
||||
await this.settingsRepository.save(setting);
|
||||
resolve();
|
||||
});
|
||||
await this.settingsRepository.save(
|
||||
settings.map((s) => this.settingsRepository.create(s)),
|
||||
);
|
||||
}
|
||||
|
||||
async getTeamTransactions(teamId: string | number, userId: string | number) {
|
||||
const start = new Date();
|
||||
teamId = Number(teamId);
|
||||
|
||||
await this.access.assertMember(Number(userId), teamId);
|
||||
|
||||
const team = await this.repository.findOneOrFail({
|
||||
where: {
|
||||
id: teamId,
|
||||
@@ -343,13 +351,24 @@ export class TeamsService {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
async updatePlayer(playerDTO: UpdatePlayerProfileDto) {
|
||||
async updatePlayer(
|
||||
teamId: string,
|
||||
playerDTO: UpdatePlayerProfileDto,
|
||||
actorUserId: string,
|
||||
) {
|
||||
const player = await this.playerRepository.findOneOrFail({
|
||||
where: {
|
||||
id: playerDTO.id,
|
||||
},
|
||||
relations: ['team'],
|
||||
});
|
||||
|
||||
if (player.team?.id !== Number(teamId)) {
|
||||
throw new HttpException('No Player Found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.access.assertManager(Number(actorUserId), Number(teamId));
|
||||
|
||||
player.firstName = playerDTO.firstName;
|
||||
player.lastName = playerDTO.lastName;
|
||||
|
||||
|
||||
@@ -6,19 +6,14 @@ import { TransactionsService } from './transactions.service';
|
||||
import { TransactionsController } from './transactions.controller';
|
||||
import { Player } from 'src/players/entities/player.entity';
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
|
||||
import { LoggingModule } from 'src/database/logging/logging.module';
|
||||
import { TeamsModule } from 'src/teams/teams.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
TransactionType,
|
||||
Transaction,
|
||||
Player,
|
||||
User,
|
||||
TeamSetting,
|
||||
]),
|
||||
TypeOrmModule.forFeature([TransactionType, Transaction, Player, User]),
|
||||
LoggingModule,
|
||||
TeamsModule,
|
||||
],
|
||||
providers: [TransactionsService],
|
||||
controllers: [TransactionsController],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
@@ -8,6 +7,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LoggingService } from 'src/database/logging/logging.service';
|
||||
import { Player } from 'src/players/entities/player.entity';
|
||||
import { RoleEnum } from 'src/roles/roles.enum';
|
||||
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
|
||||
import { TeamAccessService } from 'src/teams/team-access.service';
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateTransactionDto } from './dto/create-transaction.dto';
|
||||
@@ -27,6 +28,7 @@ export class TransactionsService {
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
private logger: LoggingService,
|
||||
private access: TeamAccessService,
|
||||
) {}
|
||||
|
||||
async createTransactions(data: CreateTransactionDto[], userId: string) {
|
||||
@@ -40,50 +42,35 @@ export class TransactionsService {
|
||||
async create(data: CreateTransactionDto, userId: string) {
|
||||
const player = await this.playersRepository.findOne({
|
||||
where: { id: data.playerId },
|
||||
relations: ['team'],
|
||||
});
|
||||
|
||||
const creatingUser = await this.usersRepository.findOne({
|
||||
where: {
|
||||
id: Number(userId),
|
||||
},
|
||||
relations: ['players'],
|
||||
});
|
||||
|
||||
const transactionType = await this.transactionTypesRepository.findOne({
|
||||
where: { id: TransactionTypeEnum[TransactionTypeEnum[data.type]] },
|
||||
});
|
||||
|
||||
if (!player || !transactionType || !creatingUser) {
|
||||
await this.logger.warn({
|
||||
event: 'transaction_create_fail',
|
||||
details: `Player: ${data.playerId}, amount: ${data.amount}, typeEnum: ${data.type}`,
|
||||
userId: Number(userId),
|
||||
});
|
||||
throw new NotFoundException('Spieler oder Buchungstyp nicht gefunden.');
|
||||
}
|
||||
|
||||
if (creatingUser.role.id != RoleEnum.admin) {
|
||||
if (
|
||||
!player ||
|
||||
!transactionType ||
|
||||
!creatingUser ||
|
||||
!creatingUser.players ||
|
||||
creatingUser.players.length == 0
|
||||
) {
|
||||
await this.logger.warn({
|
||||
event: 'transaction_create_fail',
|
||||
details: `Player: ${data.playerId}, amount: ${data.amount}, typeEnum: ${data.type}`,
|
||||
userId: Number(userId),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const teamPlayer = creatingUser.players.find(
|
||||
(p) =>
|
||||
p.team.id == player.team.id &&
|
||||
p.teamRole.id >=
|
||||
Number(
|
||||
player.team.settings.find(
|
||||
(s) => s.key == 'transaction_create_min_role',
|
||||
)['value'],
|
||||
),
|
||||
await this.access.assertAtLeast(
|
||||
Number(userId),
|
||||
player.team.id,
|
||||
'transaction_create_min_role',
|
||||
TeamRolesEnum.scnd_treasurer,
|
||||
);
|
||||
|
||||
if (!teamPlayer) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const transaction = this.transactionsRepository.create({
|
||||
@@ -131,26 +118,15 @@ export class TransactionsService {
|
||||
|
||||
const creatingUser = await this.usersRepository.findOne({
|
||||
where: { id: Number(userId) },
|
||||
relations: ['players'],
|
||||
});
|
||||
|
||||
if (creatingUser.role.id != RoleEnum.admin) {
|
||||
const teamPlayer = creatingUser.players?.find(
|
||||
(p) =>
|
||||
p.team.id == original.player.team.id &&
|
||||
p.teamRole.id >=
|
||||
Number(
|
||||
original.player.team.settings.find(
|
||||
(s) => s.key == 'transaction_create_min_role',
|
||||
)?.['value'] ?? 0,
|
||||
),
|
||||
await this.access.assertAtLeast(
|
||||
Number(userId),
|
||||
original.player.team.id,
|
||||
'transaction_reverse_min_role',
|
||||
TeamRolesEnum.scnd_treasurer,
|
||||
);
|
||||
|
||||
if (!teamPlayer) {
|
||||
throw new ForbiddenException(
|
||||
'Keine Berechtigung, diese Buchung zu stornieren',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const originalAmount = Math.abs(Number(original.amount));
|
||||
|
||||
Reference in New Issue
Block a user