@vita-mojo/audit-logs
v0.1.9
Published
NestJS building blocks for capturing an audit trail of HTTP requests/responses per `x-audit-id`, shipping them to SQS, and storing/querying them against a TypeORM-backed `audit_logs` table.
Downloads
3,843
Keywords
Readme
@vita-mojo/audit-logs
NestJS building blocks for capturing an audit trail of HTTP requests/responses
per x-audit-id, shipping them to SQS, and storing/querying them against a
TypeORM-backed audit_logs table.
How it fits together
AuditMiddlewarereads thex-audit-idrequest header and binds it to an async-local-storage scope (AuditContext) for the lifetime of the request, so any code downstream — including code outside Nest's request object, e.g. background subscribers — can recover the current audit id viaAuditContext.getAuditId().AuditInterceptor(wired up automatically byAuditModuleas a globalAPP_INTERCEPTOR) fires once on the way in and once on the way out of every request that carries anx-audit-idheader. It builds anAuditLogentry (service, tenant, user, path, headers/body or response, timestamp, and an optionalscope) and pushes it onto theaudit-logsSQS queue via@ssut/nestjs-sqs. Requests withoutx-audit-idare skipped, and any error while building/sending a log is swallowed (logged viaconsole.warn) so audit logging can never break the actual request.AuditScopeis an optional method decorator consumers can attach to a controller handler to attach extra structured context (stores,settings,entities) to that endpoint's audit log entries. The interceptor reads it back viaReflectorand merges the return value intoAuditLog.scope.AuditControllerexposesGET /audit/:auditIdandDELETE /audit/:auditId, guarded byAuditGuard, which checks the request'sAuthorization: Bearer <token>header againstoptions.serviceAccountToken.AuditServiceruns the rawSELECT/DELETEqueries behind those two endpoints against theaudit_logstable, through the connection described below.addAuditLogTable/addAuditLogTriggerare TypeORM migration helpers: the former creates theaudit_logstable (+ an index onaudit_id), the latter installs/removes per-entity MySQL triggers that populate it onINSERT/UPDATE/DELETE, driven by the@AuditTrigger()class decorator (see below).
Setup
import { AuditModule } from '@vita-mojo/audit-logs';
import { getConnectionToken } from '@nestjs/typeorm';
@Module({
imports: [
AuditModule.register({
queueUrl: process.env.AUDIT_LOGS_QUEUE_URL,
region: process.env.AWS_REGION,
serviceAccountToken: process.env.AUDIT_SERVICE_ACCOUNT_TOKEN,
service: 'my-service',
connectionToken: getConnectionToken(), // getDataSourceToken() for typorm version >= 0.3.x
}),
],
})
export class AppModule {}Why connectionToken is required
AuditService needs the app's TypeORM connection injected, but the class
that connection is registered under differs by TypeORM major version:
Connection on 0.2.x, DataSource from 0.3.x onwards (0.2.x never exported
DataSource; 1.x removed Connection entirely). NestJS's implicit,
type-based DI can only resolve a concrete class reference — an interface or
any parameter type erases to Object at compile time and can't be resolved
at all — so there is no single TypeScript type that works across every
supported TypeORM version.
Instead, AuditModule.register() takes the DI token the connection is
already registered under in your app (connectionToken) and rebinds it
internally to this package's own token (AUDIT_DATA_SOURCE) via
useExisting. AuditService and the migration helpers only depend on a
narrow structural type (AuditConnection: query() + getMetadata()) that
has been stable across TypeORM 0.2.x–1.1.x, so the package itself never
imports a TypeORM class.
The easiest way to get the right value regardless of your TypeORM version is
@nestjs/typeorm's own getConnectionToken() (deprecated alias) /
getDataSourceToken() helper — it already resolves to whichever class your
installed TypeORM version actually exports. Passing the class directly
(Connection or DataSource) also works if you don't use @nestjs/typeorm's
default connection name.
Supported TypeORM range: >=0.2.0 (peer dependency), verified against
0.2.45, 0.3.x, and 1.1.x.
Other options
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------- |
| queueUrl | SQS queue URL audit logs are sent to. No producer is registered if falsy. |
| region | AWS region for the SQS producer. |
| serviceAccountToken | Bearer token required to call the AuditController endpoints. |
| service | Label stamped on every AuditLog entry (AuditLog.service). |
| connectionToken | DI token for the app's TypeORM connection — see above. |
Tracking a table with triggers
import { AuditTrigger } from '@vita-mojo/audit-logs';
@AuditTrigger({ trackOldValuesOnUpdate: ['status'] })
@Entity()
export class Order {
/* ... */
}trackOldValuesOnUpdate is optional; listed columns get both col (new
value) and old_col (previous value) recorded on UPDATE, everything else
just gets the new value. Wire the triggers up per-entity in a migration:
import { addAuditLogTrigger } from '@vita-mojo/audit-logs';
import { Order } from '../entities/Order';
export class AddOrderAuditTrigger implements MigrationInterface {
up(queryRunner: QueryRunner) {
return addAuditLogTrigger.up(queryRunner, Order);
}
down(queryRunner: QueryRunner) {
return addAuditLogTrigger.down(queryRunner, Order);
}
}Run addAuditLogTable.up(queryRunner) once (e.g. in an initial migration) to
create the audit_logs table these triggers write into.
Building
Run nx build audit-logs to build the library.
Testing
Run nx test audit-logs. Coverage includes AuditService, AuditModule's
connection-token wiring, AuditInterceptor (audit-id gating, tenant/user
resolution, the superadmin tenant fallback, scope resolution and its error
handling, and the swallow-all-errors behavior), AuditContext/
AuditMiddleware, decodeAuthorizationToken, the @AuditTrigger decorator
and Trigger SQL rendering, and both migration helpers (addAuditLogTable,
addAuditLogTrigger).
