@hacimertgokhan/next-audit 1.0.2 → 1.0.4

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.
@@ -71,6 +71,9 @@ function Audit(options) {
71
71
  const duration = Date.now() - startTime;
72
72
  const config = (0, config_1.loadConfig)();
73
73
  if (config) {
74
+ const clientIp = req?.headers.get('x-forwarded-for')?.split(',')[0] ||
75
+ req?.ip ||
76
+ req?.socket?.remoteAddress || '';
74
77
  const entry = {
75
78
  timestamp: new Date(),
76
79
  method,
@@ -79,6 +82,7 @@ function Audit(options) {
79
82
  status,
80
83
  duration_ms: duration,
81
84
  user_agent: userAgent,
85
+ ip: clientIp,
82
86
  old_value: oldData,
83
87
  new_value: newData,
84
88
  };
package/dist/lib/mysql.js CHANGED
@@ -3,78 +3,140 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getDbConnection = getDbConnection;
4
4
  exports.saveAuditLog = saveAuditLog;
5
5
  const promise_1 = require("mysql2/promise");
6
- let pool = null;
6
+ // Use global to persist pool across HMR reloads in Next.js development
7
+ const globalForAudit = global;
7
8
  async function getDbConnection(config) {
8
- if (pool)
9
- return pool;
10
- const dbConfig = config.database;
9
+ const configKey = JSON.stringify(config.database);
10
+ if (globalForAudit._nextAuditPool && globalForAudit._nextAuditConfig === configKey) {
11
+ return globalForAudit._nextAuditPool;
12
+ }
13
+ // Cleanup old pool if config changed
14
+ if (globalForAudit._nextAuditPool) {
15
+ await globalForAudit._nextAuditPool.end().catch(() => { });
16
+ globalForAudit._nextAuditTableInitialized = false;
17
+ }
18
+ const { database: dbConfig } = config;
19
+ const poolOptions = {
20
+ waitForConnections: true,
21
+ connectionLimit: 10,
22
+ maxIdle: 10,
23
+ idleTimeout: 60000,
24
+ queueLimit: 0,
25
+ enableKeepAlive: true,
26
+ keepAliveInitialDelay: 10000,
27
+ connectTimeout: 10000,
28
+ };
29
+ let pool;
11
30
  if (dbConfig.url) {
12
- pool = (0, promise_1.createPool)(dbConfig.url);
31
+ // Try to parse URL to avoid driver quirks with some IP formats
32
+ try {
33
+ const mysqlUrl = new URL(dbConfig.url.replace('mysql://', 'http://'));
34
+ pool = (0, promise_1.createPool)({
35
+ ...poolOptions,
36
+ host: mysqlUrl.hostname,
37
+ port: parseInt(mysqlUrl.port) || 3306,
38
+ user: mysqlUrl.username,
39
+ password: mysqlUrl.password,
40
+ database: mysqlUrl.pathname.substring(1),
41
+ });
42
+ }
43
+ catch (e) {
44
+ pool = (0, promise_1.createPool)(dbConfig.url);
45
+ }
13
46
  }
14
47
  else {
15
48
  pool = (0, promise_1.createPool)({
49
+ ...poolOptions,
16
50
  host: dbConfig.host,
17
51
  user: dbConfig.user,
18
52
  password: dbConfig.password,
19
53
  database: dbConfig.database,
20
- port: dbConfig.port,
21
- waitForConnections: true,
22
- connectionLimit: 10,
23
- queueLimit: 0,
54
+ port: dbConfig.port || 3306,
24
55
  });
25
56
  }
57
+ // Explicit error listener to clear global pool on fatal errors
58
+ pool.on('error', (err) => {
59
+ if (err.code === 'PROTOCOL_CONNECTION_LOST' || err.fatal) {
60
+ globalForAudit._nextAuditPool = undefined;
61
+ globalForAudit._nextAuditTableInitialized = false;
62
+ }
63
+ });
64
+ globalForAudit._nextAuditPool = pool;
65
+ globalForAudit._nextAuditConfig = configKey;
66
+ globalForAudit._nextAuditTableInitialized = false;
26
67
  return pool;
27
68
  }
69
+ async function ensureTableExists(connection, tableName) {
70
+ if (globalForAudit._nextAuditTableInitialized)
71
+ return;
72
+ await connection.query(`
73
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
74
+ id INT AUTO_INCREMENT PRIMARY KEY,
75
+ timestamp DATETIME,
76
+ method VARCHAR(10),
77
+ url VARCHAR(255),
78
+ user_id VARCHAR(50),
79
+ entity VARCHAR(50),
80
+ entity_id VARCHAR(50),
81
+ old_value JSON,
82
+ new_value JSON,
83
+ action_type VARCHAR(50),
84
+ status INT,
85
+ duration_ms INT,
86
+ ip VARCHAR(45),
87
+ user_agent VARCHAR(255)
88
+ )
89
+ `);
90
+ globalForAudit._nextAuditTableInitialized = true;
91
+ }
28
92
  async function saveAuditLog(entry, config) {
93
+ if (config.testMode && config.debug) {
94
+ console.log(`[Next-Audit] (Test Mode) Log: ${entry.method} ${entry.url}`);
95
+ return;
96
+ }
97
+ let connection = null;
29
98
  try {
30
- if (config.testMode) {
31
- if (config.debug) {
32
- console.log(`[Next-Audit] (Test Mode) Log would be saved: ${entry.method} ${entry.url}`);
33
- }
34
- return;
35
- }
36
- const db = await getDbConnection(config);
37
- const tableName = config.tableName || 'audit_logs';
38
- await db.query(`
39
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
40
- id INT AUTO_INCREMENT PRIMARY KEY,
41
- timestamp DATETIME,
42
- method VARCHAR(10),
43
- url VARCHAR(255),
44
- user_id VARCHAR(50),
45
- entity VARCHAR(50),
46
- entity_id VARCHAR(50),
47
- old_value JSON,
48
- new_value JSON,
49
- action_type VARCHAR(50),
50
- status INT,
51
- duration_ms INT,
52
- ip VARCHAR(45),
53
- user_agent VARCHAR(255)
54
- )
55
- `);
56
- await db.query(`INSERT INTO \`${tableName}\`
57
- (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
58
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
99
+ const pool = await getDbConnection(config);
100
+ // Checkout connection from pool
101
+ connection = await pool.getConnection();
102
+ // One-time table initialization per pool lifecycle
103
+ await ensureTableExists(connection, config.tableName || 'audit_logs');
104
+ // Prepare values - ensure JSON is stringified or null
105
+ const values = [
59
106
  entry.timestamp,
60
107
  entry.method,
61
108
  entry.url,
62
- entry.user_id,
63
- entry.entity,
64
- entry.entity_id,
65
- JSON.stringify(entry.old_value),
66
- JSON.stringify(entry.new_value),
109
+ entry.user_id || null,
110
+ entry.entity || null,
111
+ entry.entity_id || null,
112
+ entry.old_value ? JSON.stringify(entry.old_value) : null,
113
+ entry.new_value ? JSON.stringify(entry.new_value) : null,
67
114
  entry.action_type,
68
115
  entry.status,
69
116
  entry.duration_ms,
70
- entry.ip,
71
- entry.user_agent
72
- ]);
117
+ entry.ip || null,
118
+ entry.user_agent || null
119
+ ];
120
+ const tableName = config.tableName || 'audit_logs';
121
+ await connection.query(`INSERT INTO \`${tableName}\`
122
+ (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
123
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values);
73
124
  if (config.debug) {
74
125
  console.log(`[Next-Audit] Log saved: ${entry.method} ${entry.url}`);
75
126
  }
76
127
  }
77
128
  catch (error) {
78
- console.error('[Next-Audit] Failed to save audit log:', error);
129
+ const dbHost = config.database.url || config.database.host || 'unknown';
130
+ console.error(`[Next-Audit] Failed to save audit log to ${dbHost}:`, error.message || error);
131
+ // If it's a connection error, clear the pool so it recreates next time
132
+ if (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ETIMEDOUT' || error.fatal) {
133
+ globalForAudit._nextAuditPool = undefined;
134
+ globalForAudit._nextAuditTableInitialized = false;
135
+ }
136
+ }
137
+ finally {
138
+ if (connection) {
139
+ connection.release();
140
+ }
79
141
  }
80
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hacimertgokhan/next-audit",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Next.js backend audit system with @Audit decorator",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
-
33
32
 
34
- let oldData = null;
33
+
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;
@@ -88,6 +88,10 @@ export function Audit(options?: { actionType?: string }) {
88
88
  const config = loadConfig();
89
89
 
90
90
  if (config) {
91
+ const clientIp = req?.headers.get('x-forwarded-for')?.split(',')[0] ||
92
+ (req as any)?.ip ||
93
+ (req as any)?.socket?.remoteAddress || '';
94
+
91
95
  const entry: AuditLogEntry = {
92
96
  timestamp: new Date(),
93
97
  method,
@@ -96,9 +100,9 @@ export function Audit(options?: { actionType?: string }) {
96
100
  status,
97
101
  duration_ms: duration,
98
102
  user_agent: userAgent,
103
+ ip: clientIp,
99
104
  old_value: oldData,
100
105
  new_value: newData,
101
-
102
106
  };
103
107
 
104
108
  saveAuditLog(entry, config).catch(e => console.error('Audit Log Error', e));
package/src/lib/mysql.ts CHANGED
@@ -1,88 +1,162 @@
1
- import { createPool, Pool } from 'mysql2/promise';
1
+ import { createPool, Pool, PoolConnection } from 'mysql2/promise';
2
2
  import { AuditConfig, AuditLogEntry } from '../types';
3
3
 
4
- let pool: Pool | null = null;
4
+ // Use global to persist pool across HMR reloads in Next.js development
5
+ const globalForAudit = global as unknown as {
6
+ _nextAuditPool?: Pool;
7
+ _nextAuditConfig?: string;
8
+ _nextAuditTableInitialized?: boolean;
9
+ };
5
10
 
6
- export async function getDbConnection(config: AuditConfig) {
7
- if (pool) return pool;
11
+ export async function getDbConnection(config: AuditConfig): Promise<Pool> {
12
+ const configKey = JSON.stringify(config.database);
13
+
14
+ if (globalForAudit._nextAuditPool && globalForAudit._nextAuditConfig === configKey) {
15
+ return globalForAudit._nextAuditPool;
16
+ }
17
+
18
+ // Cleanup old pool if config changed
19
+ if (globalForAudit._nextAuditPool) {
20
+ await globalForAudit._nextAuditPool.end().catch(() => { });
21
+ globalForAudit._nextAuditTableInitialized = false;
22
+ }
23
+
24
+ const { database: dbConfig } = config;
25
+ const poolOptions: any = {
26
+ waitForConnections: true,
27
+ connectionLimit: 10,
28
+ maxIdle: 10,
29
+ idleTimeout: 60000,
30
+ queueLimit: 0,
31
+ enableKeepAlive: true,
32
+ keepAliveInitialDelay: 10000,
33
+ connectTimeout: 10000,
34
+ };
35
+
36
+ let pool: Pool;
8
37
 
9
- const dbConfig = config.database;
10
38
  if (dbConfig.url) {
11
- pool = createPool(dbConfig.url);
39
+ // Try to parse URL to avoid driver quirks with some IP formats
40
+ try {
41
+ const mysqlUrl = new URL(dbConfig.url.replace('mysql://', 'http://'));
42
+ pool = createPool({
43
+ ...poolOptions,
44
+ host: mysqlUrl.hostname,
45
+ port: parseInt(mysqlUrl.port) || 3306,
46
+ user: mysqlUrl.username,
47
+ password: mysqlUrl.password,
48
+ database: mysqlUrl.pathname.substring(1),
49
+ });
50
+ } catch (e) {
51
+ pool = createPool(dbConfig.url);
52
+ }
12
53
  } else {
13
54
  pool = createPool({
55
+ ...poolOptions,
14
56
  host: dbConfig.host,
15
57
  user: dbConfig.user,
16
58
  password: dbConfig.password,
17
59
  database: dbConfig.database,
18
- port: dbConfig.port,
19
- waitForConnections: true,
20
- connectionLimit: 10,
21
- queueLimit: 0,
60
+ port: dbConfig.port || 3306,
22
61
  });
23
62
  }
63
+
64
+ // Explicit error listener to clear global pool on fatal errors
65
+ (pool as any).on('error', (err: any) => {
66
+ if (err.code === 'PROTOCOL_CONNECTION_LOST' || err.fatal) {
67
+ globalForAudit._nextAuditPool = undefined;
68
+ globalForAudit._nextAuditTableInitialized = false;
69
+ }
70
+ });
71
+
72
+ globalForAudit._nextAuditPool = pool;
73
+ globalForAudit._nextAuditConfig = configKey;
74
+ globalForAudit._nextAuditTableInitialized = false;
75
+
24
76
  return pool;
25
77
  }
26
78
 
79
+ async function ensureTableExists(connection: PoolConnection, tableName: string) {
80
+ if (globalForAudit._nextAuditTableInitialized) return;
81
+
82
+ await connection.query(`
83
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
84
+ id INT AUTO_INCREMENT PRIMARY KEY,
85
+ timestamp DATETIME,
86
+ method VARCHAR(10),
87
+ url VARCHAR(255),
88
+ user_id VARCHAR(50),
89
+ entity VARCHAR(50),
90
+ entity_id VARCHAR(50),
91
+ old_value JSON,
92
+ new_value JSON,
93
+ action_type VARCHAR(50),
94
+ status INT,
95
+ duration_ms INT,
96
+ ip VARCHAR(45),
97
+ user_agent VARCHAR(255)
98
+ )
99
+ `);
100
+
101
+ globalForAudit._nextAuditTableInitialized = true;
102
+ }
103
+
27
104
  export async function saveAuditLog(entry: AuditLogEntry, config: AuditConfig) {
105
+ if (config.testMode && config.debug) {
106
+ console.log(`[Next-Audit] (Test Mode) Log: ${entry.method} ${entry.url}`);
107
+ return;
108
+ }
109
+
110
+ let connection: PoolConnection | null = null;
28
111
  try {
29
- if (config.testMode) {
30
- if (config.debug) {
31
- console.log(`[Next-Audit] (Test Mode) Log would be saved: ${entry.method} ${entry.url}`);
32
- }
33
- return;
34
- }
112
+ const pool = await getDbConnection(config);
35
113
 
36
- const db = await getDbConnection(config);
37
- const tableName = config.tableName || 'audit_logs';
114
+ // Checkout connection from pool
115
+ connection = await pool.getConnection();
38
116
 
39
-
40
-
41
- await db.query(`
42
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
43
- id INT AUTO_INCREMENT PRIMARY KEY,
44
- timestamp DATETIME,
45
- method VARCHAR(10),
46
- url VARCHAR(255),
47
- user_id VARCHAR(50),
48
- entity VARCHAR(50),
49
- entity_id VARCHAR(50),
50
- old_value JSON,
51
- new_value JSON,
52
- action_type VARCHAR(50),
53
- status INT,
54
- duration_ms INT,
55
- ip VARCHAR(45),
56
- user_agent VARCHAR(255)
57
- )
58
- `);
117
+ // One-time table initialization per pool lifecycle
118
+ await ensureTableExists(connection, config.tableName || 'audit_logs');
119
+
120
+ // Prepare values - ensure JSON is stringified or null
121
+ const values = [
122
+ entry.timestamp,
123
+ entry.method,
124
+ entry.url,
125
+ entry.user_id || null,
126
+ entry.entity || null,
127
+ entry.entity_id || null,
128
+ entry.old_value ? JSON.stringify(entry.old_value) : null,
129
+ entry.new_value ? JSON.stringify(entry.new_value) : null,
130
+ entry.action_type,
131
+ entry.status,
132
+ entry.duration_ms,
133
+ entry.ip || null,
134
+ entry.user_agent || null
135
+ ];
59
136
 
60
- await db.query(
137
+ const tableName = config.tableName || 'audit_logs';
138
+ await connection.query(
61
139
  `INSERT INTO \`${tableName}\`
62
- (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
63
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
64
- [
65
- entry.timestamp,
66
- entry.method,
67
- entry.url,
68
- entry.user_id,
69
- entry.entity,
70
- entry.entity_id,
71
- JSON.stringify(entry.old_value),
72
- JSON.stringify(entry.new_value),
73
- entry.action_type,
74
- entry.status,
75
- entry.duration_ms,
76
- entry.ip,
77
- entry.user_agent
78
- ]
140
+ (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
142
+ values
79
143
  );
80
144
 
81
145
  if (config.debug) {
82
146
  console.log(`[Next-Audit] Log saved: ${entry.method} ${entry.url}`);
83
147
  }
148
+ } catch (error: any) {
149
+ const dbHost = config.database.url || config.database.host || 'unknown';
150
+ console.error(`[Next-Audit] Failed to save audit log to ${dbHost}:`, error.message || error);
84
151
 
85
- } catch (error) {
86
- console.error('[Next-Audit] Failed to save audit log:', error);
152
+ // If it's a connection error, clear the pool so it recreates next time
153
+ if (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ETIMEDOUT' || error.fatal) {
154
+ globalForAudit._nextAuditPool = undefined;
155
+ globalForAudit._nextAuditTableInitialized = false;
156
+ }
157
+ } finally {
158
+ if (connection) {
159
+ connection.release();
160
+ }
87
161
  }
88
162
  }