feat: log and emit notification events on public-access enable/rotate
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user