@hacimertgokhan/next-audit 1.0.0 → 1.0.2
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/.idea/modules.xml +8 -0
- package/.idea/next-audit.iml +12 -0
- package/README.md +58 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +42 -17
- package/dist/decorators/audit.js +1 -19
- package/dist/index.js +0 -1
- package/dist/lib/mysql.js +0 -2
- package/dist/types.d.ts +0 -9
- package/package.json +2 -2
- package/src/config.ts +41 -17
- package/src/decorators/audit.ts +15 -19
- package/src/index.ts +1 -1
- package/src/lib/mysql.ts +2 -2
- package/src/types.ts +0 -9
- package/test/manual_verification_draft.ts +9 -9
- package/test/verify_dist.ts +17 -17
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/next-audit.iml" filepath="$PROJECT_DIR$/.idea/next-audit.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @hacimertgokhan/next-audit
|
|
2
|
+
|
|
3
|
+
A lightweight TypeScript library for Next.js to audit backend requests automatically using decorators.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Easy Integration**: Simply add the `@Audit()` decorator to your controller methods.
|
|
7
|
+
- **Automated Logging**: Captures method, URL, status code, execution duration, and IP.
|
|
8
|
+
- **Data Tracking**: Automatically records `old_value` (for DELETE) and `new_value` (for POST/PUT/PATCH).
|
|
9
|
+
- **MySQL Support**: Built-in adapter for MySQL storage.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @hacimertgokhan/next-audit mysql2 reflect-metadata
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Create a `audit.conf.ts` file in your project root:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
export default {
|
|
23
|
+
database: {
|
|
24
|
+
url: process.env.DATABASE_URL,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
tableName: 'audit_logs',
|
|
28
|
+
debug: false
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Use the decorator in your Next.js API route class methods:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Audit } from '@hacimertgokhan/next-audit';
|
|
38
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
39
|
+
|
|
40
|
+
class UserController {
|
|
41
|
+
|
|
42
|
+
@Audit()
|
|
43
|
+
async createUser(req: NextRequest) {
|
|
44
|
+
const data = await req.json();
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({ id: 1, ...data });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Audit()
|
|
50
|
+
async deleteUser(req: NextRequest) {
|
|
51
|
+
|
|
52
|
+
return NextResponse.json({ success: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
ISC
|
package/dist/config.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import { AuditConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Manually set the audit configuration.
|
|
4
|
+
* This is useful if you want to load config from a different source.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setAuditConfig(config: AuditConfig): void;
|
|
7
|
+
/**
|
|
8
|
+
* Loads the configuration from audit.conf.js or audit.conf.ts in the project root.
|
|
9
|
+
* Falls back to environment variables if no config file is found.
|
|
10
|
+
*/
|
|
2
11
|
export declare function loadConfig(): AuditConfig;
|
package/dist/config.js
CHANGED
|
@@ -3,10 +3,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setAuditConfig = setAuditConfig;
|
|
6
7
|
exports.loadConfig = loadConfig;
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
let cachedConfig = null;
|
|
11
|
+
/**
|
|
12
|
+
* Manually set the audit configuration.
|
|
13
|
+
* This is useful if you want to load config from a different source.
|
|
14
|
+
*/
|
|
15
|
+
function setAuditConfig(config) {
|
|
16
|
+
cachedConfig = config;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Loads the configuration from audit.conf.js or audit.conf.ts in the project root.
|
|
20
|
+
* Falls back to environment variables if no config file is found.
|
|
21
|
+
*/
|
|
10
22
|
function loadConfig() {
|
|
11
23
|
if (cachedConfig)
|
|
12
24
|
return cachedConfig;
|
|
@@ -14,42 +26,55 @@ function loadConfig() {
|
|
|
14
26
|
const configPathTs = path_1.default.resolve(process.cwd(), 'audit.conf.ts');
|
|
15
27
|
const configPathJs = path_1.default.resolve(process.cwd(), 'audit.conf.js');
|
|
16
28
|
try {
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
// Use eval('require') to bypass Webpack's static analysis in Next.js
|
|
30
|
+
// This allows loading a file from the disk at runtime.
|
|
31
|
+
const safeRequire = (p) => {
|
|
32
|
+
try {
|
|
33
|
+
// eslint-disable-next-line no-eval
|
|
34
|
+
return eval('require')(p);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
// Fallback for non-webpack environments
|
|
38
|
+
if (typeof require !== 'undefined') {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
40
|
+
return require(p);
|
|
41
|
+
}
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
21
45
|
if (fs_1.default.existsSync(configPathJs)) {
|
|
22
|
-
|
|
23
|
-
const userConfig = require(configPathJs);
|
|
46
|
+
const userConfig = safeRequire(configPathJs);
|
|
24
47
|
cachedConfig = userConfig.default || userConfig;
|
|
25
48
|
}
|
|
26
49
|
else if (fs_1.default.existsSync(configPathTs)) {
|
|
27
|
-
// Only works if running via ts-node or if we invoke a transpiler on fly
|
|
28
|
-
// For now, we warn or rely on an environment that supports it.
|
|
29
50
|
try {
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
// Try to register ts-node if it's available for .ts config files
|
|
52
|
+
try {
|
|
53
|
+
safeRequire('ts-node/register');
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// ts-node not available, hope for the best or it might already be registered
|
|
57
|
+
}
|
|
58
|
+
const userConfig = safeRequire(configPathTs);
|
|
34
59
|
cachedConfig = userConfig.default || userConfig;
|
|
35
60
|
}
|
|
36
61
|
catch (e) {
|
|
37
|
-
console.warn('[Next-Audit] Found audit.conf.ts but could not load it directly
|
|
62
|
+
console.warn('[Next-Audit] Found audit.conf.ts but could not load it directly. Please ensure it is compiled to js or use audit.conf.js');
|
|
38
63
|
}
|
|
39
64
|
}
|
|
40
65
|
}
|
|
41
66
|
catch (error) {
|
|
67
|
+
// Only log if it's not a "module not found" error for the config files themselves
|
|
68
|
+
// but wait, we already check fs.existsSync, so any error here is likely real.
|
|
42
69
|
console.error('[Next-Audit] Error loading config:', error);
|
|
43
70
|
}
|
|
44
71
|
if (!cachedConfig) {
|
|
45
|
-
// Fallback:
|
|
46
|
-
// For MVP we assume environment variables might also be used
|
|
72
|
+
// Fallback: environment variables
|
|
47
73
|
cachedConfig = {
|
|
48
74
|
database: {
|
|
49
75
|
url: process.env.DATABASE_URL
|
|
50
|
-
// default to local if nothing found?
|
|
51
76
|
},
|
|
52
|
-
debug:
|
|
77
|
+
debug: !!process.env.AUDIT_DEBUG
|
|
53
78
|
};
|
|
54
79
|
if (!process.env.DATABASE_URL) {
|
|
55
80
|
console.warn('[Next-Audit] No config file found and DATABASE_URL not set. Audit might fail.');
|
package/dist/decorators/audit.js
CHANGED
|
@@ -11,39 +11,30 @@ function Audit(options) {
|
|
|
11
11
|
const startTime = Date.now();
|
|
12
12
|
let req;
|
|
13
13
|
let context;
|
|
14
|
-
// Try to find NextRequest in arguments
|
|
15
14
|
for (const arg of args) {
|
|
16
15
|
if (arg instanceof Request || (arg && arg.constructor && arg.constructor.name === 'NextRequest')) {
|
|
17
16
|
req = arg;
|
|
18
17
|
break;
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
|
-
// Capture inputs
|
|
22
20
|
const url = req?.url || '';
|
|
23
21
|
const method = req?.method || 'UNKNOWN';
|
|
24
22
|
const userAgent = req?.headers.get('user-agent') || '';
|
|
25
|
-
|
|
26
|
-
let oldData = null; // For delete operations, we hope to capture this
|
|
23
|
+
let oldData = null;
|
|
27
24
|
let newData = null;
|
|
28
|
-
// Execute original method
|
|
29
25
|
let result;
|
|
30
26
|
let status = 200;
|
|
31
27
|
let errorOccurred = false;
|
|
32
28
|
try {
|
|
33
29
|
result = await originalMethod.apply(this, args);
|
|
34
|
-
// Analyze Result
|
|
35
30
|
if (result instanceof Response) {
|
|
36
31
|
status = result.status;
|
|
37
|
-
// Clone to read body without consuming the original stream if possible,
|
|
38
|
-
// or assume we can't read it if it's a stream already locked.
|
|
39
|
-
// For JSON responses, we might want to peek.
|
|
40
32
|
try {
|
|
41
33
|
const clone = result.clone();
|
|
42
34
|
const bodyText = await clone.text();
|
|
43
35
|
if (bodyText) {
|
|
44
36
|
try {
|
|
45
37
|
const json = JSON.parse(bodyText);
|
|
46
|
-
// If DELETE, assume result is the deleted data or confirmation
|
|
47
38
|
if (method === 'DELETE') {
|
|
48
39
|
oldData = json;
|
|
49
40
|
}
|
|
@@ -52,16 +43,13 @@ function Audit(options) {
|
|
|
52
43
|
}
|
|
53
44
|
}
|
|
54
45
|
catch (e) {
|
|
55
|
-
// Not JSON
|
|
56
46
|
}
|
|
57
47
|
}
|
|
58
48
|
}
|
|
59
49
|
catch (e) {
|
|
60
|
-
// Failed to read response body
|
|
61
50
|
}
|
|
62
51
|
}
|
|
63
52
|
else {
|
|
64
|
-
// If the function returns a plain object (not a Response), treat it as data
|
|
65
53
|
if (method === 'DELETE') {
|
|
66
54
|
oldData = result;
|
|
67
55
|
}
|
|
@@ -73,7 +61,6 @@ function Audit(options) {
|
|
|
73
61
|
catch (error) {
|
|
74
62
|
errorOccurred = true;
|
|
75
63
|
status = 500;
|
|
76
|
-
// Try to get status from error if available
|
|
77
64
|
if (error.status)
|
|
78
65
|
status = error.status;
|
|
79
66
|
if (error.statusCode)
|
|
@@ -94,12 +81,7 @@ function Audit(options) {
|
|
|
94
81
|
user_agent: userAgent,
|
|
95
82
|
old_value: oldData,
|
|
96
83
|
new_value: newData,
|
|
97
|
-
// user_id, entity, entity_id need more specific logic or config to extract
|
|
98
84
|
};
|
|
99
|
-
// Fire and forget audit log to not block response?
|
|
100
|
-
// Or await it? User might want reliability.
|
|
101
|
-
// We'll await it for now to ensure it's logged,
|
|
102
|
-
// but catching errors so we don't break the app if logging fails.
|
|
103
85
|
(0, mysql_1.saveAuditLog)(entry, config).catch(e => console.error('Audit Log Error', e));
|
|
104
86
|
}
|
|
105
87
|
}
|
package/dist/index.js
CHANGED
|
@@ -17,4 +17,3 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./decorators/audit"), exports);
|
|
18
18
|
__exportStar(require("./types"), exports);
|
|
19
19
|
__exportStar(require("./config"), exports);
|
|
20
|
-
// We might not export lib/mysql directly unless user needs custom access
|
package/dist/lib/mysql.js
CHANGED
|
@@ -35,8 +35,6 @@ async function saveAuditLog(entry, config) {
|
|
|
35
35
|
}
|
|
36
36
|
const db = await getDbConnection(config);
|
|
37
37
|
const tableName = config.tableName || 'audit_logs';
|
|
38
|
-
// Simple table check/creation (naive implementation for MVP)
|
|
39
|
-
// In production, migrations are preferred.
|
|
40
38
|
await db.query(`
|
|
41
39
|
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
|
42
40
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
package/dist/types.d.ts
CHANGED
|
@@ -7,17 +7,8 @@ export interface AuditConfig {
|
|
|
7
7
|
password?: string;
|
|
8
8
|
database?: string;
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
* Table name to store audit logs. Default: 'audit_logs'
|
|
12
|
-
*/
|
|
13
10
|
tableName?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Enable console logging for debugging
|
|
16
|
-
*/
|
|
17
11
|
debug?: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* If true, database writes are skipped (for testing)
|
|
20
|
-
*/
|
|
21
12
|
testMode?: boolean;
|
|
22
13
|
}
|
|
23
14
|
export type AuditLogEntry = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hacimertgokhan/next-audit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Next.js backend audit system with @Audit decorator",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,4 +28,4 @@
|
|
|
28
28
|
"next": "^16.1.3",
|
|
29
29
|
"typescript": "^5.5.0"
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,18 @@ import { AuditConfig } from './types';
|
|
|
4
4
|
|
|
5
5
|
let cachedConfig: AuditConfig | null = null;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Manually set the audit configuration.
|
|
9
|
+
* This is useful if you want to load config from a different source.
|
|
10
|
+
*/
|
|
11
|
+
export function setAuditConfig(config: AuditConfig) {
|
|
12
|
+
cachedConfig = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads the configuration from audit.conf.js or audit.conf.ts in the project root.
|
|
17
|
+
* Falls back to environment variables if no config file is found.
|
|
18
|
+
*/
|
|
7
19
|
export function loadConfig(): AuditConfig {
|
|
8
20
|
if (cachedConfig) return cachedConfig;
|
|
9
21
|
|
|
@@ -12,42 +24,54 @@ export function loadConfig(): AuditConfig {
|
|
|
12
24
|
const configPathJs = path.resolve(process.cwd(), 'audit.conf.js');
|
|
13
25
|
|
|
14
26
|
try {
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
// Use eval('require') to bypass Webpack's static analysis in Next.js
|
|
28
|
+
// This allows loading a file from the disk at runtime.
|
|
29
|
+
const safeRequire = (p: string) => {
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line no-eval
|
|
32
|
+
return eval('require')(p);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Fallback for non-webpack environments
|
|
35
|
+
if (typeof require !== 'undefined') {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
37
|
+
return require(p);
|
|
38
|
+
}
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
19
42
|
|
|
20
43
|
if (fs.existsSync(configPathJs)) {
|
|
21
|
-
|
|
22
|
-
const userConfig = require(configPathJs);
|
|
44
|
+
const userConfig = safeRequire(configPathJs);
|
|
23
45
|
cachedConfig = userConfig.default || userConfig;
|
|
24
46
|
} else if (fs.existsSync(configPathTs)) {
|
|
25
|
-
// Only works if running via ts-node or if we invoke a transpiler on fly
|
|
26
|
-
// For now, we warn or rely on an environment that supports it.
|
|
27
47
|
try {
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
// Try to register ts-node if it's available for .ts config files
|
|
49
|
+
try {
|
|
50
|
+
safeRequire('ts-node/register');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// ts-node not available, hope for the best or it might already be registered
|
|
53
|
+
}
|
|
54
|
+
const userConfig = safeRequire(configPathTs);
|
|
32
55
|
cachedConfig = userConfig.default || userConfig;
|
|
33
56
|
} catch (e) {
|
|
34
|
-
console.warn('[Next-Audit] Found audit.conf.ts but could not load it directly
|
|
57
|
+
console.warn('[Next-Audit] Found audit.conf.ts but could not load it directly. Please ensure it is compiled to js or use audit.conf.js');
|
|
35
58
|
}
|
|
36
59
|
}
|
|
37
60
|
} catch (error) {
|
|
61
|
+
// Only log if it's not a "module not found" error for the config files themselves
|
|
62
|
+
// but wait, we already check fs.existsSync, so any error here is likely real.
|
|
38
63
|
console.error('[Next-Audit] Error loading config:', error);
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
if (!cachedConfig) {
|
|
42
|
-
// Fallback:
|
|
43
|
-
// For MVP we assume environment variables might also be used
|
|
67
|
+
// Fallback: environment variables
|
|
44
68
|
cachedConfig = {
|
|
45
69
|
database: {
|
|
46
70
|
url: process.env.DATABASE_URL
|
|
47
|
-
// default to local if nothing found?
|
|
48
71
|
},
|
|
49
|
-
debug:
|
|
72
|
+
debug: !!process.env.AUDIT_DEBUG
|
|
50
73
|
};
|
|
74
|
+
|
|
51
75
|
if (!process.env.DATABASE_URL) {
|
|
52
76
|
console.warn('[Next-Audit] No config file found and DATABASE_URL not set. Audit might fail.');
|
|
53
77
|
}
|
package/src/decorators/audit.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function Audit(options?: { actionType?: string }) {
|
|
|
17
17
|
let req: NextRequest | undefined;
|
|
18
18
|
let context: any;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
for (const arg of args) {
|
|
22
22
|
if (arg instanceof Request || (arg && arg.constructor && arg.constructor.name === 'NextRequest')) {
|
|
23
23
|
req = arg as NextRequest;
|
|
@@ -25,16 +25,16 @@ export function Audit(options?: { actionType?: string }) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
const url = req?.url || '';
|
|
30
30
|
const method = req?.method || 'UNKNOWN';
|
|
31
31
|
const userAgent = req?.headers.get('user-agent') || '';
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
|
|
34
|
-
let oldData = null;
|
|
34
|
+
let oldData = null;
|
|
35
35
|
let newData = null;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
let result;
|
|
39
39
|
let status = 200;
|
|
40
40
|
let errorOccurred = false;
|
|
@@ -42,33 +42,33 @@ export function Audit(options?: { actionType?: string }) {
|
|
|
42
42
|
try {
|
|
43
43
|
result = await originalMethod.apply(this, args);
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
if (result instanceof Response) {
|
|
47
47
|
status = result.status;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
51
|
try {
|
|
52
52
|
const clone = result.clone();
|
|
53
53
|
const bodyText = await clone.text();
|
|
54
54
|
if (bodyText) {
|
|
55
55
|
try {
|
|
56
56
|
const json = JSON.parse(bodyText);
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
if (method === 'DELETE') {
|
|
59
59
|
oldData = json;
|
|
60
60
|
} else if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
61
61
|
newData = json;
|
|
62
62
|
}
|
|
63
63
|
} catch (e) {
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
} catch (e) {
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
}
|
|
70
70
|
} else {
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
if (method === 'DELETE') {
|
|
73
73
|
oldData = result;
|
|
74
74
|
} else {
|
|
@@ -79,7 +79,7 @@ export function Audit(options?: { actionType?: string }) {
|
|
|
79
79
|
} catch (error: any) {
|
|
80
80
|
errorOccurred = true;
|
|
81
81
|
status = 500;
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
if (error.status) status = error.status;
|
|
84
84
|
if (error.statusCode) status = error.statusCode;
|
|
85
85
|
throw error;
|
|
@@ -98,13 +98,9 @@ export function Audit(options?: { actionType?: string }) {
|
|
|
98
98
|
user_agent: userAgent,
|
|
99
99
|
old_value: oldData,
|
|
100
100
|
new_value: newData,
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
// Fire and forget audit log to not block response?
|
|
105
|
-
// Or await it? User might want reliability.
|
|
106
|
-
// We'll await it for now to ensure it's logged,
|
|
107
|
-
// but catching errors so we don't break the app if logging fails.
|
|
108
104
|
saveAuditLog(entry, config).catch(e => console.error('Audit Log Error', e));
|
|
109
105
|
}
|
|
110
106
|
}
|
package/src/index.ts
CHANGED
package/src/lib/mysql.ts
CHANGED
|
@@ -36,8 +36,8 @@ export async function saveAuditLog(entry: AuditLogEntry, config: AuditConfig) {
|
|
|
36
36
|
const db = await getDbConnection(config);
|
|
37
37
|
const tableName = config.tableName || 'audit_logs';
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
|
|
40
|
+
|
|
41
41
|
await db.query(`
|
|
42
42
|
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
|
43
43
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
package/src/types.ts
CHANGED
|
@@ -7,17 +7,8 @@ export interface AuditConfig {
|
|
|
7
7
|
password?: string;
|
|
8
8
|
database?: string;
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
* Table name to store audit logs. Default: 'audit_logs'
|
|
12
|
-
*/
|
|
13
10
|
tableName?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Enable console logging for debugging
|
|
16
|
-
*/
|
|
17
11
|
debug?: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* If true, database writes are skipped (for testing)
|
|
20
|
-
*/
|
|
21
12
|
testMode?: boolean;
|
|
22
13
|
}
|
|
23
14
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Audit } from '../src/index';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
class MockNextRequest {
|
|
4
4
|
public url: string;
|
|
5
5
|
public method: string;
|
|
@@ -11,15 +11,15 @@ class MockNextRequest {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
jest.mock('../src/config', () => ({
|
|
16
16
|
loadConfig: () => ({
|
|
17
|
-
database: {},
|
|
17
|
+
database: {},
|
|
18
18
|
debug: true
|
|
19
19
|
})
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
jest.mock('../src/lib/mysql', () => ({
|
|
24
24
|
saveAuditLog: async (entry: any) => {
|
|
25
25
|
console.log('MOCK DB SAVE:', JSON.stringify(entry, null, 2));
|
|
@@ -45,15 +45,15 @@ async function runTest() {
|
|
|
45
45
|
|
|
46
46
|
console.log('--- Test 1: DELETE ---');
|
|
47
47
|
const req1 = new MockNextRequest('http://localhost:3000/api/users/123', 'DELETE');
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
await controller.deleteUser(req1);
|
|
50
50
|
|
|
51
51
|
console.log('\n--- Test 2: POST (Response object) ---');
|
|
52
52
|
const req2 = new MockNextRequest('http://localhost:3000/api/posts', 'POST');
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
await controller.createPost(req2);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
package/test/verify_dist.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import { Audit } from '../dist/index';
|
|
3
|
-
// Usage of dist means we should build again before running this.
|
|
4
|
-
// But technically in node we can import src if we use ts-node, but I am forcing dist usage to match real world.
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
7
|
class NextRequest {
|
|
8
8
|
public url: string;
|
|
9
9
|
public method: string;
|
|
@@ -15,10 +15,10 @@ class NextRequest {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
22
|
|
|
23
23
|
import fs from 'fs';
|
|
24
24
|
import path from 'path';
|
|
@@ -28,7 +28,7 @@ const dummyConfigPath = path.resolve(process.cwd(), 'audit.conf.js');
|
|
|
28
28
|
class TestController {
|
|
29
29
|
|
|
30
30
|
@Audit()
|
|
31
|
-
async deleteUser(req: any) {
|
|
31
|
+
async deleteUser(req: any) {
|
|
32
32
|
console.log(' -> Controller: deleteUser called');
|
|
33
33
|
return { success: true, deletedId: 101, name: 'Deleted User' };
|
|
34
34
|
}
|
|
@@ -36,15 +36,15 @@ class TestController {
|
|
|
36
36
|
@Audit()
|
|
37
37
|
async createPost(req: any) {
|
|
38
38
|
console.log(' -> Controller: createPost called');
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
return { status: 201, json: () => ({ id: 202, title: 'New Post' }) };
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
|
|
42
|
+
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function runTest() {
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
fs.writeFileSync(dummyConfigPath, `
|
|
49
49
|
module.exports = {
|
|
50
50
|
database: { url: 'mysql://mock:3306/db' },
|
|
@@ -59,23 +59,23 @@ async function runTest() {
|
|
|
59
59
|
|
|
60
60
|
console.log('\n--- Test 1: DELETE ---');
|
|
61
61
|
const req1 = new NextRequest('http://api.test/users/101', 'DELETE');
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
await controller.deleteUser(req1);
|
|
64
64
|
|
|
65
65
|
console.log('\n--- Test 2: POST ---');
|
|
66
66
|
const req2 = new NextRequest('http://api.test/posts', 'POST');
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
await controller.createPost(req2);
|
|
69
69
|
|
|
70
70
|
console.log('\n--- Test 3: PATCH ---');
|
|
71
71
|
const req3 = new NextRequest('http://api.test/users/101', 'PATCH');
|
|
72
|
-
|
|
73
|
-
await controller.createPost(req3);
|
|
72
|
+
|
|
73
|
+
await controller.createPost(req3);
|
|
74
74
|
|
|
75
75
|
} catch (e) {
|
|
76
76
|
console.error('Test Failed:', e);
|
|
77
77
|
} finally {
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
if (fs.existsSync(dummyConfigPath)) {
|
|
80
80
|
fs.unlinkSync(dummyConfigPath);
|
|
81
81
|
console.log('Removed dummy audit.conf.js');
|