mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat: implement audit logs
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
# Copy to .env for local/docker compose (not committed).
|
||||
PORT=3001
|
||||
# @tria-plc/auditlog's client interceptor stamps every AuditLog row's
|
||||
# `application` from this env var directly, bypassing MezgebModule.forRoot's
|
||||
# applicationName option (package quirk). audit.controller.ts reads the same
|
||||
# var when filtering reads, so this can be anything as long as it's set.
|
||||
APPLICATION_NAME=freight-api
|
||||
# Also required for @tria-plc/auditlog: its producer (AuditClientModule)
|
||||
# reads the RMQ URL at package IMPORT time, before MezgebModule.forRoot's
|
||||
# rmqUrl option ever runs, so only an env var reaches it — an in-code
|
||||
# override is too late. Without this, audit events are silently dropped
|
||||
# (no error, nothing published). Point it at whatever broker/vhost your
|
||||
# RabbitMQ actually has a user provisioned on.
|
||||
RABBITMQ_URL=amqp://localhost:5672
|
||||
# GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables.
|
||||
GT06_TCP_PORT=5023
|
||||
DB_HOST=localhost
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { IamBaselineSeeder, IamSeedModule } from "@edr/iam-seed";
|
||||
import { IamModule } from "@tria-plc/iamapi-common";
|
||||
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
||||
import { MezgebModule } from "@tria-plc/auditlog";
|
||||
|
||||
import appConfig from "./config/app.config";
|
||||
import databaseConfig from "./config/database.config";
|
||||
@@ -105,9 +106,14 @@ import { LastMileModule } from "./modules/last-mile/last-mile.module";
|
||||
import { InterchangeDocumentsModule } from "./modules/interchange-documents/interchange-documents.module";
|
||||
import { ImportOperationsModule } from "./modules/import-operations/import-operations.module";
|
||||
import { AiModule } from "./modules/ai/ai.module";
|
||||
import { AuditModule } from "./modules/audit/audit.module";
|
||||
import { LoggerMiddleware } from "./logger.middleware";
|
||||
import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware";
|
||||
|
||||
if (!process.env.APPLICATION_NAME) {
|
||||
process.env.APPLICATION_NAME = "freight";
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@@ -155,6 +161,19 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
|
||||
return dataSource;
|
||||
},
|
||||
}),
|
||||
// Request + entity-level audit logging over RabbitMQ (@tria-plc/auditlog).
|
||||
// Must come after TypeOrmModule above so it picks up this app's DataSource.
|
||||
// rmqUrl falls back the same way notifications.module.ts's RABBITMQ_URL
|
||||
// does: the dev broker only provisions the `edr` user on the `payment`
|
||||
// vhost (docker-compose's RABBITMQ_DEFAULT_USER/VHOST), so an unset
|
||||
// RABBITMQ_URL must land there too, not on guest@'/' (403 ACCESS_REFUSED).
|
||||
MezgebModule.forRoot({
|
||||
applicationName: "freight-api",
|
||||
rmqUrl:
|
||||
process.env.RABBITMQ_URL ??
|
||||
process.env.PAYMENT_RABBITMQ_URL ??
|
||||
"amqp://localhost:5672",
|
||||
}),
|
||||
SharedAuthModule,
|
||||
IamModule.forRoot({
|
||||
applications: [EDR_FREIGHT_APPLICATION],
|
||||
@@ -227,6 +246,7 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
|
||||
VerifaydaModule,
|
||||
FleetHistoryModule,
|
||||
AiModule,
|
||||
AuditModule,
|
||||
],
|
||||
providers: [
|
||||
EdrOrgSeeder,
|
||||
|
||||
@@ -56,6 +56,11 @@ import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/i
|
||||
import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity";
|
||||
import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity";
|
||||
import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity";
|
||||
import { AuditLog, AuditLogCommand } from "@tria-plc/auditlog";
|
||||
|
||||
// @tria-plc/auditlog's entities live in node_modules, same as the iam ones —
|
||||
// the glob below only matches this app's own src/**/*.entity.ts.
|
||||
const auditEntities = [AuditLog, AuditLogCommand];
|
||||
|
||||
const iamEntities = [
|
||||
UnitSetting,
|
||||
@@ -177,7 +182,11 @@ export function buildDataSourceOptions(): DataSourceOptions {
|
||||
return {
|
||||
...buildConnectionOptions(),
|
||||
schema: "public",
|
||||
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
||||
entities: [
|
||||
__dirname + "/../**/*.entity.{ts,js}",
|
||||
...iamEntities,
|
||||
...auditEntities,
|
||||
],
|
||||
migrations: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ResponseTransformInterceptor,
|
||||
createValidationPipe,
|
||||
} from "@edr/api-common";
|
||||
import { getAuditLoggerConfig } from "@tria-plc/auditlog";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
@@ -160,6 +161,13 @@ export async function createFreightApp(): Promise<NestExpressApplication> {
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalInterceptors(new ResponseTransformInterceptor());
|
||||
|
||||
// Audit listener: consumes the RMQ events MezgebModule's client interceptor
|
||||
// (app.module.ts) emits and persists them via the AuditLogController /
|
||||
// AuditLogCommandController @EventPattern handlers. Same queue config the
|
||||
// client side uses, reused from the package so the two never drift apart.
|
||||
app.connectMicroservice(getAuditLoggerConfig());
|
||||
await app.startAllMicroservices();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle("EDR Freight API")
|
||||
.setDescription("API for the EDR Freight Management application")
|
||||
|
||||
32
apps/edr-freight-api/src/modules/audit/audit.controller.ts
Normal file
32
apps/edr-freight-api/src/modules/audit/audit.controller.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Controller, Get, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { AuditService } from "./audit.service";
|
||||
|
||||
@ApiTags("audit")
|
||||
@Controller("audit")
|
||||
@BookingStaff(FREIGHT_PERMS.audit.view)
|
||||
export class AuditController {
|
||||
constructor(private readonly auditService: AuditService) {}
|
||||
|
||||
@Get("logs")
|
||||
@ApiOperation({ summary: "List freight-api audit log commands" })
|
||||
@ApiQuery({ name: "skip", type: Number, required: false })
|
||||
@ApiQuery({ name: "take", type: Number, required: false })
|
||||
list(@Query("skip") skip?: string, @Query("take") take?: string) {
|
||||
// Same fallback chain @tria-plc/auditlog's client interceptor uses to
|
||||
// stamp AuditLog.application (mezgeb/client/client-audit.interceptor.js)
|
||||
// — reading it here instead of a hardcoded literal means this can't
|
||||
// silently drift out of sync with whatever APPLICATION_NAME/APP_NAME
|
||||
// actually is at runtime.
|
||||
const application =
|
||||
process.env.APPLICATION_NAME ?? process.env.APP_NAME ?? "DEFAULT";
|
||||
return this.auditService.list(
|
||||
application,
|
||||
skip !== undefined ? parseInt(skip, 10) : undefined,
|
||||
take !== undefined ? parseInt(take, 10) : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
13
apps/edr-freight-api/src/modules/audit/audit.module.ts
Normal file
13
apps/edr-freight-api/src/modules/audit/audit.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { AuditLogCommand } from "@tria-plc/auditlog";
|
||||
|
||||
import { AuditController } from "./audit.controller";
|
||||
import { AuditService } from "./audit.service";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AuditLogCommand])],
|
||||
controllers: [AuditController],
|
||||
providers: [AuditService],
|
||||
})
|
||||
export class AuditModule {}
|
||||
59
apps/edr-freight-api/src/modules/audit/audit.service.ts
Normal file
59
apps/edr-freight-api/src/modules/audit/audit.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { AuditLogCommand } from "@tria-plc/auditlog";
|
||||
|
||||
export interface AuditLogListResult {
|
||||
count: number;
|
||||
items: AuditLogCommand[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Own read path onto @tria-plc/auditlog's tables, gated by AuditController's
|
||||
* @BookingStaff — the package's own AuditLogCommandController (mounted at
|
||||
* /api/audit-log-commands) ships with no guards at all, so it can't be used
|
||||
* directly for a permission-gated UI. Query mirrors the package's
|
||||
* AuditLogCommandService.buildAuditLogQuery/getAllAuditLogs exactly.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(
|
||||
@InjectRepository(AuditLogCommand)
|
||||
private readonly auditLogCommandRepository: Repository<AuditLogCommand>,
|
||||
) {}
|
||||
|
||||
async list(
|
||||
application: string,
|
||||
skip = 0,
|
||||
take = 10,
|
||||
): Promise<AuditLogListResult> {
|
||||
const [items, count] = await this.auditLogCommandRepository
|
||||
.createQueryBuilder("audit_log_commands")
|
||||
.leftJoinAndSelect("audit_log_commands.auditLog", "auditLog")
|
||||
.andWhere(
|
||||
"(audit_log_commands.auditLogId IS NULL OR auditLog.application = :application)",
|
||||
{ application },
|
||||
)
|
||||
.andWhere(
|
||||
"(audit_log_commands.auditLogId IS NULL OR auditLog.status = :status)",
|
||||
{ status: "Commit" },
|
||||
)
|
||||
.select([
|
||||
"audit_log_commands.id",
|
||||
"audit_log_commands.createdAt",
|
||||
"audit_log_commands.deletedAt",
|
||||
"audit_log_commands.entityName",
|
||||
"audit_log_commands.queryMethod",
|
||||
"audit_log_commands.changes",
|
||||
"audit_log_commands.payload",
|
||||
"auditLog.id",
|
||||
"auditLog.user",
|
||||
])
|
||||
.addOrderBy("audit_log_commands.createdAt", "DESC")
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getManyAndCount();
|
||||
|
||||
return { count, items };
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||
import { DataSource, EntityManager, In } from "typeorm";
|
||||
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { CompaniesService } from "../companies/companies.service";
|
||||
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
|
||||
import { PaymentService } from "../payment/payment.service";
|
||||
@@ -1192,6 +1193,17 @@ export class BillingService {
|
||||
.getRepository(Invoice)
|
||||
.update({ id: invoice.id }, { paymentId: result.intentId });
|
||||
|
||||
// CBE_BILL: the bill reference IS the booking's PNR — the number the customer pays against
|
||||
// at any CBE channel. Persist it on the booking so it survives the initiate response and
|
||||
// shows on the booking/contract everywhere. The payment service reissues the same reference
|
||||
// while the bill stays open, so re-initiating overwrites with an identical value.
|
||||
const billReference = result.response.clientAction?.billReference;
|
||||
if (billReference && invoice.source === Freight.InvoiceSource.Booking) {
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update({ id: invoice.sourceId }, { pnrCode: billReference });
|
||||
}
|
||||
|
||||
// Settlement is driven by the payment API (webhook/outbox → payment.succeeded);
|
||||
// billing must not simulate it. Kept for local demos only.
|
||||
// An OTP intent (CAC Bank) is NOT paid yet — the payer still has to enter the
|
||||
|
||||
@@ -366,6 +366,7 @@ export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [
|
||||
perm('b4a00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:file_upload:manage', 'Manage file-upload settings'),
|
||||
perm('b4b00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:dropdown:view', 'View dropdown settings'),
|
||||
perm('b4b00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:dropdown:manage', 'Manage dropdown settings'),
|
||||
perm('b4c00001-0001-4000-8000-000000000001', 'edr_freight_app:audit:view', 'View audit logs'),
|
||||
];
|
||||
|
||||
// M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment
|
||||
@@ -700,6 +701,9 @@ export const FREIGHT_PERMS = {
|
||||
manage: 'edr_freight_app:settings:dropdown:manage',
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
view: 'edr_freight_app:audit:view',
|
||||
},
|
||||
staff: {
|
||||
roles: {
|
||||
view: 'edr_freight_app:staff:roles:view',
|
||||
|
||||
Reference in New Issue
Block a user