@rodit/rodit-auth-be 9.11.14
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 +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
const { ulid } = require('ulid');
|
|
2
|
+
const logger = require("../../services/logger");
|
|
3
|
+
const { createLogContext, logErrorWithMetrics } = logger;
|
|
4
|
+
const config = require('../../services/configsdk');
|
|
5
|
+
const SESSION_CLEANUP_INTERVAL = config.get('SESSION_CLEANUP_INTERVAL'); // 1 hour in milliseconds
|
|
6
|
+
const SESSION_TOKEN_RETENTION_PERIOD = config.get('SESSION_TOKEN_RETENTION_PERIOD'); // 7 days in seconds
|
|
7
|
+
|
|
8
|
+
// Try to load express-session if available in host app
|
|
9
|
+
let sessionLib = null;
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
12
|
+
sessionLib = require('express-session');
|
|
13
|
+
logger.infoWithContext('express-session detected; SessionManager can use express-compatible stores', {
|
|
14
|
+
component: 'SessionManager',
|
|
15
|
+
operation: 'session.storage.detect'
|
|
16
|
+
});
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Optional dependency — we gracefully fall back to internal memory storage
|
|
19
|
+
sessionLib = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Adapter to wrap an express-session Store and expose the SDK's storage interface
|
|
24
|
+
* Required SDK interface: { get, set, delete, keys, size, clear, getAll? }
|
|
25
|
+
*/
|
|
26
|
+
class ExpressSessionStoreAdapter {
|
|
27
|
+
constructor(store) {
|
|
28
|
+
if (!store || typeof store !== 'object') {
|
|
29
|
+
throw new Error('ExpressSessionStoreAdapter requires a valid store instance');
|
|
30
|
+
}
|
|
31
|
+
// Minimal express-session Store methods we rely on
|
|
32
|
+
const required = ['get', 'set', 'destroy'];
|
|
33
|
+
const missing = required.filter((m) => typeof store[m] !== 'function');
|
|
34
|
+
if (missing.length) {
|
|
35
|
+
throw new Error(`Provided express-session store is missing methods: ${missing.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
this.store = store;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get(sessionId) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
this.store.get(sessionId, (err, sess) => {
|
|
43
|
+
if (err) return resolve(null);
|
|
44
|
+
// Allow storing our own session objects directly
|
|
45
|
+
resolve(sess || null);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async set(sessionId, session) {
|
|
51
|
+
// Help express-session stores compute TTL by providing cookie.maxAge when possible
|
|
52
|
+
try {
|
|
53
|
+
if (session && typeof session === 'object' && session.expiresAt) {
|
|
54
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
55
|
+
const ttlMs = Math.max(0, (session.expiresAt - nowSec) * 1000);
|
|
56
|
+
if (!session.cookie || typeof session.cookie !== 'object') {
|
|
57
|
+
session.cookie = {};
|
|
58
|
+
}
|
|
59
|
+
if (typeof session.cookie.maxAge !== 'number') {
|
|
60
|
+
session.cookie.maxAge = ttlMs; // Many stores read cookie.maxAge for TTL
|
|
61
|
+
}
|
|
62
|
+
if (typeof session.cookie.originalMaxAge !== 'number') {
|
|
63
|
+
session.cookie.originalMaxAge = ttlMs;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// Non-fatal: continue without cookie hints
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
this.store.set(sessionId, session, (err) => {
|
|
72
|
+
if (err) return resolve(false);
|
|
73
|
+
resolve(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async delete(sessionId) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
this.store.destroy(sessionId, (err) => {
|
|
81
|
+
if (err) return resolve(false);
|
|
82
|
+
resolve(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async keys() {
|
|
88
|
+
// Non-standard; best-effort using .all() if available
|
|
89
|
+
if (typeof this.store.all === 'function') {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
this.store.all((err, sessions) => {
|
|
92
|
+
if (err || !sessions) return resolve([]);
|
|
93
|
+
if (Array.isArray(sessions)) {
|
|
94
|
+
// MemoryStore typically returns array of session objects without ids; not standardized
|
|
95
|
+
// Some stores return array of session records with an id property
|
|
96
|
+
const ids = sessions
|
|
97
|
+
.map((s) => s?.id || s?.sessionId || s?.sid)
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
return resolve(ids);
|
|
100
|
+
}
|
|
101
|
+
// Some stores return an object map { sid: session }
|
|
102
|
+
resolve(Object.keys(sessions));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// If not supported, return empty (SDK falls back where possible)
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async size() {
|
|
111
|
+
if (typeof this.store.length === 'function') {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
this.store.length((err, length) => {
|
|
114
|
+
if (err) return resolve(0);
|
|
115
|
+
resolve(typeof length === 'number' ? length : 0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (typeof this.store.all === 'function') {
|
|
120
|
+
const all = await this.getAll();
|
|
121
|
+
return Array.isArray(all) ? all.length : (all ? Object.keys(all).length : 0);
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async clear() {
|
|
127
|
+
if (typeof this.store.clear === 'function') {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
this.store.clear((err) => resolve(!err));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Fallback: enumerate keys and destroy
|
|
133
|
+
const ids = await this.keys();
|
|
134
|
+
let ok = true;
|
|
135
|
+
for (const id of ids) {
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
137
|
+
const res = await this.delete(id);
|
|
138
|
+
ok = ok && res;
|
|
139
|
+
}
|
|
140
|
+
return ok;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getAll() {
|
|
144
|
+
if (typeof this.store.all === 'function') {
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
this.store.all((err, sessions) => {
|
|
147
|
+
if (err) return resolve([]);
|
|
148
|
+
resolve(sessions || []);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Not supported; reconstruct is not feasible without keys -> return empty
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getStorageInfo() {
|
|
157
|
+
const info = {
|
|
158
|
+
type: 'ExpressSessionStoreAdapter',
|
|
159
|
+
storeType: this.store?.constructor?.name || 'UnknownStore',
|
|
160
|
+
features: {
|
|
161
|
+
hasAll: typeof this.store.all === 'function',
|
|
162
|
+
hasLength: typeof this.store.length === 'function',
|
|
163
|
+
hasClear: typeof this.store.clear === 'function',
|
|
164
|
+
},
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
const count = await this.size();
|
|
169
|
+
info.sessionCount = count;
|
|
170
|
+
} catch (_) {
|
|
171
|
+
info.sessionCount = undefined;
|
|
172
|
+
}
|
|
173
|
+
return info;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class InMemorySessionStorage {
|
|
177
|
+
constructor() {
|
|
178
|
+
this.sessions = new Map();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async get(sessionId) {
|
|
182
|
+
const session = this.sessions.get(sessionId);
|
|
183
|
+
if (!session) return null;
|
|
184
|
+
|
|
185
|
+
// Check if session is expired and auto-cleanup
|
|
186
|
+
const now = Math.floor(Date.now() / 1000);
|
|
187
|
+
if (session.expiresAt && session.expiresAt < now) {
|
|
188
|
+
// Session is expired, remove it
|
|
189
|
+
this.sessions.delete(sessionId);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return session;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async set(sessionId, session) {
|
|
197
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
198
|
+
throw new Error('sessionId must be a non-empty string');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add timestamp for tracking
|
|
202
|
+
if (session && typeof session === 'object') {
|
|
203
|
+
session.updatedAt = Math.floor(Date.now() / 1000);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.sessions.set(sessionId, session);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async delete(sessionId) {
|
|
211
|
+
return this.sessions.delete(sessionId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async keys() {
|
|
215
|
+
return Array.from(this.sessions.keys());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async size() {
|
|
219
|
+
return this.sessions.size;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async clear() {
|
|
223
|
+
this.sessions.clear();
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getAll() {
|
|
228
|
+
// Return an array of all session objects
|
|
229
|
+
return Array.from(this.sessions.values());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async getStorageInfo() {
|
|
234
|
+
return {
|
|
235
|
+
type: 'InMemorySessionStorage',
|
|
236
|
+
sessionCount: this.sessions.size,
|
|
237
|
+
memoryUsage: process.memoryUsage ? process.memoryUsage() : 'unavailable',
|
|
238
|
+
timestamp: new Date().toISOString()
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Validate session structure
|
|
243
|
+
_validateSession(session) {
|
|
244
|
+
if (!session || typeof session !== 'object') {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const required = ['id', 'status', 'createdAt'];
|
|
248
|
+
return required.every(prop => prop in session);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Default storage: standalone in-memory store (no express-session dependency)
|
|
253
|
+
// If applications want Redis/DB/etc they must provide an express-session Store via setExpressSessionStore()
|
|
254
|
+
const defaultStorage = new InMemorySessionStorage();
|
|
255
|
+
|
|
256
|
+
let currentStorage = defaultStorage;
|
|
257
|
+
|
|
258
|
+
function setStorage(customStorage) {
|
|
259
|
+
if (!customStorage || typeof customStorage !== 'object') {
|
|
260
|
+
throw new Error('setStorage(customStorage) requires a storage object');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const required = ['get', 'set', 'delete', 'keys', 'size', 'clear'];
|
|
264
|
+
const missing = required.filter(method => typeof customStorage[method] !== 'function');
|
|
265
|
+
|
|
266
|
+
if (missing.length) {
|
|
267
|
+
throw new Error(`Injected storage is missing methods: ${missing.join(', ')}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
currentStorage = customStorage;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Allow direct injection of an express-session Store and wrap it with our adapter
|
|
276
|
+
function setExpressSessionStore(expressSessionStore) {
|
|
277
|
+
if (!sessionLib) {
|
|
278
|
+
throw new Error('express-session is not installed; cannot set express-session store');
|
|
279
|
+
}
|
|
280
|
+
const adapter = new ExpressSessionStoreAdapter(expressSessionStore);
|
|
281
|
+
currentStorage = adapter;
|
|
282
|
+
logger.infoWithContext('Session storage set via express-session store', {
|
|
283
|
+
component: 'SessionManager',
|
|
284
|
+
operation: 'session.storage.set',
|
|
285
|
+
storeType: expressSessionStore?.constructor?.name,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function configureStorageFromConfig() {
|
|
290
|
+
let storageType;
|
|
291
|
+
try {
|
|
292
|
+
storageType = config.get('SESSION_STORAGE_TYPE');
|
|
293
|
+
} catch (_) {
|
|
294
|
+
// Keep default storage
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const type = String(storageType || '').toLowerCase();
|
|
299
|
+
|
|
300
|
+
switch (type) {
|
|
301
|
+
case 'memory':
|
|
302
|
+
// Use standalone in-memory store
|
|
303
|
+
currentStorage = new InMemorySessionStorage();
|
|
304
|
+
logger.infoWithContext('Configured session storage: standalone InMemorySessionStorage', {
|
|
305
|
+
component: 'SessionManager',
|
|
306
|
+
operation: 'session.storage.configure',
|
|
307
|
+
storageType: 'memory'
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
|
|
311
|
+
case 'express':
|
|
312
|
+
case 'express-session':
|
|
313
|
+
if (!sessionLib || !sessionLib.MemoryStore) {
|
|
314
|
+
logger.warnWithContext('express-session not installed. Falling back to standalone InMemorySessionStorage', {
|
|
315
|
+
component: 'SessionManager',
|
|
316
|
+
operation: 'session.storage.configure',
|
|
317
|
+
storageType: 'express-session'
|
|
318
|
+
});
|
|
319
|
+
currentStorage = new InMemorySessionStorage();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
currentStorage = new ExpressSessionStoreAdapter(new sessionLib.MemoryStore());
|
|
323
|
+
logger.infoWithContext('Configured session storage: express-session MemoryStore (override with setExpressSessionStore for Redis/DB/etc)', {
|
|
324
|
+
component: 'SessionManager',
|
|
325
|
+
operation: 'session.storage.configure',
|
|
326
|
+
storageType: 'express-session'
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
|
|
330
|
+
default:
|
|
331
|
+
logger.warnWithContext(`Unknown SESSION_STORAGE_TYPE='${storageType}'. Using standalone InMemorySessionStorage.`, {
|
|
332
|
+
component: 'SessionManager',
|
|
333
|
+
operation: 'session.storage.configure',
|
|
334
|
+
storageType: String(storageType)
|
|
335
|
+
});
|
|
336
|
+
currentStorage = new InMemorySessionStorage();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Optional helper to create a real express-session middleware using the current store
|
|
342
|
+
function createExpressSessionMiddleware(options = {}) {
|
|
343
|
+
if (!sessionLib) {
|
|
344
|
+
logger.warnWithContext('express-session not installed; returning no-op session middleware', {
|
|
345
|
+
component: 'SessionManager',
|
|
346
|
+
operation: 'session.middleware.create'
|
|
347
|
+
});
|
|
348
|
+
return (req, res, next) => next();
|
|
349
|
+
}
|
|
350
|
+
const secret = config.get('SECURITY_OPTIONS.SESSION_SECRET');
|
|
351
|
+
const store = (currentStorage instanceof ExpressSessionStoreAdapter)
|
|
352
|
+
? currentStorage.store
|
|
353
|
+
: new sessionLib.MemoryStore();
|
|
354
|
+
|
|
355
|
+
return sessionLib({
|
|
356
|
+
saveUninitialized: false,
|
|
357
|
+
resave: false,
|
|
358
|
+
...options,
|
|
359
|
+
secret,
|
|
360
|
+
store,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
class SessionManager {
|
|
365
|
+
constructor() {
|
|
366
|
+
const instanceId = ulid();
|
|
367
|
+
const baseContext = createLogContext("SessionManager", "constructor", { instanceId });
|
|
368
|
+
|
|
369
|
+
logger.infoWithContext("SessionManager instance created", {
|
|
370
|
+
...baseContext,
|
|
371
|
+
instanceId,
|
|
372
|
+
timestamp: new Date().toISOString(),
|
|
373
|
+
isSingleton: true
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Note: Token invalidation is now handled via session state checking
|
|
377
|
+
// No separate invalidatedTokens Map needed - tokens are invalid when their session is closed
|
|
378
|
+
|
|
379
|
+
// Session validation cache - trades security for performance
|
|
380
|
+
// Cache stores validation results with TTL to reduce storage lookups
|
|
381
|
+
this._validationCache = new Map();
|
|
382
|
+
this._validationCacheTTL = config.get('SESSION_VALIDATION_CACHE_TTL', 5000);
|
|
383
|
+
|
|
384
|
+
// Cleanup interval reference
|
|
385
|
+
this.cleanupInterval = null;
|
|
386
|
+
|
|
387
|
+
this._instanceId = instanceId;
|
|
388
|
+
|
|
389
|
+
logger.infoWithContext("Token validation cache initialized", {
|
|
390
|
+
...baseContext,
|
|
391
|
+
cacheTTL: this._validationCacheTTL,
|
|
392
|
+
cacheEnabled: this._validationCacheTTL > 0
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Storage facade - delegates to current storage with proper binding
|
|
397
|
+
get storage() {
|
|
398
|
+
return {
|
|
399
|
+
get: currentStorage.get.bind(currentStorage),
|
|
400
|
+
set: currentStorage.set.bind(currentStorage),
|
|
401
|
+
delete: currentStorage.delete.bind(currentStorage),
|
|
402
|
+
keys: currentStorage.keys.bind(currentStorage),
|
|
403
|
+
size: currentStorage.size.bind(currentStorage),
|
|
404
|
+
getAll: currentStorage.getAll ? currentStorage.getAll.bind(currentStorage) : undefined
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get cached validation result for a token
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
_getCachedValidation(token) {
|
|
413
|
+
if (this._validationCacheTTL <= 0) return undefined;
|
|
414
|
+
|
|
415
|
+
const entry = this._validationCache.get(token);
|
|
416
|
+
if (!entry) return undefined;
|
|
417
|
+
|
|
418
|
+
// Check if entry has expired
|
|
419
|
+
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
|
|
420
|
+
this._validationCache.delete(token);
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return entry.value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Cache validation result for a token
|
|
429
|
+
* @private
|
|
430
|
+
*/
|
|
431
|
+
_setCachedValidation(token, isInvalidated, sessionId = null) {
|
|
432
|
+
if (this._validationCacheTTL <= 0) return; // Caching disabled
|
|
433
|
+
|
|
434
|
+
const expiresAt = this._validationCacheTTL > 0 ? Date.now() + this._validationCacheTTL : 0;
|
|
435
|
+
this._validationCache.set(token, {
|
|
436
|
+
value: isInvalidated,
|
|
437
|
+
expiresAt,
|
|
438
|
+
sessionId,
|
|
439
|
+
cachedAt: Date.now()
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Invalidate cache entry for a token (called when session is closed)
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
_invalidateCachedValidation(token) {
|
|
448
|
+
this._validationCache.delete(token);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Invalidate all cache entries for a specific session
|
|
453
|
+
* This is called when a session is closed to immediately invalidate all tokens for that session
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
_invalidateCachedValidationBySession(sessionId) {
|
|
457
|
+
if (!sessionId) return;
|
|
458
|
+
|
|
459
|
+
let invalidatedCount = 0;
|
|
460
|
+
for (const [token, entry] of this._validationCache.entries()) {
|
|
461
|
+
if (entry.sessionId === sessionId) {
|
|
462
|
+
this._validationCache.delete(token);
|
|
463
|
+
invalidatedCount++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (invalidatedCount > 0) {
|
|
468
|
+
logger.debugWithContext("Invalidated cached validations for closed session", {
|
|
469
|
+
component: "SessionManager",
|
|
470
|
+
method: "_invalidateCachedValidationBySession",
|
|
471
|
+
sessionId,
|
|
472
|
+
invalidatedCount
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get cache statistics for monitoring
|
|
479
|
+
*/
|
|
480
|
+
getValidationCacheStats() {
|
|
481
|
+
const now = Date.now();
|
|
482
|
+
let validEntries = 0;
|
|
483
|
+
let expiredEntries = 0;
|
|
484
|
+
|
|
485
|
+
for (const [, entry] of this._validationCache.entries()) {
|
|
486
|
+
if (entry.expiresAt && entry.expiresAt <= now) {
|
|
487
|
+
expiredEntries++;
|
|
488
|
+
} else {
|
|
489
|
+
validEntries++;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
totalEntries: this._validationCache.size,
|
|
495
|
+
validEntries,
|
|
496
|
+
expiredEntries,
|
|
497
|
+
cacheTTL: this._validationCacheTTL,
|
|
498
|
+
cacheEnabled: this._validationCacheTTL > 0
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
_generateSessionId(roditId) {
|
|
504
|
+
const requestId = ulid();
|
|
505
|
+
const startTime = Date.now();
|
|
506
|
+
const baseContext = createLogContext("SessionManager", "_generateSessionId", { requestId, roditId });
|
|
507
|
+
|
|
508
|
+
const sessionId = `sess_${roditId}_${ulid()}`;
|
|
509
|
+
|
|
510
|
+
logger.debugWithContext("Generated session ID", {
|
|
511
|
+
...baseContext,
|
|
512
|
+
sessionId,
|
|
513
|
+
roditId
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return sessionId;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async createSession(sessionData) {
|
|
520
|
+
const requestId = ulid();
|
|
521
|
+
const startTime = Date.now();
|
|
522
|
+
const baseContext = createLogContext("SessionManager", "createSession", { requestId });
|
|
523
|
+
|
|
524
|
+
// Get current active session count for metrics
|
|
525
|
+
const activeSessionCount = await this.getActiveSessionCount();
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
if (!sessionData || !sessionData.roditId) {
|
|
529
|
+
throw new Error('Missing required session data');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const sessionId = this._generateSessionId(sessionData.roditId);
|
|
533
|
+
const now = Math.floor(Date.now() / 1000);
|
|
534
|
+
|
|
535
|
+
// Create the session object
|
|
536
|
+
const session = {
|
|
537
|
+
id: sessionId,
|
|
538
|
+
roditId: sessionData.roditId,
|
|
539
|
+
ownerId: sessionData.ownerId,
|
|
540
|
+
createdAt: sessionData.createdAt || now,
|
|
541
|
+
expiresAt: sessionData.expiresAt,
|
|
542
|
+
lastAccessedAt: now,
|
|
543
|
+
status: 'active',
|
|
544
|
+
metadata: sessionData.metadata || {},
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Store the session
|
|
548
|
+
await this.storage.set(sessionId, session);
|
|
549
|
+
|
|
550
|
+
const duration = Date.now() - startTime;
|
|
551
|
+
|
|
552
|
+
// Verify the session was stored correctly
|
|
553
|
+
const storedSession = await this.storage.get(sessionId);
|
|
554
|
+
const verificationSuccess = !!storedSession;
|
|
555
|
+
|
|
556
|
+
logger.infoWithContext("Session created and stored", {
|
|
557
|
+
...baseContext,
|
|
558
|
+
sessionId,
|
|
559
|
+
sessionStatus: session.status,
|
|
560
|
+
expiresAt: session.expiresAt,
|
|
561
|
+
createdAt: session.createdAt,
|
|
562
|
+
verificationSuccess,
|
|
563
|
+
sessionManagerInstanceId: this._instanceId,
|
|
564
|
+
storageBackend: currentStorage?.store ? currentStorage.store.constructor.name : currentStorage.constructor.name,
|
|
565
|
+
duration,
|
|
566
|
+
sessionObjectValid: !!session,
|
|
567
|
+
sessionIdValid: !!session?.id,
|
|
568
|
+
sessionIdType: typeof session?.id
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Validate session object before returning
|
|
572
|
+
if (!session || !session.id) {
|
|
573
|
+
logger.errorWithContext("Created session is invalid", {
|
|
574
|
+
...baseContext,
|
|
575
|
+
sessionObject: session,
|
|
576
|
+
sessionId,
|
|
577
|
+
sessionManagerInstanceId: this._instanceId
|
|
578
|
+
});
|
|
579
|
+
throw new Error("Session creation resulted in invalid session object");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return session;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
const duration = Date.now() - startTime;
|
|
585
|
+
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
async getSession(sessionId) {
|
|
592
|
+
const requestId = ulid();
|
|
593
|
+
const startTime = Date.now();
|
|
594
|
+
const baseContext = createLogContext("SessionManager", "getSession", {
|
|
595
|
+
requestId,
|
|
596
|
+
sessionId
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const session = await this.storage.get(sessionId);
|
|
601
|
+
const now = Math.floor(Date.now() / 1000);
|
|
602
|
+
|
|
603
|
+
if (!session) {
|
|
604
|
+
const duration = Date.now() - startTime;
|
|
605
|
+
|
|
606
|
+
logger.warnWithContext("Session not found in storage", {
|
|
607
|
+
...baseContext,
|
|
608
|
+
sessionId,
|
|
609
|
+
sessionManagerInstanceId: this._instanceId,
|
|
610
|
+
storageBackend: currentStorage?.store ? currentStorage.store.constructor.name : currentStorage.constructor.name,
|
|
611
|
+
duration,
|
|
612
|
+
currentTimestamp: now
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Update last accessed time
|
|
619
|
+
session.lastAccessedAt = now;
|
|
620
|
+
// Persist updated access time back to storage
|
|
621
|
+
await this.storage.set(sessionId, session);
|
|
622
|
+
|
|
623
|
+
const duration = Date.now() - startTime;
|
|
624
|
+
|
|
625
|
+
logger.debugWithContext("Session retrieved successfully", {
|
|
626
|
+
...baseContext,
|
|
627
|
+
sessionId,
|
|
628
|
+
sessionStatus: session.status,
|
|
629
|
+
expiresAt: session.expiresAt,
|
|
630
|
+
lastAccessedAt: session.lastAccessedAt,
|
|
631
|
+
sessionManagerInstanceId: this._instanceId,
|
|
632
|
+
storageBackend: currentStorage?.store ? currentStorage.store.constructor.name : currentStorage.constructor.name,
|
|
633
|
+
duration
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return session;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
const duration = Date.now() - startTime;
|
|
639
|
+
|
|
640
|
+
logger.errorWithContext("Error retrieving session", {
|
|
641
|
+
...baseContext,
|
|
642
|
+
sessionId,
|
|
643
|
+
error: error.message,
|
|
644
|
+
sessionManagerInstanceId: this._instanceId,
|
|
645
|
+
duration
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
async updateSession(sessionId, updates) {
|
|
654
|
+
const requestId = ulid();
|
|
655
|
+
const startTime = Date.now();
|
|
656
|
+
const baseContext = createLogContext("SessionManager", "updateSession", {
|
|
657
|
+
requestId,
|
|
658
|
+
sessionId,
|
|
659
|
+
updatedFields: updates ? Object.keys(updates) : []
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
// Load session from configured storage
|
|
666
|
+
const session = await this.storage.get(sessionId);
|
|
667
|
+
|
|
668
|
+
if (!session) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Apply updates except id which should be immutable
|
|
673
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
674
|
+
if (key !== 'id') {
|
|
675
|
+
session[key] = value;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Update last accessed time
|
|
680
|
+
session.lastAccessedAt = Math.floor(Date.now() / 1000);
|
|
681
|
+
|
|
682
|
+
// Store updated session in storage
|
|
683
|
+
await this.storage.set(sessionId, session);
|
|
684
|
+
|
|
685
|
+
const duration = Date.now() - startTime;
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
return session;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
const duration = Date.now() - startTime;
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async closeSession(sessionId, reason = 'user_logout', token = null) {
|
|
698
|
+
const requestId = ulid();
|
|
699
|
+
const startTime = Date.now();
|
|
700
|
+
const baseContext = createLogContext("SessionManager", "closeSession", {
|
|
701
|
+
requestId,
|
|
702
|
+
sessionId,
|
|
703
|
+
reason,
|
|
704
|
+
hasToken: !!token
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const session = await this.storage.get(sessionId);
|
|
709
|
+
|
|
710
|
+
if (!session) {
|
|
711
|
+
// Enhanced debugging for session not found
|
|
712
|
+
const allSessionIds = await this.storage.keys();
|
|
713
|
+
const sessionCount = await this.storage.size();
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
return true; // Changed to true - allow logout to succeed
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Update session status
|
|
720
|
+
session.status = 'closed';
|
|
721
|
+
session.closedAt = Math.floor(Date.now() / 1000);
|
|
722
|
+
session.closeReason = reason;
|
|
723
|
+
|
|
724
|
+
// Store updated session
|
|
725
|
+
await this.storage.set(sessionId, session);
|
|
726
|
+
|
|
727
|
+
// CRITICAL: Immediately invalidate all cached validations for this session
|
|
728
|
+
// This ensures tokens are rejected immediately after logout, not after cache TTL
|
|
729
|
+
this._invalidateCachedValidationBySession(sessionId);
|
|
730
|
+
|
|
731
|
+
// Also invalidate the specific token if provided
|
|
732
|
+
if (token) {
|
|
733
|
+
this._invalidateCachedValidation(token);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
logger.debugWithContext("Session closed and cache invalidated", {
|
|
737
|
+
...baseContext,
|
|
738
|
+
sessionId,
|
|
739
|
+
cacheInvalidated: true
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const duration = Date.now() - startTime;
|
|
743
|
+
|
|
744
|
+
return true;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const duration = Date.now() - startTime;
|
|
747
|
+
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
async invalidateToken(token, reason = 'user_logout', sessionId = null) {
|
|
754
|
+
const requestId = ulid();
|
|
755
|
+
const startTime = Date.now();
|
|
756
|
+
|
|
757
|
+
const baseContext = createLogContext("SessionManager", "invalidateToken", {
|
|
758
|
+
requestId,
|
|
759
|
+
reason,
|
|
760
|
+
sessionId: sessionId || 'will_extract_from_token'
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
// If sessionId not provided, extract it from the token
|
|
765
|
+
let targetSessionId = sessionId;
|
|
766
|
+
if (!targetSessionId && token) {
|
|
767
|
+
try {
|
|
768
|
+
const tokenParts = token.split('.');
|
|
769
|
+
if (tokenParts.length === 3) {
|
|
770
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64url').toString());
|
|
771
|
+
targetSessionId = payload.session_id;
|
|
772
|
+
}
|
|
773
|
+
} catch (decodeError) {
|
|
774
|
+
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!targetSessionId) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Close the session - this will invalidate the token
|
|
783
|
+
const sessionClosed = await this.closeSession(targetSessionId, reason, null); // Don't pass token to avoid recursion
|
|
784
|
+
|
|
785
|
+
const duration = Date.now() - startTime;
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
return sessionClosed;
|
|
789
|
+
} catch (error) {
|
|
790
|
+
const duration = Date.now() - startTime;
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
async isTokenInvalidated(token) {
|
|
799
|
+
const requestId = ulid();
|
|
800
|
+
const startTime = Date.now();
|
|
801
|
+
const baseContext = createLogContext("SessionManager", "isTokenInvalidated", { requestId });
|
|
802
|
+
|
|
803
|
+
if (!token) {
|
|
804
|
+
return true; // No token = invalidated
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
// Check cache first for performance
|
|
809
|
+
const cachedResult = this._getCachedValidation(token);
|
|
810
|
+
if (cachedResult !== undefined) {
|
|
811
|
+
const duration = Date.now() - startTime;
|
|
812
|
+
logger.debugWithContext("Token validation cache hit", {
|
|
813
|
+
...baseContext,
|
|
814
|
+
isInvalidated: cachedResult,
|
|
815
|
+
cacheHit: true,
|
|
816
|
+
duration,
|
|
817
|
+
tokenLength: token?.length || 0
|
|
818
|
+
});
|
|
819
|
+
return cachedResult;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Cache miss - perform full validation
|
|
823
|
+
// Decode JWT token to extract session_id
|
|
824
|
+
const tokenParts = token.split('.');
|
|
825
|
+
if (tokenParts.length !== 3) {
|
|
826
|
+
return true; // Invalid format = invalidated
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Decode the payload (second part) using base64url
|
|
830
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64url').toString());
|
|
831
|
+
const sessionId = payload.session_id;
|
|
832
|
+
|
|
833
|
+
if (!sessionId) {
|
|
834
|
+
return true; // No session ID = invalidated
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Check if session exists and is active
|
|
838
|
+
const session = await this.storage.get(sessionId);
|
|
839
|
+
const now = Math.floor(Date.now() / 1000);
|
|
840
|
+
|
|
841
|
+
let isInvalidated = false;
|
|
842
|
+
let reason = null;
|
|
843
|
+
|
|
844
|
+
if (!session) {
|
|
845
|
+
isInvalidated = true;
|
|
846
|
+
reason = "session_not_found";
|
|
847
|
+
} else if (session.status !== 'active') {
|
|
848
|
+
isInvalidated = true;
|
|
849
|
+
reason = `session_status_${session.status}`;
|
|
850
|
+
} else if (session.expiresAt && session.expiresAt < now) {
|
|
851
|
+
isInvalidated = true;
|
|
852
|
+
reason = "session_expired";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Cache the result for future requests
|
|
856
|
+
this._setCachedValidation(token, isInvalidated, sessionId);
|
|
857
|
+
|
|
858
|
+
const duration = Date.now() - startTime;
|
|
859
|
+
|
|
860
|
+
logger.infoWithContext("Token invalidation check completed", {
|
|
861
|
+
...baseContext,
|
|
862
|
+
sessionId,
|
|
863
|
+
isInvalidated,
|
|
864
|
+
reason,
|
|
865
|
+
sessionFound: !!session,
|
|
866
|
+
sessionStatus: session?.status,
|
|
867
|
+
sessionExpiresAt: session?.expiresAt,
|
|
868
|
+
currentTimestamp: now,
|
|
869
|
+
sessionManagerInstanceId: this._instanceId,
|
|
870
|
+
tokenLength: token?.length || 0,
|
|
871
|
+
cacheHit: false,
|
|
872
|
+
cacheTTL: this._validationCacheTTL,
|
|
873
|
+
duration
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
return isInvalidated;
|
|
877
|
+
} catch (error) {
|
|
878
|
+
const duration = Date.now() - startTime;
|
|
879
|
+
|
|
880
|
+
// If we can't check due to error, assume it's invalidated for security
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
async getTokenInvalidationInfo(token) {
|
|
887
|
+
const requestId = ulid();
|
|
888
|
+
const startTime = Date.now();
|
|
889
|
+
const baseContext = createLogContext("SessionManager", "getTokenInvalidationInfo", { requestId });
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
if (!token) {
|
|
893
|
+
return {
|
|
894
|
+
reason: "no_token_provided",
|
|
895
|
+
invalidatedAt: Math.floor(Date.now() / 1000),
|
|
896
|
+
timestamp: new Date().toISOString(),
|
|
897
|
+
sessionId: null
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
// Decode JWT token to extract session_id
|
|
903
|
+
const tokenParts = token.split('.');
|
|
904
|
+
if (tokenParts.length !== 3) {
|
|
905
|
+
return {
|
|
906
|
+
reason: "invalid_jwt_format",
|
|
907
|
+
invalidatedAt: Math.floor(Date.now() / 1000),
|
|
908
|
+
timestamp: new Date().toISOString(),
|
|
909
|
+
sessionId: null
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
|
|
914
|
+
const sessionId = payload.session_id;
|
|
915
|
+
|
|
916
|
+
if (!sessionId) {
|
|
917
|
+
return {
|
|
918
|
+
reason: "no_session_id_in_token",
|
|
919
|
+
invalidatedAt: Math.floor(Date.now() / 1000),
|
|
920
|
+
timestamp: new Date().toISOString(),
|
|
921
|
+
sessionId: null
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Check session state
|
|
926
|
+
const session = await this.storage.get(sessionId);
|
|
927
|
+
const now = Math.floor(Date.now() / 1000);
|
|
928
|
+
|
|
929
|
+
let invalidationInfo = null;
|
|
930
|
+
|
|
931
|
+
if (!session) {
|
|
932
|
+
invalidationInfo = {
|
|
933
|
+
reason: "session_not_found",
|
|
934
|
+
invalidatedAt: now,
|
|
935
|
+
timestamp: new Date().toISOString(),
|
|
936
|
+
sessionId
|
|
937
|
+
};
|
|
938
|
+
} else if (session.status !== 'active') {
|
|
939
|
+
invalidationInfo = {
|
|
940
|
+
reason: `session_status_${session.status}`,
|
|
941
|
+
invalidatedAt: session.closedAt || now,
|
|
942
|
+
timestamp: session.closedAt ? new Date(session.closedAt * 1000).toISOString() : new Date().toISOString(),
|
|
943
|
+
sessionId,
|
|
944
|
+
closeReason: session.closeReason
|
|
945
|
+
};
|
|
946
|
+
} else if (session.expiresAt && session.expiresAt < now) {
|
|
947
|
+
invalidationInfo = {
|
|
948
|
+
reason: "session_expired",
|
|
949
|
+
invalidatedAt: session.expiresAt,
|
|
950
|
+
timestamp: new Date(session.expiresAt * 1000).toISOString(),
|
|
951
|
+
sessionId
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const duration = Date.now() - startTime;
|
|
956
|
+
return invalidationInfo;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
const duration = Date.now() - startTime;
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
// Return error info
|
|
962
|
+
return {
|
|
963
|
+
reason: "error_checking_session",
|
|
964
|
+
invalidatedAt: Math.floor(Date.now() / 1000),
|
|
965
|
+
timestamp: new Date().toISOString(),
|
|
966
|
+
sessionId: null,
|
|
967
|
+
error: error.message
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
hashToken(token) {
|
|
974
|
+
const requestId = ulid();
|
|
975
|
+
const startTime = Date.now();
|
|
976
|
+
const baseContext = createLogContext("SessionManager", "hashToken", { requestId });
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
const crypto = require('crypto');
|
|
980
|
+
const hash = crypto.createHash('sha256').update(token).digest('hex');
|
|
981
|
+
|
|
982
|
+
const duration = Date.now() - startTime;
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
return hash;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
const duration = Date.now() - startTime;
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
throw error; // Rethrow as this is a critical operation
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
async isSessionActive(sessionId) {
|
|
996
|
+
if (!sessionId) return false;
|
|
997
|
+
const session = await this.getSession(sessionId);
|
|
998
|
+
// Session is active if it exists, isn't closed or expired
|
|
999
|
+
return !!(session && session.status === 'active');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async cleanupExpiredSessions() {
|
|
1003
|
+
const requestId = ulid();
|
|
1004
|
+
const startTime = Date.now();
|
|
1005
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1006
|
+
let removedCount = 0;
|
|
1007
|
+
const baseContext = createLogContext("SessionManager", "cleanupExpiredSessions", { requestId });
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
// Get all sessions from storage (support backends without getAll)
|
|
1011
|
+
let allSessions = [];
|
|
1012
|
+
if (typeof this.storage.getAll === 'function') {
|
|
1013
|
+
allSessions = await this.storage.getAll();
|
|
1014
|
+
} else {
|
|
1015
|
+
const ids = await this.storage.keys();
|
|
1016
|
+
for (const id of ids) {
|
|
1017
|
+
const s = await this.storage.get(id);
|
|
1018
|
+
if (s) allSessions.push(s);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
logger.infoWithContext("Starting session cleanup", {
|
|
1023
|
+
...baseContext,
|
|
1024
|
+
totalSessions: allSessions.length,
|
|
1025
|
+
currentTimestamp: now,
|
|
1026
|
+
sessionManagerInstanceId: this._instanceId
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Find expired sessions
|
|
1030
|
+
for (const session of allSessions) {
|
|
1031
|
+
const sessionId = session.id || session.sessionId;
|
|
1032
|
+
if (!sessionId) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const isExpired = session.expiresAt && session.expiresAt < now;
|
|
1037
|
+
const isOldClosed = session.status === 'closed' && session.closedAt < now - 86400;
|
|
1038
|
+
|
|
1039
|
+
if (isExpired || isOldClosed) {
|
|
1040
|
+
logger.infoWithContext("Removing expired/old session", {
|
|
1041
|
+
...baseContext,
|
|
1042
|
+
sessionId,
|
|
1043
|
+
sessionStatus: session.status,
|
|
1044
|
+
expiresAt: session.expiresAt,
|
|
1045
|
+
closedAt: session.closedAt,
|
|
1046
|
+
currentTimestamp: now,
|
|
1047
|
+
reason: isExpired ? 'expired' : 'old_closed',
|
|
1048
|
+
sessionManagerInstanceId: this._instanceId
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
await this.storage.delete(sessionId);
|
|
1052
|
+
removedCount++;
|
|
1053
|
+
} else {
|
|
1054
|
+
logger.debugWithContext("Session kept during cleanup", {
|
|
1055
|
+
...baseContext,
|
|
1056
|
+
sessionId,
|
|
1057
|
+
sessionStatus: session.status,
|
|
1058
|
+
expiresAt: session.expiresAt,
|
|
1059
|
+
currentTimestamp: now,
|
|
1060
|
+
sessionManagerInstanceId: this._instanceId
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const duration = Date.now() - startTime;
|
|
1066
|
+
|
|
1067
|
+
logger.infoWithContext("Session cleanup completed", {
|
|
1068
|
+
...baseContext,
|
|
1069
|
+
removedCount,
|
|
1070
|
+
totalSessionsBefore: allSessions.length,
|
|
1071
|
+
remainingSessions: allSessions.length - removedCount,
|
|
1072
|
+
sessionManagerInstanceId: this._instanceId,
|
|
1073
|
+
duration
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
return removedCount;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
return 0;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
async findSessionsByRoditId(roditId) {
|
|
1086
|
+
const requestId = ulid();
|
|
1087
|
+
const startTime = Date.now();
|
|
1088
|
+
const baseContext = createLogContext("SessionManager", "findSessionsByRoditId", { requestId, roditId });
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
let result = [];
|
|
1093
|
+
// Get sessions from storage
|
|
1094
|
+
let allSessions = [];
|
|
1095
|
+
if (typeof this.storage.getAll === 'function') {
|
|
1096
|
+
allSessions = await this.storage.getAll();
|
|
1097
|
+
} else {
|
|
1098
|
+
const ids = await this.storage.keys();
|
|
1099
|
+
for (const id of ids) {
|
|
1100
|
+
const s = await this.storage.get(id);
|
|
1101
|
+
if (s) allSessions.push(s);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
result = allSessions.filter(s => s && s.roditId === roditId);
|
|
1105
|
+
|
|
1106
|
+
const duration = Date.now() - startTime;
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
return result;
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
const duration = Date.now() - startTime;
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
return [];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
async getActiveSessionCount() {
|
|
1120
|
+
const requestId = ulid();
|
|
1121
|
+
const startTime = Date.now();
|
|
1122
|
+
const baseContext = createLogContext("SessionManager", "getActiveSessionCount", { requestId });
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
let count = 0;
|
|
1126
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1127
|
+
|
|
1128
|
+
// Get all sessions from storage with fallback mechanisms
|
|
1129
|
+
let allSessions = [];
|
|
1130
|
+
try {
|
|
1131
|
+
// First try to use getAll if available
|
|
1132
|
+
if (this.storage.getAll) {
|
|
1133
|
+
allSessions = await this.storage.getAll();
|
|
1134
|
+
} else {
|
|
1135
|
+
// Fall back to getting keys and fetching each one
|
|
1136
|
+
const ids = await this.storage.keys();
|
|
1137
|
+
for (const id of ids) {
|
|
1138
|
+
try {
|
|
1139
|
+
const s = await this.storage.get(id);
|
|
1140
|
+
if (s) allSessions.push(s);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
// Skip any invalid sessions
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
// If we can't get sessions, return 0
|
|
1149
|
+
logger.errorWithContext('Error getting sessions', baseContext, err);
|
|
1150
|
+
return 0;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Count active sessions
|
|
1154
|
+
for (const session of allSessions) {
|
|
1155
|
+
try {
|
|
1156
|
+
if (session &&
|
|
1157
|
+
typeof session === 'object' &&
|
|
1158
|
+
session.status === 'active' &&
|
|
1159
|
+
(!session.expiresAt || session.expiresAt > now)) {
|
|
1160
|
+
count++;
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
// Skip any invalid session objects
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const duration = Date.now() - startTime;
|
|
1169
|
+
logger.debug('Active session count calculated', { ...baseContext, count, duration });
|
|
1170
|
+
|
|
1171
|
+
return count;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
const duration = Date.now() - startTime;
|
|
1174
|
+
logger.errorWithContext('Error in getActiveSessionCount', {
|
|
1175
|
+
...baseContext,
|
|
1176
|
+
duration
|
|
1177
|
+
}, error);
|
|
1178
|
+
|
|
1179
|
+
// Return 0 on any error to ensure the application remains available
|
|
1180
|
+
return 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
startCleanupJob(interval = SESSION_CLEANUP_INTERVAL) {
|
|
1185
|
+
const requestId = ulid();
|
|
1186
|
+
|
|
1187
|
+
const baseContext = createLogContext(
|
|
1188
|
+
"SessionManager",
|
|
1189
|
+
"startCleanupJob",
|
|
1190
|
+
{
|
|
1191
|
+
requestId,
|
|
1192
|
+
intervalMs: interval,
|
|
1193
|
+
intervalSeconds: interval / 1000
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
// Clear any existing interval
|
|
1198
|
+
if (this.cleanupInterval) {
|
|
1199
|
+
clearInterval(this.cleanupInterval);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Schedule the cleanup job
|
|
1203
|
+
this.cleanupInterval = setInterval(async () => {
|
|
1204
|
+
const cleanupRequestId = ulid();
|
|
1205
|
+
const startTime = Date.now();
|
|
1206
|
+
|
|
1207
|
+
const cleanupContext = createLogContext(
|
|
1208
|
+
"SessionManager",
|
|
1209
|
+
"scheduledCleanup",
|
|
1210
|
+
{
|
|
1211
|
+
requestId: cleanupRequestId,
|
|
1212
|
+
intervalSeconds: interval / 1000
|
|
1213
|
+
}
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
try {
|
|
1218
|
+
const removedCount = await this.cleanupExpiredSessions();
|
|
1219
|
+
|
|
1220
|
+
const duration = Date.now() - startTime;
|
|
1221
|
+
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
const duration = Date.now() - startTime;
|
|
1224
|
+
|
|
1225
|
+
}
|
|
1226
|
+
}, interval);
|
|
1227
|
+
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
stopCleanupJob() {
|
|
1232
|
+
const requestId = ulid();
|
|
1233
|
+
|
|
1234
|
+
const baseContext = createLogContext(
|
|
1235
|
+
"SessionManager",
|
|
1236
|
+
"stopCleanupJob",
|
|
1237
|
+
{ requestId }
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
if (this.cleanupInterval) {
|
|
1241
|
+
clearInterval(this.cleanupInterval);
|
|
1242
|
+
this.cleanupInterval = null;
|
|
1243
|
+
|
|
1244
|
+
} else {
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async runManualCleanup(tokenRetentionPeriod = SESSION_TOKEN_RETENTION_PERIOD) {
|
|
1249
|
+
const requestId = ulid();
|
|
1250
|
+
const startTime = Date.now();
|
|
1251
|
+
|
|
1252
|
+
const baseContext = createLogContext(
|
|
1253
|
+
"SessionManager",
|
|
1254
|
+
"runManualCleanup",
|
|
1255
|
+
{ requestId }
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
try {
|
|
1260
|
+
const removedSessionsCount = await this.cleanupExpiredSessions();
|
|
1261
|
+
|
|
1262
|
+
const duration = Date.now() - startTime;
|
|
1263
|
+
|
|
1264
|
+
const resultContext = {
|
|
1265
|
+
...baseContext,
|
|
1266
|
+
duration,
|
|
1267
|
+
removedSessionsCount,
|
|
1268
|
+
remainingSessions: await this.getActiveSessionCount()
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
return {
|
|
1272
|
+
removedSessionsCount,
|
|
1273
|
+
remainingSessions: await this.getActiveSessionCount(),
|
|
1274
|
+
duration
|
|
1275
|
+
};
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
const duration = Date.now() - startTime;
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const sessionManager = new SessionManager();
|
|
1284
|
+
|
|
1285
|
+
logger.infoWithContext("SessionManager singleton created and exported", {
|
|
1286
|
+
component: "SessionManager",
|
|
1287
|
+
event: "singleton_export",
|
|
1288
|
+
instanceId: sessionManager._instanceId,
|
|
1289
|
+
timestamp: new Date().toISOString(),
|
|
1290
|
+
storageBackend: currentStorage?.store ? currentStorage.store.constructor.name : currentStorage.constructor.name
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
module.exports = {
|
|
1294
|
+
sessionManager,
|
|
1295
|
+
InMemorySessionStorage,
|
|
1296
|
+
setStorage,
|
|
1297
|
+
setExpressSessionStore,
|
|
1298
|
+
configureStorageFromConfig,
|
|
1299
|
+
createExpressSessionMiddleware,
|
|
1300
|
+
SESSION_CLEANUP_INTERVAL,
|
|
1301
|
+
SESSION_TOKEN_RETENTION_PERIOD
|
|
1302
|
+
};
|