@nest-toolbox/typeorm-audit-log 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/audit-context.d.ts +67 -0
- package/dist/audit-context.js +96 -0
- package/dist/audit-context.js.map +1 -0
- package/dist/audit-context.middleware.d.ts +76 -0
- package/dist/audit-context.middleware.js +91 -0
- package/dist/audit-context.middleware.js.map +1 -0
- package/dist/audit-log.entity.d.ts +56 -0
- package/dist/audit-log.entity.js +89 -0
- package/dist/audit-log.entity.js.map +1 -0
- package/dist/audit-log.module.d.ts +56 -0
- package/dist/audit-log.module.js +130 -0
- package/dist/audit-log.module.js.map +1 -0
- package/dist/audit-log.service.d.ts +43 -0
- package/dist/audit-log.service.js +177 -0
- package/dist/audit-log.service.js.map +1 -0
- package/dist/audit-log.subscriber.d.ts +66 -0
- package/dist/audit-log.subscriber.js +135 -0
- package/dist/audit-log.subscriber.js.map +1 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +49 -0
- package/dist/constants.js.map +1 -0
- package/dist/decorators.d.ts +109 -0
- package/dist/decorators.js +161 -0
- package/dist/decorators.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +161 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/diff.d.ts +16 -0
- package/dist/utils/diff.js +142 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/mask.d.ts +15 -0
- package/dist/utils/mask.js +69 -0
- package/dist/utils/mask.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 bogdanlupu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @nest-toolbox/typeorm-audit-log
|
|
2
|
+
|
|
3
|
+
Automatic audit logging for TypeORM entities with user attribution, diff tracking, and queryable audit trails.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@nest-toolbox/typeorm-audit-log)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🔄 **Automatic tracking** - Entity changes logged via TypeORM subscribers
|
|
11
|
+
- 👤 **User attribution** - Captures who made each change via AsyncLocalStorage
|
|
12
|
+
- 📊 **Diff calculation** - Shows exactly what changed between old and new values
|
|
13
|
+
- 🔍 **Queryable history** - Find audit logs by entity, user, or time range
|
|
14
|
+
- 🛡️ **Field exclusions** - Skip sensitive fields like passwords
|
|
15
|
+
- ⚡ **Async mode** - Non-blocking audit writes for performance
|
|
16
|
+
- 🎯 **Selective auditing** - Only track entities you care about
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @nest-toolbox/typeorm-audit-log
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Import the module
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// app.module.ts
|
|
30
|
+
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
|
31
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
32
|
+
import { AuditLogModule, AuditContextMiddleware } from '@nest-toolbox/typeorm-audit-log';
|
|
33
|
+
|
|
34
|
+
@Module({
|
|
35
|
+
imports: [
|
|
36
|
+
TypeOrmModule.forRoot({
|
|
37
|
+
// ... your database config
|
|
38
|
+
}),
|
|
39
|
+
AuditLogModule.forRoot({
|
|
40
|
+
retentionDays: 90,
|
|
41
|
+
excludeFields: ['password', 'refreshToken'],
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
export class AppModule implements NestModule {
|
|
46
|
+
configure(consumer: MiddlewareConsumer) {
|
|
47
|
+
// Required: captures user context for audit logs
|
|
48
|
+
consumer.apply(AuditContextMiddleware).forRoutes('*');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Mark entities as auditable
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// user.entity.ts
|
|
57
|
+
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
|
58
|
+
import { Auditable } from '@nest-toolbox/typeorm-audit-log';
|
|
59
|
+
|
|
60
|
+
@Entity()
|
|
61
|
+
@Auditable()
|
|
62
|
+
export class User {
|
|
63
|
+
@PrimaryGeneratedColumn()
|
|
64
|
+
id: number;
|
|
65
|
+
|
|
66
|
+
@Column()
|
|
67
|
+
name: string;
|
|
68
|
+
|
|
69
|
+
@Column()
|
|
70
|
+
email: string;
|
|
71
|
+
|
|
72
|
+
@Column()
|
|
73
|
+
role: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it! All INSERT, UPDATE, and DELETE operations on `User` will be automatically logged.
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
### Module Options
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
AuditLogModule.forRoot({
|
|
85
|
+
// Days to keep audit logs (0 = forever)
|
|
86
|
+
retentionDays: 90,
|
|
87
|
+
|
|
88
|
+
// Fields to exclude from all audit logs
|
|
89
|
+
excludeFields: ['password', 'token', 'secret'],
|
|
90
|
+
|
|
91
|
+
// Entity names to exclude from auditing
|
|
92
|
+
excludeEntities: ['Session', 'Cache'],
|
|
93
|
+
|
|
94
|
+
// Don't block on audit writes (fire-and-forget)
|
|
95
|
+
async: false,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Async Configuration
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
AuditLogModule.forRootAsync({
|
|
103
|
+
imports: [ConfigModule],
|
|
104
|
+
useFactory: (config: ConfigService) => ({
|
|
105
|
+
retentionDays: config.get('AUDIT_RETENTION_DAYS'),
|
|
106
|
+
async: config.get('AUDIT_ASYNC') === 'true',
|
|
107
|
+
excludeFields: config.get('AUDIT_EXCLUDE_FIELDS')?.split(',') || [],
|
|
108
|
+
}),
|
|
109
|
+
inject: [ConfigService],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Entity Options
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
@Entity()
|
|
117
|
+
@Auditable({
|
|
118
|
+
// Custom name in audit logs (default: class name)
|
|
119
|
+
entityName: 'UserAccount',
|
|
120
|
+
|
|
121
|
+
// Fields to exclude for this entity only
|
|
122
|
+
excludeFields: ['lastLoginAt', 'sessionData'],
|
|
123
|
+
})
|
|
124
|
+
export class User {
|
|
125
|
+
// ...
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Querying Audit Logs
|
|
130
|
+
|
|
131
|
+
Inject `AuditLogService` to query the audit trail:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { Injectable } from '@nestjs/common';
|
|
135
|
+
import { AuditLogService } from '@nest-toolbox/typeorm-audit-log';
|
|
136
|
+
|
|
137
|
+
@Injectable()
|
|
138
|
+
export class AuditService {
|
|
139
|
+
constructor(private auditLogService: AuditLogService) {}
|
|
140
|
+
|
|
141
|
+
// Get history for a specific entity
|
|
142
|
+
async getEntityHistory(entityName: string, entityId: string) {
|
|
143
|
+
return this.auditLogService.findByEntity(entityName, entityId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get all changes by a user
|
|
147
|
+
async getUserActivity(userId: string) {
|
|
148
|
+
return this.auditLogService.findByUser(userId, {
|
|
149
|
+
since: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24h
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Search all audit logs with filters
|
|
154
|
+
async searchAuditLogs() {
|
|
155
|
+
return this.auditLogService.findAll({
|
|
156
|
+
entityName: 'User',
|
|
157
|
+
action: 'UPDATE',
|
|
158
|
+
since: new Date('2024-01-01'),
|
|
159
|
+
page: 1,
|
|
160
|
+
limit: 50,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Manual Context Setting
|
|
167
|
+
|
|
168
|
+
For non-HTTP contexts (queues, cron jobs, CLI), set the audit context manually:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { AuditContext } from '@nest-toolbox/typeorm-audit-log';
|
|
172
|
+
|
|
173
|
+
@Injectable()
|
|
174
|
+
export class OrderProcessor {
|
|
175
|
+
@Process('process-order')
|
|
176
|
+
async processOrder(job: Job) {
|
|
177
|
+
await AuditContext.run(
|
|
178
|
+
{
|
|
179
|
+
userId: 'system',
|
|
180
|
+
userName: 'Order Processor',
|
|
181
|
+
metadata: { jobId: job.id },
|
|
182
|
+
},
|
|
183
|
+
async () => {
|
|
184
|
+
// All entity changes here will be attributed to "system"
|
|
185
|
+
await this.orderService.updateStatus(job.data.orderId, 'processed');
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Custom Context Extraction
|
|
193
|
+
|
|
194
|
+
Customize how user info is extracted from requests:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { AuditContextMiddleware, createAuditContextMiddleware } from '@nest-toolbox/typeorm-audit-log';
|
|
198
|
+
|
|
199
|
+
// Custom extractor for JWT claims
|
|
200
|
+
const customMiddleware = createAuditContextMiddleware((req) => ({
|
|
201
|
+
userId: req.user?.sub,
|
|
202
|
+
userName: req.user?.preferred_username,
|
|
203
|
+
ip: req.headers['x-forwarded-for'] as string || req.ip,
|
|
204
|
+
userAgent: req.headers['user-agent'],
|
|
205
|
+
metadata: {
|
|
206
|
+
tenantId: req.headers['x-tenant-id'],
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
// In your module
|
|
211
|
+
consumer.apply(customMiddleware.use.bind(customMiddleware)).forRoutes('*');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Audit Log Entry Structure
|
|
215
|
+
|
|
216
|
+
Each audit log entry contains:
|
|
217
|
+
|
|
218
|
+
| Field | Type | Description |
|
|
219
|
+
|-------|------|-------------|
|
|
220
|
+
| `id` | UUID | Unique identifier |
|
|
221
|
+
| `entityName` | string | Name of the entity |
|
|
222
|
+
| `entityId` | string | Primary key of the entity |
|
|
223
|
+
| `action` | enum | CREATE, UPDATE, or DELETE |
|
|
224
|
+
| `userId` | string | ID of the user who made the change |
|
|
225
|
+
| `userName` | string | Name/email of the user |
|
|
226
|
+
| `oldValues` | JSON | Previous values (UPDATE/DELETE) |
|
|
227
|
+
| `newValues` | JSON | New values (CREATE/UPDATE) |
|
|
228
|
+
| `diff` | JSON | Array of changed fields |
|
|
229
|
+
| `ip` | string | IP address of the request |
|
|
230
|
+
| `userAgent` | string | Browser/client user agent |
|
|
231
|
+
| `metadata` | JSON | Custom metadata |
|
|
232
|
+
| `timestamp` | Date | When the change occurred |
|
|
233
|
+
|
|
234
|
+
## Database Migration
|
|
235
|
+
|
|
236
|
+
The module will create an `audit_logs` table. If you need to create it manually:
|
|
237
|
+
|
|
238
|
+
```sql
|
|
239
|
+
CREATE TABLE audit_logs (
|
|
240
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
241
|
+
entity_name VARCHAR(255) NOT NULL,
|
|
242
|
+
entity_id VARCHAR(255) NOT NULL,
|
|
243
|
+
action VARCHAR(10) NOT NULL,
|
|
244
|
+
user_id VARCHAR(255),
|
|
245
|
+
user_name VARCHAR(255),
|
|
246
|
+
old_values JSONB,
|
|
247
|
+
new_values JSONB,
|
|
248
|
+
diff JSONB,
|
|
249
|
+
metadata JSONB,
|
|
250
|
+
ip VARCHAR(45),
|
|
251
|
+
user_agent VARCHAR(500),
|
|
252
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
CREATE INDEX idx_audit_entity ON audit_logs(entity_name, entity_id);
|
|
256
|
+
CREATE INDEX idx_audit_user ON audit_logs(user_id);
|
|
257
|
+
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## API Reference
|
|
261
|
+
|
|
262
|
+
### Decorators
|
|
263
|
+
|
|
264
|
+
- `@Auditable(options?)` - Mark an entity for audit logging
|
|
265
|
+
- `@AuditIgnore()` - Exclude a field from audit logs (Phase 2)
|
|
266
|
+
- `@AuditMask()` - Mask a field value in audit logs (Phase 2)
|
|
267
|
+
|
|
268
|
+
### Services
|
|
269
|
+
|
|
270
|
+
- `AuditLogService.log(params)` - Manually log an audit entry
|
|
271
|
+
- `AuditLogService.findByEntity(name, id, options?)` - Query by entity
|
|
272
|
+
- `AuditLogService.findByUser(userId, options?)` - Query by user
|
|
273
|
+
- `AuditLogService.findAll(options?)` - Paginated search
|
|
274
|
+
|
|
275
|
+
### Context
|
|
276
|
+
|
|
277
|
+
- `AuditContext.run(data, fn)` - Run code with audit context
|
|
278
|
+
- `AuditContext.get()` - Get current audit context
|
|
279
|
+
- `AuditContext.set(data)` - Update current audit context
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT © [Bogdan Lupu](https://github.com/lupu60)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AuditContextData } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Run a function with the given audit context.
|
|
4
|
+
* All code executed within fn (including async operations) will have access
|
|
5
|
+
* to the provided context data.
|
|
6
|
+
*
|
|
7
|
+
* @param data - The audit context data to set
|
|
8
|
+
* @param fn - The function to run with the context
|
|
9
|
+
* @returns The return value of fn
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // In middleware or guard
|
|
14
|
+
* auditContextRun({
|
|
15
|
+
* userId: request.user.id,
|
|
16
|
+
* userName: request.user.email,
|
|
17
|
+
* ip: request.ip,
|
|
18
|
+
* }, () => next());
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function auditContextRun<T>(data: AuditContextData, fn: () => T): T;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current audit context data.
|
|
24
|
+
* Returns undefined if called outside of a run() context.
|
|
25
|
+
*
|
|
26
|
+
* @returns The current context data or undefined
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const context = getAuditContext();
|
|
31
|
+
* console.log(context?.userId);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function getAuditContext(): AuditContextData | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Update the current audit context with additional data.
|
|
37
|
+
* Merges the provided data with existing context.
|
|
38
|
+
* No-op if called outside of a run() context.
|
|
39
|
+
*
|
|
40
|
+
* @param data - Partial data to merge into the current context
|
|
41
|
+
*/
|
|
42
|
+
export declare function setAuditContext(data: Partial<AuditContextData>): void;
|
|
43
|
+
/**
|
|
44
|
+
* AuditContext class for backward compatibility and namespacing.
|
|
45
|
+
* Provides static methods that wrap the exported functions.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* AuditContext.run({ userId: 'user-1' }, () => {
|
|
50
|
+
* const ctx = AuditContext.get();
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare class AuditContext {
|
|
55
|
+
/**
|
|
56
|
+
* Run a function with the given audit context.
|
|
57
|
+
*/
|
|
58
|
+
static run<T>(data: AuditContextData, fn: () => T): T;
|
|
59
|
+
/**
|
|
60
|
+
* Get the current audit context data.
|
|
61
|
+
*/
|
|
62
|
+
static get(): AuditContextData | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Update the current audit context with additional data.
|
|
65
|
+
*/
|
|
66
|
+
static set(data: Partial<AuditContextData>): void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuditContext = void 0;
|
|
4
|
+
exports.auditContextRun = auditContextRun;
|
|
5
|
+
exports.getAuditContext = getAuditContext;
|
|
6
|
+
exports.setAuditContext = setAuditContext;
|
|
7
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
8
|
+
/**
|
|
9
|
+
* Internal storage for audit context data.
|
|
10
|
+
* Uses AsyncLocalStorage to propagate context across async boundaries.
|
|
11
|
+
*/
|
|
12
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
13
|
+
/**
|
|
14
|
+
* Run a function with the given audit context.
|
|
15
|
+
* All code executed within fn (including async operations) will have access
|
|
16
|
+
* to the provided context data.
|
|
17
|
+
*
|
|
18
|
+
* @param data - The audit context data to set
|
|
19
|
+
* @param fn - The function to run with the context
|
|
20
|
+
* @returns The return value of fn
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // In middleware or guard
|
|
25
|
+
* auditContextRun({
|
|
26
|
+
* userId: request.user.id,
|
|
27
|
+
* userName: request.user.email,
|
|
28
|
+
* ip: request.ip,
|
|
29
|
+
* }, () => next());
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function auditContextRun(data, fn) {
|
|
33
|
+
return storage.run(data, fn);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the current audit context data.
|
|
37
|
+
* Returns undefined if called outside of a run() context.
|
|
38
|
+
*
|
|
39
|
+
* @returns The current context data or undefined
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const context = getAuditContext();
|
|
44
|
+
* console.log(context?.userId);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function getAuditContext() {
|
|
48
|
+
return storage.getStore();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Update the current audit context with additional data.
|
|
52
|
+
* Merges the provided data with existing context.
|
|
53
|
+
* No-op if called outside of a run() context.
|
|
54
|
+
*
|
|
55
|
+
* @param data - Partial data to merge into the current context
|
|
56
|
+
*/
|
|
57
|
+
function setAuditContext(data) {
|
|
58
|
+
const current = getAuditContext();
|
|
59
|
+
if (current) {
|
|
60
|
+
Object.assign(current, data);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* AuditContext class for backward compatibility and namespacing.
|
|
65
|
+
* Provides static methods that wrap the exported functions.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* AuditContext.run({ userId: 'user-1' }, () => {
|
|
70
|
+
* const ctx = AuditContext.get();
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: class provides namespacing for cleaner API
|
|
75
|
+
class AuditContext {
|
|
76
|
+
/**
|
|
77
|
+
* Run a function with the given audit context.
|
|
78
|
+
*/
|
|
79
|
+
static run(data, fn) {
|
|
80
|
+
return auditContextRun(data, fn);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the current audit context data.
|
|
84
|
+
*/
|
|
85
|
+
static get() {
|
|
86
|
+
return getAuditContext();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Update the current audit context with additional data.
|
|
90
|
+
*/
|
|
91
|
+
static set(data) {
|
|
92
|
+
setAuditContext(data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.AuditContext = AuditContext;
|
|
96
|
+
//# sourceMappingURL=audit-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-context.js","sourceRoot":"","sources":["../src/audit-context.ts"],"names":[],"mappings":";;;AA4BA,0CAEC;AAcD,0CAEC;AASD,0CAKC;AA5DD,uDAAqD;AAGrD;;;GAGG;AACH,MAAM,OAAO,GAAG,IAAI,oCAAiB,EAAoB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,eAAe,CAAI,IAAsB,EAAE,EAAW;IACpE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,eAAe;IAC7B,OAAO,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,IAA+B;IAC7D,MAAM,OAAO,GAAG,eAAe,EAAE,CAAC;IAClC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,6FAA6F;AAC7F,MAAa,YAAY;IACvB;;OAEG;IACH,MAAM,CAAC,GAAG,CAAI,IAAsB,EAAE,EAAW;QAC/C,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAG;QACR,OAAO,eAAe,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,IAA+B;QACxC,eAAe,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;CACF;AArBD,oCAqBC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
3
|
+
import type { AuditContextData } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Express request with optional user property.
|
|
6
|
+
* Extend this interface if your user object has different properties.
|
|
7
|
+
*/
|
|
8
|
+
interface RequestWithUser extends Request {
|
|
9
|
+
user?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Type for custom context extractor function.
|
|
18
|
+
* Allows customization of how audit context is extracted from requests.
|
|
19
|
+
*/
|
|
20
|
+
export type AuditContextExtractor = (req: RequestWithUser) => AuditContextData;
|
|
21
|
+
/**
|
|
22
|
+
* Default context extractor.
|
|
23
|
+
* Extracts userId, userName, IP, and userAgent from the request.
|
|
24
|
+
*/
|
|
25
|
+
export declare const defaultContextExtractor: AuditContextExtractor;
|
|
26
|
+
/**
|
|
27
|
+
* NestJS middleware that initializes audit context from the HTTP request.
|
|
28
|
+
* Automatically captures user information, IP address, and user agent
|
|
29
|
+
* for all audit log entries created during the request lifecycle.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // app.module.ts
|
|
34
|
+
* export class AppModule implements NestModule {
|
|
35
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
36
|
+
* consumer
|
|
37
|
+
* .apply(AuditContextMiddleware)
|
|
38
|
+
* .forRoutes('*');
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // With custom extractor
|
|
46
|
+
* export class AppModule implements NestModule {
|
|
47
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
48
|
+
* const middleware = new AuditContextMiddleware((req) => ({
|
|
49
|
+
* userId: req.user?.sub,
|
|
50
|
+
* userName: req.user?.preferred_username,
|
|
51
|
+
* ip: req.headers['x-forwarded-for'] as string || req.ip,
|
|
52
|
+
* userAgent: req.headers['user-agent'],
|
|
53
|
+
* metadata: { tenantId: req.headers['x-tenant-id'] },
|
|
54
|
+
* }));
|
|
55
|
+
* consumer.apply(middleware.use.bind(middleware)).forRoutes('*');
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare class AuditContextMiddleware implements NestMiddleware {
|
|
61
|
+
private extractor;
|
|
62
|
+
constructor(extractor?: AuditContextExtractor);
|
|
63
|
+
/**
|
|
64
|
+
* Middleware handler that wraps the request in an audit context.
|
|
65
|
+
*/
|
|
66
|
+
use(req: RequestWithUser, _res: Response, next: NextFunction): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Factory function to create a middleware with a custom extractor.
|
|
70
|
+
* Useful when you need to inject dependencies into the extractor.
|
|
71
|
+
*
|
|
72
|
+
* @param extractor - Custom function to extract audit context from request
|
|
73
|
+
* @returns A new middleware instance with the custom extractor
|
|
74
|
+
*/
|
|
75
|
+
export declare function createAuditContextMiddleware(extractor: AuditContextExtractor): AuditContextMiddleware;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AuditContextMiddleware = exports.defaultContextExtractor = void 0;
|
|
13
|
+
exports.createAuditContextMiddleware = createAuditContextMiddleware;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const audit_context_1 = require("./audit-context");
|
|
16
|
+
/**
|
|
17
|
+
* Default context extractor.
|
|
18
|
+
* Extracts userId, userName, IP, and userAgent from the request.
|
|
19
|
+
*/
|
|
20
|
+
const defaultContextExtractor = (req) => {
|
|
21
|
+
var _a, _b, _c, _d, _e, _f;
|
|
22
|
+
return ({
|
|
23
|
+
userId: (_a = req.user) === null || _a === void 0 ? void 0 : _a.id,
|
|
24
|
+
userName: (_c = (_b = req.user) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : (_d = req.user) === null || _d === void 0 ? void 0 : _d.email,
|
|
25
|
+
ip: (_e = req.ip) !== null && _e !== void 0 ? _e : (_f = req.socket) === null || _f === void 0 ? void 0 : _f.remoteAddress,
|
|
26
|
+
userAgent: req.headers['user-agent'],
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
exports.defaultContextExtractor = defaultContextExtractor;
|
|
30
|
+
/**
|
|
31
|
+
* NestJS middleware that initializes audit context from the HTTP request.
|
|
32
|
+
* Automatically captures user information, IP address, and user agent
|
|
33
|
+
* for all audit log entries created during the request lifecycle.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // app.module.ts
|
|
38
|
+
* export class AppModule implements NestModule {
|
|
39
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
40
|
+
* consumer
|
|
41
|
+
* .apply(AuditContextMiddleware)
|
|
42
|
+
* .forRoutes('*');
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // With custom extractor
|
|
50
|
+
* export class AppModule implements NestModule {
|
|
51
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
52
|
+
* const middleware = new AuditContextMiddleware((req) => ({
|
|
53
|
+
* userId: req.user?.sub,
|
|
54
|
+
* userName: req.user?.preferred_username,
|
|
55
|
+
* ip: req.headers['x-forwarded-for'] as string || req.ip,
|
|
56
|
+
* userAgent: req.headers['user-agent'],
|
|
57
|
+
* metadata: { tenantId: req.headers['x-tenant-id'] },
|
|
58
|
+
* }));
|
|
59
|
+
* consumer.apply(middleware.use.bind(middleware)).forRoutes('*');
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
let AuditContextMiddleware = class AuditContextMiddleware {
|
|
65
|
+
constructor(extractor) {
|
|
66
|
+
this.extractor = extractor !== null && extractor !== void 0 ? extractor : exports.defaultContextExtractor;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Middleware handler that wraps the request in an audit context.
|
|
70
|
+
*/
|
|
71
|
+
use(req, _res, next) {
|
|
72
|
+
const contextData = this.extractor(req);
|
|
73
|
+
(0, audit_context_1.auditContextRun)(contextData, () => next());
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
exports.AuditContextMiddleware = AuditContextMiddleware;
|
|
77
|
+
exports.AuditContextMiddleware = AuditContextMiddleware = __decorate([
|
|
78
|
+
(0, common_1.Injectable)(),
|
|
79
|
+
__metadata("design:paramtypes", [Function])
|
|
80
|
+
], AuditContextMiddleware);
|
|
81
|
+
/**
|
|
82
|
+
* Factory function to create a middleware with a custom extractor.
|
|
83
|
+
* Useful when you need to inject dependencies into the extractor.
|
|
84
|
+
*
|
|
85
|
+
* @param extractor - Custom function to extract audit context from request
|
|
86
|
+
* @returns A new middleware instance with the custom extractor
|
|
87
|
+
*/
|
|
88
|
+
function createAuditContextMiddleware(extractor) {
|
|
89
|
+
return new AuditContextMiddleware(extractor);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=audit-context.middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-context.middleware.js","sourceRoot":"","sources":["../src/audit-context.middleware.ts"],"names":[],"mappings":";;;;;;;;;;;;AA8FA,oEAEC;AAhGD,2CAAiE;AAEjE,mDAAkD;AAuBlD;;;GAGG;AACI,MAAM,uBAAuB,GAA0B,CAAC,GAAoB,EAAoB,EAAE;;IAAC,OAAA,CAAC;QACzG,MAAM,EAAE,MAAA,GAAG,CAAC,IAAI,0CAAE,EAAE;QACpB,QAAQ,EAAE,MAAA,MAAA,GAAG,CAAC,IAAI,0CAAE,IAAI,mCAAI,MAAA,GAAG,CAAC,IAAI,0CAAE,KAAK;QAC3C,EAAE,EAAE,MAAA,GAAG,CAAC,EAAE,mCAAI,MAAA,GAAG,CAAC,MAAM,0CAAE,aAAa;QACvC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC;KACrC,CAAC,CAAA;CAAA,CAAC;AALU,QAAA,uBAAuB,2BAKjC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEI,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAGjC,YAAY,SAAiC;QAC3C,IAAI,CAAC,SAAS,GAAG,SAAS,aAAT,SAAS,cAAT,SAAS,GAAI,+BAAuB,CAAC;IACxD,CAAC;IAED;;OAEG;IACH,GAAG,CAAC,GAAoB,EAAE,IAAc,EAAE,IAAkB;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACxC,IAAA,+BAAe,EAAC,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF,CAAA;AAdY,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,mBAAU,GAAE;;GACA,sBAAsB,CAclC;AAED;;;;;;GAMG;AACH,SAAgB,4BAA4B,CAAC,SAAgC;IAC3E,OAAO,IAAI,sBAAsB,CAAC,SAAS,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AuditAction, AuditDiff } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Entity for storing audit log entries.
|
|
4
|
+
* Tracks all changes to entities decorated with @Auditable().
|
|
5
|
+
*/
|
|
6
|
+
export declare class AuditLog {
|
|
7
|
+
id: string;
|
|
8
|
+
/**
|
|
9
|
+
* Name of the entity that was modified
|
|
10
|
+
*/
|
|
11
|
+
entityName: string;
|
|
12
|
+
/**
|
|
13
|
+
* ID of the entity that was modified
|
|
14
|
+
*/
|
|
15
|
+
entityId: string;
|
|
16
|
+
/**
|
|
17
|
+
* Type of action performed (CREATE, UPDATE, DELETE)
|
|
18
|
+
*/
|
|
19
|
+
action: AuditAction;
|
|
20
|
+
/**
|
|
21
|
+
* ID of the user who made the change
|
|
22
|
+
*/
|
|
23
|
+
userId: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Name/email of the user who made the change
|
|
26
|
+
*/
|
|
27
|
+
userName: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Previous values of changed fields (for UPDATE/DELETE)
|
|
30
|
+
*/
|
|
31
|
+
oldValues: Record<string, any> | null;
|
|
32
|
+
/**
|
|
33
|
+
* New values of changed fields (for CREATE/UPDATE)
|
|
34
|
+
*/
|
|
35
|
+
newValues: Record<string, any> | null;
|
|
36
|
+
/**
|
|
37
|
+
* Computed diff of changes between old and new values
|
|
38
|
+
*/
|
|
39
|
+
diff: AuditDiff[] | null;
|
|
40
|
+
/**
|
|
41
|
+
* Additional metadata captured at audit time
|
|
42
|
+
*/
|
|
43
|
+
metadata: Record<string, any> | null;
|
|
44
|
+
/**
|
|
45
|
+
* IP address of the request that triggered the change
|
|
46
|
+
*/
|
|
47
|
+
ip: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* User agent of the request that triggered the change
|
|
50
|
+
*/
|
|
51
|
+
userAgent: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Timestamp when the audit log was created
|
|
54
|
+
*/
|
|
55
|
+
timestamp: Date;
|
|
56
|
+
}
|