@govish/shared-services 1.4.0 â 1.5.1
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 +7 -0
- 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/package.json +7 -2
|
@@ -13,6 +13,7 @@ export interface ApiKeyData {
|
|
|
13
13
|
}
|
|
14
14
|
export declare class ApiKeyService {
|
|
15
15
|
private logger;
|
|
16
|
+
private redisClient;
|
|
16
17
|
private safeRedisGet?;
|
|
17
18
|
private safeRedisSet?;
|
|
18
19
|
constructor(deps: SharedServicesDependencies);
|
|
@@ -27,7 +28,7 @@ export declare class ApiKeyService {
|
|
|
27
28
|
createApiKey(microservice: string, description?: string, expiresInDays?: number, createdBy?: number): Promise<ApiKeyData>;
|
|
28
29
|
/**
|
|
29
30
|
* Validate an API key
|
|
30
|
-
* Uses Redis cache
|
|
31
|
+
* Uses Redis cache only (matches auth microservice cache format)
|
|
31
32
|
*/
|
|
32
33
|
validateApiKey(apiKey: string): Promise<ApiKeyData | null>;
|
|
33
34
|
/**
|
|
@@ -17,6 +17,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
17
17
|
class ApiKeyService {
|
|
18
18
|
constructor(deps) {
|
|
19
19
|
this.logger = deps.logger;
|
|
20
|
+
this.redisClient = deps.redisClient;
|
|
20
21
|
this.safeRedisGet = deps.safeRedisGet;
|
|
21
22
|
this.safeRedisSet = deps.safeRedisSet;
|
|
22
23
|
}
|
|
@@ -74,72 +75,263 @@ class ApiKeyService {
|
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Validate an API key
|
|
77
|
-
* Uses Redis cache
|
|
78
|
+
* Uses Redis cache only (matches auth microservice cache format)
|
|
78
79
|
*/
|
|
79
80
|
validateApiKey(apiKey) {
|
|
80
81
|
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
const apiKeyPreview = apiKey.substring(0, 10) + '...'; // Show first 10 chars for logging
|
|
83
|
+
console.log('đ [API Key Validation] Step 1: Starting API key validation', {
|
|
84
|
+
apiKeyPreview,
|
|
85
|
+
timestamp: new Date().toISOString()
|
|
86
|
+
});
|
|
87
|
+
this.logger.info('[API Key Validation] Step 1: Starting API key validation', {
|
|
88
|
+
apiKeyPreview
|
|
89
|
+
});
|
|
81
90
|
try {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Ensure Redis is connected
|
|
92
|
+
if (!this.redisClient.isOpen) {
|
|
93
|
+
yield this.redisClient.connect();
|
|
94
|
+
}
|
|
95
|
+
// Step 2: Get API key ID from lookup key (matches auth microservice format)
|
|
96
|
+
console.log('đ [API Key Validation] Step 2: Checking Redis cache for API key');
|
|
97
|
+
this.logger.info('[API Key Validation] Step 2: Checking Redis cache for API key');
|
|
98
|
+
const lookupKey = `apikey:by_key:${apiKey}`;
|
|
99
|
+
const apiKeyIdStr = yield this.redisClient.get(lookupKey);
|
|
85
100
|
let key = null;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
let cacheHit = false;
|
|
102
|
+
if (apiKeyIdStr) {
|
|
103
|
+
// Step 3: Get API key hash data using the ID
|
|
104
|
+
const apiKeyId = parseInt(apiKeyIdStr, 10);
|
|
105
|
+
if (!isNaN(apiKeyId)) {
|
|
106
|
+
cacheHit = true;
|
|
107
|
+
console.log('â
[API Key Validation] Step 2a: API key ID found in cache (CACHE HIT)', {
|
|
108
|
+
apiKeyId,
|
|
109
|
+
lookupKey
|
|
110
|
+
});
|
|
111
|
+
this.logger.info('[API Key Validation] Step 2a: API key ID found in cache (CACHE HIT)', {
|
|
112
|
+
apiKeyId,
|
|
113
|
+
lookupKey
|
|
114
|
+
});
|
|
115
|
+
// Get hash data from apikey:${id}
|
|
116
|
+
const hashKey = `apikey:${apiKeyId}`;
|
|
117
|
+
console.log('[API Key Validation] Step 2b: Retrieving API key hash data from cache');
|
|
118
|
+
this.logger.info('[API Key Validation] Step 2b: Retrieving API key hash data from cache', {
|
|
119
|
+
hashKey
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const hashData = yield this.redisClient.hGetAll(hashKey);
|
|
123
|
+
if (hashData && hashData.id) {
|
|
124
|
+
// Parse hash data (matches auth microservice format)
|
|
125
|
+
// Parse creator field safely (it's stored as JSON string)
|
|
126
|
+
let creator = undefined;
|
|
127
|
+
if (hashData.creator) {
|
|
128
|
+
try {
|
|
129
|
+
creator = JSON.parse(hashData.creator);
|
|
130
|
+
}
|
|
131
|
+
catch (parseError) {
|
|
132
|
+
// Ignore parse errors for creator field
|
|
133
|
+
console.warn('[API Key Validation] Failed to parse creator field', {
|
|
134
|
+
error: parseError instanceof Error ? parseError.message : 'Unknown error'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
key = {
|
|
139
|
+
id: parseInt(hashData.id),
|
|
140
|
+
key: hashData.full_key || hashData.key || apiKey, // Use full_key if available (matches auth microservice behavior)
|
|
141
|
+
microservice: hashData.microservice || '',
|
|
142
|
+
description: hashData.description || undefined,
|
|
143
|
+
is_active: hashData.is_active === 'true', // Stored as 'true'/'false' string
|
|
144
|
+
last_used_at: hashData.last_used_at ? new Date(hashData.last_used_at) : undefined,
|
|
145
|
+
expires_at: hashData.expires_at ? new Date(hashData.expires_at) : undefined,
|
|
146
|
+
created_at: hashData.created_at ? new Date(hashData.created_at) : new Date(),
|
|
147
|
+
updated_at: hashData.updated_at ? new Date(hashData.updated_at) : new Date(),
|
|
148
|
+
created_by: hashData.created_by ? parseInt(hashData.created_by) : undefined,
|
|
149
|
+
creator: creator
|
|
150
|
+
};
|
|
151
|
+
console.log('â
[API Key Validation] Step 2c: Successfully retrieved cached API key', {
|
|
152
|
+
apiKeyId: key.id,
|
|
153
|
+
microservice: key.microservice,
|
|
154
|
+
isActive: key.is_active,
|
|
155
|
+
expiresAt: key.expires_at || 'Never',
|
|
156
|
+
expiresAtFormatted: key.expires_at ? new Date(key.expires_at).toISOString() : 'N/A'
|
|
157
|
+
});
|
|
158
|
+
this.logger.info('[API Key Validation] Step 2c: Successfully retrieved cached API key', {
|
|
159
|
+
apiKeyId: key.id,
|
|
160
|
+
microservice: key.microservice,
|
|
161
|
+
isActive: key.is_active,
|
|
162
|
+
expiresAt: key.expires_at
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.warn('â ī¸ [API Key Validation] Step 2c: API key hash data not found', {
|
|
167
|
+
hashKey,
|
|
168
|
+
apiKeyId
|
|
169
|
+
});
|
|
170
|
+
this.logger.warn('[API Key Validation] Step 2c: API key hash data not found', {
|
|
171
|
+
hashKey,
|
|
172
|
+
apiKeyId
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (hashError) {
|
|
177
|
+
console.error('â [API Key Validation] Step 2c: Failed to retrieve API key hash data', {
|
|
178
|
+
error: hashError instanceof Error ? hashError.message : 'Unknown error',
|
|
179
|
+
errorStack: hashError instanceof Error ? hashError.stack : undefined,
|
|
180
|
+
hashKey
|
|
181
|
+
});
|
|
182
|
+
this.logger.warn('[API Key Validation] Step 2c: Failed to retrieve API key hash data', {
|
|
183
|
+
error: hashError instanceof Error ? hashError.message : 'Unknown error',
|
|
184
|
+
hashKey
|
|
185
|
+
});
|
|
186
|
+
}
|
|
90
187
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
188
|
+
else {
|
|
189
|
+
console.warn('â ī¸ [API Key Validation] Step 2a: Invalid API key ID in cache', {
|
|
190
|
+
apiKeyIdStr,
|
|
191
|
+
lookupKey
|
|
192
|
+
});
|
|
193
|
+
this.logger.warn('[API Key Validation] Step 2a: Invalid API key ID in cache', {
|
|
194
|
+
apiKeyIdStr,
|
|
195
|
+
lookupKey
|
|
94
196
|
});
|
|
95
197
|
}
|
|
96
198
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
const response = yield fetch(`http://${authHost}/api/v1/api-key/validate`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'Content-Type': 'application/json' },
|
|
106
|
-
body: JSON.stringify({ apiKey })
|
|
199
|
+
else {
|
|
200
|
+
cacheHit = false;
|
|
201
|
+
console.log('â ī¸ [API Key Validation] Step 2a: API key NOT found in cache (CACHE MISS)');
|
|
202
|
+
this.logger.info('[API Key Validation] Step 2a: API key not found in cache (CACHE MISS)', {
|
|
203
|
+
lookupKey
|
|
107
204
|
});
|
|
108
|
-
if (!response.ok) {
|
|
109
|
-
if (response.status === 404 || response.status === 401) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
this.logger.warn('Failed to validate API key from auth service', {
|
|
113
|
-
status: response.status
|
|
114
|
-
});
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
const result = yield response.json();
|
|
118
|
-
key = result.data || result;
|
|
119
|
-
// Cache the result if found (15 minutes TTL for API keys)
|
|
120
|
-
if (key && this.safeRedisSet) {
|
|
121
|
-
yield this.safeRedisSet(cacheKey, JSON.stringify(key));
|
|
122
|
-
this.logger.debug('API key cached', { apiKeyId: key.id });
|
|
123
|
-
}
|
|
124
205
|
}
|
|
206
|
+
// Step 3: Validate key exists (cache-only, no auth service fallback)
|
|
125
207
|
if (!key) {
|
|
208
|
+
console.error('â [API Key Validation] Step 3: API key not found in cache', {
|
|
209
|
+
cacheChecked: true,
|
|
210
|
+
cacheHit,
|
|
211
|
+
apiKeyPreview,
|
|
212
|
+
lookupKey
|
|
213
|
+
});
|
|
214
|
+
this.logger.warn('[API Key Validation] Step 3: API key not found in cache', {
|
|
215
|
+
cacheChecked: true,
|
|
216
|
+
cacheHit,
|
|
217
|
+
lookupKey
|
|
218
|
+
});
|
|
126
219
|
return null;
|
|
127
220
|
}
|
|
128
|
-
|
|
221
|
+
console.log('â
[API Key Validation] Step 3: API key found, validating status', {
|
|
222
|
+
apiKeyId: key.id,
|
|
223
|
+
microservice: key.microservice,
|
|
224
|
+
cacheHit,
|
|
225
|
+
expiresAt: key.expires_at || 'Never',
|
|
226
|
+
expiresAtFormatted: key.expires_at ? new Date(key.expires_at).toISOString() : 'N/A'
|
|
227
|
+
});
|
|
228
|
+
this.logger.info('[API Key Validation] Step 3: API key found, validating status', {
|
|
229
|
+
apiKeyId: key.id,
|
|
230
|
+
microservice: key.microservice,
|
|
231
|
+
cacheHit
|
|
232
|
+
});
|
|
233
|
+
// Step 4: Check if key is active
|
|
234
|
+
console.log('[API Key Validation] Step 4: Checking if API key is active', {
|
|
235
|
+
apiKeyId: key.id,
|
|
236
|
+
isActive: key.is_active
|
|
237
|
+
});
|
|
238
|
+
this.logger.info('[API Key Validation] Step 4: Checking if API key is active', {
|
|
239
|
+
apiKeyId: key.id,
|
|
240
|
+
isActive: key.is_active
|
|
241
|
+
});
|
|
129
242
|
if (!key.is_active) {
|
|
130
|
-
|
|
243
|
+
console.error('â [API Key Validation] Step 4a: API key is INACTIVE', {
|
|
244
|
+
apiKeyId: key.id,
|
|
245
|
+
microservice: key.microservice,
|
|
246
|
+
cacheHit
|
|
247
|
+
});
|
|
248
|
+
this.logger.warn('[API Key Validation] Step 4a: API key is inactive', {
|
|
249
|
+
apiKeyId: key.id
|
|
250
|
+
});
|
|
131
251
|
return null;
|
|
132
252
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
253
|
+
console.log('â
[API Key Validation] Step 4a: API key is active â');
|
|
254
|
+
// Step 5: Check if key has expired
|
|
255
|
+
if (key.expires_at) {
|
|
256
|
+
const expiresAt = new Date(key.expires_at);
|
|
257
|
+
const now = new Date();
|
|
258
|
+
const isExpired = expiresAt < now;
|
|
259
|
+
const timeUntilExpiry = expiresAt.getTime() - now.getTime();
|
|
260
|
+
const daysUntilExpiry = Math.floor(timeUntilExpiry / (1000 * 60 * 60 * 24));
|
|
261
|
+
console.log('[API Key Validation] Step 5: Checking API key expiration', {
|
|
262
|
+
apiKeyId: key.id,
|
|
263
|
+
expiresAt: key.expires_at,
|
|
264
|
+
expiresAtFormatted: expiresAt.toISOString(),
|
|
265
|
+
currentTime: now.toISOString(),
|
|
266
|
+
isExpired,
|
|
267
|
+
daysUntilExpiry: isExpired ? 'EXPIRED' : daysUntilExpiry
|
|
268
|
+
});
|
|
269
|
+
this.logger.info('[API Key Validation] Step 5: Checking API key expiration', {
|
|
270
|
+
apiKeyId: key.id,
|
|
271
|
+
expiresAt: key.expires_at,
|
|
272
|
+
currentTime: new Date().toISOString(),
|
|
273
|
+
isExpired
|
|
274
|
+
});
|
|
275
|
+
if (isExpired) {
|
|
276
|
+
console.error('â [API Key Validation] Step 5a: API key has EXPIRED', {
|
|
277
|
+
apiKeyId: key.id,
|
|
278
|
+
expiresAt: expiresAt.toISOString(),
|
|
279
|
+
expiredAt: expiresAt.toISOString(),
|
|
280
|
+
currentTime: now.toISOString(),
|
|
281
|
+
daysExpired: Math.abs(daysUntilExpiry)
|
|
282
|
+
});
|
|
283
|
+
this.logger.warn('[API Key Validation] Step 5a: API key has expired', {
|
|
284
|
+
apiKeyId: key.id,
|
|
285
|
+
expiresAt: key.expires_at,
|
|
286
|
+
currentTime: now.toISOString()
|
|
287
|
+
});
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
console.log('â
[API Key Validation] Step 5a: API key has not expired â', {
|
|
291
|
+
daysUntilExpiry,
|
|
292
|
+
expiresAtFormatted: expiresAt.toISOString()
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
console.log('âšī¸ [API Key Validation] Step 5: API key has no expiration date (permanent)');
|
|
297
|
+
this.logger.info('[API Key Validation] Step 5: API key has no expiration date (permanent)');
|
|
137
298
|
}
|
|
299
|
+
console.log('â
[API Key Validation] Step 6: API key validation SUCCESSFUL', {
|
|
300
|
+
apiKeyId: key.id,
|
|
301
|
+
microservice: key.microservice,
|
|
302
|
+
isActive: key.is_active,
|
|
303
|
+
expiresAt: key.expires_at || 'Never',
|
|
304
|
+
expiresAtFormatted: key.expires_at ? new Date(key.expires_at).toISOString() : 'N/A',
|
|
305
|
+
cacheHit,
|
|
306
|
+
timestamp: new Date().toISOString()
|
|
307
|
+
});
|
|
308
|
+
this.logger.info('[API Key Validation] Step 6: API key validation successful', {
|
|
309
|
+
apiKeyId: key.id,
|
|
310
|
+
microservice: key.microservice,
|
|
311
|
+
isActive: key.is_active,
|
|
312
|
+
expiresAt: key.expires_at || 'Never',
|
|
313
|
+
cacheHit
|
|
314
|
+
});
|
|
138
315
|
return key;
|
|
139
316
|
}
|
|
140
317
|
catch (error) {
|
|
141
|
-
|
|
142
|
-
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
319
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
320
|
+
const errorName = error instanceof Error ? error.name : 'Unknown';
|
|
321
|
+
console.error('â [API Key Validation] ERROR during validation', {
|
|
322
|
+
step: 'Unknown',
|
|
323
|
+
error: errorMessage,
|
|
324
|
+
errorName,
|
|
325
|
+
errorStack,
|
|
326
|
+
apiKeyPreview,
|
|
327
|
+
timestamp: new Date().toISOString()
|
|
328
|
+
});
|
|
329
|
+
this.logger.error('[API Key Validation] Error during validation', {
|
|
330
|
+
step: 'Unknown',
|
|
331
|
+
error: errorMessage,
|
|
332
|
+
errorStack,
|
|
333
|
+
errorName,
|
|
334
|
+
apiKeyPreview
|
|
143
335
|
});
|
|
144
336
|
return null;
|
|
145
337
|
}
|
|
@@ -21,7 +21,15 @@ export declare enum AuditEventType {
|
|
|
21
21
|
ARREST_OB_GENERATED = "arrest_ob_generated",
|
|
22
22
|
ENDPOINT_ACCESSED = "endpoint_accessed",
|
|
23
23
|
UNAUTHORIZED_ACCESS = "unauthorized_access",
|
|
24
|
-
FORBIDDEN_ACCESS = "forbidden_access"
|
|
24
|
+
FORBIDDEN_ACCESS = "forbidden_access",
|
|
25
|
+
IPRS_PERSON_SEARCHED = "iprs_person_searched",
|
|
26
|
+
IPRS_PERSON_FETCHED_FROM_VPS = "iprs_person_fetched_from_vps",
|
|
27
|
+
IPRS_PERSON_STORED = "iprs_person_stored",
|
|
28
|
+
IPRS_PERSON_UPDATED = "iprs_person_updated",
|
|
29
|
+
VEHICLE_SEARCHED = "vehicle_searched",
|
|
30
|
+
VEHICLE_FETCHED_FROM_IPRS = "vehicle_fetched_from_iprs",
|
|
31
|
+
VEHICLE_STORED = "vehicle_stored",
|
|
32
|
+
VEHICLE_UPDATED = "vehicle_updated"
|
|
25
33
|
}
|
|
26
34
|
export interface AuditEventPayload {
|
|
27
35
|
timestamp: string;
|
|
@@ -36,7 +44,7 @@ export interface AuditEventPayload {
|
|
|
36
44
|
ip_address?: string;
|
|
37
45
|
user_agent?: string;
|
|
38
46
|
user_id?: number;
|
|
39
|
-
user_type?: 'officer' | 'device' | 'both' | 'api_key' | 'microservice';
|
|
47
|
+
user_type?: 'officer' | 'device' | 'both' | 'api_key' | 'microservice' | 'api_key_and_officer';
|
|
40
48
|
officer_id?: number;
|
|
41
49
|
officer_name?: string;
|
|
42
50
|
officer_service_number?: string;
|
|
@@ -44,7 +52,11 @@ export interface AuditEventPayload {
|
|
|
44
52
|
device_id?: number;
|
|
45
53
|
device_device_id?: string;
|
|
46
54
|
api_key_id?: number;
|
|
55
|
+
api_key_name?: string;
|
|
56
|
+
api_key_description?: string;
|
|
57
|
+
api_key_preview?: string;
|
|
47
58
|
authenticated_microservice?: string;
|
|
59
|
+
source_microservice?: string;
|
|
48
60
|
arrest_id?: number;
|
|
49
61
|
ob_number?: string;
|
|
50
62
|
sync_id?: any;
|