@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.
- package/CHANGELOG.md +38 -0
- package/README.md +376 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +36 -0
- package/dist/middleware/authenticateDevice.d.ts +15 -0
- package/dist/middleware/authenticateDevice.js +310 -0
- package/dist/services/apiKeyService.d.ts +53 -0
- package/dist/services/apiKeyService.js +293 -0
- package/dist/services/auditService.d.ts +109 -0
- package/dist/services/auditService.js +785 -0
- package/dist/services/deviceCacheService.d.ts +46 -0
- package/dist/services/deviceCacheService.js +432 -0
- package/dist/services/deviceService.d.ts +21 -0
- package/dist/services/deviceService.js +103 -0
- package/dist/services/officerCacheService.d.ts +50 -0
- package/dist/services/officerCacheService.js +434 -0
- package/dist/services/officerService.d.ts +25 -0
- package/dist/services/officerService.js +177 -0
- package/dist/services/penalCodeCacheService.d.ts +20 -0
- package/dist/services/penalCodeCacheService.js +109 -0
- package/dist/types/dependencies.d.ts +23 -0
- package/dist/types/dependencies.js +2 -0
- package/dist/utils/logMode.d.ts +33 -0
- package/dist/utils/logMode.js +90 -0
- package/dist/utils/redis.d.ts +24 -0
- package/dist/utils/redis.js +122 -0
- package/package.json +52 -0
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.createAuthenticateDeviceOrOfficer = void 0;
|
|
16
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
17
|
+
const apiKeyService_1 = require("../services/apiKeyService");
|
|
18
|
+
const auditService_1 = require("../services/auditService");
|
|
19
|
+
const deviceService_1 = require("../services/deviceService");
|
|
20
|
+
const officerService_1 = require("../services/officerService");
|
|
21
|
+
const logMode_1 = require("../utils/logMode");
|
|
22
|
+
/**
|
|
23
|
+
* Middleware to authenticate either a device, an officer, or a microservice (via API key)
|
|
24
|
+
* Checks JWT token and validates against Device or Officer table
|
|
25
|
+
* Also checks for API keys for microservice authentication
|
|
26
|
+
*
|
|
27
|
+
* Supports multiple authentication methods:
|
|
28
|
+
* - X-API-Key: <api_key> (for microservice authentication)
|
|
29
|
+
* - Device-Token: <device_jwt_token> (for device authentication)
|
|
30
|
+
* - Authorization: Bearer <user_jwt_token> (for officer/user authentication)
|
|
31
|
+
*
|
|
32
|
+
* Priority: API Key > Device Token > Authorization Token
|
|
33
|
+
*/
|
|
34
|
+
const createAuthenticateDeviceOrOfficer = (deps) => {
|
|
35
|
+
const apiKeyService = new apiKeyService_1.ApiKeyService(deps);
|
|
36
|
+
const logger = deps.logger;
|
|
37
|
+
const enableDebugLogs = deps.enableDebugLogs || false;
|
|
38
|
+
// Debug log helper for middleware
|
|
39
|
+
const debugLog = (message, data) => {
|
|
40
|
+
if (enableDebugLogs) {
|
|
41
|
+
if (data) {
|
|
42
|
+
console.log(`[AUDIT MIDDLEWARE] ${message}`, data);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(`[AUDIT MIDDLEWARE] ${message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const debugError = (message, data) => {
|
|
50
|
+
if (enableDebugLogs) {
|
|
51
|
+
if (data) {
|
|
52
|
+
console.error(`[AUDIT MIDDLEWARE] ${message}`, data);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.error(`[AUDIT MIDDLEWARE] ${message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// Use internal services instead of external functions
|
|
60
|
+
const deviceService = new deviceService_1.DeviceService(deps);
|
|
61
|
+
const officerService = new officerService_1.OfficerService(deps);
|
|
62
|
+
const jwtConfig = deps.jwtConfig || {
|
|
63
|
+
secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production',
|
|
64
|
+
expiresIn: 24 * 60 * 60,
|
|
65
|
+
issuer: deps.serverName || 'citizen-app',
|
|
66
|
+
audience: deps.serverName || 'citizen-users',
|
|
67
|
+
};
|
|
68
|
+
// Initialize audit service (it will handle Kafka unavailability gracefully)
|
|
69
|
+
// Always initialize so we can log to console/logger even if Kafka is not available
|
|
70
|
+
const auditService = new auditService_1.AuditService(deps);
|
|
71
|
+
return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
try {
|
|
73
|
+
// First, check for API key (for microservice authentication)
|
|
74
|
+
const apiKeyHeader = req.headers['x-api-key'];
|
|
75
|
+
if (apiKeyHeader) {
|
|
76
|
+
const apiKey = yield apiKeyService.validateApiKey(apiKeyHeader);
|
|
77
|
+
if (apiKey) {
|
|
78
|
+
// Attach API key info to request
|
|
79
|
+
req.apiKey = apiKey;
|
|
80
|
+
req.microservice = apiKey.microservice;
|
|
81
|
+
req.authType = 'api_key';
|
|
82
|
+
// Log audit event
|
|
83
|
+
if (auditService) {
|
|
84
|
+
auditService.logAuditEvent(req, {
|
|
85
|
+
// Event type will be determined automatically
|
|
86
|
+
}).catch((err) => {
|
|
87
|
+
logger.error('Failed to log audit event', { error: err.message });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return next();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return res.status(401).json({
|
|
94
|
+
message: 'Invalid, expired, or inactive API key'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Check for device-token header (case-insensitive)
|
|
99
|
+
// Express normalizes headers to lowercase, but check multiple variations
|
|
100
|
+
// Also check rawHeaders in case Express hasn't normalized it
|
|
101
|
+
let deviceTokenHeader;
|
|
102
|
+
// Check normalized headers first
|
|
103
|
+
deviceTokenHeader =
|
|
104
|
+
req.headers['device-token'] ||
|
|
105
|
+
req.headers['device_token']; // Also check underscore variant
|
|
106
|
+
// If not found, check rawHeaders
|
|
107
|
+
if (!deviceTokenHeader && req.rawHeaders) {
|
|
108
|
+
const index = req.rawHeaders.findIndex((h) => h.toLowerCase() === 'device-token' || h.toLowerCase() === 'device_token');
|
|
109
|
+
if (index >= 0 && index < req.rawHeaders.length - 1) {
|
|
110
|
+
deviceTokenHeader = req.rawHeaders[index + 1];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const authHeader = req.headers.authorization;
|
|
114
|
+
let deviceAuthenticated = false;
|
|
115
|
+
let officerAuthenticated = false;
|
|
116
|
+
// Authenticate device if Device-Token header is present
|
|
117
|
+
if (deviceTokenHeader) {
|
|
118
|
+
try {
|
|
119
|
+
const decoded = jsonwebtoken_1.default.verify(deviceTokenHeader, jwtConfig.secret, {
|
|
120
|
+
issuer: jwtConfig.issuer,
|
|
121
|
+
audience: jwtConfig.audience,
|
|
122
|
+
});
|
|
123
|
+
// Get device from service (uses Redis cache with auth service fallback)
|
|
124
|
+
const device = yield deviceService.getDeviceById(decoded.sub);
|
|
125
|
+
if (!device) {
|
|
126
|
+
// Log audit event for device not found
|
|
127
|
+
if (auditService) {
|
|
128
|
+
req.authType = 'device';
|
|
129
|
+
auditService.logAuditEvent(req, {
|
|
130
|
+
event_type: undefined, // Will be determined automatically
|
|
131
|
+
response_status: 401,
|
|
132
|
+
error_message: 'Device not found',
|
|
133
|
+
error_code: 'DEVICE_NOT_FOUND',
|
|
134
|
+
}).catch((err) => {
|
|
135
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return res.status(401).json({ message: 'Device not found' });
|
|
139
|
+
}
|
|
140
|
+
if (!device.is_active) {
|
|
141
|
+
// Log audit event for inactive device
|
|
142
|
+
if (auditService) {
|
|
143
|
+
req.authType = 'device';
|
|
144
|
+
req.device = device;
|
|
145
|
+
auditService.logAuditEvent(req, {
|
|
146
|
+
event_type: undefined, // Will be determined automatically
|
|
147
|
+
response_status: 403,
|
|
148
|
+
error_message: 'Device is not active',
|
|
149
|
+
error_code: 'DEVICE_INACTIVE',
|
|
150
|
+
}).catch((err) => {
|
|
151
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return res.status(403).json({ message: 'Device is not active' });
|
|
155
|
+
}
|
|
156
|
+
// Attach device to request
|
|
157
|
+
req.device = device;
|
|
158
|
+
deviceAuthenticated = true;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// Log audit event for invalid device token
|
|
162
|
+
if (auditService) {
|
|
163
|
+
req.authType = 'device';
|
|
164
|
+
auditService.logAuditEvent(req, {
|
|
165
|
+
event_type: undefined, // Will be determined automatically
|
|
166
|
+
response_status: 401,
|
|
167
|
+
error_message: error instanceof Error ? error.message : 'Invalid or expired device token',
|
|
168
|
+
error_code: 'INVALID_DEVICE_TOKEN',
|
|
169
|
+
}).catch((err) => {
|
|
170
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return res.status(401).json({ message: 'Invalid or expired device token' });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Authenticate officer if Authorization header is present
|
|
177
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
178
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
179
|
+
try {
|
|
180
|
+
const decoded = jsonwebtoken_1.default.verify(token, jwtConfig.secret, {
|
|
181
|
+
issuer: jwtConfig.issuer,
|
|
182
|
+
audience: jwtConfig.audience,
|
|
183
|
+
});
|
|
184
|
+
// Skip if this is a device token (should use Device-Token header instead)
|
|
185
|
+
if (decoded.type === 'device') {
|
|
186
|
+
if (!deviceAuthenticated) {
|
|
187
|
+
return res.status(401).json({ message: 'Device tokens should use Device-Token header' });
|
|
188
|
+
}
|
|
189
|
+
// If device already authenticated via Device-Token, continue
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Authenticate as officer
|
|
193
|
+
const officerId = decoded.id || decoded.sub;
|
|
194
|
+
if (!officerId) {
|
|
195
|
+
return res.status(401).json({ message: 'Invalid token payload' });
|
|
196
|
+
}
|
|
197
|
+
// Get officer from service (uses Redis cache with auth service fallback)
|
|
198
|
+
const officer = yield officerService.getOfficerById(officerId);
|
|
199
|
+
if (!officer) {
|
|
200
|
+
// Log audit event for officer not found
|
|
201
|
+
if (auditService) {
|
|
202
|
+
req.authType = 'officer';
|
|
203
|
+
auditService.logAuditEvent(req, {
|
|
204
|
+
event_type: undefined, // Will be determined automatically
|
|
205
|
+
response_status: 401,
|
|
206
|
+
error_message: 'Officer not found',
|
|
207
|
+
error_code: 'OFFICER_NOT_FOUND',
|
|
208
|
+
}).catch((err) => {
|
|
209
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return res.status(401).json({ message: 'Officer not found' });
|
|
213
|
+
}
|
|
214
|
+
// Attach officer to request
|
|
215
|
+
req.officer = officer;
|
|
216
|
+
req.user = officer;
|
|
217
|
+
officerAuthenticated = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
// Log audit event for invalid authorization token
|
|
222
|
+
if (auditService) {
|
|
223
|
+
req.authType = 'officer';
|
|
224
|
+
auditService.logAuditEvent(req, {
|
|
225
|
+
event_type: undefined, // Will be determined automatically
|
|
226
|
+
response_status: 401,
|
|
227
|
+
error_message: error instanceof Error ? error.message : 'Invalid or expired authorization token',
|
|
228
|
+
error_code: 'INVALID_AUTH_TOKEN',
|
|
229
|
+
}).catch((err) => {
|
|
230
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return res.status(401).json({ message: 'Invalid or expired authorization token' });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// At least one authentication method must succeed
|
|
237
|
+
if (!deviceAuthenticated && !officerAuthenticated) {
|
|
238
|
+
// Log audit event for missing authentication
|
|
239
|
+
if (auditService) {
|
|
240
|
+
auditService.logAuditEvent(req, {
|
|
241
|
+
event_type: undefined, // Will be determined automatically
|
|
242
|
+
response_status: 401,
|
|
243
|
+
error_message: 'No valid token provided. Provide either Device-Token or Authorization header',
|
|
244
|
+
error_code: 'NO_AUTH_TOKEN',
|
|
245
|
+
}).catch((err) => {
|
|
246
|
+
logger.debug('Failed to log audit event', { error: err.message });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return res.status(401).json({ message: 'No valid token provided. Provide either Device-Token or Authorization header' });
|
|
250
|
+
}
|
|
251
|
+
// Set auth type
|
|
252
|
+
if (deviceAuthenticated && officerAuthenticated) {
|
|
253
|
+
req.authType = 'both';
|
|
254
|
+
}
|
|
255
|
+
else if (deviceAuthenticated) {
|
|
256
|
+
req.authType = 'device';
|
|
257
|
+
req.user = null;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
req.authType = 'officer';
|
|
261
|
+
req.device = null;
|
|
262
|
+
}
|
|
263
|
+
(0, logMode_1.devLog)('Authentication successful', {
|
|
264
|
+
authType: req.authType,
|
|
265
|
+
device: req.device,
|
|
266
|
+
officer: req.officer,
|
|
267
|
+
apiKey: req.apiKey,
|
|
268
|
+
microservice: req.microservice,
|
|
269
|
+
});
|
|
270
|
+
// Log audit event for endpoint access (non-blocking)
|
|
271
|
+
if (auditService) {
|
|
272
|
+
(0, logMode_1.devLog)('Calling audit service for endpoint access', {
|
|
273
|
+
path: req.path,
|
|
274
|
+
originalUrl: req.originalUrl,
|
|
275
|
+
baseUrl: req.baseUrl,
|
|
276
|
+
method: req.method,
|
|
277
|
+
authType: req.authType,
|
|
278
|
+
});
|
|
279
|
+
auditService.logAuditEvent(req, {
|
|
280
|
+
event_type: undefined, // Will be determined automatically
|
|
281
|
+
// Custom keys can be added here
|
|
282
|
+
}).catch((err) => {
|
|
283
|
+
logger.error('Failed to log audit event', {
|
|
284
|
+
error: err.message,
|
|
285
|
+
stack: err.stack,
|
|
286
|
+
path: req.path,
|
|
287
|
+
originalUrl: req.originalUrl,
|
|
288
|
+
});
|
|
289
|
+
(0, logMode_1.devError)('Audit logging failed', err);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
debugError('Audit service not available', {
|
|
294
|
+
hasKafkaProducer: !!deps.kafkaProducer,
|
|
295
|
+
hasIsProducerConnected: !!deps.isProducerConnected,
|
|
296
|
+
});
|
|
297
|
+
(0, logMode_1.devLog)('Audit service not available', {
|
|
298
|
+
hasKafkaProducer: !!deps.kafkaProducer,
|
|
299
|
+
hasIsProducerConnected: !!deps.isProducerConnected,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return next();
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
(0, logMode_1.devError)('Authentication error:', error);
|
|
306
|
+
return res.status(500).json({ message: 'Authentication error' });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
exports.createAuthenticateDeviceOrOfficer = createAuthenticateDeviceOrOfficer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { SharedServicesDependencies } from '../types/dependencies';
|
|
2
|
+
export interface ApiKeyData {
|
|
3
|
+
id: number;
|
|
4
|
+
key: string;
|
|
5
|
+
microservice: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
is_active: boolean;
|
|
8
|
+
last_used_at?: Date;
|
|
9
|
+
expires_at?: Date;
|
|
10
|
+
created_at: Date;
|
|
11
|
+
updated_at: Date;
|
|
12
|
+
created_by?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class ApiKeyService {
|
|
15
|
+
private logger;
|
|
16
|
+
private safeRedisGet?;
|
|
17
|
+
private safeRedisSet?;
|
|
18
|
+
constructor(deps: SharedServicesDependencies);
|
|
19
|
+
/**
|
|
20
|
+
* Generate a new API key
|
|
21
|
+
*/
|
|
22
|
+
generateApiKey(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Create a new API key for a microservice
|
|
25
|
+
* Creates via auth service API
|
|
26
|
+
*/
|
|
27
|
+
createApiKey(microservice: string, description?: string, expiresInDays?: number, createdBy?: number): Promise<ApiKeyData>;
|
|
28
|
+
/**
|
|
29
|
+
* Validate an API key
|
|
30
|
+
* Uses Redis cache with auth service fallback (similar to officer service pattern)
|
|
31
|
+
*/
|
|
32
|
+
validateApiKey(apiKey: string): Promise<ApiKeyData | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Get all API keys from auth service
|
|
35
|
+
*/
|
|
36
|
+
getAllApiKeys(microservice?: string): Promise<ApiKeyData[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Get API key by ID from auth service
|
|
39
|
+
*/
|
|
40
|
+
getApiKeyById(id: number): Promise<ApiKeyData | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Revoke/deactivate an API key via auth service
|
|
43
|
+
*/
|
|
44
|
+
revokeApiKey(id: number): Promise<ApiKeyData>;
|
|
45
|
+
/**
|
|
46
|
+
* Reactivate an API key via auth service
|
|
47
|
+
*/
|
|
48
|
+
reactivateApiKey(id: number): Promise<ApiKeyData>;
|
|
49
|
+
/**
|
|
50
|
+
* Delete an API key via auth service
|
|
51
|
+
*/
|
|
52
|
+
deleteApiKey(id: number): Promise<void>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ApiKeyService = void 0;
|
|
16
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
17
|
+
class ApiKeyService {
|
|
18
|
+
constructor(deps) {
|
|
19
|
+
this.logger = deps.logger;
|
|
20
|
+
this.safeRedisGet = deps.safeRedisGet;
|
|
21
|
+
this.safeRedisSet = deps.safeRedisSet;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a new API key
|
|
25
|
+
*/
|
|
26
|
+
generateApiKey() {
|
|
27
|
+
// Generate a secure random API key
|
|
28
|
+
const randomBytes = crypto_1.default.randomBytes(32);
|
|
29
|
+
const apiKey = `ak_${randomBytes.toString('base64url')}`;
|
|
30
|
+
return apiKey;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a new API key for a microservice
|
|
34
|
+
* Creates via auth service API
|
|
35
|
+
*/
|
|
36
|
+
createApiKey(microservice, description, expiresInDays, createdBy) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
try {
|
|
39
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
40
|
+
if (!authHost.includes(':')) {
|
|
41
|
+
authHost = `${authHost}:3000`;
|
|
42
|
+
}
|
|
43
|
+
// Create API key via auth service
|
|
44
|
+
const response = yield fetch(`http://${authHost}/api/v1/api-key`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
microservice,
|
|
49
|
+
description,
|
|
50
|
+
expiresInDays,
|
|
51
|
+
createdBy
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const errorData = yield response.json().catch(() => ({}));
|
|
56
|
+
throw new Error(errorData.message || `Failed to create API key: ${response.status}`);
|
|
57
|
+
}
|
|
58
|
+
const apiKey = yield response.json();
|
|
59
|
+
const keyData = apiKey.data || apiKey;
|
|
60
|
+
this.logger.info('API key created successfully', {
|
|
61
|
+
apiKeyId: keyData.id,
|
|
62
|
+
microservice
|
|
63
|
+
});
|
|
64
|
+
return keyData;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.logger.error('Error creating API key', {
|
|
68
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
69
|
+
microservice
|
|
70
|
+
});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate an API key
|
|
77
|
+
* Uses Redis cache with auth service fallback (similar to officer service pattern)
|
|
78
|
+
*/
|
|
79
|
+
validateApiKey(apiKey) {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
try {
|
|
82
|
+
// Try to get from Redis cache first
|
|
83
|
+
const cacheKey = `api_key:${apiKey}`;
|
|
84
|
+
const cachedData = this.safeRedisGet ? yield this.safeRedisGet(cacheKey) : null;
|
|
85
|
+
let key = null;
|
|
86
|
+
if (cachedData) {
|
|
87
|
+
try {
|
|
88
|
+
key = JSON.parse(cachedData);
|
|
89
|
+
this.logger.debug('API key found in cache', { apiKeyId: key.id });
|
|
90
|
+
}
|
|
91
|
+
catch (parseError) {
|
|
92
|
+
this.logger.warn('Failed to parse cached API key', {
|
|
93
|
+
error: parseError instanceof Error ? parseError.message : 'Unknown error'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// If not in cache, fetch from auth service
|
|
98
|
+
if (!key) {
|
|
99
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
100
|
+
if (!authHost.includes(':')) {
|
|
101
|
+
authHost = `${authHost}:3000`;
|
|
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 })
|
|
107
|
+
});
|
|
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
|
+
}
|
|
125
|
+
if (!key) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
// Check if key is active
|
|
129
|
+
if (!key.is_active) {
|
|
130
|
+
this.logger.warn('Inactive API key used', { apiKeyId: key.id });
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// Check if key has expired
|
|
134
|
+
if (key.expires_at && new Date(key.expires_at) < new Date()) {
|
|
135
|
+
this.logger.warn('Expired API key used', { apiKeyId: key.id });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return key;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.logger.error('Error validating API key', {
|
|
142
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
143
|
+
});
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get all API keys from auth service
|
|
150
|
+
*/
|
|
151
|
+
getAllApiKeys(microservice) {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
try {
|
|
154
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
155
|
+
if (!authHost.includes(':')) {
|
|
156
|
+
authHost = `${authHost}:3000`;
|
|
157
|
+
}
|
|
158
|
+
const url = microservice
|
|
159
|
+
? `http://${authHost}/api/v1/api-key?microservice=${microservice}`
|
|
160
|
+
: `http://${authHost}/api/v1/api-key`;
|
|
161
|
+
const response = yield fetch(url);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`Failed to fetch API keys: ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
const result = yield response.json();
|
|
166
|
+
return result.data || result;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.logger.error('Error fetching API keys', {
|
|
170
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
171
|
+
});
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get API key by ID from auth service
|
|
178
|
+
*/
|
|
179
|
+
getApiKeyById(id) {
|
|
180
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
181
|
+
try {
|
|
182
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
183
|
+
if (!authHost.includes(':')) {
|
|
184
|
+
authHost = `${authHost}:3000`;
|
|
185
|
+
}
|
|
186
|
+
const response = yield fetch(`http://${authHost}/api/v1/api-key/${id}`);
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
if (response.status === 404) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`Failed to fetch API key: ${response.status}`);
|
|
192
|
+
}
|
|
193
|
+
const result = yield response.json();
|
|
194
|
+
return result.data || result;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.logger.error('Error fetching API key by ID', {
|
|
198
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
199
|
+
id
|
|
200
|
+
});
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Revoke/deactivate an API key via auth service
|
|
207
|
+
*/
|
|
208
|
+
revokeApiKey(id) {
|
|
209
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
210
|
+
try {
|
|
211
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
212
|
+
if (!authHost.includes(':')) {
|
|
213
|
+
authHost = `${authHost}:3000`;
|
|
214
|
+
}
|
|
215
|
+
const response = yield fetch(`http://${authHost}/api/v1/api-key/${id}/revoke`, {
|
|
216
|
+
method: 'PUT'
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`Failed to revoke API key: ${response.status}`);
|
|
220
|
+
}
|
|
221
|
+
const result = yield response.json();
|
|
222
|
+
const apiKey = result.data || result;
|
|
223
|
+
this.logger.info('API key revoked', { apiKeyId: id });
|
|
224
|
+
return apiKey;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.logger.error('Error revoking API key', {
|
|
228
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
229
|
+
id
|
|
230
|
+
});
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Reactivate an API key via auth service
|
|
237
|
+
*/
|
|
238
|
+
reactivateApiKey(id) {
|
|
239
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
240
|
+
try {
|
|
241
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
242
|
+
if (!authHost.includes(':')) {
|
|
243
|
+
authHost = `${authHost}:3000`;
|
|
244
|
+
}
|
|
245
|
+
const response = yield fetch(`http://${authHost}/api/v1/api-key/${id}/reactivate`, {
|
|
246
|
+
method: 'PUT'
|
|
247
|
+
});
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(`Failed to reactivate API key: ${response.status}`);
|
|
250
|
+
}
|
|
251
|
+
const result = yield response.json();
|
|
252
|
+
const apiKey = result.data || result;
|
|
253
|
+
this.logger.info('API key reactivated', { apiKeyId: id });
|
|
254
|
+
return apiKey;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
this.logger.error('Error reactivating API key', {
|
|
258
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
259
|
+
id
|
|
260
|
+
});
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Delete an API key via auth service
|
|
267
|
+
*/
|
|
268
|
+
deleteApiKey(id) {
|
|
269
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
270
|
+
try {
|
|
271
|
+
let authHost = process.env.AUTH_HOST || 'localhost:3000';
|
|
272
|
+
if (!authHost.includes(':')) {
|
|
273
|
+
authHost = `${authHost}:3000`;
|
|
274
|
+
}
|
|
275
|
+
const response = yield fetch(`http://${authHost}/api/v1/api-key/${id}`, {
|
|
276
|
+
method: 'DELETE'
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
throw new Error(`Failed to delete API key: ${response.status}`);
|
|
280
|
+
}
|
|
281
|
+
this.logger.info('API key deleted', { apiKeyId: id });
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
this.logger.error('Error deleting API key', {
|
|
285
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
286
|
+
id
|
|
287
|
+
});
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
exports.ApiKeyService = ApiKeyService;
|