@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseModel } from '@adonisjs/lucid/orm';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
export default class AuditLog extends BaseModel {
|
|
4
|
+
static table: string;
|
|
5
|
+
id: number;
|
|
6
|
+
actionType: string;
|
|
7
|
+
metadata: Record<string, unknown>;
|
|
8
|
+
userId: number | null;
|
|
9
|
+
createdAt: DateTime;
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Audit Log Model
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Lucid model for the audit_logs table. Stores custom application logs
|
|
7
|
+
| for tracking user actions and business events.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
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;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
|
17
|
+
export default class AuditLog extends BaseModel {
|
|
18
|
+
static table = 'audit_logs';
|
|
19
|
+
}
|
|
20
|
+
__decorate([
|
|
21
|
+
column({ isPrimary: true })
|
|
22
|
+
], AuditLog.prototype, "id", void 0);
|
|
23
|
+
__decorate([
|
|
24
|
+
column()
|
|
25
|
+
], AuditLog.prototype, "actionType", void 0);
|
|
26
|
+
__decorate([
|
|
27
|
+
column({
|
|
28
|
+
prepare: (value) => JSON.stringify(value),
|
|
29
|
+
consume: (value) => (typeof value === 'string' ? JSON.parse(value) : value),
|
|
30
|
+
})
|
|
31
|
+
], AuditLog.prototype, "metadata", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
column()
|
|
34
|
+
], AuditLog.prototype, "userId", void 0);
|
|
35
|
+
__decorate([
|
|
36
|
+
column.dateTime({ autoCreate: true })
|
|
37
|
+
], AuditLog.prototype, "createdAt", void 0);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseModel } from '@adonisjs/lucid/orm';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
export default class ErrorLog extends BaseModel {
|
|
4
|
+
static table: string;
|
|
5
|
+
id: number;
|
|
6
|
+
errorType: string;
|
|
7
|
+
message: string;
|
|
8
|
+
stack: string | null;
|
|
9
|
+
url: string | null;
|
|
10
|
+
method: string | null;
|
|
11
|
+
statusCode: number | null;
|
|
12
|
+
userId: number | null;
|
|
13
|
+
context: Record<string, unknown> | null;
|
|
14
|
+
createdAt: DateTime;
|
|
15
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Error Log Model
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Lucid model for the error_logs table. Stores application error tracking
|
|
7
|
+
| data.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
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;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
|
17
|
+
export default class ErrorLog extends BaseModel {
|
|
18
|
+
static table = 'error_logs';
|
|
19
|
+
}
|
|
20
|
+
__decorate([
|
|
21
|
+
column({ isPrimary: true })
|
|
22
|
+
], ErrorLog.prototype, "id", void 0);
|
|
23
|
+
__decorate([
|
|
24
|
+
column()
|
|
25
|
+
], ErrorLog.prototype, "errorType", void 0);
|
|
26
|
+
__decorate([
|
|
27
|
+
column()
|
|
28
|
+
], ErrorLog.prototype, "message", void 0);
|
|
29
|
+
__decorate([
|
|
30
|
+
column()
|
|
31
|
+
], ErrorLog.prototype, "stack", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
column()
|
|
34
|
+
], ErrorLog.prototype, "url", void 0);
|
|
35
|
+
__decorate([
|
|
36
|
+
column()
|
|
37
|
+
], ErrorLog.prototype, "method", void 0);
|
|
38
|
+
__decorate([
|
|
39
|
+
column()
|
|
40
|
+
], ErrorLog.prototype, "statusCode", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
column()
|
|
43
|
+
], ErrorLog.prototype, "userId", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
column({
|
|
46
|
+
prepare: (value) => (value ? JSON.stringify(value) : null),
|
|
47
|
+
consume: (value) => value ? (typeof value === 'string' ? JSON.parse(value) : value) : null,
|
|
48
|
+
})
|
|
49
|
+
], ErrorLog.prototype, "context", void 0);
|
|
50
|
+
__decorate([
|
|
51
|
+
column.dateTime({ autoCreate: true })
|
|
52
|
+
], ErrorLog.prototype, "createdAt", void 0);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseModel } from '@adonisjs/lucid/orm';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
export default class RequestLog extends BaseModel {
|
|
4
|
+
static table: string;
|
|
5
|
+
id: number;
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
routeName: string | null;
|
|
9
|
+
statusCode: number;
|
|
10
|
+
responseTimeMs: number;
|
|
11
|
+
ip: string | null;
|
|
12
|
+
userAgent: string | null;
|
|
13
|
+
userId: number | null;
|
|
14
|
+
requestBody: Record<string, unknown> | null;
|
|
15
|
+
requestQuery: Record<string, unknown> | null;
|
|
16
|
+
createdAt: DateTime;
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Request Log Model
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Lucid model for the request_logs table. Stores automatic HTTP request
|
|
7
|
+
| logging data.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
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;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
|
17
|
+
export default class RequestLog extends BaseModel {
|
|
18
|
+
static table = 'request_logs';
|
|
19
|
+
}
|
|
20
|
+
__decorate([
|
|
21
|
+
column({ isPrimary: true })
|
|
22
|
+
], RequestLog.prototype, "id", void 0);
|
|
23
|
+
__decorate([
|
|
24
|
+
column()
|
|
25
|
+
], RequestLog.prototype, "method", void 0);
|
|
26
|
+
__decorate([
|
|
27
|
+
column()
|
|
28
|
+
], RequestLog.prototype, "url", void 0);
|
|
29
|
+
__decorate([
|
|
30
|
+
column()
|
|
31
|
+
], RequestLog.prototype, "routeName", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
column()
|
|
34
|
+
], RequestLog.prototype, "statusCode", void 0);
|
|
35
|
+
__decorate([
|
|
36
|
+
column()
|
|
37
|
+
], RequestLog.prototype, "responseTimeMs", void 0);
|
|
38
|
+
__decorate([
|
|
39
|
+
column()
|
|
40
|
+
], RequestLog.prototype, "ip", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
column()
|
|
43
|
+
], RequestLog.prototype, "userAgent", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
column()
|
|
46
|
+
], RequestLog.prototype, "userId", void 0);
|
|
47
|
+
__decorate([
|
|
48
|
+
column({
|
|
49
|
+
prepare: (value) => (value ? JSON.stringify(value) : null),
|
|
50
|
+
consume: (value) => value ? (typeof value === 'string' ? JSON.parse(value) : value) : null,
|
|
51
|
+
})
|
|
52
|
+
], RequestLog.prototype, "requestBody", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
column({
|
|
55
|
+
prepare: (value) => (value ? JSON.stringify(value) : null),
|
|
56
|
+
consume: (value) => value ? (typeof value === 'string' ? JSON.parse(value) : value) : null,
|
|
57
|
+
})
|
|
58
|
+
], RequestLog.prototype, "requestQuery", void 0);
|
|
59
|
+
__decorate([
|
|
60
|
+
column.dateTime({ autoCreate: true })
|
|
61
|
+
], RequestLog.prototype, "createdAt", void 0);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AuditConfig } from './types.js';
|
|
3
|
+
import RequestLog from './models/request_log.js';
|
|
4
|
+
export declare class RequestLogger {
|
|
5
|
+
#private;
|
|
6
|
+
protected config: AuditConfig;
|
|
7
|
+
constructor(config: AuditConfig);
|
|
8
|
+
/**
|
|
9
|
+
* Check if the request should be logged
|
|
10
|
+
*/
|
|
11
|
+
shouldLog(ctx: HttpContext): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize sensitive fields from an object
|
|
14
|
+
*/
|
|
15
|
+
sanitize(data: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
16
|
+
/**
|
|
17
|
+
* Log the request (fire-and-forget, non-blocking)
|
|
18
|
+
*/
|
|
19
|
+
logAsync(ctx: HttpContext, responseTimeMs: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Log the request (blocking, returns the created log)
|
|
22
|
+
*/
|
|
23
|
+
log(ctx: HttpContext, responseTimeMs: number): Promise<RequestLog | null>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Request Logger
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Logic for logging HTTP requests. Used by the request logger middleware.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
import { getUserIdFromContext } from './types.js';
|
|
10
|
+
import RequestLog from './models/request_log.js';
|
|
11
|
+
export class RequestLogger {
|
|
12
|
+
config;
|
|
13
|
+
#excludedMethods;
|
|
14
|
+
#excludedRoutes;
|
|
15
|
+
#sanitizeFields;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
// Pre-compute sets for O(1) lookups
|
|
19
|
+
this.#excludedMethods = new Set(config.requestLog.excludeMethods);
|
|
20
|
+
this.#excludedRoutes = config.requestLog.excludeRoutes;
|
|
21
|
+
this.#sanitizeFields = new Set(config.requestLog.sanitizeFields);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if the request should be logged
|
|
25
|
+
*/
|
|
26
|
+
shouldLog(ctx) {
|
|
27
|
+
if (!this.config.enabled || !this.config.requestLog.enabled) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const { request } = ctx;
|
|
31
|
+
const method = request.method();
|
|
32
|
+
const url = request.url();
|
|
33
|
+
// O(1) lookup instead of O(n)
|
|
34
|
+
if (this.#excludedMethods.has(method)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Check excluded routes
|
|
38
|
+
for (const excludedRoute of this.#excludedRoutes) {
|
|
39
|
+
if (url.startsWith(excludedRoute)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if object needs sanitization
|
|
47
|
+
*/
|
|
48
|
+
#needsSanitization(data) {
|
|
49
|
+
for (const key of Object.keys(data)) {
|
|
50
|
+
if (this.#sanitizeFields.has(key)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const value = data[key];
|
|
54
|
+
if (typeof value === 'object' &&
|
|
55
|
+
value !== null &&
|
|
56
|
+
this.#needsSanitization(value)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sanitize sensitive fields from an object
|
|
64
|
+
*/
|
|
65
|
+
sanitize(data) {
|
|
66
|
+
if (!data || this.#sanitizeFields.size === 0) {
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
// Skip copy if no sanitization needed
|
|
70
|
+
if (!this.#needsSanitization(data)) {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
const sanitized = {};
|
|
74
|
+
for (const [key, value] of Object.entries(data)) {
|
|
75
|
+
if (this.#sanitizeFields.has(key)) {
|
|
76
|
+
sanitized[key] = '[REDACTED]';
|
|
77
|
+
}
|
|
78
|
+
else if (typeof value === 'object' && value !== null) {
|
|
79
|
+
sanitized[key] = this.sanitize(value);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
sanitized[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return sanitized;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Log the request (fire-and-forget, non-blocking)
|
|
89
|
+
*/
|
|
90
|
+
logAsync(ctx, responseTimeMs) {
|
|
91
|
+
if (!this.shouldLog(ctx)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Fire-and-forget to not block the response
|
|
95
|
+
setImmediate(() => {
|
|
96
|
+
this.#createLog(ctx, responseTimeMs).catch((error) => {
|
|
97
|
+
console.error('[RequestLogger] Failed to log request:', error);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Log the request (blocking, returns the created log)
|
|
103
|
+
*/
|
|
104
|
+
async log(ctx, responseTimeMs) {
|
|
105
|
+
if (!this.shouldLog(ctx)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return this.#createLog(ctx, responseTimeMs);
|
|
109
|
+
}
|
|
110
|
+
async #createLog(ctx, responseTimeMs) {
|
|
111
|
+
const { request, response, route } = ctx;
|
|
112
|
+
const entry = {
|
|
113
|
+
method: request.method(),
|
|
114
|
+
url: request.url(true),
|
|
115
|
+
routeName: route?.name ?? null,
|
|
116
|
+
statusCode: response.getStatus(),
|
|
117
|
+
responseTimeMs,
|
|
118
|
+
ip: request.ip(),
|
|
119
|
+
userAgent: request.header('user-agent')?.substring(0, 512) ?? null,
|
|
120
|
+
userId: getUserIdFromContext(ctx),
|
|
121
|
+
requestBody: this.config.requestLog.logBody
|
|
122
|
+
? this.sanitize(request.body())
|
|
123
|
+
: null,
|
|
124
|
+
requestQuery: this.config.requestLog.logQuery
|
|
125
|
+
? this.sanitize(request.qs())
|
|
126
|
+
: null,
|
|
127
|
+
};
|
|
128
|
+
return RequestLog.create(entry);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* Interface for authenticated user with minimal required properties
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthUser {
|
|
6
|
+
id: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Interface for auth object when @adonisjs/auth is installed
|
|
10
|
+
*/
|
|
11
|
+
export interface AuthContract {
|
|
12
|
+
user?: AuthUser | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extended HttpContext that may include auth from @adonisjs/auth package
|
|
16
|
+
*/
|
|
17
|
+
export interface HttpContextWithAuth extends HttpContext {
|
|
18
|
+
auth?: AuthContract;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to check if context has auth
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasAuth(ctx: HttpContext): ctx is HttpContextWithAuth;
|
|
24
|
+
/**
|
|
25
|
+
* Helper to safely get user ID from context
|
|
26
|
+
*/
|
|
27
|
+
export declare function getUserIdFromContext(ctx: HttpContext): number | null;
|
|
28
|
+
/**
|
|
29
|
+
* Interface for HTTP errors with status code
|
|
30
|
+
*/
|
|
31
|
+
export interface HttpError extends Error {
|
|
32
|
+
status?: number;
|
|
33
|
+
code?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Type guard to check if error is an HttpError
|
|
37
|
+
*/
|
|
38
|
+
export declare function isHttpError(error: unknown): error is HttpError;
|
|
39
|
+
/**
|
|
40
|
+
* Helper to get status code from error
|
|
41
|
+
*/
|
|
42
|
+
export declare function getErrorStatusCode(error: unknown): number | undefined;
|
|
43
|
+
export interface AuditConfig {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
requestLog: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
excludeRoutes: string[];
|
|
48
|
+
excludeMethods: string[];
|
|
49
|
+
logBody: boolean;
|
|
50
|
+
logQuery: boolean;
|
|
51
|
+
sanitizeFields: string[];
|
|
52
|
+
};
|
|
53
|
+
errorLog: {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
excludeStatusCodes: number[];
|
|
56
|
+
includeStack: boolean;
|
|
57
|
+
};
|
|
58
|
+
auditLog: {
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
};
|
|
61
|
+
retention: {
|
|
62
|
+
requestLogs: number;
|
|
63
|
+
errorLogs: number;
|
|
64
|
+
auditLogs: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface AuditLogEntry {
|
|
68
|
+
actionType: string;
|
|
69
|
+
metadata: Record<string, unknown>;
|
|
70
|
+
userId?: number | null;
|
|
71
|
+
}
|
|
72
|
+
export interface RequestLogEntry {
|
|
73
|
+
method: string;
|
|
74
|
+
url: string;
|
|
75
|
+
routeName: string | null;
|
|
76
|
+
statusCode: number;
|
|
77
|
+
responseTimeMs: number;
|
|
78
|
+
ip: string | null;
|
|
79
|
+
userAgent: string | null;
|
|
80
|
+
userId: number | null;
|
|
81
|
+
requestBody: Record<string, unknown> | null;
|
|
82
|
+
requestQuery: Record<string, unknown> | null;
|
|
83
|
+
}
|
|
84
|
+
export interface ErrorLogEntry {
|
|
85
|
+
errorType: string;
|
|
86
|
+
message: string;
|
|
87
|
+
stack: string | null;
|
|
88
|
+
url: string | null;
|
|
89
|
+
method: string | null;
|
|
90
|
+
statusCode: number | null;
|
|
91
|
+
userId: number | null;
|
|
92
|
+
context: Record<string, unknown> | null;
|
|
93
|
+
}
|
|
94
|
+
export declare function defineConfig(config: Partial<AuditConfig>): AuditConfig;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Types
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| TypeScript interfaces and types for the audit log package.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Type guard to check if context has auth
|
|
11
|
+
*/
|
|
12
|
+
export function hasAuth(ctx) {
|
|
13
|
+
return 'auth' in ctx && ctx.auth !== undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Helper to safely get user ID from context
|
|
17
|
+
*/
|
|
18
|
+
export function getUserIdFromContext(ctx) {
|
|
19
|
+
if (hasAuth(ctx)) {
|
|
20
|
+
return ctx.auth?.user?.id ?? null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Type guard to check if error is an HttpError
|
|
26
|
+
*/
|
|
27
|
+
export function isHttpError(error) {
|
|
28
|
+
return error instanceof Error;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Helper to get status code from error
|
|
32
|
+
*/
|
|
33
|
+
export function getErrorStatusCode(error) {
|
|
34
|
+
if (isHttpError(error)) {
|
|
35
|
+
return error.status;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function defineConfig(config) {
|
|
40
|
+
return {
|
|
41
|
+
enabled: config.enabled ?? true,
|
|
42
|
+
requestLog: {
|
|
43
|
+
enabled: config.requestLog?.enabled ?? true,
|
|
44
|
+
excludeRoutes: config.requestLog?.excludeRoutes ?? ['/health', '/metrics'],
|
|
45
|
+
excludeMethods: config.requestLog?.excludeMethods ?? ['OPTIONS'],
|
|
46
|
+
logBody: config.requestLog?.logBody ?? false,
|
|
47
|
+
logQuery: config.requestLog?.logQuery ?? true,
|
|
48
|
+
sanitizeFields: config.requestLog?.sanitizeFields ?? ['password', 'token', 'secret'],
|
|
49
|
+
},
|
|
50
|
+
errorLog: {
|
|
51
|
+
enabled: config.errorLog?.enabled ?? true,
|
|
52
|
+
excludeStatusCodes: config.errorLog?.excludeStatusCodes ?? [404],
|
|
53
|
+
includeStack: config.errorLog?.includeStack ?? true,
|
|
54
|
+
},
|
|
55
|
+
auditLog: {
|
|
56
|
+
enabled: config.auditLog?.enabled ?? true,
|
|
57
|
+
},
|
|
58
|
+
retention: {
|
|
59
|
+
requestLogs: config.retention?.requestLogs ?? 30,
|
|
60
|
+
errorLogs: config.retention?.errorLogs ?? 90,
|
|
61
|
+
auditLogs: config.retention?.auditLogs ?? 365,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({
|
|
3
|
+
to: app.configPath('audit.ts')
|
|
4
|
+
})
|
|
5
|
+
}}}
|
|
6
|
+
import { defineConfig } from '@cepseudo/adonis-audit-log'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
// Enable/disable all logging
|
|
10
|
+
enabled: true,
|
|
11
|
+
|
|
12
|
+
// Request logging options
|
|
13
|
+
requestLog: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
excludeRoutes: ['/health', '/metrics'],
|
|
16
|
+
excludeMethods: ['OPTIONS'],
|
|
17
|
+
logBody: false,
|
|
18
|
+
logQuery: true,
|
|
19
|
+
sanitizeFields: ['password', 'token', 'secret'],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Error logging options
|
|
23
|
+
errorLog: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
excludeStatusCodes: [404],
|
|
26
|
+
includeStack: true,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Audit log options
|
|
30
|
+
auditLog: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Retention (days) - for cleanup job
|
|
35
|
+
retention: {
|
|
36
|
+
requestLogs: 30,
|
|
37
|
+
errorLogs: 90,
|
|
38
|
+
auditLogs: 365,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Path to the root directory where the stubs are stored. We use
|
|
5
|
+
* this path within commands and the configure hook
|
|
6
|
+
*/
|
|
7
|
+
export const stubsRoot = dirname(fileURLToPath(import.meta.url));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{#var tableName = 'audit_logs'}}
|
|
2
|
+
{{#var migrationName = 'create_' + tableName + '_table'}}
|
|
3
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = '{{ tableName }}'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.increments('id')
|
|
11
|
+
table.string('action_type').notNullable().index()
|
|
12
|
+
table.jsonb('metadata').notNullable().defaultTo('{}')
|
|
13
|
+
table.integer('user_id').unsigned().nullable().references('id').inTable('users').onDelete('SET NULL')
|
|
14
|
+
table.timestamp('created_at').notNullable().defaultTo(this.now())
|
|
15
|
+
|
|
16
|
+
table.index(['created_at'])
|
|
17
|
+
table.index(['user_id'])
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async down() {
|
|
22
|
+
this.schema.dropTable(this.tableName)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{{#var tableName = 'error_logs'}}
|
|
2
|
+
{{#var migrationName = 'create_' + tableName + '_table'}}
|
|
3
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = '{{ tableName }}'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.increments('id')
|
|
11
|
+
table.string('error_type').notNullable()
|
|
12
|
+
table.text('message').notNullable()
|
|
13
|
+
table.text('stack').nullable()
|
|
14
|
+
table.string('url', 2048).nullable()
|
|
15
|
+
table.string('method', 10).nullable()
|
|
16
|
+
table.integer('status_code').nullable()
|
|
17
|
+
table.integer('user_id').unsigned().nullable().references('id').inTable('users').onDelete('SET NULL')
|
|
18
|
+
table.jsonb('context').nullable()
|
|
19
|
+
table.timestamp('created_at').notNullable().defaultTo(this.now())
|
|
20
|
+
|
|
21
|
+
table.index(['created_at'])
|
|
22
|
+
table.index(['user_id'])
|
|
23
|
+
table.index(['error_type'])
|
|
24
|
+
table.index(['status_code'])
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async down() {
|
|
29
|
+
this.schema.dropTable(this.tableName)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{{#var tableName = 'request_logs'}}
|
|
2
|
+
{{#var migrationName = 'create_' + tableName + '_table'}}
|
|
3
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = '{{ tableName }}'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.increments('id')
|
|
11
|
+
table.string('method', 10).notNullable()
|
|
12
|
+
table.string('url', 2048).notNullable()
|
|
13
|
+
table.string('route_name').nullable()
|
|
14
|
+
table.integer('status_code').notNullable()
|
|
15
|
+
table.integer('response_time_ms').notNullable()
|
|
16
|
+
table.string('ip', 45).nullable()
|
|
17
|
+
table.string('user_agent', 512).nullable()
|
|
18
|
+
table.integer('user_id').unsigned().nullable().references('id').inTable('users').onDelete('SET NULL')
|
|
19
|
+
table.jsonb('request_body').nullable()
|
|
20
|
+
table.jsonb('request_query').nullable()
|
|
21
|
+
table.timestamp('created_at').notNullable().defaultTo(this.now())
|
|
22
|
+
|
|
23
|
+
table.index(['created_at'])
|
|
24
|
+
table.index(['user_id'])
|
|
25
|
+
table.index(['status_code'])
|
|
26
|
+
table.index(['method'])
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async down() {
|
|
31
|
+
this.schema.dropTable(this.tableName)
|
|
32
|
+
}
|
|
33
|
+
}
|