feat: add admin log viewer, log retention cleanup, and manual job triggers
Global admins couldn't see the app's event log (no read endpoint or UI existed for it) and had no way to clean up old entries or re-run a scheduled job without touching the database or server directly. Backend: - LoggingService.findLogs() + admin-only LogsController (GET admin/logs) with level/event/date-range/search filtering and pagination, mirroring AdminUsersService.findPlayers(). - LogRetentionScheduler deletes log entries older than LOG_RETENTION_DAYS (default 365, via app.config.ts), following the existing @Cron scheduler pattern. - Admin-only POST admin/run endpoints on CashboxExportController and RecurringTransactionsController that invoke the existing schedulers' public run methods on demand - both are safe to re-run since their "due" queries advance nextRunDate only after a successful run. Frontend: - New /logs page (global-admin gated, same pattern as /users): AG-Grid infinite-scroll table with level/event/date-range/search filters, plus buttons to trigger the two jobs now and see the result land in the grid immediately. - LogsApi, and triggerRunNow() added to the existing CashboxExportApi and RecurringTransactionApi. - Discoverability link from /users to /logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { GUARDS_METADATA } from '@nestjs/common/constants';
|
||||
import { RoleEnum } from '../roles/roles.enum';
|
||||
import { RolesGuard } from '../roles/roles.guard';
|
||||
import { RecurringTransactionsController } from './recurring-transactions.controller';
|
||||
|
||||
describe('RecurringTransactionsController.runDueRecurringTransactionsNow', () => {
|
||||
const service = {
|
||||
getTeamRecurringTransactions: jest.fn(),
|
||||
createRecurringTransaction: jest.fn(),
|
||||
updateRecurringTransaction: jest.fn(),
|
||||
deleteRecurringTransaction: jest.fn(),
|
||||
};
|
||||
const scheduler = { runDueRecurringTransactions: jest.fn() };
|
||||
const controller = new RecurringTransactionsController(service as any, scheduler as any);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('is guarded by the global admin role', () => {
|
||||
expect(
|
||||
Reflect.getMetadata(
|
||||
'roles',
|
||||
RecurringTransactionsController.prototype.runDueRecurringTransactionsNow,
|
||||
),
|
||||
).toEqual([RoleEnum.admin]);
|
||||
expect(
|
||||
Reflect.getMetadata(
|
||||
GUARDS_METADATA,
|
||||
RecurringTransactionsController.prototype.runDueRecurringTransactionsNow,
|
||||
),
|
||||
).toContain(RolesGuard);
|
||||
});
|
||||
|
||||
it('delegates to the scheduler', async () => {
|
||||
await controller.runDueRecurringTransactionsNow();
|
||||
|
||||
expect(scheduler.runDueRecurringTransactions).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,12 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Roles } from '../roles/roles.decorator';
|
||||
import { RoleEnum } from '../roles/roles.enum';
|
||||
import { RolesGuard } from '../roles/roles.guard';
|
||||
import { CreateRecurringTransactionDTO } from './dto/create-recurring-transaction.dto';
|
||||
import { UpdateRecurringTransactionDTO } from './dto/update-recurring-transaction.dto';
|
||||
import { RecurringTransactionsScheduler } from './recurring-transactions.scheduler';
|
||||
import { RecurringTransactionsService } from './recurring-transactions.service';
|
||||
|
||||
type AuthenticatedRequest = { user: { id: number } };
|
||||
@@ -24,7 +28,10 @@ type AuthenticatedRequest = { user: { id: number } };
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Controller({ path: 'recurring-transactions', version: '1' })
|
||||
export class RecurringTransactionsController {
|
||||
constructor(private readonly service: RecurringTransactionsService) {}
|
||||
constructor(
|
||||
private readonly service: RecurringTransactionsService,
|
||||
private readonly scheduler: RecurringTransactionsScheduler,
|
||||
) {}
|
||||
|
||||
@Get(':teamId')
|
||||
getTeamRecurringTransactions(
|
||||
@@ -59,4 +66,12 @@ export class RecurringTransactionsController {
|
||||
): Promise<void> {
|
||||
await this.service.deleteRecurringTransaction(id, request.user.id);
|
||||
}
|
||||
|
||||
@Post('admin/run')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles([RoleEnum.admin])
|
||||
runDueRecurringTransactionsNow(): Promise<void> {
|
||||
return this.scheduler.runDueRecurringTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import validationOptions from '../utils/validation-options';
|
||||
import { TransactionTypeEnum } from '../transactions/transaction-type.enum';
|
||||
import { RecurringTransactionIntervalEnum } from './recurring-transaction-interval.enum';
|
||||
import { RecurringTransactionsController } from './recurring-transactions.controller';
|
||||
import { RecurringTransactionsScheduler } from './recurring-transactions.scheduler';
|
||||
import { RecurringTransactionsService } from './recurring-transactions.service';
|
||||
|
||||
describe('recurring transactions HTTP boundary', () => {
|
||||
@@ -31,12 +32,14 @@ describe('recurring transactions HTTP boundary', () => {
|
||||
updateRecurringTransaction: jest.fn(() => entry),
|
||||
deleteRecurringTransaction: jest.fn(),
|
||||
};
|
||||
const scheduler = { runDueRecurringTransactions: jest.fn() };
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [RecurringTransactionsController],
|
||||
providers: [
|
||||
{ provide: RecurringTransactionsService, useValue: service },
|
||||
{ provide: RecurringTransactionsScheduler, useValue: scheduler },
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard('jwt'))
|
||||
|
||||
Reference in New Issue
Block a user