feat: log and emit notification events on public-access enable/rotate

This commit is contained in:
Bastian Wagner
2026-08-04 19:21:01 +02:00
parent d6733eff0d
commit 017e6445fa
3 changed files with 85 additions and 1 deletions

View File

@@ -39,7 +39,9 @@ export type LOGEVENT =
| 'cashbox_export_subscription_run_fail' | 'cashbox_export_subscription_run_fail'
| 'log_retention_cleanup_run' | 'log_retention_cleanup_run'
| 'log_retention_cleanup_run_fail' | 'log_retention_cleanup_run_fail'
| 'notification_create_fail'; | 'notification_create_fail'
| 'public_access_enabled'
| 'public_access_rotated';
export const LOGEVENT_VALUES: LOGEVENT[] = [ export const LOGEVENT_VALUES: LOGEVENT[] = [
'user_create', 'user_create',
@@ -82,6 +84,8 @@ export const LOGEVENT_VALUES: LOGEVENT[] = [
'log_retention_cleanup_run', 'log_retention_cleanup_run',
'log_retention_cleanup_run_fail', 'log_retention_cleanup_run_fail',
'notification_create_fail', 'notification_create_fail',
'public_access_enabled',
'public_access_rotated',
]; ];
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE'; export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';

View File

@@ -13,6 +13,8 @@ describe('PublicTeamAccessService', () => {
const penaltyRepository = { find: jest.fn() }; const penaltyRepository = { find: jest.fn() };
const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() }; const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() };
let service: PublicTeamAccessService; let service: PublicTeamAccessService;
let logger: any;
let eventEmitter: any;
const managedTeam = { const managedTeam = {
id: 7, id: 7,
@@ -25,12 +27,16 @@ describe('PublicTeamAccessService', () => {
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
teamRepository.save.mockImplementation(async (team) => team); teamRepository.save.mockImplementation(async (team) => team);
logger = { info: jest.fn() };
eventEmitter = { emit: jest.fn() };
service = new PublicTeamAccessService( service = new PublicTeamAccessService(
teamRepository as any, teamRepository as any,
playerRepository as any, playerRepository as any,
transactionRepository as any, transactionRepository as any,
penaltyRepository as any, penaltyRepository as any,
access as any, access as any,
logger as any,
eventEmitter as any,
); );
}); });
@@ -82,6 +88,47 @@ describe('PublicTeamAccessService', () => {
expect(status.token).not.toBe('a'.repeat(64)); expect(status.token).not.toBe('a'.repeat(64));
}); });
it('logs and emits when public access is enabled', async () => {
mockManagedTeam();
await service.setEnabled(4, 7, true);
expect(logger.info).toHaveBeenCalledWith({
event: 'public_access_enabled',
details: 'teamId=7',
userId: 4,
});
expect(eventEmitter.emit).toHaveBeenCalledWith(
'notifications.public_access.enabled',
expect.objectContaining({ teamId: 7, actorUserId: 4 }),
);
});
it('does not log or emit when public access is disabled', async () => {
mockManagedTeam({ ...managedTeam, publicAccessEnabled: true, publicAccessToken: 'a'.repeat(64) });
await service.setEnabled(4, 7, false);
expect(logger.info).not.toHaveBeenCalled();
expect(eventEmitter.emit).not.toHaveBeenCalled();
});
it('logs and emits when the token is rotated', async () => {
mockManagedTeam({ ...managedTeam, publicAccessEnabled: true, publicAccessToken: 'a'.repeat(64) });
await service.rotate(4, 7);
expect(logger.info).toHaveBeenCalledWith({
event: 'public_access_rotated',
details: 'teamId=7',
userId: 4,
});
expect(eventEmitter.emit).toHaveBeenCalledWith(
'notifications.public_access.rotated',
expect.objectContaining({ teamId: 7, actorUserId: 4 }),
);
});
it('returns only whitelisted public team fields and active players', async () => { it('returns only whitelisted public team fields and active players', async () => {
teamRepository.findOne.mockResolvedValue({ teamRepository.findOne.mockResolvedValue({
id: 7, id: 7,

View File

@@ -1,6 +1,13 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { LoggingService } from '../database/logging/logging.service';
import { NOTIFICATION_EVENT_NAME } from '../notifications/events/notification-event-names';
import {
PublicAccessEnabledEvent,
PublicAccessRotatedEvent,
} from '../notifications/events/public-access-changed.event';
import { PenaltyEntity } from '../penalty/entities/penalty.entity'; import { PenaltyEntity } from '../penalty/entities/penalty.entity';
import { Player } from '../players/entities/player.entity'; import { Player } from '../players/entities/player.entity';
import { TeamRolesEnum } from '../team-roles/team-roles.enum'; import { TeamRolesEnum } from '../team-roles/team-roles.enum';
@@ -28,6 +35,8 @@ export class PublicTeamAccessService {
@InjectRepository(PenaltyEntity) @InjectRepository(PenaltyEntity)
private readonly penaltyRepository: Repository<PenaltyEntity>, private readonly penaltyRepository: Repository<PenaltyEntity>,
private readonly access: TeamAccessService, private readonly access: TeamAccessService,
private readonly logger: LoggingService,
private readonly eventEmitter: EventEmitter2,
) {} ) {}
async getStatus( async getStatus(
@@ -55,6 +64,19 @@ export class PublicTeamAccessService {
} }
team.publicAccessEnabled = enabled; team.publicAccessEnabled = enabled;
await this.teamRepository.save(team); await this.teamRepository.save(team);
if (enabled) {
await this.logger.info({
event: 'public_access_enabled',
details: `teamId=${teamId}`,
userId,
});
this.eventEmitter.emit(
NOTIFICATION_EVENT_NAME.publicAccessEnabled,
new PublicAccessEnabledEvent(teamId, userId),
);
}
return this.toStatus(team); return this.toStatus(team);
} }
@@ -68,6 +90,17 @@ export class PublicTeamAccessService {
const team = await this.loadManagedTeam(teamId); const team = await this.loadManagedTeam(teamId);
team.publicAccessToken = this.createToken(); team.publicAccessToken = this.createToken();
await this.teamRepository.save(team); await this.teamRepository.save(team);
await this.logger.info({
event: 'public_access_rotated',
details: `teamId=${teamId}`,
userId,
});
this.eventEmitter.emit(
NOTIFICATION_EVENT_NAME.publicAccessRotated,
new PublicAccessRotatedEvent(teamId, userId),
);
return this.toStatus(team); return this.toStatus(team);
} }