diff --git a/myteamwallet_backend/src/database/logging/model/logging-event.type.ts b/myteamwallet_backend/src/database/logging/model/logging-event.type.ts index 6b75200..f2cd9a0 100644 --- a/myteamwallet_backend/src/database/logging/model/logging-event.type.ts +++ b/myteamwallet_backend/src/database/logging/model/logging-event.type.ts @@ -39,7 +39,9 @@ export type LOGEVENT = | 'cashbox_export_subscription_run_fail' | 'log_retention_cleanup_run' | 'log_retention_cleanup_run_fail' - | 'notification_create_fail'; + | 'notification_create_fail' + | 'public_access_enabled' + | 'public_access_rotated'; export const LOGEVENT_VALUES: LOGEVENT[] = [ 'user_create', @@ -82,6 +84,8 @@ export const LOGEVENT_VALUES: LOGEVENT[] = [ 'log_retention_cleanup_run', 'log_retention_cleanup_run_fail', 'notification_create_fail', + 'public_access_enabled', + 'public_access_rotated', ]; export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE'; diff --git a/myteamwallet_backend/src/teams/public-team-access.service.spec.ts b/myteamwallet_backend/src/teams/public-team-access.service.spec.ts index 9d820a8..bb3ce9d 100644 --- a/myteamwallet_backend/src/teams/public-team-access.service.spec.ts +++ b/myteamwallet_backend/src/teams/public-team-access.service.spec.ts @@ -13,6 +13,8 @@ describe('PublicTeamAccessService', () => { const penaltyRepository = { find: jest.fn() }; const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() }; let service: PublicTeamAccessService; + let logger: any; + let eventEmitter: any; const managedTeam = { id: 7, @@ -25,12 +27,16 @@ describe('PublicTeamAccessService', () => { beforeEach(() => { jest.resetAllMocks(); teamRepository.save.mockImplementation(async (team) => team); + logger = { info: jest.fn() }; + eventEmitter = { emit: jest.fn() }; service = new PublicTeamAccessService( teamRepository as any, playerRepository as any, transactionRepository as any, penaltyRepository 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)); }); + 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 () => { teamRepository.findOne.mockResolvedValue({ id: 7, diff --git a/myteamwallet_backend/src/teams/public-team-access.service.ts b/myteamwallet_backend/src/teams/public-team-access.service.ts index 368e620..905ea03 100644 --- a/myteamwallet_backend/src/teams/public-team-access.service.ts +++ b/myteamwallet_backend/src/teams/public-team-access.service.ts @@ -1,6 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { EventEmitter2 } from '@nestjs/event-emitter'; 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 { Player } from '../players/entities/player.entity'; import { TeamRolesEnum } from '../team-roles/team-roles.enum'; @@ -28,6 +35,8 @@ export class PublicTeamAccessService { @InjectRepository(PenaltyEntity) private readonly penaltyRepository: Repository, private readonly access: TeamAccessService, + private readonly logger: LoggingService, + private readonly eventEmitter: EventEmitter2, ) {} async getStatus( @@ -55,6 +64,19 @@ export class PublicTeamAccessService { } team.publicAccessEnabled = enabled; 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); } @@ -68,6 +90,17 @@ export class PublicTeamAccessService { const team = await this.loadManagedTeam(teamId); team.publicAccessToken = this.createToken(); 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); }