@govish/shared-services 1.4.0 → 1.6.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/CHANGELOG.md +14 -0
- package/README.md +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/middleware/authenticateDevice.d.ts +13 -2
- package/dist/middleware/authenticateDevice.js +551 -69
- package/dist/services/apiKeyService.d.ts +2 -1
- package/dist/services/apiKeyService.js +237 -45
- package/dist/services/auditService.d.ts +14 -2
- package/dist/services/auditService.js +289 -12
- package/dist/utils/rollingCache.d.ts +34 -0
- package/dist/utils/rollingCache.js +203 -0
- package/package.json +7 -2
|
@@ -39,6 +39,16 @@ var AuditEventType;
|
|
|
39
39
|
AuditEventType["ENDPOINT_ACCESSED"] = "endpoint_accessed";
|
|
40
40
|
AuditEventType["UNAUTHORIZED_ACCESS"] = "unauthorized_access";
|
|
41
41
|
AuditEventType["FORBIDDEN_ACCESS"] = "forbidden_access";
|
|
42
|
+
// IPRS events
|
|
43
|
+
AuditEventType["IPRS_PERSON_SEARCHED"] = "iprs_person_searched";
|
|
44
|
+
AuditEventType["IPRS_PERSON_FETCHED_FROM_VPS"] = "iprs_person_fetched_from_vps";
|
|
45
|
+
AuditEventType["IPRS_PERSON_STORED"] = "iprs_person_stored";
|
|
46
|
+
AuditEventType["IPRS_PERSON_UPDATED"] = "iprs_person_updated";
|
|
47
|
+
// Vehicle events
|
|
48
|
+
AuditEventType["VEHICLE_SEARCHED"] = "vehicle_searched";
|
|
49
|
+
AuditEventType["VEHICLE_FETCHED_FROM_IPRS"] = "vehicle_fetched_from_iprs";
|
|
50
|
+
AuditEventType["VEHICLE_STORED"] = "vehicle_stored";
|
|
51
|
+
AuditEventType["VEHICLE_UPDATED"] = "vehicle_updated";
|
|
42
52
|
})(AuditEventType || (exports.AuditEventType = AuditEventType = {}));
|
|
43
53
|
class AuditService {
|
|
44
54
|
constructor(deps) {
|
|
@@ -294,6 +304,7 @@ class AuditService {
|
|
|
294
304
|
const officer = req.officer; // Use officer directly, as it has all the properties
|
|
295
305
|
const device = req.device;
|
|
296
306
|
const apiKey = req.apiKey;
|
|
307
|
+
console.log('🔑 [Audit] API Key:', apiKey);
|
|
297
308
|
const authenticatedMicroservice = req.microservice;
|
|
298
309
|
// Get microservice name from environment or default
|
|
299
310
|
const microserviceName = this.serverName || 'Package Name';
|
|
@@ -303,9 +314,11 @@ class AuditService {
|
|
|
303
314
|
eventType = AuditService.determineEventType(req);
|
|
304
315
|
}
|
|
305
316
|
// Build audit payload - start with base fields
|
|
317
|
+
// Note: API key fields will be set later if API key is detected
|
|
318
|
+
// microservice should always be the receiving service (from SERVER_NAME), not the source microservice
|
|
306
319
|
const auditPayload = {
|
|
307
320
|
timestamp: new Date().toISOString(),
|
|
308
|
-
microservice:
|
|
321
|
+
microservice: microserviceName, // Always use receiving service name from SERVER_NAME
|
|
309
322
|
event_type: eventType,
|
|
310
323
|
endpoint: req.originalUrl || req.url,
|
|
311
324
|
method: req.method,
|
|
@@ -313,6 +326,12 @@ class AuditService {
|
|
|
313
326
|
query_params: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
314
327
|
ip_address,
|
|
315
328
|
user_agent,
|
|
329
|
+
api_key_id: apiKey === null || apiKey === void 0 ? void 0 : apiKey.id,
|
|
330
|
+
api_key_name: apiKey === null || apiKey === void 0 ? void 0 : apiKey.description,
|
|
331
|
+
api_key_description: apiKey === null || apiKey === void 0 ? void 0 : apiKey.description,
|
|
332
|
+
api_key_preview: (apiKey === null || apiKey === void 0 ? void 0 : apiKey.key) ? apiKey.key.substring(0, 15) + '...' : undefined,
|
|
333
|
+
source_microservice: apiKey === null || apiKey === void 0 ? void 0 : apiKey.microservice,
|
|
334
|
+
authenticated_microservice: apiKey === null || apiKey === void 0 ? void 0 : apiKey.microservice,
|
|
316
335
|
};
|
|
317
336
|
// Add officer information if authenticated
|
|
318
337
|
if (officer) {
|
|
@@ -336,25 +355,92 @@ class AuditService {
|
|
|
336
355
|
auditPayload.device_device_id = device.device_id;
|
|
337
356
|
}
|
|
338
357
|
// Set user type and add API Key / Microservice information
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
// IMPORTANT: Always check for API key presence regardless of authType, as API key might be present
|
|
359
|
+
// even when authType is set to 'officer' or 'both' due to priority logic
|
|
360
|
+
const hasApiKey = !!(apiKey || authenticatedMicroservice);
|
|
361
|
+
const sourceMicroservice = (apiKey === null || apiKey === void 0 ? void 0 : apiKey.microservice) || authenticatedMicroservice;
|
|
362
|
+
// Always include API key information if present
|
|
363
|
+
if (hasApiKey) {
|
|
364
|
+
// Include API key object fields only if apiKey object exists
|
|
365
|
+
if (apiKey) {
|
|
366
|
+
auditPayload.api_key_id = apiKey.id;
|
|
367
|
+
auditPayload.api_key_name = apiKey.description || undefined; // API key name (from description field)
|
|
368
|
+
auditPayload.api_key_description = apiKey.description || undefined; // Keep description for backward compatibility
|
|
369
|
+
auditPayload.api_key_preview = apiKey.key ? apiKey.key.substring(0, 15) + '...' : undefined;
|
|
370
|
+
}
|
|
371
|
+
// ALWAYS include microservice information if authenticatedMicroservice is detected
|
|
372
|
+
// This works even if apiKey object is not attached
|
|
373
|
+
// Note: microservice field should always be the receiving service (from SERVER_NAME)
|
|
374
|
+
// source_microservice and authenticated_microservice are the API key's microservice
|
|
375
|
+
if (sourceMicroservice) {
|
|
376
|
+
auditPayload.source_microservice = sourceMicroservice;
|
|
377
|
+
auditPayload.authenticated_microservice = sourceMicroservice;
|
|
378
|
+
// Do NOT overwrite microservice - it should always be the receiving service
|
|
379
|
+
}
|
|
380
|
+
else if (authenticatedMicroservice) {
|
|
381
|
+
// Fallback: use authenticatedMicroservice directly if sourceMicroservice is not set
|
|
382
|
+
auditPayload.source_microservice = authenticatedMicroservice;
|
|
383
|
+
auditPayload.authenticated_microservice = authenticatedMicroservice;
|
|
384
|
+
// Do NOT overwrite microservice - it should always be the receiving service
|
|
346
385
|
}
|
|
347
|
-
//
|
|
386
|
+
// Log API key presence with full details
|
|
387
|
+
this.debugLog('API Key Detected', {
|
|
388
|
+
apiKeyId: (apiKey === null || apiKey === void 0 ? void 0 : apiKey.id) || 'N/A',
|
|
389
|
+
apiKeyName: (apiKey === null || apiKey === void 0 ? void 0 : apiKey.description) || sourceMicroservice || authenticatedMicroservice || 'N/A',
|
|
390
|
+
apiKeyPreview: (apiKey === null || apiKey === void 0 ? void 0 : apiKey.key) ? apiKey.key.substring(0, 15) + '...' : 'N/A',
|
|
391
|
+
sourceMicroservice: sourceMicroservice || authenticatedMicroservice,
|
|
392
|
+
hasApiKeyObject: !!apiKey,
|
|
393
|
+
endpoint: auditPayload.endpoint
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// Now set user_type based on authType and what's actually present
|
|
397
|
+
if (authType === 'api_key_and_officer') {
|
|
398
|
+
// API Key + Officer authentication (may also have device)
|
|
399
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
400
|
+
// Include officer information (officer is already set above)
|
|
401
|
+
if (officer) {
|
|
402
|
+
auditPayload.user_id = officer.id;
|
|
403
|
+
auditPayload.officer_id = officer.id;
|
|
404
|
+
auditPayload.officer_name = officer.name || undefined;
|
|
405
|
+
auditPayload.officer_service_number = officer.service_number || undefined;
|
|
406
|
+
auditPayload.officer_email = officer.email || undefined;
|
|
407
|
+
}
|
|
408
|
+
// Explicitly preserve device information if present (device info was added earlier, but ensure it's preserved)
|
|
409
|
+
if (device) {
|
|
410
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
411
|
+
if (!isNaN(deviceId)) {
|
|
412
|
+
auditPayload.device_id = deviceId;
|
|
413
|
+
}
|
|
414
|
+
auditPayload.device_device_id = device.device_id;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (authType === 'api_key' || authType === 'microservice') {
|
|
418
|
+
auditPayload.user_type = 'microservice';
|
|
419
|
+
// For microservice authentication, we don't have a user_id unless device is present
|
|
420
|
+
// (device info is already set above if present)
|
|
348
421
|
}
|
|
349
422
|
else if (authType === 'both') {
|
|
350
|
-
|
|
423
|
+
// Device + Officer - but check if API key is also present
|
|
424
|
+
if (hasApiKey) {
|
|
425
|
+
// All three: API Key + Device + Officer
|
|
426
|
+
auditPayload.user_type = 'api_key_and_officer'; // Upgrade to api_key_and_officer since API key is present
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
auditPayload.user_type = 'both';
|
|
430
|
+
}
|
|
351
431
|
// When both are authenticated, prioritize officer as user_id
|
|
352
432
|
if (officer) {
|
|
353
433
|
auditPayload.user_id = officer.id;
|
|
354
434
|
}
|
|
355
435
|
}
|
|
356
436
|
else if (authType === 'device') {
|
|
357
|
-
|
|
437
|
+
// Device only - but check if API key is also present
|
|
438
|
+
if (hasApiKey) {
|
|
439
|
+
auditPayload.user_type = 'api_key'; // Upgrade to api_key since API key is present
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
auditPayload.user_type = 'device';
|
|
443
|
+
}
|
|
358
444
|
// Use device id as user_id when only device is authenticated
|
|
359
445
|
if (device) {
|
|
360
446
|
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
@@ -364,12 +450,85 @@ class AuditService {
|
|
|
364
450
|
}
|
|
365
451
|
}
|
|
366
452
|
else if (authType === 'officer') {
|
|
367
|
-
|
|
453
|
+
// Officer only - but check if API key is also present
|
|
454
|
+
if (hasApiKey) {
|
|
455
|
+
auditPayload.user_type = 'api_key_and_officer'; // Upgrade to api_key_and_officer since API key is present
|
|
456
|
+
this.debugLog('Upgraded user_type from "officer" to "api_key_and_officer" because API key detected', {
|
|
457
|
+
sourceMicroservice: sourceMicroservice,
|
|
458
|
+
apiKeyId: apiKey === null || apiKey === void 0 ? void 0 : apiKey.id,
|
|
459
|
+
hasApiKeyObject: !!apiKey
|
|
460
|
+
});
|
|
461
|
+
// Explicitly preserve device information if present when upgrading from officer to api_key_and_officer
|
|
462
|
+
if (device) {
|
|
463
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
464
|
+
if (!isNaN(deviceId)) {
|
|
465
|
+
auditPayload.device_id = deviceId;
|
|
466
|
+
}
|
|
467
|
+
auditPayload.device_device_id = device.device_id;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
auditPayload.user_type = 'officer';
|
|
472
|
+
}
|
|
368
473
|
// Use officer id as user_id when only officer is authenticated
|
|
369
474
|
if (officer) {
|
|
370
475
|
auditPayload.user_id = officer.id;
|
|
371
476
|
}
|
|
372
477
|
}
|
|
478
|
+
// Log when API key is present but wasn't reflected in authType
|
|
479
|
+
if (hasApiKey && authType !== 'api_key' && authType !== 'api_key_and_officer' && authType !== 'microservice') {
|
|
480
|
+
this.debugLog('API Key detected but authType was different', {
|
|
481
|
+
authType,
|
|
482
|
+
apiKeyId: apiKey === null || apiKey === void 0 ? void 0 : apiKey.id,
|
|
483
|
+
sourceMicroservice: sourceMicroservice,
|
|
484
|
+
finalUserType: auditPayload.user_type,
|
|
485
|
+
hasApiKeyObject: !!apiKey,
|
|
486
|
+
authenticatedMicroservice: authenticatedMicroservice
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
// Final check: Ensure user_type matches what we actually have
|
|
490
|
+
if (hasApiKey && officer && auditPayload.user_type !== 'api_key_and_officer') {
|
|
491
|
+
this.debugLog('Force upgrading user_type to api_key_and_officer (final check)', {
|
|
492
|
+
currentUserType: auditPayload.user_type,
|
|
493
|
+
hasApiKey: hasApiKey,
|
|
494
|
+
hasOfficer: !!officer,
|
|
495
|
+
sourceMicroservice: sourceMicroservice
|
|
496
|
+
});
|
|
497
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
498
|
+
// Explicitly preserve device information when force upgrading to api_key_and_officer
|
|
499
|
+
if (device) {
|
|
500
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
501
|
+
if (!isNaN(deviceId)) {
|
|
502
|
+
auditPayload.device_id = deviceId;
|
|
503
|
+
}
|
|
504
|
+
auditPayload.device_device_id = device.device_id;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Final validation: Ensure API key information is included if payload.microservice was provided
|
|
508
|
+
// This is a safety check in case payload parameter contained microservice info that wasn't processed
|
|
509
|
+
// Note: auditPayload.microservice should always be microserviceName (receiving service) at this point
|
|
510
|
+
if (payload.microservice && payload.microservice !== microserviceName && !auditPayload.source_microservice) {
|
|
511
|
+
// Payload contained a microservice different from receiving service, use it as source
|
|
512
|
+
auditPayload.source_microservice = payload.microservice;
|
|
513
|
+
auditPayload.authenticated_microservice = payload.microservice;
|
|
514
|
+
// Upgrade user_type if officer is present
|
|
515
|
+
if (auditPayload.officer_id && auditPayload.user_type === 'officer') {
|
|
516
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
517
|
+
this.debugLog('Final safety check: Upgraded user_type to api_key_and_officer', {
|
|
518
|
+
microservice: auditPayload.microservice,
|
|
519
|
+
receivingMicroservice: microserviceName,
|
|
520
|
+
officerId: auditPayload.officer_id
|
|
521
|
+
});
|
|
522
|
+
// Explicitly preserve device information when upgrading from officer to api_key_and_officer in final validation
|
|
523
|
+
if (device) {
|
|
524
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
525
|
+
if (!isNaN(deviceId)) {
|
|
526
|
+
auditPayload.device_id = deviceId;
|
|
527
|
+
}
|
|
528
|
+
auditPayload.device_device_id = device.device_id;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
373
532
|
// Extract arrest-specific information from request body or response
|
|
374
533
|
this.addArrestInformation(req, auditPayload, payload);
|
|
375
534
|
// Merge custom keys from payload
|
|
@@ -382,6 +541,69 @@ class AuditService {
|
|
|
382
541
|
auditPayload[key] = payload[key];
|
|
383
542
|
}
|
|
384
543
|
});
|
|
544
|
+
// CRITICAL: Final check AFTER payload merge to ensure API key detection is not overridden
|
|
545
|
+
// If microservice indicates API key usage but user_type doesn't reflect it, fix it
|
|
546
|
+
// Check if API key was used by checking if authenticatedMicroservice or apiKey exists
|
|
547
|
+
// This works regardless of SERVER_NAME configuration
|
|
548
|
+
const finalHasApiKey = !!(apiKey || authenticatedMicroservice || auditPayload.source_microservice);
|
|
549
|
+
const finalSourceMicroservice = sourceMicroservice || auditPayload.source_microservice || auditPayload.microservice;
|
|
550
|
+
// CRITICAL: Preserve device information after payload merge (in case payload merge cleared it)
|
|
551
|
+
// Device information should always be preserved if device was authenticated
|
|
552
|
+
if (device) {
|
|
553
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
554
|
+
if (!isNaN(deviceId)) {
|
|
555
|
+
auditPayload.device_id = deviceId;
|
|
556
|
+
}
|
|
557
|
+
auditPayload.device_device_id = device.device_id;
|
|
558
|
+
}
|
|
559
|
+
if (finalHasApiKey && (authenticatedMicroservice || apiKey)) {
|
|
560
|
+
// ALWAYS set microservice fields when API key is detected
|
|
561
|
+
// Don't check if they already exist - ensure they're set (in case payload merge cleared them)
|
|
562
|
+
// Note: microservice field should always be the receiving service (from SERVER_NAME)
|
|
563
|
+
auditPayload.source_microservice = finalSourceMicroservice;
|
|
564
|
+
auditPayload.authenticated_microservice = finalSourceMicroservice;
|
|
565
|
+
// Do NOT overwrite microservice - it should always be the receiving service
|
|
566
|
+
// Include API key object fields if available
|
|
567
|
+
if (apiKey) {
|
|
568
|
+
// Always set these if apiKey exists, even if already set (in case payload merge cleared them)
|
|
569
|
+
auditPayload.api_key_id = apiKey.id;
|
|
570
|
+
auditPayload.api_key_name = apiKey.description || undefined;
|
|
571
|
+
auditPayload.api_key_description = apiKey.description || undefined;
|
|
572
|
+
auditPayload.api_key_preview = apiKey.key ? apiKey.key.substring(0, 15) + '...' : undefined;
|
|
573
|
+
}
|
|
574
|
+
// ALWAYS upgrade user_type when API key is detected and officer is present
|
|
575
|
+
if (auditPayload.officer_id && auditPayload.user_type === 'officer') {
|
|
576
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
577
|
+
this.debugLog('🔧 [FINAL] Upgraded user_type to api_key_and_officer after payload merge', {
|
|
578
|
+
microservice: auditPayload.microservice,
|
|
579
|
+
receivingMicroservice: microserviceName,
|
|
580
|
+
officerId: auditPayload.officer_id,
|
|
581
|
+
sourceMicroservice: finalSourceMicroservice
|
|
582
|
+
});
|
|
583
|
+
// Explicitly preserve device information when upgrading from officer to api_key_and_officer after payload merge
|
|
584
|
+
if (device) {
|
|
585
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
586
|
+
if (!isNaN(deviceId)) {
|
|
587
|
+
auditPayload.device_id = deviceId;
|
|
588
|
+
}
|
|
589
|
+
auditPayload.device_device_id = device.device_id;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (auditPayload.user_type === 'both' && finalHasApiKey) {
|
|
593
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
594
|
+
this.debugLog('🔧 [FINAL] Upgraded user_type from "both" to "api_key_and_officer" after payload merge', {
|
|
595
|
+
microservice: auditPayload.microservice,
|
|
596
|
+
sourceMicroservice: finalSourceMicroservice
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
else if (!auditPayload.officer_id && auditPayload.user_type === 'device' && finalHasApiKey) {
|
|
600
|
+
auditPayload.user_type = 'api_key';
|
|
601
|
+
this.debugLog('🔧 [FINAL] Upgraded user_type from "device" to "api_key" after payload merge', {
|
|
602
|
+
microservice: auditPayload.microservice,
|
|
603
|
+
sourceMicroservice: finalSourceMicroservice
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
385
607
|
// Build user info string for logging
|
|
386
608
|
const userInfo = auditPayload.officer_name
|
|
387
609
|
? `${auditPayload.officer_name} (Officer ID: ${auditPayload.officer_id})`
|
|
@@ -460,6 +682,31 @@ class AuditService {
|
|
|
460
682
|
return;
|
|
461
683
|
}
|
|
462
684
|
this.debugLog('All checks passed - proceeding to send');
|
|
685
|
+
// Final safety check: If microservice indicates API key but fields are missing, add them
|
|
686
|
+
// This catches any edge cases where fields might have been cleared
|
|
687
|
+
if (auditPayload.microservice &&
|
|
688
|
+
auditPayload.microservice !== microserviceName &&
|
|
689
|
+
(!auditPayload.source_microservice || !auditPayload.authenticated_microservice)) {
|
|
690
|
+
auditPayload.source_microservice = auditPayload.microservice;
|
|
691
|
+
auditPayload.authenticated_microservice = auditPayload.microservice;
|
|
692
|
+
// Upgrade user_type if needed
|
|
693
|
+
if (auditPayload.officer_id && auditPayload.user_type === 'officer') {
|
|
694
|
+
auditPayload.user_type = 'api_key_and_officer';
|
|
695
|
+
this.debugLog('🔧 [PRE-SEND] Final safety check: Upgraded user_type to api_key_and_officer', {
|
|
696
|
+
microservice: auditPayload.microservice,
|
|
697
|
+
receivingMicroservice: microserviceName,
|
|
698
|
+
officerId: auditPayload.officer_id
|
|
699
|
+
});
|
|
700
|
+
// Explicitly preserve device information when upgrading from officer to api_key_and_officer in pre-send check
|
|
701
|
+
if (device) {
|
|
702
|
+
const deviceId = typeof device.id === 'string' ? parseInt(device.id, 10) : device.id;
|
|
703
|
+
if (!isNaN(deviceId)) {
|
|
704
|
+
auditPayload.device_id = deviceId;
|
|
705
|
+
}
|
|
706
|
+
auditPayload.device_device_id = device.device_id;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
463
710
|
const topic = this.auditTopic || 'audit-log';
|
|
464
711
|
const kafkaMessage = {
|
|
465
712
|
key: `${auditPayload.method}-${auditPayload.endpoint}-${Date.now()}`,
|
|
@@ -654,6 +901,36 @@ class AuditService {
|
|
|
654
901
|
return AuditEventType.ARRESTS_LISTED;
|
|
655
902
|
}
|
|
656
903
|
}
|
|
904
|
+
// IPRS events
|
|
905
|
+
if (fullPath.includes('/iprs') || path.includes('/iprs')) {
|
|
906
|
+
if (method === 'POST' && (fullPath.includes('/search') || path.includes('/search'))) {
|
|
907
|
+
return AuditEventType.IPRS_PERSON_SEARCHED;
|
|
908
|
+
}
|
|
909
|
+
else if (method === 'GET' && (fullPath.includes('/fetch') || path.includes('/fetch'))) {
|
|
910
|
+
return AuditEventType.IPRS_PERSON_FETCHED_FROM_VPS;
|
|
911
|
+
}
|
|
912
|
+
else if (method === 'POST' && (fullPath.includes('/store') || path.includes('/store'))) {
|
|
913
|
+
return AuditEventType.IPRS_PERSON_STORED;
|
|
914
|
+
}
|
|
915
|
+
else if (method === 'PUT') {
|
|
916
|
+
return AuditEventType.IPRS_PERSON_UPDATED;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// Vehicle events
|
|
920
|
+
if (fullPath.includes('/vehicle') || path.includes('/vehicles')) {
|
|
921
|
+
if (method === 'GET' && (fullPath.includes('/search') || path.includes('/search'))) {
|
|
922
|
+
return AuditEventType.VEHICLE_SEARCHED;
|
|
923
|
+
}
|
|
924
|
+
else if (method === 'GET' && (fullPath.includes('/fetch') || path.includes('/fetch'))) {
|
|
925
|
+
return AuditEventType.VEHICLE_FETCHED_FROM_IPRS;
|
|
926
|
+
}
|
|
927
|
+
else if (method === 'POST' && (fullPath.includes('/store') || path.includes('/store'))) {
|
|
928
|
+
return AuditEventType.VEHICLE_STORED;
|
|
929
|
+
}
|
|
930
|
+
else if (method === 'PUT') {
|
|
931
|
+
return AuditEventType.VEHICLE_UPDATED;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
657
934
|
// Default to endpoint accessed
|
|
658
935
|
return AuditEventType.ENDPOINT_ACCESSED;
|
|
659
936
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RollingCacheTtl = number | {
|
|
2
|
+
days: number;
|
|
3
|
+
};
|
|
4
|
+
export type RollingCacheOptions = {
|
|
5
|
+
redisClient: any;
|
|
6
|
+
/** If omitted, uses createSafeRedisGet(redisClient). */
|
|
7
|
+
safeRedisGet?: (key: string) => Promise<string | null>;
|
|
8
|
+
/** Env var name for enable flag; default ROLLING_CACHE_ENABLED. Also reads CACHE_ENABLED if unset. */
|
|
9
|
+
enableEnvKey?: string;
|
|
10
|
+
logWarn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
11
|
+
};
|
|
12
|
+
export type RollingCacheInstance = {
|
|
13
|
+
buildKey: (entity: string, ...parts: (string | number)[]) => string;
|
|
14
|
+
isCacheEnabled: () => boolean;
|
|
15
|
+
get: <T = unknown>(key: string) => Promise<T | null>;
|
|
16
|
+
set: (key: string, value: unknown, ttl?: RollingCacheTtl) => Promise<boolean>;
|
|
17
|
+
getOrSet: <T>(key: string, ttl: RollingCacheTtl | undefined, loader: () => Promise<T>) => Promise<T>;
|
|
18
|
+
invalidate: (key: string) => Promise<void>;
|
|
19
|
+
invalidateByPrefix: (prefix: string) => Promise<number>;
|
|
20
|
+
recordAccess: (zsetKey: string, member: string, incrementBy?: number) => Promise<void>;
|
|
21
|
+
getDefaultListTtl: () => {
|
|
22
|
+
days: number;
|
|
23
|
+
};
|
|
24
|
+
getDefaultItemTtl: () => {
|
|
25
|
+
days: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Factory for list/item rolling cache (Redis string keys + JSON values).
|
|
30
|
+
* Pass the same redis client used by the host app. Optional safeRedisGet for custom wiring.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createRollingCache(options: RollingCacheOptions): RollingCacheInstance;
|
|
33
|
+
/** Key segment separator used by buildKey (exported for invalidation helpers). */
|
|
34
|
+
export declare const ROLLING_CACHE_SEP = "|";
|
|
@@ -0,0 +1,203 @@
|
|
|
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.ROLLING_CACHE_SEP = void 0;
|
|
13
|
+
exports.createRollingCache = createRollingCache;
|
|
14
|
+
const redis_1 = require("./redis");
|
|
15
|
+
const SEP = '|';
|
|
16
|
+
const DEFAULT_LIST_DAYS = 7;
|
|
17
|
+
const DEFAULT_ITEM_DAYS = 30;
|
|
18
|
+
const DEFAULT_ENABLE_ENV = 'ROLLING_CACHE_ENABLED';
|
|
19
|
+
function ensureConnected(redisClient) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
var _a;
|
|
22
|
+
if (redisClient === null || redisClient === void 0 ? void 0 : redisClient.isOpen) {
|
|
23
|
+
resolve(true);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
(_a = redisClient === null || redisClient === void 0 ? void 0 : redisClient.connect) === null || _a === void 0 ? void 0 : _a.call(redisClient).then(() => resolve(true)).catch(() => resolve(false));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function normalizePart(part) {
|
|
30
|
+
if (part === null || part === undefined)
|
|
31
|
+
return '';
|
|
32
|
+
const s = String(part).trim();
|
|
33
|
+
return s.replace(/\|/g, '_').replace(/:/g, '_');
|
|
34
|
+
}
|
|
35
|
+
function ttlSeconds(ttl) {
|
|
36
|
+
if (ttl == null)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof ttl === 'number')
|
|
39
|
+
return ttl > 0 ? ttl : undefined;
|
|
40
|
+
if (ttl.days > 0)
|
|
41
|
+
return ttl.days * 86400;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function getCacheDaysList() {
|
|
45
|
+
const v = process.env.CACHE_DAYS_LIST;
|
|
46
|
+
if (v != null && v !== '') {
|
|
47
|
+
const n = parseInt(v, 10);
|
|
48
|
+
if (!Number.isNaN(n) && n > 0)
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
return DEFAULT_LIST_DAYS;
|
|
52
|
+
}
|
|
53
|
+
function getCacheDaysItem() {
|
|
54
|
+
const v = process.env.CACHE_DAYS_ITEM;
|
|
55
|
+
if (v != null && v !== '') {
|
|
56
|
+
const n = parseInt(v, 10);
|
|
57
|
+
if (!Number.isNaN(n) && n > 0)
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
return DEFAULT_ITEM_DAYS;
|
|
61
|
+
}
|
|
62
|
+
const SCAN_COUNT = 100;
|
|
63
|
+
/**
|
|
64
|
+
* Factory for list/item rolling cache (Redis string keys + JSON values).
|
|
65
|
+
* Pass the same redis client used by the host app. Optional safeRedisGet for custom wiring.
|
|
66
|
+
*/
|
|
67
|
+
function createRollingCache(options) {
|
|
68
|
+
var _a;
|
|
69
|
+
const { redisClient, enableEnvKey = DEFAULT_ENABLE_ENV, logWarn } = options;
|
|
70
|
+
const safeRedisGet = (_a = options.safeRedisGet) !== null && _a !== void 0 ? _a : (0, redis_1.createSafeRedisGet)(redisClient);
|
|
71
|
+
const warn = (message, meta) => {
|
|
72
|
+
if (logWarn)
|
|
73
|
+
logWarn(message, meta);
|
|
74
|
+
else
|
|
75
|
+
console.warn(`[RollingCache] ${message}`, meta !== null && meta !== void 0 ? meta : '');
|
|
76
|
+
};
|
|
77
|
+
const isCacheEnabled = () => {
|
|
78
|
+
var _a, _b;
|
|
79
|
+
const v = (_b = (_a = process.env[enableEnvKey]) !== null && _a !== void 0 ? _a : process.env.CACHE_ENABLED) !== null && _b !== void 0 ? _b : '';
|
|
80
|
+
const s = String(v).trim().toLowerCase();
|
|
81
|
+
return s === '1' || s === 'true' || s === 'yes';
|
|
82
|
+
};
|
|
83
|
+
const buildKey = (entity, ...parts) => {
|
|
84
|
+
const normalized = parts.map(normalizePart);
|
|
85
|
+
return [entity, ...normalized].join(SEP);
|
|
86
|
+
};
|
|
87
|
+
const get = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
if (!isCacheEnabled())
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const raw = yield safeRedisGet(key);
|
|
92
|
+
if (raw == null)
|
|
93
|
+
return null;
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
warn('get parse error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const set = (key, value, ttl) => __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
if (!isCacheEnabled())
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const str = JSON.stringify(value);
|
|
106
|
+
const sec = ttlSeconds(ttl);
|
|
107
|
+
const ok = yield ensureConnected(redisClient);
|
|
108
|
+
if (!ok)
|
|
109
|
+
return false;
|
|
110
|
+
if (sec != null) {
|
|
111
|
+
yield redisClient.setEx(key, sec, str);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
yield redisClient.set(key, str);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
warn('set error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const getOrSet = (key, ttl, loader) => __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
const hit = yield get(key);
|
|
125
|
+
if (hit !== null && hit !== undefined) {
|
|
126
|
+
return hit;
|
|
127
|
+
}
|
|
128
|
+
const v = yield loader();
|
|
129
|
+
yield set(key, v, ttl);
|
|
130
|
+
return v;
|
|
131
|
+
});
|
|
132
|
+
const invalidate = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
if (!isCacheEnabled())
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
const ok = yield ensureConnected(redisClient);
|
|
137
|
+
if (!ok)
|
|
138
|
+
return;
|
|
139
|
+
yield redisClient.del(key);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
warn('invalidate error', { key, error: e instanceof Error ? e.message : String(e) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const invalidateByPrefix = (prefix) => __awaiter(this, void 0, void 0, function* () {
|
|
146
|
+
if (!isCacheEnabled())
|
|
147
|
+
return 0;
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
try {
|
|
150
|
+
const ok = yield ensureConnected(redisClient);
|
|
151
|
+
if (!ok)
|
|
152
|
+
return 0;
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const scanResult = yield redisClient.scan(cursor, {
|
|
156
|
+
MATCH: `${prefix}*`,
|
|
157
|
+
COUNT: SCAN_COUNT,
|
|
158
|
+
});
|
|
159
|
+
const nextCursor = scanResult.cursor;
|
|
160
|
+
const keys = scanResult.keys;
|
|
161
|
+
if (keys.length > 0) {
|
|
162
|
+
yield redisClient.del(keys);
|
|
163
|
+
deleted += keys.length;
|
|
164
|
+
}
|
|
165
|
+
const c = typeof nextCursor === 'bigint' ? Number(nextCursor) : Number(nextCursor);
|
|
166
|
+
if (c === 0)
|
|
167
|
+
break;
|
|
168
|
+
cursor = nextCursor;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
warn('invalidateByPrefix error', { prefix, error: e instanceof Error ? e.message : String(e) });
|
|
173
|
+
}
|
|
174
|
+
return deleted;
|
|
175
|
+
});
|
|
176
|
+
const recordAccess = (zsetKey_1, member_1, ...args_1) => __awaiter(this, [zsetKey_1, member_1, ...args_1], void 0, function* (zsetKey, member, incrementBy = 1) {
|
|
177
|
+
if (!isCacheEnabled())
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
const ok = yield ensureConnected(redisClient);
|
|
181
|
+
if (!ok)
|
|
182
|
+
return;
|
|
183
|
+
yield redisClient.zIncrBy(zsetKey, incrementBy, member);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
warn('recordAccess error', { zsetKey, error: e instanceof Error ? e.message : String(e) });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
buildKey,
|
|
191
|
+
isCacheEnabled,
|
|
192
|
+
get,
|
|
193
|
+
set,
|
|
194
|
+
getOrSet,
|
|
195
|
+
invalidate,
|
|
196
|
+
invalidateByPrefix,
|
|
197
|
+
recordAccess,
|
|
198
|
+
getDefaultListTtl: () => ({ days: getCacheDaysList() }),
|
|
199
|
+
getDefaultItemTtl: () => ({ days: getCacheDaysItem() }),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Key segment separator used by buildKey (exported for invalidation helpers). */
|
|
203
|
+
exports.ROLLING_CACHE_SEP = SEP;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govish/shared-services",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Govish shared services package - officer, device, station, penal code, station-duty block, and rank leave days cache services; API key service; audit logging; authentication middleware",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Govish shared services package - officer, device, station, penal code, station-duty block, and rank leave days cache services; API key service; audit logging; authentication middleware with support for API key + officer combined authentication",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"audit",
|
|
24
24
|
"authentication",
|
|
25
25
|
"middleware",
|
|
26
|
+
"api-key",
|
|
27
|
+
"jwt",
|
|
28
|
+
"device-authentication",
|
|
29
|
+
"officer-authentication",
|
|
30
|
+
"microservice-authentication",
|
|
26
31
|
"typescript"
|
|
27
32
|
],
|
|
28
33
|
"author": "",
|