@cepseudo/adonis-audit-log 1.0.0
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.md +9 -0
- package/README.md +190 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +58 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +16 -0
- package/build/middleware/request_logger.d.ts +5 -0
- package/build/middleware/request_logger.js +36 -0
- package/build/providers/audit_provider.d.ts +13 -0
- package/build/providers/audit_provider.js +52 -0
- package/build/services/main.d.ts +3 -0
- package/build/services/main.js +15 -0
- package/build/src/audit_service.d.ts +43 -0
- package/build/src/audit_service.js +76 -0
- package/build/src/error_logger.d.ts +35 -0
- package/build/src/error_logger.js +106 -0
- package/build/src/models/audit_log.d.ts +10 -0
- package/build/src/models/audit_log.js +37 -0
- package/build/src/models/error_log.d.ts +15 -0
- package/build/src/models/error_log.js +52 -0
- package/build/src/models/request_log.d.ts +17 -0
- package/build/src/models/request_log.js +61 -0
- package/build/src/request_logger.d.ts +24 -0
- package/build/src/request_logger.js +130 -0
- package/build/src/types.d.ts +94 -0
- package/build/src/types.js +64 -0
- package/build/stubs/config.stub +40 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/stubs/migrations/create_audit_logs_table.stub +24 -0
- package/build/stubs/migrations/create_error_logs_table.stub +31 -0
- package/build/stubs/migrations/create_request_logs_table.stub +33 -0
- package/package.json +102 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @cepseudo/adonis-audit-log
|
|
2
|
+
|
|
3
|
+
Simple audit logging package for AdonisJS 6.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Custom audit logs for tracking user actions and business events
|
|
8
|
+
- Automatic HTTP request logging via middleware
|
|
9
|
+
- Error logging with exception handler integration
|
|
10
|
+
- Configurable field sanitization for sensitive data
|
|
11
|
+
- Retention settings for log cleanup
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @cepseudo/adonis-audit-log
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node ace configure @cepseudo/adonis-audit-log
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This will:
|
|
26
|
+
|
|
27
|
+
1. Publish migrations to `database/migrations/`
|
|
28
|
+
2. Publish config to `config/audit.ts`
|
|
29
|
+
3. Register the provider in `adonisrc.ts`
|
|
30
|
+
|
|
31
|
+
Then run the migrations:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node ace migration:run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration Options
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// config/audit.ts
|
|
41
|
+
import { defineConfig } from '@cepseudo/adonis-audit-log'
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
enabled: true,
|
|
45
|
+
|
|
46
|
+
requestLog: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
excludeRoutes: ['/health', '/metrics'],
|
|
49
|
+
excludeMethods: ['OPTIONS'],
|
|
50
|
+
logBody: false,
|
|
51
|
+
logQuery: true,
|
|
52
|
+
sanitizeFields: ['password', 'token', 'secret'],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
errorLog: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
excludeStatusCodes: [404],
|
|
58
|
+
includeStack: true,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
auditLog: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
retention: {
|
|
66
|
+
requestLogs: 30,
|
|
67
|
+
errorLogs: 90,
|
|
68
|
+
auditLogs: 365,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
### Custom Audit Logs
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import audit from '@cepseudo/adonis-audit-log/services/main'
|
|
79
|
+
|
|
80
|
+
// Simple log
|
|
81
|
+
await audit.log('user.login', { user_id: 1 })
|
|
82
|
+
|
|
83
|
+
// With metadata
|
|
84
|
+
await audit.log('submission.create', {
|
|
85
|
+
user_id: auth.user.id,
|
|
86
|
+
submission_id: submission.id,
|
|
87
|
+
project_acronym: submission.projectAcronym,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Track changes
|
|
91
|
+
await audit.logChange('user.update', {
|
|
92
|
+
userId: auth.user.id,
|
|
93
|
+
targetId: targetUser.id,
|
|
94
|
+
changes: {
|
|
95
|
+
email: { from: 'old@example.com', to: 'new@example.com' },
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Request Logging (Middleware)
|
|
101
|
+
|
|
102
|
+
Add the middleware to your router in `start/kernel.ts`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
router.use([() => import('@cepseudo/adonis-audit-log/middleware/request_logger')])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Error Logging (Exception Handler)
|
|
109
|
+
|
|
110
|
+
Integrate with your exception handler in `app/exceptions/handler.ts`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { AuditErrorLogger } from '@cepseudo/adonis-audit-log'
|
|
114
|
+
|
|
115
|
+
export default class HttpExceptionHandler extends ExceptionHandler {
|
|
116
|
+
async report(error: unknown, ctx: HttpContext) {
|
|
117
|
+
await AuditErrorLogger.log(error, ctx)
|
|
118
|
+
return super.report(error, ctx)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Database Schema
|
|
124
|
+
|
|
125
|
+
The package creates three tables:
|
|
126
|
+
|
|
127
|
+
### audit_logs
|
|
128
|
+
|
|
129
|
+
- `id` - Primary key
|
|
130
|
+
- `action_type` - Event type (e.g., 'user.login', 'submission.create')
|
|
131
|
+
- `metadata` - JSON object with additional data
|
|
132
|
+
- `user_id` - Optional reference to users table
|
|
133
|
+
- `created_at` - Timestamp
|
|
134
|
+
|
|
135
|
+
### request_logs
|
|
136
|
+
|
|
137
|
+
- `id` - Primary key
|
|
138
|
+
- `method` - HTTP method
|
|
139
|
+
- `url` - Request URL
|
|
140
|
+
- `route_name` - Named route if available
|
|
141
|
+
- `status_code` - HTTP response status
|
|
142
|
+
- `response_time_ms` - Response time in milliseconds
|
|
143
|
+
- `ip` - Client IP address
|
|
144
|
+
- `user_agent` - Browser/client user agent
|
|
145
|
+
- `user_id` - Optional reference to users table
|
|
146
|
+
- `request_body` - Sanitized request body (optional)
|
|
147
|
+
- `request_query` - Query parameters
|
|
148
|
+
- `created_at` - Timestamp
|
|
149
|
+
|
|
150
|
+
### error_logs
|
|
151
|
+
|
|
152
|
+
- `id` - Primary key
|
|
153
|
+
- `error_type` - Error class name
|
|
154
|
+
- `message` - Error message
|
|
155
|
+
- `stack` - Stack trace (optional)
|
|
156
|
+
- `url` - URL where error occurred
|
|
157
|
+
- `method` - HTTP method
|
|
158
|
+
- `status_code` - HTTP status returned
|
|
159
|
+
- `user_id` - Optional reference to users table
|
|
160
|
+
- `context` - Additional context (params, query)
|
|
161
|
+
- `created_at` - Timestamp
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Install dependencies
|
|
167
|
+
npm install
|
|
168
|
+
|
|
169
|
+
# Run tests
|
|
170
|
+
npm run test
|
|
171
|
+
|
|
172
|
+
# Run tests without linting
|
|
173
|
+
npm run quick:test
|
|
174
|
+
|
|
175
|
+
# Build
|
|
176
|
+
npm run build
|
|
177
|
+
|
|
178
|
+
# Type check
|
|
179
|
+
npm run typecheck
|
|
180
|
+
|
|
181
|
+
# Lint
|
|
182
|
+
npm run lint
|
|
183
|
+
|
|
184
|
+
# Format
|
|
185
|
+
npm run format
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Configure hook
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| The configure hook is called when someone runs "node ace configure <package>"
|
|
7
|
+
| command. It publishes migrations, config, and registers the provider.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import { stubsRoot } from './stubs/main.js';
|
|
11
|
+
export async function configure(command) {
|
|
12
|
+
const codemods = await command.createCodemods();
|
|
13
|
+
// Publish config file
|
|
14
|
+
await codemods.makeUsingStub(stubsRoot, 'config.stub', {});
|
|
15
|
+
// Publish migrations
|
|
16
|
+
const now = new Date();
|
|
17
|
+
const timestamp = now
|
|
18
|
+
.toISOString()
|
|
19
|
+
.replace(/[-:]/g, '')
|
|
20
|
+
.replace('T', '_')
|
|
21
|
+
.replace(/\.\d{3}Z$/, '');
|
|
22
|
+
await codemods.makeUsingStub(stubsRoot, 'migrations/create_audit_logs_table.stub', {
|
|
23
|
+
migration: {
|
|
24
|
+
folder: 'database/migrations',
|
|
25
|
+
fileName: `${timestamp}_create_audit_logs_table.ts`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// Add 1 second to timestamp for ordering
|
|
29
|
+
const timestamp2 = new Date(now.getTime() + 1000)
|
|
30
|
+
.toISOString()
|
|
31
|
+
.replace(/[-:]/g, '')
|
|
32
|
+
.replace('T', '_')
|
|
33
|
+
.replace(/\.\d{3}Z$/, '');
|
|
34
|
+
await codemods.makeUsingStub(stubsRoot, 'migrations/create_request_logs_table.stub', {
|
|
35
|
+
migration: {
|
|
36
|
+
folder: 'database/migrations',
|
|
37
|
+
fileName: `${timestamp2}_create_request_logs_table.ts`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
// Add 2 seconds to timestamp for ordering
|
|
41
|
+
const timestamp3 = new Date(now.getTime() + 2000)
|
|
42
|
+
.toISOString()
|
|
43
|
+
.replace(/[-:]/g, '')
|
|
44
|
+
.replace('T', '_')
|
|
45
|
+
.replace(/\.\d{3}Z$/, '');
|
|
46
|
+
await codemods.makeUsingStub(stubsRoot, 'migrations/create_error_logs_table.stub', {
|
|
47
|
+
migration: {
|
|
48
|
+
folder: 'database/migrations',
|
|
49
|
+
fileName: `${timestamp3}_create_error_logs_table.ts`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
// Register provider in adonisrc.ts
|
|
53
|
+
await codemods.updateRcFile((rcFile) => {
|
|
54
|
+
rcFile.addProvider('@cepseudo/adonis-audit-log/providers/audit_provider');
|
|
55
|
+
});
|
|
56
|
+
command.logger.success('Audit log package configured successfully!');
|
|
57
|
+
command.logger.info('Run "node ace migration:run" to create the audit tables.');
|
|
58
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { configure } from './configure.js';
|
|
2
|
+
export { defineConfig } from './src/types.js';
|
|
3
|
+
export { AuditService } from './src/audit_service.js';
|
|
4
|
+
export { RequestLogger } from './src/request_logger.js';
|
|
5
|
+
export { ErrorLogger, AuditErrorLogger } from './src/error_logger.js';
|
|
6
|
+
export { default as AuditLog } from './src/models/audit_log.js';
|
|
7
|
+
export { default as RequestLog } from './src/models/request_log.js';
|
|
8
|
+
export { default as ErrorLog } from './src/models/error_log.js';
|
|
9
|
+
export type { AuditConfig, AuditLogEntry, RequestLogEntry, ErrorLogEntry } from './src/types.js';
|
package/build/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Package entrypoint
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Export values from the package entrypoint.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
export { configure } from './configure.js';
|
|
10
|
+
export { defineConfig } from './src/types.js';
|
|
11
|
+
export { AuditService } from './src/audit_service.js';
|
|
12
|
+
export { RequestLogger } from './src/request_logger.js';
|
|
13
|
+
export { ErrorLogger, AuditErrorLogger } from './src/error_logger.js';
|
|
14
|
+
export { default as AuditLog } from './src/models/audit_log.js';
|
|
15
|
+
export { default as RequestLog } from './src/models/request_log.js';
|
|
16
|
+
export { default as ErrorLog } from './src/models/error_log.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Request Logger Middleware
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| This middleware logs HTTP requests automatically. Add it to your router
|
|
7
|
+
| middleware stack in start/kernel.ts.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import app from '@adonisjs/core/services/app';
|
|
11
|
+
import { RequestLogger } from '../src/request_logger.js';
|
|
12
|
+
let requestLogger = null;
|
|
13
|
+
export default class RequestLoggerMiddleware {
|
|
14
|
+
async handle(ctx, next) {
|
|
15
|
+
const startTime = performance.now();
|
|
16
|
+
// Execute the request
|
|
17
|
+
await next();
|
|
18
|
+
// Log the request after response (non-blocking)
|
|
19
|
+
try {
|
|
20
|
+
if (!requestLogger) {
|
|
21
|
+
const config = app.config.get('audit');
|
|
22
|
+
if (config) {
|
|
23
|
+
requestLogger = new RequestLogger(config);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (requestLogger) {
|
|
27
|
+
const responseTimeMs = Math.round(performance.now() - startTime);
|
|
28
|
+
// Use async version to not block the response
|
|
29
|
+
requestLogger.logAsync(ctx, responseTimeMs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error('[RequestLoggerMiddleware] Error:', error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
import { AuditService } from '../src/audit_service.js';
|
|
3
|
+
declare module '@adonisjs/core/types' {
|
|
4
|
+
interface ContainerBindings {
|
|
5
|
+
audit: AuditService;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export default class AuditProvider {
|
|
9
|
+
protected app: ApplicationService;
|
|
10
|
+
constructor(app: ApplicationService);
|
|
11
|
+
register(): void;
|
|
12
|
+
boot(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Audit Provider
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| This provider registers the audit service in the container and initializes
|
|
7
|
+
| the error logger.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import { AuditService } from '../src/audit_service.js';
|
|
11
|
+
import { AuditErrorLogger } from '../src/error_logger.js';
|
|
12
|
+
export default class AuditProvider {
|
|
13
|
+
app;
|
|
14
|
+
constructor(app) {
|
|
15
|
+
this.app = app;
|
|
16
|
+
}
|
|
17
|
+
register() {
|
|
18
|
+
this.app.container.singleton('audit', () => {
|
|
19
|
+
const config = this.app.config.get('audit', {
|
|
20
|
+
enabled: true,
|
|
21
|
+
requestLog: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
excludeRoutes: [],
|
|
24
|
+
excludeMethods: ['OPTIONS'],
|
|
25
|
+
logBody: false,
|
|
26
|
+
logQuery: true,
|
|
27
|
+
sanitizeFields: ['password', 'token', 'secret'],
|
|
28
|
+
},
|
|
29
|
+
errorLog: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
excludeStatusCodes: [404],
|
|
32
|
+
includeStack: true,
|
|
33
|
+
},
|
|
34
|
+
auditLog: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
retention: {
|
|
38
|
+
requestLogs: 30,
|
|
39
|
+
errorLogs: 90,
|
|
40
|
+
auditLogs: 365,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return new AuditService(config);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async boot() {
|
|
47
|
+
const config = this.app.config.get('audit');
|
|
48
|
+
if (config) {
|
|
49
|
+
AuditErrorLogger.init(config);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Audit Service Singleton
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| This file exports the audit service singleton that can be imported
|
|
7
|
+
| anywhere in the application.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import app from '@adonisjs/core/services/app';
|
|
11
|
+
let audit;
|
|
12
|
+
await app.booted(async () => {
|
|
13
|
+
audit = await app.container.make('audit');
|
|
14
|
+
});
|
|
15
|
+
export { audit as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AuditConfig } from './types.js';
|
|
2
|
+
import AuditLog from './models/audit_log.js';
|
|
3
|
+
export declare class AuditService {
|
|
4
|
+
#private;
|
|
5
|
+
protected config: AuditConfig;
|
|
6
|
+
constructor(config: AuditConfig);
|
|
7
|
+
/**
|
|
8
|
+
* Check if logging is enabled
|
|
9
|
+
*/
|
|
10
|
+
get isEnabled(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Create an audit log entry (fire-and-forget, non-blocking)
|
|
13
|
+
*/
|
|
14
|
+
logAsync(actionType: string, metadata?: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* Create an audit log entry (blocking, returns the created log)
|
|
17
|
+
*/
|
|
18
|
+
log(actionType: string, metadata?: Record<string, unknown>): Promise<AuditLog | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Log with change tracking (fire-and-forget)
|
|
21
|
+
*/
|
|
22
|
+
logChangeAsync(actionType: string, options: {
|
|
23
|
+
userId?: number | null;
|
|
24
|
+
targetId?: number | string;
|
|
25
|
+
changes: Record<string, {
|
|
26
|
+
from: unknown;
|
|
27
|
+
to: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
extra?: Record<string, unknown>;
|
|
30
|
+
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Log with change tracking (blocking)
|
|
33
|
+
*/
|
|
34
|
+
logChange(actionType: string, options: {
|
|
35
|
+
userId?: number | null;
|
|
36
|
+
targetId?: number | string;
|
|
37
|
+
changes: Record<string, {
|
|
38
|
+
from: unknown;
|
|
39
|
+
to: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
extra?: Record<string, unknown>;
|
|
42
|
+
}): Promise<AuditLog | null>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Audit Service
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Main service for creating custom audit logs. Provides a simple API
|
|
7
|
+
| for logging user actions and business events.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import AuditLog from './models/audit_log.js';
|
|
11
|
+
export class AuditService {
|
|
12
|
+
config;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if logging is enabled
|
|
18
|
+
*/
|
|
19
|
+
get isEnabled() {
|
|
20
|
+
return this.config.enabled && this.config.auditLog.enabled;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create an audit log entry (fire-and-forget, non-blocking)
|
|
24
|
+
*/
|
|
25
|
+
logAsync(actionType, metadata = {}) {
|
|
26
|
+
if (!this.isEnabled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setImmediate(() => {
|
|
30
|
+
this.#createLog(actionType, metadata).catch((error) => {
|
|
31
|
+
console.error('[AuditService] Failed to log:', error);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an audit log entry (blocking, returns the created log)
|
|
37
|
+
*/
|
|
38
|
+
async log(actionType, metadata = {}) {
|
|
39
|
+
if (!this.isEnabled) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return this.#createLog(actionType, metadata);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Log with change tracking (fire-and-forget)
|
|
46
|
+
*/
|
|
47
|
+
logChangeAsync(actionType, options) {
|
|
48
|
+
this.logAsync(actionType, {
|
|
49
|
+
user_id: options.userId,
|
|
50
|
+
target_id: options.targetId,
|
|
51
|
+
changes: options.changes,
|
|
52
|
+
...options.extra,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Log with change tracking (blocking)
|
|
57
|
+
*/
|
|
58
|
+
async logChange(actionType, options) {
|
|
59
|
+
return this.log(actionType, {
|
|
60
|
+
user_id: options.userId,
|
|
61
|
+
target_id: options.targetId,
|
|
62
|
+
changes: options.changes,
|
|
63
|
+
...options.extra,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async #createLog(actionType, metadata) {
|
|
67
|
+
const userId = (metadata.user_id ?? metadata.userId ?? null);
|
|
68
|
+
// Create clean metadata without user_id fields
|
|
69
|
+
const cleanMetadata = Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== 'user_id' && key !== 'userId'));
|
|
70
|
+
return AuditLog.create({
|
|
71
|
+
actionType,
|
|
72
|
+
metadata: cleanMetadata,
|
|
73
|
+
userId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AuditConfig } from './types.js';
|
|
3
|
+
import ErrorLog from './models/error_log.js';
|
|
4
|
+
export declare class ErrorLogger {
|
|
5
|
+
#private;
|
|
6
|
+
protected config: AuditConfig;
|
|
7
|
+
constructor(config: AuditConfig);
|
|
8
|
+
/**
|
|
9
|
+
* Check if the error should be logged
|
|
10
|
+
*/
|
|
11
|
+
shouldLog(error: unknown, ctx?: HttpContext): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Log an error (fire-and-forget, non-blocking)
|
|
14
|
+
*/
|
|
15
|
+
logAsync(error: unknown, ctx?: HttpContext): void;
|
|
16
|
+
/**
|
|
17
|
+
* Log an error (blocking, returns the created log)
|
|
18
|
+
*/
|
|
19
|
+
log(error: unknown, ctx?: HttpContext): Promise<ErrorLog | null>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Static helper for easy integration with exception handler
|
|
23
|
+
*/
|
|
24
|
+
export declare const AuditErrorLogger: {
|
|
25
|
+
instance: ErrorLogger | null;
|
|
26
|
+
init(config: AuditConfig): void;
|
|
27
|
+
/**
|
|
28
|
+
* Log error asynchronously (non-blocking)
|
|
29
|
+
*/
|
|
30
|
+
logAsync(error: unknown, ctx?: HttpContext): void;
|
|
31
|
+
/**
|
|
32
|
+
* Log error and wait for result
|
|
33
|
+
*/
|
|
34
|
+
log(error: unknown, ctx?: HttpContext): Promise<ErrorLog | null>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Error Logger
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Logic for logging application errors. Designed to be integrated with
|
|
7
|
+
| the AdonisJS exception handler.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import { getUserIdFromContext, getErrorStatusCode, isHttpError } from './types.js';
|
|
11
|
+
import ErrorLog from './models/error_log.js';
|
|
12
|
+
export class ErrorLogger {
|
|
13
|
+
config;
|
|
14
|
+
#excludedStatusCodes;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
// Pre-compute set for O(1) lookup
|
|
18
|
+
this.#excludedStatusCodes = new Set(config.errorLog.excludeStatusCodes);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if the error should be logged
|
|
22
|
+
*/
|
|
23
|
+
shouldLog(error, ctx) {
|
|
24
|
+
if (!this.config.enabled || !this.config.errorLog.enabled) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
// Check if status code is excluded (O(1) lookup)
|
|
28
|
+
if (ctx) {
|
|
29
|
+
const statusCode = getErrorStatusCode(error) ?? ctx.response.getStatus();
|
|
30
|
+
if (this.#excludedStatusCodes.has(statusCode)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Log an error (fire-and-forget, non-blocking)
|
|
38
|
+
*/
|
|
39
|
+
logAsync(error, ctx) {
|
|
40
|
+
if (!this.shouldLog(error, ctx)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setImmediate(() => {
|
|
44
|
+
this.#createLog(error, ctx).catch((err) => {
|
|
45
|
+
console.error('[ErrorLogger] Failed to log error:', err);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Log an error (blocking, returns the created log)
|
|
51
|
+
*/
|
|
52
|
+
async log(error, ctx) {
|
|
53
|
+
if (!this.shouldLog(error, ctx)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return this.#createLog(error, ctx);
|
|
57
|
+
}
|
|
58
|
+
async #createLog(error, ctx) {
|
|
59
|
+
const err = isHttpError(error) ? error : new Error(String(error));
|
|
60
|
+
const entry = {
|
|
61
|
+
errorType: err.constructor?.name ?? 'Error',
|
|
62
|
+
message: err.message,
|
|
63
|
+
stack: this.config.errorLog.includeStack ? (err.stack ?? null) : null,
|
|
64
|
+
url: ctx?.request.url(true) ?? null,
|
|
65
|
+
method: ctx?.request.method() ?? null,
|
|
66
|
+
statusCode: err.status ?? ctx?.response.getStatus() ?? 500,
|
|
67
|
+
userId: ctx ? getUserIdFromContext(ctx) : null,
|
|
68
|
+
context: ctx
|
|
69
|
+
? {
|
|
70
|
+
params: ctx.params,
|
|
71
|
+
query: ctx.request.qs(),
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
};
|
|
75
|
+
return ErrorLog.create(entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Static helper for easy integration with exception handler
|
|
80
|
+
*/
|
|
81
|
+
export const AuditErrorLogger = {
|
|
82
|
+
instance: null,
|
|
83
|
+
init(config) {
|
|
84
|
+
this.instance = new ErrorLogger(config);
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Log error asynchronously (non-blocking)
|
|
88
|
+
*/
|
|
89
|
+
logAsync(error, ctx) {
|
|
90
|
+
if (!this.instance) {
|
|
91
|
+
console.warn('[AuditErrorLogger] Not initialized. Call AuditErrorLogger.init() first.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.instance.logAsync(error, ctx);
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Log error and wait for result
|
|
98
|
+
*/
|
|
99
|
+
async log(error, ctx) {
|
|
100
|
+
if (!this.instance) {
|
|
101
|
+
console.warn('[AuditErrorLogger] Not initialized. Call AuditErrorLogger.init() first.');
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return this.instance.log(error, ctx);
|
|
105
|
+
},
|
|
106
|
+
};
|