feat: secure admin user management

This commit is contained in:
Bastian Wagner
2026-07-31 23:37:54 +02:00
parent 4105460400
commit e6acfdcac7
22 changed files with 1598 additions and 148 deletions

View File

@@ -1,18 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingService } from './logging.service';
describe('LoggingService', () => {
let service: LoggingService;
it('can persist an event through the caller transaction manager', async () => {
const defaultRepository = { save: jest.fn() };
const transactionRepository = { save: jest.fn() };
const manager = {
getRepository: jest.fn(() => transactionRepository),
} as any;
const service = new LoggingService(defaultRepository as any);
const event = {
event: 'admin_user_profile_update' as const,
details: 'targetUserId=2',
userId: 1,
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LoggingService],
}).compile();
await service.info(event, manager);
service = module.get<LoggingService>(LoggingService);
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(transactionRepository.save).toHaveBeenCalledWith({
...event,
level: 'INFO',
});
expect(defaultRepository.save).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EntityManager, Repository } from 'typeorm';
import { CreateLogDTO } from './dto/create-log.dto';
import { LogEntry } from './entities/log-entry.entity';
import { LOGEVENT } from './model/logging-event.type';
@@ -23,22 +23,25 @@ export class LoggingService {
});
}
async info({
event,
details,
userId,
}: {
event: LOGEVENT;
details: string;
userId: number;
}) {
async info(
{
event,
details,
userId,
}: {
event: LOGEVENT;
details: string;
userId: number;
},
manager?: EntityManager,
) {
const e: CreateLogDTO = {
event,
details,
userId,
level: 'INFO',
};
await this.repository.save(e);
await (manager?.getRepository(LogEntry) ?? this.repository).save(e);
}
async error({

View File

@@ -15,6 +15,11 @@ export type LOGEVENT =
| 'transaction_create_fail'
| 'transaction_reverse'
| 'player_creation'
| 'admin_user_profile_update'
| 'admin_user_role_update'
| 'admin_user_status_update'
| 'admin_player_assign'
| 'admin_player_unlink'
| 'team_create';
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPlayerLookupIndexes1785517200000 implements MigrationInterface {
name = 'AddPlayerLookupIndexes1785517200000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE INDEX "IDX_player_team_id" ON "player" ("teamId")',
);
await queryRunner.query(
'CREATE INDEX "IDX_player_user_id" ON "player" ("userId")',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "IDX_player_user_id"');
await queryRunner.query('DROP INDEX "IDX_player_team_id"');
}
}

View File

@@ -0,0 +1,33 @@
import { getMetadataArgsStorage } from 'typeorm';
import { Player } from '../../players/entities/player.entity';
import { AddPlayerLookupIndexes1785517200000 } from './1785517200000-AddPlayerLookupIndexes';
describe('AddPlayerLookupIndexes1785517200000', () => {
it('adds reversible indexes for team and user foreign-key lookups', async () => {
const queryRunner = { query: jest.fn() } as any;
const migration = new AddPlayerLookupIndexes1785517200000();
await migration.up(queryRunner);
expect(queryRunner.query.mock.calls.map(([sql]) => sql)).toEqual([
'CREATE INDEX "IDX_player_team_id" ON "player" ("teamId")',
'CREATE INDEX "IDX_player_user_id" ON "player" ("userId")',
]);
queryRunner.query.mockClear();
await migration.down(queryRunner);
expect(queryRunner.query.mock.calls.map(([sql]) => sql)).toEqual([
'DROP INDEX "IDX_player_user_id"',
'DROP INDEX "IDX_player_team_id"',
]);
});
it('keeps entity index metadata aligned with the migration', () => {
const playerIndexes = getMetadataArgsStorage()
.indices.filter((index) => index.target === Player)
.map((index) => index.name);
expect(playerIndexes).toEqual(
expect.arrayContaining(['IDX_player_team_id', 'IDX_player_user_id']),
);
});
});