@govish/shared-services 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.
@@ -0,0 +1,785 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AuditService = exports.AuditEventType = void 0;
13
+ const kafkajs_1 = require("kafkajs");
14
+ /**
15
+ * Audit event types
16
+ */
17
+ var AuditEventType;
18
+ (function (AuditEventType) {
19
+ // Authentication events
20
+ AuditEventType["LOGIN_SUCCESS"] = "login_success";
21
+ AuditEventType["LOGIN_FAILED"] = "login_failed";
22
+ AuditEventType["LOGIN_NOT_PERMITTED"] = "login_not_permitted";
23
+ AuditEventType["LOGOUT"] = "logout";
24
+ AuditEventType["TOKEN_GENERATED"] = "token_generated";
25
+ // Officer events (read-only consumption)
26
+ AuditEventType["OFFICER_RETRIEVED"] = "officer_retrieved";
27
+ AuditEventType["OFFICERS_LISTED"] = "officers_listed";
28
+ // Device events (read-only consumption)
29
+ AuditEventType["DEVICE_RETRIEVED"] = "device_retrieved";
30
+ AuditEventType["DEVICES_LISTED"] = "devices_listed";
31
+ // Arrest events
32
+ AuditEventType["ARREST_CREATED"] = "arrest_created";
33
+ AuditEventType["ARREST_UPDATED"] = "arrest_updated";
34
+ AuditEventType["ARREST_DELETED"] = "arrest_deleted";
35
+ AuditEventType["ARREST_RETRIEVED"] = "arrest_retrieved";
36
+ AuditEventType["ARRESTS_LISTED"] = "arrests_listed";
37
+ AuditEventType["ARREST_OB_GENERATED"] = "arrest_ob_generated";
38
+ // General access events
39
+ AuditEventType["ENDPOINT_ACCESSED"] = "endpoint_accessed";
40
+ AuditEventType["UNAUTHORIZED_ACCESS"] = "unauthorized_access";
41
+ AuditEventType["FORBIDDEN_ACCESS"] = "forbidden_access";
42
+ })(AuditEventType || (exports.AuditEventType = AuditEventType = {}));
43
+ class AuditService {
44
+ constructor(deps) {
45
+ this.isInternalConnection = false;
46
+ this.connectionAttempted = false;
47
+ this.logger = deps.logger;
48
+ this.serverName = deps.serverName;
49
+ this.auditTopic = deps.auditTopic;
50
+ this.enableDebugLogs = deps.enableDebugLogs || false;
51
+ // If Kafka brokers are provided, create internal connection
52
+ if (deps.kafkaBrokers) {
53
+ this.initializeInternalKafkaConnection(deps);
54
+ }
55
+ else if (deps.kafkaProducer) {
56
+ // Use provided producer (backward compatibility)
57
+ this.kafkaProducer = deps.kafkaProducer;
58
+ this.isProducerConnected = deps.isProducerConnected;
59
+ }
60
+ }
61
+ /**
62
+ * Initialize internal Kafka connection from brokers
63
+ */
64
+ initializeInternalKafkaConnection(deps) {
65
+ try {
66
+ // Parse brokers
67
+ const brokers = Array.isArray(deps.kafkaBrokers)
68
+ ? deps.kafkaBrokers
69
+ : typeof deps.kafkaBrokers === 'string'
70
+ ? deps.kafkaBrokers.split(',').map(b => b.trim())
71
+ : [];
72
+ if (brokers.length === 0) {
73
+ this.logger.warn('No valid Kafka brokers provided');
74
+ return;
75
+ }
76
+ const clientId = deps.kafkaClientId || 'audit-service';
77
+ // Create Kafka client
78
+ this.internalKafkaClient = new kafkajs_1.Kafka({
79
+ clientId,
80
+ brokers,
81
+ retry: {
82
+ retries: 3,
83
+ initialRetryTime: 1000,
84
+ multiplier: 2,
85
+ maxRetryTime: 5000,
86
+ },
87
+ connectionTimeout: 5000,
88
+ requestTimeout: 30000,
89
+ });
90
+ // Create producer
91
+ this.internalProducer = this.internalKafkaClient.producer({
92
+ createPartitioner: kafkajs_1.Partitioners.LegacyPartitioner,
93
+ retry: {
94
+ retries: 8,
95
+ initialRetryTime: 100,
96
+ multiplier: 2,
97
+ maxRetryTime: 30000,
98
+ },
99
+ });
100
+ this.kafkaProducer = this.internalProducer;
101
+ this.isInternalConnection = true;
102
+ // Create connection check function
103
+ this.isProducerConnected = () => {
104
+ // For internal producer, we'll check connection status
105
+ // KafkaJS doesn't expose connection state directly, so we track it
106
+ return this.connectionAttempted && this.internalProducer !== undefined;
107
+ };
108
+ this.logger.info('Internal Kafka connection initialized', {
109
+ brokers,
110
+ clientId,
111
+ });
112
+ // Try to connect immediately (non-blocking)
113
+ this.ensureConnected().catch((error) => {
114
+ this.logger.warn('Initial connection attempt failed, will retry on first send', {
115
+ error: error instanceof Error ? error.message : String(error),
116
+ });
117
+ });
118
+ }
119
+ catch (error) {
120
+ this.logger.error('Failed to initialize internal Kafka connection', {
121
+ error: error instanceof Error ? error.message : String(error),
122
+ });
123
+ }
124
+ }
125
+ /**
126
+ * Debug log helper - only logs if debug is enabled
127
+ */
128
+ debugLog(message, data) {
129
+ if (this.enableDebugLogs) {
130
+ if (data) {
131
+ console.log(`[AUDIT KAFKA] ${message}`, data);
132
+ }
133
+ else {
134
+ console.log(`[AUDIT KAFKA] ${message}`);
135
+ }
136
+ }
137
+ }
138
+ /**
139
+ * Debug error helper - only logs if debug is enabled
140
+ */
141
+ debugError(message, data) {
142
+ if (this.enableDebugLogs) {
143
+ if (data) {
144
+ console.error(`[AUDIT KAFKA] ${message}`, data);
145
+ }
146
+ else {
147
+ console.error(`[AUDIT KAFKA] ${message}`);
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Ensure producer is connected (for internal connections)
153
+ */
154
+ ensureConnected() {
155
+ return __awaiter(this, void 0, void 0, function* () {
156
+ if (!this.isInternalConnection || !this.internalProducer) {
157
+ this.debugLog('Not internal connection or no internal producer', {
158
+ isInternalConnection: this.isInternalConnection,
159
+ hasInternalProducer: !!this.internalProducer,
160
+ hasKafkaProducer: !!this.kafkaProducer,
161
+ });
162
+ return !!this.kafkaProducer;
163
+ }
164
+ // Always try to connect if not already connected
165
+ // Don't rely on connectionAttempted flag - actually check connection
166
+ try {
167
+ if (!this.connectionAttempted) {
168
+ this.connectionAttempted = true;
169
+ yield this.internalProducer.connect();
170
+ this.logger.info('Internal Kafka producer connected');
171
+ return true;
172
+ }
173
+ else {
174
+ // Already attempted - assume connected
175
+ return true;
176
+ }
177
+ }
178
+ catch (error) {
179
+ this.connectionAttempted = false;
180
+ const errorMessage = error instanceof Error ? error.message : String(error);
181
+ this.debugError('Failed to connect internal Kafka producer', {
182
+ error: errorMessage,
183
+ stack: error instanceof Error ? error.stack : undefined,
184
+ });
185
+ this.logger.warn('Failed to connect internal Kafka producer', {
186
+ error: errorMessage,
187
+ });
188
+ return false;
189
+ }
190
+ });
191
+ }
192
+ /**
193
+ * Send audit event to Kafka
194
+ */
195
+ logAuditEvent(req, payload) {
196
+ return __awaiter(this, void 0, void 0, function* () {
197
+ var _a, _b, _c, _d;
198
+ try {
199
+ // Get IP address - try multiple sources in order of reliability
200
+ let ip_address = 'unknown';
201
+ // Log available IP sources for debugging
202
+ const ipSources = {
203
+ 'x-forwarded-for': req.headers['x-forwarded-for'],
204
+ 'x-real-ip': req.headers['x-real-ip'],
205
+ 'cf-connecting-ip': req.headers['cf-connecting-ip'], // Cloudflare
206
+ 'true-client-ip': req.headers['true-client-ip'], // Cloudflare Enterprise
207
+ 'x-client-ip': req.headers['x-client-ip'],
208
+ 'req.ip': req.ip,
209
+ 'req.socket.remoteAddress': (_a = req.socket) === null || _a === void 0 ? void 0 : _a.remoteAddress,
210
+ 'req.connection.remoteAddress': (_b = req.connection) === null || _b === void 0 ? void 0 : _b.remoteAddress,
211
+ };
212
+ this.debugLog('IP address sources', ipSources);
213
+ // Try x-forwarded-for first (most common behind proxies/load balancers)
214
+ // Format: "client, proxy1, proxy2" - we want the first (original client)
215
+ if (req.headers['x-forwarded-for']) {
216
+ const forwardedFor = String(req.headers['x-forwarded-for']).trim();
217
+ if (forwardedFor) {
218
+ // Split by comma and get first IP, then trim whitespace
219
+ const firstIp = forwardedFor.split(',')[0].trim();
220
+ if (firstIp && firstIp !== 'unknown') {
221
+ ip_address = firstIp;
222
+ this.logger.debug('Using IP from x-forwarded-for', { ip: ip_address });
223
+ }
224
+ }
225
+ }
226
+ // Try Cloudflare specific header
227
+ if (ip_address === 'unknown' && req.headers['cf-connecting-ip']) {
228
+ const cfIp = String(req.headers['cf-connecting-ip']).trim();
229
+ if (cfIp) {
230
+ ip_address = cfIp;
231
+ this.logger.debug('Using IP from cf-connecting-ip', { ip: ip_address });
232
+ }
233
+ }
234
+ // Try x-real-ip (common with nginx)
235
+ if (ip_address === 'unknown' && req.headers['x-real-ip']) {
236
+ const realIp = String(req.headers['x-real-ip']).trim();
237
+ if (realIp) {
238
+ ip_address = realIp;
239
+ this.logger.debug('Using IP from x-real-ip', { ip: ip_address });
240
+ }
241
+ }
242
+ // Try true-client-ip (Cloudflare Enterprise)
243
+ if (ip_address === 'unknown' && req.headers['true-client-ip']) {
244
+ const trueClientIp = String(req.headers['true-client-ip']).trim();
245
+ if (trueClientIp) {
246
+ ip_address = trueClientIp;
247
+ this.logger.debug('Using IP from true-client-ip', { ip: ip_address });
248
+ }
249
+ }
250
+ // Try x-client-ip
251
+ if (ip_address === 'unknown' && req.headers['x-client-ip']) {
252
+ const clientIp = String(req.headers['x-client-ip']).trim();
253
+ if (clientIp) {
254
+ ip_address = clientIp;
255
+ this.logger.debug('Using IP from x-client-ip', { ip: ip_address });
256
+ }
257
+ }
258
+ // Try req.ip (requires trust proxy to be set)
259
+ if (ip_address === 'unknown' && req.ip) {
260
+ const reqIp = String(req.ip).trim();
261
+ if (reqIp && reqIp !== '::1' && reqIp !== '127.0.0.1') {
262
+ ip_address = reqIp;
263
+ this.logger.debug('Using IP from req.ip', { ip: ip_address });
264
+ }
265
+ }
266
+ // Try socket.remoteAddress (direct connection)
267
+ if (ip_address === 'unknown' && ((_c = req.socket) === null || _c === void 0 ? void 0 : _c.remoteAddress)) {
268
+ const socketIp = String(req.socket.remoteAddress).trim();
269
+ // Remove IPv6 prefix if present
270
+ const cleanIp = socketIp.replace(/^::ffff:/, '');
271
+ if (cleanIp && cleanIp !== '::1' && cleanIp !== '127.0.0.1') {
272
+ ip_address = cleanIp;
273
+ this.logger.debug('Using IP from socket.remoteAddress', { ip: ip_address, original: socketIp });
274
+ }
275
+ }
276
+ // Try connection.remoteAddress (fallback)
277
+ if (ip_address === 'unknown' && ((_d = req.connection) === null || _d === void 0 ? void 0 : _d.remoteAddress)) {
278
+ const connIp = String(req.connection.remoteAddress).trim();
279
+ const cleanIp = connIp.replace(/^::ffff:/, '');
280
+ if (cleanIp && cleanIp !== '::1' && cleanIp !== '127.0.0.1') {
281
+ ip_address = cleanIp;
282
+ this.logger.debug('Using IP from connection.remoteAddress', { ip: ip_address, original: connIp });
283
+ }
284
+ }
285
+ // Log final IP address
286
+ this.logger.debug('Final IP address determined', {
287
+ ip_address,
288
+ all_sources: ipSources
289
+ });
290
+ // Get user agent
291
+ const user_agent = req.headers['user-agent'] || 'unknown';
292
+ // Extract user/device/microservice information from request
293
+ const authType = req.authType;
294
+ const officer = req.officer; // Use officer directly, as it has all the properties
295
+ const device = req.device;
296
+ const apiKey = req.apiKey;
297
+ const authenticatedMicroservice = req.microservice;
298
+ // Get microservice name from environment or default
299
+ const microserviceName = this.serverName || 'Package Name';
300
+ // Determine event type if not provided
301
+ let eventType = payload.event_type;
302
+ if (!eventType) {
303
+ eventType = AuditService.determineEventType(req);
304
+ }
305
+ // Build audit payload - start with base fields
306
+ const auditPayload = {
307
+ timestamp: new Date().toISOString(),
308
+ microservice: authenticatedMicroservice || microserviceName,
309
+ event_type: eventType,
310
+ endpoint: req.originalUrl || req.url,
311
+ method: req.method,
312
+ path: req.path,
313
+ query_params: Object.keys(req.query).length > 0 ? req.query : undefined,
314
+ ip_address,
315
+ user_agent,
316
+ };
317
+ // Add officer information if authenticated
318
+ if (officer) {
319
+ auditPayload.user_id = officer.id;
320
+ auditPayload.officer_id = officer.id;
321
+ auditPayload.officer_name = officer.name || undefined;
322
+ auditPayload.officer_service_number = officer.service_number || undefined;
323
+ auditPayload.officer_email = officer.email || undefined;
324
+ }
325
+ // Add device information if authenticated (minimal - just ID)
326
+ if (device) {
327
+ // Ensure device.id is a number
328
+ const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
329
+ // Set user_id to device_id if no officer is authenticated
330
+ if (!auditPayload.user_id && !isNaN(deviceId)) {
331
+ auditPayload.user_id = deviceId;
332
+ }
333
+ if (!isNaN(deviceId)) {
334
+ auditPayload.device_id = deviceId;
335
+ }
336
+ auditPayload.device_device_id = device.device_id;
337
+ }
338
+ // Set user type and add API Key / Microservice information
339
+ if (authType === 'api_key' || authType === 'microservice') {
340
+ auditPayload.user_type = 'microservice';
341
+ auditPayload.authenticated_microservice = authenticatedMicroservice || (apiKey === null || apiKey === void 0 ? void 0 : apiKey.microservice);
342
+ auditPayload.api_key_id = apiKey === null || apiKey === void 0 ? void 0 : apiKey.id;
343
+ // Set microservice name in the main microservice field
344
+ if (authenticatedMicroservice || (apiKey === null || apiKey === void 0 ? void 0 : apiKey.microservice)) {
345
+ auditPayload.microservice = authenticatedMicroservice || apiKey.microservice;
346
+ }
347
+ // For microservice authentication, we don't have a user_id
348
+ }
349
+ else if (authType === 'both') {
350
+ auditPayload.user_type = 'both';
351
+ // When both are authenticated, prioritize officer as user_id
352
+ if (officer) {
353
+ auditPayload.user_id = officer.id;
354
+ }
355
+ }
356
+ else if (authType === 'device') {
357
+ auditPayload.user_type = 'device';
358
+ // Use device id as user_id when only device is authenticated
359
+ if (device) {
360
+ const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
361
+ if (!isNaN(deviceId)) {
362
+ auditPayload.user_id = deviceId;
363
+ }
364
+ }
365
+ }
366
+ else if (authType === 'officer') {
367
+ auditPayload.user_type = 'officer';
368
+ // Use officer id as user_id when only officer is authenticated
369
+ if (officer) {
370
+ auditPayload.user_id = officer.id;
371
+ }
372
+ }
373
+ // Extract arrest-specific information from request body or response
374
+ this.addArrestInformation(req, auditPayload, payload);
375
+ // Merge custom keys from payload
376
+ // Custom keys can override standard fields if explicitly provided
377
+ // This allows users to add custom metadata or override standard fields when needed
378
+ Object.keys(payload).forEach(key => {
379
+ if (payload[key] !== undefined) {
380
+ // Allow custom keys to override standard fields if explicitly provided
381
+ // This gives users full control over the audit payload
382
+ auditPayload[key] = payload[key];
383
+ }
384
+ });
385
+ // Build user info string for logging
386
+ const userInfo = auditPayload.officer_name
387
+ ? `${auditPayload.officer_name} (Officer ID: ${auditPayload.officer_id})`
388
+ : auditPayload.device_device_id
389
+ ? `Device: ${auditPayload.device_device_id} (Device ID: ${auditPayload.device_id})`
390
+ : auditPayload.authenticated_microservice
391
+ ? `Microservice: ${auditPayload.authenticated_microservice}`
392
+ : 'Unknown user';
393
+ // Log to console for visibility (can be controlled by enableDebugLogs)
394
+ if (this.enableDebugLogs) {
395
+ console.log(`[AUDIT] ${userInfo} accessed ${auditPayload.method} ${auditPayload.endpoint}`, {
396
+ user_type: auditPayload.user_type,
397
+ user_id: auditPayload.user_id,
398
+ officer_id: auditPayload.officer_id,
399
+ device_id: auditPayload.device_id,
400
+ event_type: auditPayload.event_type,
401
+ response_status: auditPayload.response_status,
402
+ ip_address: auditPayload.ip_address,
403
+ });
404
+ }
405
+ // Also log via logger (respects log level configuration)
406
+ this.logger.info(`Audit: ${userInfo} accessed ${auditPayload.method} ${auditPayload.endpoint}`, {
407
+ endpoint: auditPayload.endpoint,
408
+ method: auditPayload.method,
409
+ user_type: auditPayload.user_type,
410
+ user_id: auditPayload.user_id,
411
+ officer_id: auditPayload.officer_id,
412
+ device_id: auditPayload.device_id,
413
+ event_type: auditPayload.event_type,
414
+ response_status: auditPayload.response_status,
415
+ ip_address: auditPayload.ip_address,
416
+ topic: this.auditTopic || 'audit-log'
417
+ });
418
+ // Send to Kafka
419
+ // Check if Kafka is available before attempting to send
420
+ // Skip if Kafka producer is not available (but we've already logged to console)
421
+ if (!this.kafkaProducer) {
422
+ this.debugError('Kafka producer not available - skipping send', {
423
+ hasKafkaProducer: !!this.kafkaProducer,
424
+ hasIsProducerConnected: !!this.isProducerConnected,
425
+ });
426
+ return;
427
+ }
428
+ try {
429
+ // For internal connections, ensure we're connected
430
+ if (this.isInternalConnection) {
431
+ const connected = yield this.ensureConnected();
432
+ if (!connected) {
433
+ this.debugError('Internal producer connection failed - skipping send');
434
+ return;
435
+ }
436
+ }
437
+ else {
438
+ // For external producer, check connection status
439
+ const isConnected = this.isProducerConnected ? this.isProducerConnected() : false;
440
+ this.debugLog('Producer connection check', {
441
+ hasIsProducerConnected: !!this.isProducerConnected,
442
+ isConnected: isConnected,
443
+ isInternalConnection: this.isInternalConnection,
444
+ });
445
+ if (!this.isProducerConnected || !isConnected) {
446
+ // Producer not connected (but we've already logged to console)
447
+ this.debugError('Producer not connected - skipping send', {
448
+ hasKafkaProducer: !!this.kafkaProducer,
449
+ hasIsProducerConnected: !!this.isProducerConnected,
450
+ isConnected: isConnected,
451
+ });
452
+ return;
453
+ }
454
+ }
455
+ if (!this.kafkaProducer) {
456
+ this.debugError('Kafka producer not available (second check) - skipping send', {
457
+ hasKafkaProducer: !!this.kafkaProducer,
458
+ hasIsProducerConnected: !!this.isProducerConnected,
459
+ });
460
+ return;
461
+ }
462
+ this.debugLog('All checks passed - proceeding to send');
463
+ const topic = this.auditTopic || 'audit-log';
464
+ const kafkaMessage = {
465
+ key: `${auditPayload.method}-${auditPayload.endpoint}-${Date.now()}`,
466
+ value: JSON.stringify(auditPayload),
467
+ headers: {
468
+ 'microservice': auditPayload.microservice,
469
+ 'event-type': 'audit-log',
470
+ }
471
+ };
472
+ // Debug log the payload and topic before sending
473
+ this.debugLog(`Sending to topic: ${topic}`);
474
+ this.debugLog('Payload:', JSON.stringify(auditPayload, null, 2));
475
+ this.debugLog(`Message key: ${kafkaMessage.key}`);
476
+ yield this.kafkaProducer.send({
477
+ topic: topic,
478
+ messages: [kafkaMessage]
479
+ })
480
+ .then((result) => {
481
+ this.debugLog('Audit event sent to Kafka', {
482
+ result: result,
483
+ endpoint: req.path,
484
+ method: req.method,
485
+ authType: req.authType
486
+ });
487
+ })
488
+ .catch((error) => {
489
+ this.debugError('Error sending audit event to Kafka', {
490
+ error: error.message,
491
+ errorStack: error.stack,
492
+ endpoint: req.path,
493
+ method: req.method,
494
+ authType: req.authType
495
+ });
496
+ });
497
+ }
498
+ catch (kafkaError) {
499
+ const errorMessage = (kafkaError === null || kafkaError === void 0 ? void 0 : kafkaError.message) || String(kafkaError);
500
+ this.debugError('Error sending audit event to Kafka', {
501
+ error: errorMessage,
502
+ errorStack: kafkaError instanceof Error ? kafkaError.stack : undefined,
503
+ errorName: kafkaError instanceof Error ? kafkaError.name : undefined,
504
+ endpoint: req.path,
505
+ method: req.method,
506
+ authType: req.authType
507
+ });
508
+ // If it's a DNS resolution error or connection error, skip silently (Kafka is not available)
509
+ if (errorMessage.includes('getaddrinfo EAI_AGAIN') ||
510
+ errorMessage.includes('ENOTFOUND') ||
511
+ errorMessage.includes('Connection error') ||
512
+ errorMessage.includes('not connected') ||
513
+ errorMessage.includes('ECONNREFUSED') ||
514
+ errorMessage.includes('ETIMEDOUT')) {
515
+ // Kafka is not available, skip audit logging silently
516
+ return;
517
+ }
518
+ // If connection error, try to connect and retry once
519
+ if (kafkaError instanceof Error && errorMessage.includes('not connected') && this.kafkaProducer) {
520
+ try {
521
+ yield this.kafkaProducer.connect();
522
+ // Retry send after connection
523
+ const retryTopic = this.auditTopic || 'audit-log';
524
+ const retryMessage = {
525
+ key: `${auditPayload.method}-${auditPayload.endpoint}-${Date.now()}`,
526
+ value: JSON.stringify(auditPayload),
527
+ headers: {
528
+ 'microservice': auditPayload.microservice,
529
+ 'event-type': 'audit-log',
530
+ }
531
+ };
532
+ // Debug log the payload and topic before retry send
533
+ this.debugLog(`[RETRY] Sending to topic: ${retryTopic}`);
534
+ this.debugLog('[RETRY] Payload:', JSON.stringify(auditPayload, null, 2));
535
+ this.debugLog(`[RETRY] Message key: ${retryMessage.key}`);
536
+ yield this.kafkaProducer.send({
537
+ topic: retryTopic,
538
+ messages: [retryMessage]
539
+ });
540
+ }
541
+ catch (retryError) {
542
+ // If retry also fails with connection/DNS error, skip silently
543
+ const retryErrorMessage = retryError instanceof Error ? retryError.message : String(retryError);
544
+ if (retryErrorMessage.includes('getaddrinfo EAI_AGAIN') ||
545
+ retryErrorMessage.includes('ENOTFOUND') ||
546
+ retryErrorMessage.includes('Connection error') ||
547
+ retryErrorMessage.includes('ECONNREFUSED') ||
548
+ retryErrorMessage.includes('ETIMEDOUT')) {
549
+ return; // Skip silently
550
+ }
551
+ // Re-throw other errors to be caught by outer catch block
552
+ throw new Error(`Kafka send failed after retry: ${retryErrorMessage}`);
553
+ }
554
+ }
555
+ else {
556
+ // Re-throw to be caught by outer catch block
557
+ throw new Error(`Kafka send failed: ${errorMessage}`);
558
+ }
559
+ }
560
+ this.logger.debug('Audit event sent to Kafka', {
561
+ endpoint: auditPayload.endpoint,
562
+ method: auditPayload.method,
563
+ user_type: auditPayload.user_type,
564
+ topic: this.auditTopic || 'audit-log'
565
+ });
566
+ }
567
+ catch (error) {
568
+ const errorMessage = error instanceof Error ? error.message : String(error);
569
+ // Skip logging DNS/connection errors - Kafka is simply not available
570
+ if (errorMessage.includes('getaddrinfo EAI_AGAIN') ||
571
+ errorMessage.includes('ENOTFOUND') ||
572
+ errorMessage.includes('Connection error') ||
573
+ errorMessage.includes('not connected') ||
574
+ errorMessage.includes('ECONNREFUSED') ||
575
+ errorMessage.includes('ETIMEDOUT')) {
576
+ // Silently skip - Kafka is not available
577
+ return;
578
+ }
579
+ // Only log unexpected errors (but don't throw - audit logging should not break the request)
580
+ // Use debug level instead of error to reduce noise
581
+ this.logger.debug('Error sending audit event to Kafka', {
582
+ error: errorMessage,
583
+ errorStack: error instanceof Error ? error.stack : undefined,
584
+ errorName: error instanceof Error ? error.name : undefined,
585
+ endpoint: req.path,
586
+ method: req.method,
587
+ authType: req.authType
588
+ });
589
+ }
590
+ });
591
+ }
592
+ /**
593
+ * Determine event type based on request path and method
594
+ */
595
+ static determineEventType(req) {
596
+ // Use originalUrl or baseUrl + path to get the full path
597
+ // req.path is relative to the router mount point, so for "/" route it would be just "/"
598
+ const fullPath = (req.originalUrl || req.baseUrl + req.path || req.path).toLowerCase();
599
+ const path = req.path.toLowerCase();
600
+ const method = req.method.toUpperCase();
601
+ // Login events
602
+ if (fullPath.includes('/login') || path.includes('/login')) {
603
+ const status = req.statusCode || 200;
604
+ if (status === 200) {
605
+ return AuditEventType.LOGIN_SUCCESS;
606
+ }
607
+ else if (status === 401 || status === 403) {
608
+ return AuditEventType.LOGIN_NOT_PERMITTED;
609
+ }
610
+ else {
611
+ return AuditEventType.LOGIN_FAILED;
612
+ }
613
+ }
614
+ // Officer events (read-only consumption)
615
+ // Check both fullPath (for routes like /api/v1/officer) and path (for routes like /officer)
616
+ if (fullPath.includes('/officer') || fullPath.includes('/officers') ||
617
+ path.includes('/officer') || path.includes('/officers')) {
618
+ if (method === 'GET' && (fullPath.match(/\/officer\/\d+$/) || fullPath.match(/\/officers\/\d+$/) ||
619
+ path.match(/\/officer\/\d+$/) || path.match(/\/officers\/\d+$/))) {
620
+ return AuditEventType.OFFICER_RETRIEVED;
621
+ }
622
+ else if (method === 'GET') {
623
+ return AuditEventType.OFFICERS_LISTED;
624
+ }
625
+ }
626
+ // Device events (read-only consumption)
627
+ if (fullPath.includes('/device') || fullPath.includes('/devices') ||
628
+ path.includes('/device') || path.includes('/devices')) {
629
+ if (method === 'GET' && (fullPath.match(/\/device\/\d+$/) || fullPath.match(/\/devices\/\d+$/) ||
630
+ path.match(/\/device\/\d+$/) || path.match(/\/devices\/\d+$/))) {
631
+ return AuditEventType.DEVICE_RETRIEVED;
632
+ }
633
+ else if (method === 'GET') {
634
+ return AuditEventType.DEVICES_LISTED;
635
+ }
636
+ }
637
+ // Arrest events (sub_module_data endpoints)
638
+ if (fullPath.includes('/sub_module_data') || fullPath.includes('/arrest') ||
639
+ path.includes('/sub_module_data') || path.includes('/arrest')) {
640
+ if (method === 'POST') {
641
+ return AuditEventType.ARREST_CREATED;
642
+ }
643
+ else if (method === 'PUT') {
644
+ return AuditEventType.ARREST_UPDATED;
645
+ }
646
+ else if (method === 'DELETE') {
647
+ return AuditEventType.ARREST_DELETED;
648
+ }
649
+ else if (method === 'GET' && (fullPath.match(/\/sub_module_data\/\d+$/) || fullPath.match(/\/arrest\/\d+$/) ||
650
+ path.match(/\/sub_module_data\/\d+$/) || path.match(/\/arrest\/\d+$/))) {
651
+ return AuditEventType.ARREST_RETRIEVED;
652
+ }
653
+ else if (method === 'GET') {
654
+ return AuditEventType.ARRESTS_LISTED;
655
+ }
656
+ }
657
+ // Default to endpoint accessed
658
+ return AuditEventType.ENDPOINT_ACCESSED;
659
+ }
660
+ /**
661
+ * Log endpoint access (called from middleware)
662
+ */
663
+ logEndpointAccess(req) {
664
+ return __awaiter(this, void 0, void 0, function* () {
665
+ yield this.logAuditEvent(req, {
666
+ // Event type will be determined automatically
667
+ });
668
+ });
669
+ }
670
+ /**
671
+ * Log endpoint access with response status
672
+ */
673
+ logEndpointAccessWithResponse(req, res, startTime) {
674
+ return __awaiter(this, void 0, void 0, function* () {
675
+ const duration_ms = Date.now() - startTime;
676
+ yield this.logAuditEvent(req, {
677
+ response_status: res.statusCode,
678
+ duration_ms,
679
+ });
680
+ });
681
+ }
682
+ /**
683
+ * Extract and add arrest-specific information to audit payload
684
+ */
685
+ addArrestInformation(req, auditPayload, payload) {
686
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
687
+ // Check if this is an arrest-related endpoint
688
+ const path = req.path.toLowerCase();
689
+ if (!path.includes('/sub_module_data') && !path.includes('/arrest')) {
690
+ return;
691
+ }
692
+ // Extract from request body (for POST/PUT)
693
+ const body = req.body || {};
694
+ const responseData = req.responseData; // If response data is attached to request
695
+ // Extract arrest ID from URL params or body
696
+ if ((_a = req.params) === null || _a === void 0 ? void 0 : _a.id) {
697
+ auditPayload.arrest_id = parseInt(req.params.id) || undefined;
698
+ }
699
+ else if (body.id) {
700
+ auditPayload.arrest_id = body.id;
701
+ }
702
+ else if (responseData === null || responseData === void 0 ? void 0 : responseData.id) {
703
+ auditPayload.arrest_id = responseData.id;
704
+ }
705
+ else if ((_b = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _b === void 0 ? void 0 : _b.id) {
706
+ auditPayload.arrest_id = responseData.data.id;
707
+ }
708
+ // Extract OB number
709
+ if (body.ob_number) {
710
+ auditPayload.ob_number = body.ob_number;
711
+ }
712
+ else if (responseData === null || responseData === void 0 ? void 0 : responseData.ob_number) {
713
+ auditPayload.ob_number = responseData.ob_number;
714
+ }
715
+ else if ((_c = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _c === void 0 ? void 0 : _c.ob_number) {
716
+ auditPayload.ob_number = responseData.data.ob_number;
717
+ }
718
+ // Extract sync_id
719
+ if (body.sync_id) {
720
+ auditPayload.sync_id = body.sync_id;
721
+ }
722
+ else if (responseData === null || responseData === void 0 ? void 0 : responseData.sync_id) {
723
+ auditPayload.sync_id = responseData.sync_id;
724
+ }
725
+ else if ((_d = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _d === void 0 ? void 0 : _d.sync_id) {
726
+ auditPayload.sync_id = responseData.data.sync_id;
727
+ }
728
+ // Extract IPRS ID
729
+ if (body.iprsId || body.iprs_id) {
730
+ auditPayload.iprs_id = body.iprsId || body.iprs_id;
731
+ }
732
+ else if ((responseData === null || responseData === void 0 ? void 0 : responseData.iprsId) || (responseData === null || responseData === void 0 ? void 0 : responseData.iprs_id)) {
733
+ auditPayload.iprs_id = responseData.iprsId || responseData.iprs_id;
734
+ }
735
+ else if (((_e = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _e === void 0 ? void 0 : _e.iprsId) || ((_f = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _f === void 0 ? void 0 : _f.iprs_id)) {
736
+ auditPayload.iprs_id = responseData.data.iprsId || responseData.data.iprs_id;
737
+ }
738
+ // Extract arresting officer ID
739
+ if (body.arrestingOfficer || body.arresting_officer) {
740
+ auditPayload.arresting_officer_id = body.arrestingOfficer || body.arresting_officer;
741
+ }
742
+ else if ((responseData === null || responseData === void 0 ? void 0 : responseData.arrestingOfficer) || (responseData === null || responseData === void 0 ? void 0 : responseData.arresting_officer)) {
743
+ auditPayload.arresting_officer_id = responseData.arrestingOfficer || responseData.arresting_officer;
744
+ }
745
+ else if (((_g = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _g === void 0 ? void 0 : _g.arrestingOfficer) || ((_h = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _h === void 0 ? void 0 : _h.arresting_officer)) {
746
+ auditPayload.arresting_officer_id = responseData.data.arrestingOfficer || responseData.data.arresting_officer;
747
+ }
748
+ // Extract arresting station ID
749
+ if (body.arrestingStation || body.arresting_station) {
750
+ auditPayload.arresting_station_id = body.arrestingStation || body.arresting_station;
751
+ }
752
+ else if ((responseData === null || responseData === void 0 ? void 0 : responseData.arrestingStation) || (responseData === null || responseData === void 0 ? void 0 : responseData.arresting_station)) {
753
+ auditPayload.arresting_station_id = responseData.arrestingStation || responseData.arresting_station;
754
+ }
755
+ else if (((_j = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _j === void 0 ? void 0 : _j.arrestingStation) || ((_k = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _k === void 0 ? void 0 : _k.arresting_station)) {
756
+ auditPayload.arresting_station_id = responseData.data.arrestingStation || responseData.data.arresting_station;
757
+ }
758
+ // Extract sub_module_id
759
+ if (body.subModuleId || body.sub_module_id || body.sub_moduleId) {
760
+ auditPayload.sub_module_id = body.subModuleId || body.sub_module_id || body.sub_moduleId;
761
+ }
762
+ else if ((responseData === null || responseData === void 0 ? void 0 : responseData.subModuleId) || (responseData === null || responseData === void 0 ? void 0 : responseData.sub_module_id) || (responseData === null || responseData === void 0 ? void 0 : responseData.sub_moduleId)) {
763
+ auditPayload.sub_module_id = responseData.subModuleId || responseData.sub_module_id || responseData.sub_moduleId;
764
+ }
765
+ else if (((_l = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _l === void 0 ? void 0 : _l.subModuleId) || ((_m = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _m === void 0 ? void 0 : _m.sub_module_id) || ((_o = responseData === null || responseData === void 0 ? void 0 : responseData.data) === null || _o === void 0 ? void 0 : _o.sub_moduleId)) {
766
+ auditPayload.sub_module_id = responseData.data.subModuleId || responseData.data.sub_module_id || responseData.data.sub_moduleId;
767
+ }
768
+ // Override with explicit payload values if provided
769
+ if (payload.arrest_id)
770
+ auditPayload.arrest_id = payload.arrest_id;
771
+ if (payload.ob_number)
772
+ auditPayload.ob_number = payload.ob_number;
773
+ if (payload.sync_id)
774
+ auditPayload.sync_id = payload.sync_id;
775
+ if (payload.iprs_id)
776
+ auditPayload.iprs_id = payload.iprs_id;
777
+ if (payload.arresting_officer_id)
778
+ auditPayload.arresting_officer_id = payload.arresting_officer_id;
779
+ if (payload.arresting_station_id)
780
+ auditPayload.arresting_station_id = payload.arresting_station_id;
781
+ if (payload.sub_module_id)
782
+ auditPayload.sub_module_id = payload.sub_module_id;
783
+ }
784
+ }
785
+ exports.AuditService = AuditService;