strapi-plugin-magic-sessionmanager 4.5.2 → 4.5.3
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/dist/server/index.js +406 -186
- package/dist/server/index.mjs +406 -186
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -161,7 +161,16 @@ var register$1 = async ({ strapi: strapi2 }) => {
|
|
|
161
161
|
try {
|
|
162
162
|
await strapi2.documents(SESSION_UID$6).update({
|
|
163
163
|
documentId: session2.documentId,
|
|
164
|
-
data: {
|
|
164
|
+
data: {
|
|
165
|
+
isActive: false,
|
|
166
|
+
// Blocked users are NOT a "manual" self-logout — mark
|
|
167
|
+
// this distinctly so the client receives a different
|
|
168
|
+
// error message ("account blocked") rather than
|
|
169
|
+
// "session terminated".
|
|
170
|
+
terminatedManually: false,
|
|
171
|
+
terminationReason: "blocked",
|
|
172
|
+
logoutTime: now
|
|
173
|
+
}
|
|
165
174
|
});
|
|
166
175
|
terminated++;
|
|
167
176
|
} catch (updateErr) {
|
|
@@ -291,27 +300,27 @@ function generateSessionId$1(userId) {
|
|
|
291
300
|
const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
|
|
292
301
|
return `sess_${timestamp2}_${userHash}_${randomBytes}`;
|
|
293
302
|
}
|
|
294
|
-
function hashToken$
|
|
303
|
+
function hashToken$6(token) {
|
|
295
304
|
if (!token) return null;
|
|
296
305
|
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
297
306
|
}
|
|
298
307
|
var encryption = {
|
|
299
308
|
encryptToken: encryptToken$2,
|
|
300
309
|
generateSessionId: generateSessionId$1,
|
|
301
|
-
hashToken: hashToken$
|
|
310
|
+
hashToken: hashToken$6
|
|
302
311
|
};
|
|
303
312
|
const USER_UID$1 = "plugin::users-permissions.user";
|
|
304
|
-
const cache = /* @__PURE__ */ new Map();
|
|
313
|
+
const cache$1 = /* @__PURE__ */ new Map();
|
|
305
314
|
const CACHE_TTL = 5 * 60 * 1e3;
|
|
306
315
|
const CACHE_MAX_SIZE = 1e3;
|
|
307
316
|
function evict() {
|
|
308
317
|
const now = Date.now();
|
|
309
|
-
for (const [key, value] of cache) {
|
|
310
|
-
if (now - value.ts >= CACHE_TTL) cache.delete(key);
|
|
318
|
+
for (const [key, value] of cache$1) {
|
|
319
|
+
if (now - value.ts >= CACHE_TTL) cache$1.delete(key);
|
|
311
320
|
}
|
|
312
|
-
if (cache.size >= CACHE_MAX_SIZE) {
|
|
313
|
-
const keysToDelete = [...cache.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
314
|
-
keysToDelete.forEach((k2) => cache.delete(k2));
|
|
321
|
+
if (cache$1.size >= CACHE_MAX_SIZE) {
|
|
322
|
+
const keysToDelete = [...cache$1.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
323
|
+
keysToDelete.forEach((k2) => cache$1.delete(k2));
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
async function resolveUserDocumentId$5(strapi2, userId) {
|
|
@@ -321,17 +330,17 @@ async function resolveUserDocumentId$5(strapi2, userId) {
|
|
|
321
330
|
}
|
|
322
331
|
const numericId = typeof userId === "number" ? userId : parseInt(userId, 10);
|
|
323
332
|
const cacheKey = `u_${numericId}`;
|
|
324
|
-
const cached2 = cache.get(cacheKey);
|
|
333
|
+
const cached2 = cache$1.get(cacheKey);
|
|
325
334
|
if (cached2 && Date.now() - cached2.ts < CACHE_TTL) {
|
|
326
335
|
return cached2.documentId;
|
|
327
336
|
}
|
|
328
|
-
if (cache.size >= CACHE_MAX_SIZE) evict();
|
|
337
|
+
if (cache$1.size >= CACHE_MAX_SIZE) evict();
|
|
329
338
|
try {
|
|
330
339
|
const user = await strapi2.entityService.findOne(USER_UID$1, numericId, {
|
|
331
340
|
fields: ["documentId"]
|
|
332
341
|
});
|
|
333
342
|
if (user?.documentId) {
|
|
334
|
-
cache.set(cacheKey, { documentId: user.documentId, ts: Date.now() });
|
|
343
|
+
cache$1.set(cacheKey, { documentId: user.documentId, ts: Date.now() });
|
|
335
344
|
return user.documentId;
|
|
336
345
|
}
|
|
337
346
|
} catch {
|
|
@@ -392,6 +401,15 @@ function normalizeStoredSettings(stored) {
|
|
|
392
401
|
if (stored.sessionCreationGraceMs !== void 0) {
|
|
393
402
|
out.sessionCreationGraceMs = toIntInRange(stored.sessionCreationGraceMs, 5e3, 0, 3e4);
|
|
394
403
|
}
|
|
404
|
+
if (stored.rateLimitWriteMax !== void 0) {
|
|
405
|
+
out.rateLimitWriteMax = toIntInRange(stored.rateLimitWriteMax, 10, 1, 1e3);
|
|
406
|
+
}
|
|
407
|
+
if (stored.rateLimitReadMax !== void 0) {
|
|
408
|
+
out.rateLimitReadMax = toIntInRange(stored.rateLimitReadMax, 120, 1, 1e4);
|
|
409
|
+
}
|
|
410
|
+
if (stored.rateLimitWindowSeconds !== void 0) {
|
|
411
|
+
out.rateLimitWindowSeconds = toIntInRange(stored.rateLimitWindowSeconds, 60, 10, 3600);
|
|
412
|
+
}
|
|
395
413
|
for (const key of passthroughBooleans) {
|
|
396
414
|
if (stored[key] !== void 0) out[key] = !!stored[key];
|
|
397
415
|
}
|
|
@@ -415,7 +433,7 @@ function normalizeStoredSettings(stored) {
|
|
|
415
433
|
}
|
|
416
434
|
return out;
|
|
417
435
|
}
|
|
418
|
-
async function getPluginSettings$
|
|
436
|
+
async function getPluginSettings$6(strapi2) {
|
|
419
437
|
const now = Date.now();
|
|
420
438
|
if (cached$1 && now - cachedAt < CACHE_TTL_MS) {
|
|
421
439
|
return cached$1;
|
|
@@ -439,12 +457,12 @@ function invalidateSettingsCache$1() {
|
|
|
439
457
|
cachedAt = 0;
|
|
440
458
|
}
|
|
441
459
|
var settingsLoader = {
|
|
442
|
-
getPluginSettings: getPluginSettings$
|
|
460
|
+
getPluginSettings: getPluginSettings$6,
|
|
443
461
|
invalidateSettingsCache: invalidateSettingsCache$1
|
|
444
462
|
};
|
|
445
463
|
const MIN_TOKEN_LENGTH = 40;
|
|
446
464
|
const MAX_TOKEN_LENGTH = 8192;
|
|
447
|
-
function extractBearerToken$
|
|
465
|
+
function extractBearerToken$5(ctx) {
|
|
448
466
|
const headers = ctx?.request?.headers || ctx?.request?.header || {};
|
|
449
467
|
const raw = headers.authorization || headers.Authorization;
|
|
450
468
|
if (!raw || typeof raw !== "string") return null;
|
|
@@ -454,9 +472,76 @@ function extractBearerToken$4(ctx) {
|
|
|
454
472
|
if (!token || token.length < MIN_TOKEN_LENGTH || token.length > MAX_TOKEN_LENGTH) return null;
|
|
455
473
|
return token;
|
|
456
474
|
}
|
|
457
|
-
var extractToken = { extractBearerToken: extractBearerToken$
|
|
475
|
+
var extractToken = { extractBearerToken: extractBearerToken$5 };
|
|
476
|
+
const TTL_MS = 60 * 1e3;
|
|
477
|
+
const MAX_ENTRIES = 1e4;
|
|
478
|
+
const cache = /* @__PURE__ */ new Map();
|
|
479
|
+
const prune$1 = () => {
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
for (const [k2, v] of cache) {
|
|
482
|
+
if (v.expiresAt <= now) cache.delete(k2);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
function setSessionRejectionReason$1(tokenHash, reason) {
|
|
486
|
+
if (!tokenHash || !reason) return;
|
|
487
|
+
if (cache.size >= MAX_ENTRIES) prune$1();
|
|
488
|
+
cache.set(tokenHash, { reason, expiresAt: Date.now() + TTL_MS });
|
|
489
|
+
}
|
|
490
|
+
function consumeSessionRejectionReason$2(tokenHash) {
|
|
491
|
+
if (!tokenHash) return null;
|
|
492
|
+
const entry = cache.get(tokenHash);
|
|
493
|
+
if (!entry) return null;
|
|
494
|
+
cache.delete(tokenHash);
|
|
495
|
+
if (entry.expiresAt <= Date.now()) return null;
|
|
496
|
+
return entry.reason;
|
|
497
|
+
}
|
|
498
|
+
var rejectionCache = {
|
|
499
|
+
setSessionRejectionReason: setSessionRejectionReason$1,
|
|
500
|
+
consumeSessionRejectionReason: consumeSessionRejectionReason$2
|
|
501
|
+
};
|
|
502
|
+
const { extractBearerToken: extractBearerToken$4 } = extractToken;
|
|
503
|
+
const { hashToken: hashToken$5 } = encryption;
|
|
504
|
+
const { consumeSessionRejectionReason: consumeSessionRejectionReason$1 } = rejectionCache;
|
|
505
|
+
const HEADER = "X-Session-Terminated-Reason";
|
|
506
|
+
const REASON_MESSAGES = {
|
|
507
|
+
manual: "Your session was terminated. Please log in again.",
|
|
508
|
+
idle: "Your session expired due to inactivity. Please log in again.",
|
|
509
|
+
expired: "Your session has reached its maximum age. Please log in again.",
|
|
510
|
+
blocked: "Your account has been blocked. Contact support."
|
|
511
|
+
};
|
|
512
|
+
const middleware = () => async (ctx, next) => {
|
|
513
|
+
await next();
|
|
514
|
+
if (ctx.status !== 401) return;
|
|
515
|
+
const token = extractBearerToken$4(ctx);
|
|
516
|
+
if (!token) return;
|
|
517
|
+
const reason = consumeSessionRejectionReason$1(hashToken$5(token));
|
|
518
|
+
if (!reason) return;
|
|
519
|
+
ctx.set(HEADER, reason);
|
|
520
|
+
const existing = ctx.body;
|
|
521
|
+
const friendlyMessage = REASON_MESSAGES[reason] || "Session invalid. Please log in again.";
|
|
522
|
+
if (existing && typeof existing === "object" && existing.error) {
|
|
523
|
+
existing.error.details = existing.error.details || {};
|
|
524
|
+
if (!existing.error.details.reason) {
|
|
525
|
+
existing.error.details.reason = reason;
|
|
526
|
+
}
|
|
527
|
+
if (!existing.error.message || existing.error.message === "Unauthorized") {
|
|
528
|
+
existing.error.message = friendlyMessage;
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
ctx.body = {
|
|
533
|
+
data: null,
|
|
534
|
+
error: {
|
|
535
|
+
status: 401,
|
|
536
|
+
name: "UnauthorizedError",
|
|
537
|
+
message: friendlyMessage,
|
|
538
|
+
details: { reason }
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
var sessionRejectionHeaders = middleware;
|
|
458
543
|
const { resolveUserDocumentId: resolveUserDocumentId$4 } = resolveUser;
|
|
459
|
-
const { getPluginSettings: getPluginSettings$
|
|
544
|
+
const { getPluginSettings: getPluginSettings$5 } = settingsLoader;
|
|
460
545
|
const { extractBearerToken: extractBearerToken$3 } = extractToken;
|
|
461
546
|
const { hashToken: hashToken$4 } = encryption;
|
|
462
547
|
const SESSION_UID$5 = "plugin::magic-sessionmanager.session";
|
|
@@ -477,65 +562,75 @@ function isAuthEndpoint(path2) {
|
|
|
477
562
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
478
563
|
return async (ctx, next) => {
|
|
479
564
|
if (isAuthEndpoint(ctx.path)) {
|
|
480
|
-
|
|
481
|
-
return;
|
|
565
|
+
return next();
|
|
482
566
|
}
|
|
483
|
-
if (ctx.state.user) {
|
|
567
|
+
if (!ctx.state.user) {
|
|
568
|
+
return next();
|
|
569
|
+
}
|
|
570
|
+
let userDocId = ctx.state.user.documentId;
|
|
571
|
+
if (!userDocId && ctx.state.user.id) {
|
|
484
572
|
try {
|
|
485
|
-
|
|
486
|
-
if (!userDocId2 && ctx.state.user.id) {
|
|
487
|
-
userDocId2 = await resolveUserDocumentId$4(strapi2, ctx.state.user.id);
|
|
488
|
-
}
|
|
489
|
-
if (userDocId2) {
|
|
490
|
-
const settings2 = await getPluginSettings$4(strapi2);
|
|
491
|
-
const strictMode = settings2.strictSessionEnforcement === true;
|
|
492
|
-
const token = extractBearerToken$3(ctx);
|
|
493
|
-
const tokenHashValue = token ? hashToken$4(token) : null;
|
|
494
|
-
const thisSession = tokenHashValue ? await strapi2.documents(SESSION_UID$5).findFirst({
|
|
495
|
-
filters: { user: { documentId: userDocId2 }, tokenHash: tokenHashValue },
|
|
496
|
-
fields: ["documentId", "isActive", "terminatedManually"]
|
|
497
|
-
}) : null;
|
|
498
|
-
if (thisSession) {
|
|
499
|
-
if (thisSession.terminatedManually === true) {
|
|
500
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session was manually terminated (user: ${userDocId2.substring(0, 8)}...)`);
|
|
501
|
-
return ctx.unauthorized("Session terminated. Please login again.");
|
|
502
|
-
}
|
|
503
|
-
ctx.state.userDocumentId = userDocId2;
|
|
504
|
-
ctx.state.__magicSessionId = thisSession.documentId;
|
|
505
|
-
await next();
|
|
506
|
-
if (thisSession.isActive) {
|
|
507
|
-
try {
|
|
508
|
-
await sessionService.touch({
|
|
509
|
-
userId: userDocId2,
|
|
510
|
-
sessionId: thisSession.documentId
|
|
511
|
-
});
|
|
512
|
-
} catch (err) {
|
|
513
|
-
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
if (strictMode) {
|
|
519
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId2.substring(0, 8)}..., strictMode)`);
|
|
520
|
-
return ctx.unauthorized("No valid session. Please login again.");
|
|
521
|
-
}
|
|
522
|
-
strapi2.log.warn(`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId2.substring(0, 8)}...) - allowing in non-strict mode`);
|
|
523
|
-
ctx.state.userDocumentId = userDocId2;
|
|
524
|
-
}
|
|
573
|
+
userDocId = await resolveUserDocumentId$4(strapi2, ctx.state.user.id);
|
|
525
574
|
} catch (err) {
|
|
526
|
-
strapi2.log.debug("[magic-sessionmanager]
|
|
575
|
+
strapi2.log.debug("[magic-sessionmanager] user doc-id lookup failed:", err.message);
|
|
576
|
+
return next();
|
|
527
577
|
}
|
|
528
578
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
579
|
+
if (!userDocId) {
|
|
580
|
+
return next();
|
|
581
|
+
}
|
|
582
|
+
const settings2 = await getPluginSettings$5(strapi2).catch(() => ({}));
|
|
583
|
+
const strictMode = settings2.strictSessionEnforcement === true;
|
|
584
|
+
const gracePeriodMs = Math.max(0, Number(settings2.sessionCreationGraceMs) || 5e3);
|
|
585
|
+
const token = extractBearerToken$3(ctx);
|
|
586
|
+
const tokenHashValue = token ? hashToken$4(token) : null;
|
|
587
|
+
let thisSession = null;
|
|
588
|
+
if (tokenHashValue) {
|
|
533
589
|
try {
|
|
534
|
-
await
|
|
590
|
+
thisSession = await strapi2.documents(SESSION_UID$5).findFirst({
|
|
591
|
+
filters: { user: { documentId: userDocId }, tokenHash: tokenHashValue },
|
|
592
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason"]
|
|
593
|
+
});
|
|
594
|
+
} catch (err) {
|
|
595
|
+
strapi2.log.debug("[magic-sessionmanager] session lookup failed:", err.message);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (thisSession) {
|
|
599
|
+
if (thisSession.isActive === false) {
|
|
600
|
+
return ctx.unauthorized("Session terminated. Please login again.");
|
|
601
|
+
}
|
|
602
|
+
ctx.state.userDocumentId = userDocId;
|
|
603
|
+
ctx.state.__magicSessionId = thisSession.documentId;
|
|
604
|
+
await next();
|
|
605
|
+
try {
|
|
606
|
+
await sessionService.touch({
|
|
607
|
+
userId: userDocId,
|
|
608
|
+
sessionId: thisSession.documentId
|
|
609
|
+
});
|
|
535
610
|
} catch (err) {
|
|
536
611
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
537
612
|
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (strictMode) {
|
|
616
|
+
const iat = ctx.state.user?.iat;
|
|
617
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
618
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
619
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
620
|
+
ctx.state.userDocumentId = userDocId;
|
|
621
|
+
return next();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
strapi2.log.info(
|
|
625
|
+
`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId.substring(0, 8)}..., strictMode)`
|
|
626
|
+
);
|
|
627
|
+
return ctx.unauthorized("No valid session. Please login again.");
|
|
538
628
|
}
|
|
629
|
+
strapi2.log.debug(
|
|
630
|
+
`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId.substring(0, 8)}...) - allowing in non-strict mode`
|
|
631
|
+
);
|
|
632
|
+
ctx.state.userDocumentId = userDocId;
|
|
633
|
+
return next();
|
|
539
634
|
};
|
|
540
635
|
};
|
|
541
636
|
var jsonwebtoken = { exports: {} };
|
|
@@ -9502,8 +9597,12 @@ const getClientIp = getClientIp_1;
|
|
|
9502
9597
|
const { encryptToken: encryptToken$1, hashToken: hashToken$3 } = encryption;
|
|
9503
9598
|
const { createLogger: createLogger$3 } = logger;
|
|
9504
9599
|
const { resolveUserDocumentId: resolveUserDocumentId$3 } = resolveUser;
|
|
9505
|
-
const { getPluginSettings: getPluginSettings$
|
|
9600
|
+
const { getPluginSettings: getPluginSettings$4 } = settingsLoader;
|
|
9506
9601
|
const { extractBearerToken: extractBearerToken$2 } = extractToken;
|
|
9602
|
+
const {
|
|
9603
|
+
setSessionRejectionReason,
|
|
9604
|
+
consumeSessionRejectionReason
|
|
9605
|
+
} = rejectionCache;
|
|
9507
9606
|
const SESSION_UID$4 = "plugin::magic-sessionmanager.session";
|
|
9508
9607
|
const JWT_WRAPPED_FLAG = Symbol.for("magic-sessionmanager.jwt.wrapped");
|
|
9509
9608
|
const LOGIN_PATHS = /* @__PURE__ */ new Set([
|
|
@@ -9576,7 +9675,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9576
9675
|
}
|
|
9577
9676
|
log.info("Running initial session cleanup...");
|
|
9578
9677
|
try {
|
|
9579
|
-
const settings2 = await getPluginSettings$
|
|
9678
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9580
9679
|
await sessionService.cleanupInactiveSessions({
|
|
9581
9680
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9582
9681
|
});
|
|
@@ -9587,7 +9686,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9587
9686
|
let intervalMs = 30 * 60 * 1e3;
|
|
9588
9687
|
let useDbDirect = false;
|
|
9589
9688
|
try {
|
|
9590
|
-
const settings2 = await getPluginSettings$
|
|
9689
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9591
9690
|
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9592
9691
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9593
9692
|
} catch {
|
|
@@ -9608,7 +9707,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9608
9707
|
const scheduleRetention = async () => {
|
|
9609
9708
|
let useDbDirect = false;
|
|
9610
9709
|
try {
|
|
9611
|
-
const settings2 = await getPluginSettings$
|
|
9710
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9612
9711
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9613
9712
|
} catch {
|
|
9614
9713
|
}
|
|
@@ -9632,6 +9731,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9632
9731
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9633
9732
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9634
9733
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9734
|
+
strapi2.server.use(
|
|
9735
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9736
|
+
);
|
|
9737
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9635
9738
|
strapi2.server.use(
|
|
9636
9739
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9637
9740
|
);
|
|
@@ -9650,7 +9753,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9650
9753
|
}
|
|
9651
9754
|
let settings2 = {};
|
|
9652
9755
|
try {
|
|
9653
|
-
settings2 = await getPluginSettings$
|
|
9756
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9654
9757
|
} catch {
|
|
9655
9758
|
settings2 = {};
|
|
9656
9759
|
}
|
|
@@ -9826,7 +9929,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9826
9929
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9827
9930
|
let maxFailed = 0;
|
|
9828
9931
|
try {
|
|
9829
|
-
const settings2 = await getPluginSettings$
|
|
9932
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9830
9933
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9831
9934
|
} catch {
|
|
9832
9935
|
maxFailed = 0;
|
|
@@ -9905,7 +10008,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9905
10008
|
}
|
|
9906
10009
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9907
10010
|
try {
|
|
9908
|
-
const settings2 = await getPluginSettings$
|
|
10011
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9909
10012
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9910
10013
|
return;
|
|
9911
10014
|
}
|
|
@@ -10222,7 +10325,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10222
10325
|
}
|
|
10223
10326
|
let settings2;
|
|
10224
10327
|
try {
|
|
10225
|
-
settings2 = await getPluginSettings$
|
|
10328
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10226
10329
|
} catch {
|
|
10227
10330
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10228
10331
|
}
|
|
@@ -10247,6 +10350,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10247
10350
|
strapi2.log.info(
|
|
10248
10351
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10249
10352
|
);
|
|
10353
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10250
10354
|
return null;
|
|
10251
10355
|
}
|
|
10252
10356
|
} catch {
|
|
@@ -10257,7 +10361,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10257
10361
|
user: { documentId: userDocId },
|
|
10258
10362
|
tokenHash: tokenHashValue
|
|
10259
10363
|
},
|
|
10260
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10364
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10261
10365
|
});
|
|
10262
10366
|
if (thisSession) {
|
|
10263
10367
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10266,15 +10370,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10266
10370
|
);
|
|
10267
10371
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10268
10372
|
documentId: thisSession.documentId,
|
|
10269
|
-
data: {
|
|
10373
|
+
data: {
|
|
10374
|
+
isActive: false,
|
|
10375
|
+
terminatedManually: false,
|
|
10376
|
+
terminationReason: "expired",
|
|
10377
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10378
|
+
}
|
|
10270
10379
|
});
|
|
10380
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10271
10381
|
return null;
|
|
10272
10382
|
}
|
|
10273
|
-
if (thisSession.
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10383
|
+
if (thisSession.isActive === false) {
|
|
10384
|
+
const reason = thisSession.terminationReason || (thisSession.terminatedManually === true ? "manual" : null);
|
|
10385
|
+
if (reason) {
|
|
10386
|
+
strapi2.log.info(
|
|
10387
|
+
`[magic-sessionmanager] [JWT-REJECTED] Session inactive (reason: ${reason}) for user ${userDocId.substring(0, 8)}...`
|
|
10388
|
+
);
|
|
10389
|
+
setSessionRejectionReason(tokenHashValue, reason);
|
|
10390
|
+
return null;
|
|
10391
|
+
}
|
|
10278
10392
|
}
|
|
10279
10393
|
if (thisSession.isActive) {
|
|
10280
10394
|
resetErrorCounter();
|
|
@@ -10289,8 +10403,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10289
10403
|
);
|
|
10290
10404
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10291
10405
|
documentId: thisSession.documentId,
|
|
10292
|
-
data: {
|
|
10406
|
+
data: {
|
|
10407
|
+
terminatedManually: false,
|
|
10408
|
+
terminationReason: "idle",
|
|
10409
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10410
|
+
}
|
|
10293
10411
|
});
|
|
10412
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10294
10413
|
return null;
|
|
10295
10414
|
}
|
|
10296
10415
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10492,6 +10611,16 @@ const attributes = {
|
|
|
10492
10611
|
"default": false,
|
|
10493
10612
|
required: false
|
|
10494
10613
|
},
|
|
10614
|
+
terminationReason: {
|
|
10615
|
+
type: "enumeration",
|
|
10616
|
+
"enum": [
|
|
10617
|
+
"manual",
|
|
10618
|
+
"idle",
|
|
10619
|
+
"expired",
|
|
10620
|
+
"blocked"
|
|
10621
|
+
],
|
|
10622
|
+
required: false
|
|
10623
|
+
},
|
|
10495
10624
|
geoLocation: {
|
|
10496
10625
|
type: "json"
|
|
10497
10626
|
},
|
|
@@ -10525,10 +10654,16 @@ var contentTypes$2 = {
|
|
|
10525
10654
|
}
|
|
10526
10655
|
};
|
|
10527
10656
|
const writeRateLimit = [
|
|
10528
|
-
{
|
|
10657
|
+
{
|
|
10658
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10659
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10660
|
+
}
|
|
10529
10661
|
];
|
|
10530
10662
|
const readRateLimit = [
|
|
10531
|
-
{
|
|
10663
|
+
{
|
|
10664
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10665
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10666
|
+
}
|
|
10532
10667
|
];
|
|
10533
10668
|
var contentApi$1 = {
|
|
10534
10669
|
type: "content-api",
|
|
@@ -10931,15 +11066,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10931
11066
|
const { hashToken: hashToken$2 } = encryption;
|
|
10932
11067
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10933
11068
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10934
|
-
const { getPluginSettings: getPluginSettings$
|
|
11069
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10935
11070
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10936
11071
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10937
11072
|
const USER_UID = "plugin::users-permissions.user";
|
|
11073
|
+
const OWN_SESSIONS_LIMIT = 200;
|
|
11074
|
+
async function resolveAuthUserDocId(ctx) {
|
|
11075
|
+
const u2 = ctx.state.user;
|
|
11076
|
+
if (!u2) return null;
|
|
11077
|
+
if (u2.documentId) return u2.documentId;
|
|
11078
|
+
if (u2.id) return resolveUserDocumentId$2(strapi, u2.id);
|
|
11079
|
+
return null;
|
|
11080
|
+
}
|
|
10938
11081
|
var session$3 = {
|
|
10939
11082
|
/**
|
|
10940
11083
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10941
11084
|
* @route GET /magic-sessionmanager/sessions
|
|
10942
|
-
* @returns {object} `{ data, meta }`
|
|
10943
11085
|
*/
|
|
10944
11086
|
async getAllSessionsAdmin(ctx) {
|
|
10945
11087
|
try {
|
|
@@ -10961,7 +11103,6 @@ var session$3 = {
|
|
|
10961
11103
|
/**
|
|
10962
11104
|
* Lists currently-active sessions only.
|
|
10963
11105
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10964
|
-
* @returns {object} `{ data, meta }`
|
|
10965
11106
|
*/
|
|
10966
11107
|
async getActiveSessions(ctx) {
|
|
10967
11108
|
try {
|
|
@@ -10977,35 +11118,33 @@ var session$3 = {
|
|
|
10977
11118
|
}
|
|
10978
11119
|
},
|
|
10979
11120
|
/**
|
|
10980
|
-
* Returns the authenticated user's own sessions,
|
|
10981
|
-
* flagged via `isCurrentSession`.
|
|
10982
|
-
*
|
|
11121
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10983
11122
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10984
|
-
* @returns {object} `{ data, meta }`
|
|
10985
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10986
11123
|
*/
|
|
10987
11124
|
async getOwnSessions(ctx) {
|
|
10988
11125
|
try {
|
|
10989
|
-
const
|
|
11126
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11127
|
+
if (!userDocId) {
|
|
11128
|
+
return ctx.unauthorized("Authentication required");
|
|
11129
|
+
}
|
|
10990
11130
|
const currentToken = extractBearerToken$1(ctx);
|
|
10991
11131
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
10992
|
-
if (!userId) {
|
|
10993
|
-
return ctx.throw(401, "Unauthorized");
|
|
10994
|
-
}
|
|
10995
11132
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
10996
|
-
filters: { user: { documentId:
|
|
11133
|
+
filters: { user: { documentId: userDocId } },
|
|
10997
11134
|
sort: { loginTime: "desc" },
|
|
10998
|
-
limit:
|
|
11135
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
10999
11136
|
});
|
|
11000
|
-
const
|
|
11137
|
+
const hasMore = allSessions.length > OWN_SESSIONS_LIMIT;
|
|
11138
|
+
const paged = hasMore ? allSessions.slice(0, OWN_SESSIONS_LIMIT) : allSessions;
|
|
11139
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11001
11140
|
const enhanceOpts = {
|
|
11002
11141
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11003
11142
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
11004
11143
|
strapi
|
|
11005
11144
|
};
|
|
11006
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11145
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
11007
11146
|
for (const s3 of sessionsWithCurrent) {
|
|
11008
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11147
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
11009
11148
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
11010
11149
|
));
|
|
11011
11150
|
}
|
|
@@ -11018,7 +11157,9 @@ var session$3 = {
|
|
|
11018
11157
|
data: sessionsWithCurrent,
|
|
11019
11158
|
meta: {
|
|
11020
11159
|
count: sessionsWithCurrent.length,
|
|
11021
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11160
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11161
|
+
hasMore,
|
|
11162
|
+
limit: OWN_SESSIONS_LIMIT
|
|
11022
11163
|
}
|
|
11023
11164
|
};
|
|
11024
11165
|
} catch (err) {
|
|
@@ -11027,18 +11168,14 @@ var session$3 = {
|
|
|
11027
11168
|
}
|
|
11028
11169
|
},
|
|
11029
11170
|
/**
|
|
11030
|
-
* Get a specific user's sessions. Admins
|
|
11171
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11031
11172
|
* users can only query themselves.
|
|
11032
|
-
*
|
|
11033
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11034
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11035
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11036
11173
|
*/
|
|
11037
11174
|
async getUserSessions(ctx) {
|
|
11038
11175
|
try {
|
|
11039
11176
|
const { userId } = ctx.params;
|
|
11040
11177
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11041
|
-
const requestingUserDocId = ctx
|
|
11178
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11042
11179
|
if (!isAdminRequest) {
|
|
11043
11180
|
if (!requestingUserDocId) {
|
|
11044
11181
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11067,26 +11204,34 @@ var session$3 = {
|
|
|
11067
11204
|
*/
|
|
11068
11205
|
async logout(ctx) {
|
|
11069
11206
|
try {
|
|
11070
|
-
const
|
|
11207
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11071
11208
|
const token = extractBearerToken$1(ctx);
|
|
11072
|
-
if (!
|
|
11073
|
-
return ctx.
|
|
11209
|
+
if (!userDocId || !token) {
|
|
11210
|
+
return ctx.unauthorized("Authentication required");
|
|
11074
11211
|
}
|
|
11075
11212
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11076
11213
|
const currentTokenHash = hashToken$2(token);
|
|
11077
11214
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11078
11215
|
filters: {
|
|
11079
|
-
user: { documentId:
|
|
11216
|
+
user: { documentId: userDocId },
|
|
11080
11217
|
tokenHash: currentTokenHash,
|
|
11081
11218
|
isActive: true
|
|
11082
11219
|
},
|
|
11083
11220
|
fields: ["documentId"]
|
|
11084
11221
|
});
|
|
11222
|
+
let terminated = false;
|
|
11085
11223
|
if (matchingSession) {
|
|
11086
|
-
await sessionService.terminateSession({
|
|
11087
|
-
|
|
11224
|
+
await sessionService.terminateSession({
|
|
11225
|
+
sessionId: matchingSession.documentId,
|
|
11226
|
+
reason: "manual"
|
|
11227
|
+
});
|
|
11228
|
+
terminated = true;
|
|
11229
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out (session ${matchingSession.documentId})`);
|
|
11088
11230
|
}
|
|
11089
|
-
ctx.body = {
|
|
11231
|
+
ctx.body = {
|
|
11232
|
+
message: "Logged out successfully",
|
|
11233
|
+
terminated
|
|
11234
|
+
};
|
|
11090
11235
|
} catch (err) {
|
|
11091
11236
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11092
11237
|
ctx.throw(500, "Error during logout");
|
|
@@ -11098,13 +11243,13 @@ var session$3 = {
|
|
|
11098
11243
|
*/
|
|
11099
11244
|
async logoutAll(ctx) {
|
|
11100
11245
|
try {
|
|
11101
|
-
const
|
|
11102
|
-
if (!
|
|
11103
|
-
return ctx.
|
|
11246
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11247
|
+
if (!userDocId) {
|
|
11248
|
+
return ctx.unauthorized("Authentication required");
|
|
11104
11249
|
}
|
|
11105
11250
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11106
|
-
await sessionService.terminateSession({ userId });
|
|
11107
|
-
strapi.log.info(`[magic-sessionmanager] User ${
|
|
11251
|
+
await sessionService.terminateSession({ userId: userDocId, reason: "manual" });
|
|
11252
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out from all devices`);
|
|
11108
11253
|
ctx.body = { message: "Logged out from all devices successfully" };
|
|
11109
11254
|
} catch (err) {
|
|
11110
11255
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
@@ -11113,27 +11258,48 @@ var session$3 = {
|
|
|
11113
11258
|
},
|
|
11114
11259
|
/**
|
|
11115
11260
|
* Returns the session associated with the current JWT.
|
|
11261
|
+
*
|
|
11262
|
+
* During the post-login grace window the session-create write may not
|
|
11263
|
+
* yet be visible. In that case we return 202 Accepted with
|
|
11264
|
+
* `{ pending: true }` so the client knows to retry shortly instead of
|
|
11265
|
+
* interpreting a 404 as "no session at all".
|
|
11266
|
+
*
|
|
11116
11267
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11117
11268
|
*/
|
|
11118
11269
|
async getCurrentSession(ctx) {
|
|
11119
11270
|
try {
|
|
11120
|
-
const
|
|
11271
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11121
11272
|
const token = extractBearerToken$1(ctx);
|
|
11122
|
-
if (!
|
|
11123
|
-
return ctx.
|
|
11273
|
+
if (!userDocId || !token) {
|
|
11274
|
+
return ctx.unauthorized("Authentication required");
|
|
11124
11275
|
}
|
|
11125
11276
|
const currentTokenHash = hashToken$2(token);
|
|
11126
11277
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11127
11278
|
filters: {
|
|
11128
|
-
user: { documentId:
|
|
11279
|
+
user: { documentId: userDocId },
|
|
11129
11280
|
tokenHash: currentTokenHash,
|
|
11130
11281
|
isActive: true
|
|
11131
11282
|
}
|
|
11132
11283
|
});
|
|
11133
11284
|
if (!currentSession) {
|
|
11285
|
+
const settings3 = await getPluginSettings$3(strapi);
|
|
11286
|
+
const gracePeriodMs = Math.max(0, Number(settings3.sessionCreationGraceMs) || 5e3);
|
|
11287
|
+
const iat = ctx.state.user?.iat || ctx.state.auth?.credentials?.iat || null;
|
|
11288
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
11289
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
11290
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
11291
|
+
ctx.status = 202;
|
|
11292
|
+
ctx.body = {
|
|
11293
|
+
data: null,
|
|
11294
|
+
meta: { pending: true, retryAfterMs: gracePeriodMs - ageMs },
|
|
11295
|
+
message: "Session is still being created — please retry shortly."
|
|
11296
|
+
};
|
|
11297
|
+
return;
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11134
11300
|
return ctx.notFound("Current session not found");
|
|
11135
11301
|
}
|
|
11136
|
-
const settings2 = await getPluginSettings$
|
|
11302
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11137
11303
|
const enhanced = await enhanceSession(currentSession, {
|
|
11138
11304
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11139
11305
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11149,17 +11315,17 @@ var session$3 = {
|
|
|
11149
11315
|
}
|
|
11150
11316
|
},
|
|
11151
11317
|
/**
|
|
11152
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11318
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11153
11319
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11154
11320
|
*/
|
|
11155
11321
|
async terminateOwnSession(ctx) {
|
|
11156
11322
|
try {
|
|
11157
|
-
const
|
|
11323
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11158
11324
|
const { sessionId } = ctx.params;
|
|
11159
11325
|
const currentToken = extractBearerToken$1(ctx);
|
|
11160
11326
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11161
|
-
if (!
|
|
11162
|
-
return ctx.
|
|
11327
|
+
if (!userDocId) {
|
|
11328
|
+
return ctx.unauthorized("Authentication required");
|
|
11163
11329
|
}
|
|
11164
11330
|
if (!sessionId) {
|
|
11165
11331
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11172,19 +11338,23 @@ var session$3 = {
|
|
|
11172
11338
|
return ctx.notFound("Session not found");
|
|
11173
11339
|
}
|
|
11174
11340
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11175
|
-
if (sessionUserId !==
|
|
11176
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
11341
|
+
if (sessionUserId !== userDocId) {
|
|
11342
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userDocId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
11177
11343
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11178
11344
|
}
|
|
11179
11345
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11180
|
-
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
11346
|
+
return ctx.badRequest("Cannot terminate the current session. Use /logout instead.");
|
|
11347
|
+
}
|
|
11348
|
+
const alreadyTerminated = sessionToTerminate.isActive === false;
|
|
11349
|
+
if (!alreadyTerminated) {
|
|
11350
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11351
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11352
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} terminated own session ${sessionId}`);
|
|
11181
11353
|
}
|
|
11182
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11183
|
-
await sessionService.terminateSession({ sessionId });
|
|
11184
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11185
11354
|
ctx.body = {
|
|
11186
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11187
|
-
success: true
|
|
11355
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11356
|
+
success: true,
|
|
11357
|
+
alreadyTerminated
|
|
11188
11358
|
};
|
|
11189
11359
|
} catch (err) {
|
|
11190
11360
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11192,9 +11362,7 @@ var session$3 = {
|
|
|
11192
11362
|
}
|
|
11193
11363
|
},
|
|
11194
11364
|
/**
|
|
11195
|
-
*
|
|
11196
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11197
|
-
*
|
|
11365
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11198
11366
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11199
11367
|
*/
|
|
11200
11368
|
async simulateTimeout(ctx) {
|
|
@@ -11213,13 +11381,16 @@ var session$3 = {
|
|
|
11213
11381
|
}
|
|
11214
11382
|
await strapi.documents(SESSION_UID$2).update({
|
|
11215
11383
|
documentId: sessionId,
|
|
11216
|
-
data: {
|
|
11384
|
+
data: {
|
|
11385
|
+
isActive: false,
|
|
11386
|
+
terminatedManually: false,
|
|
11387
|
+
terminationReason: "idle"
|
|
11388
|
+
}
|
|
11217
11389
|
});
|
|
11218
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11390
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11219
11391
|
ctx.body = {
|
|
11220
|
-
message: `Session ${sessionId} marked as timed out
|
|
11221
|
-
success: true
|
|
11222
|
-
terminatedManually: false
|
|
11392
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11393
|
+
success: true
|
|
11223
11394
|
};
|
|
11224
11395
|
} catch (err) {
|
|
11225
11396
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11228,13 +11399,12 @@ var session$3 = {
|
|
|
11228
11399
|
},
|
|
11229
11400
|
/**
|
|
11230
11401
|
* Terminates a specific session (admin action).
|
|
11231
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11232
11402
|
*/
|
|
11233
11403
|
async terminateSingleSession(ctx) {
|
|
11234
11404
|
try {
|
|
11235
11405
|
const { sessionId } = ctx.params;
|
|
11236
11406
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11237
|
-
await sessionService.terminateSession({ sessionId });
|
|
11407
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11238
11408
|
ctx.body = {
|
|
11239
11409
|
message: `Session ${sessionId} terminated`,
|
|
11240
11410
|
success: true
|
|
@@ -11246,13 +11416,12 @@ var session$3 = {
|
|
|
11246
11416
|
},
|
|
11247
11417
|
/**
|
|
11248
11418
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11249
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11250
11419
|
*/
|
|
11251
11420
|
async terminateAllUserSessions(ctx) {
|
|
11252
11421
|
try {
|
|
11253
11422
|
const { userId } = ctx.params;
|
|
11254
11423
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11255
|
-
await sessionService.terminateSession({ userId });
|
|
11424
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11256
11425
|
ctx.body = {
|
|
11257
11426
|
message: `All sessions terminated for user ${userId}`,
|
|
11258
11427
|
success: true
|
|
@@ -11264,9 +11433,6 @@ var session$3 = {
|
|
|
11264
11433
|
},
|
|
11265
11434
|
/**
|
|
11266
11435
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11267
|
-
*
|
|
11268
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11269
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11270
11436
|
*/
|
|
11271
11437
|
async getIpGeolocation(ctx) {
|
|
11272
11438
|
try {
|
|
@@ -11293,10 +11459,7 @@ var session$3 = {
|
|
|
11293
11459
|
return ctx.badRequest("Invalid IP address format");
|
|
11294
11460
|
}
|
|
11295
11461
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11296
|
-
const pluginStore = strapi.store({
|
|
11297
|
-
type: "plugin",
|
|
11298
|
-
name: "magic-sessionmanager"
|
|
11299
|
-
});
|
|
11462
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11300
11463
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11301
11464
|
if (!licenseKey) {
|
|
11302
11465
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11318,7 +11481,6 @@ var session$3 = {
|
|
|
11318
11481
|
},
|
|
11319
11482
|
/**
|
|
11320
11483
|
* Permanently deletes a session (admin action).
|
|
11321
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11322
11484
|
*/
|
|
11323
11485
|
async deleteSession(ctx) {
|
|
11324
11486
|
try {
|
|
@@ -11336,7 +11498,6 @@ var session$3 = {
|
|
|
11336
11498
|
},
|
|
11337
11499
|
/**
|
|
11338
11500
|
* Deletes all inactive sessions (admin action).
|
|
11339
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11340
11501
|
*/
|
|
11341
11502
|
async cleanInactiveSessions(ctx) {
|
|
11342
11503
|
try {
|
|
@@ -11354,9 +11515,6 @@ var session$3 = {
|
|
|
11354
11515
|
},
|
|
11355
11516
|
/**
|
|
11356
11517
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11357
|
-
*
|
|
11358
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11359
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11360
11518
|
*/
|
|
11361
11519
|
async toggleUserBlock(ctx) {
|
|
11362
11520
|
try {
|
|
@@ -11372,11 +11530,11 @@ var session$3 = {
|
|
|
11372
11530
|
}
|
|
11373
11531
|
}
|
|
11374
11532
|
if (!userDocumentId) {
|
|
11375
|
-
return ctx.
|
|
11533
|
+
return ctx.notFound("User not found");
|
|
11376
11534
|
}
|
|
11377
11535
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11378
11536
|
if (!user) {
|
|
11379
|
-
return ctx.
|
|
11537
|
+
return ctx.notFound("User not found");
|
|
11380
11538
|
}
|
|
11381
11539
|
const newBlockedStatus = !user.blocked;
|
|
11382
11540
|
await strapi.documents(USER_UID).update({
|
|
@@ -11385,7 +11543,7 @@ var session$3 = {
|
|
|
11385
11543
|
});
|
|
11386
11544
|
if (newBlockedStatus) {
|
|
11387
11545
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11388
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11546
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11389
11547
|
}
|
|
11390
11548
|
ctx.body = {
|
|
11391
11549
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11877,13 +12035,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11877
12035
|
const { parseUserAgent } = userAgentParser;
|
|
11878
12036
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11879
12037
|
const { enhanceSessions } = enhanceSession_1;
|
|
11880
|
-
const { getPluginSettings: getPluginSettings$
|
|
12038
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11881
12039
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11882
12040
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11883
12041
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11884
12042
|
const log = createLogger$1(strapi2);
|
|
11885
12043
|
async function getEnhanceOpts() {
|
|
11886
|
-
const settings2 = await getPluginSettings$
|
|
12044
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11887
12045
|
return {
|
|
11888
12046
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11889
12047
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11952,15 +12110,38 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11952
12110
|
}
|
|
11953
12111
|
},
|
|
11954
12112
|
/**
|
|
11955
|
-
* Terminates a single session or all sessions of a user
|
|
12113
|
+
* Terminates a single session or all sessions of a user with a typed
|
|
12114
|
+
* reason so the JWT-verify wrapper and downstream middleware can
|
|
12115
|
+
* communicate the cause to the client (and we can show sensible UI).
|
|
12116
|
+
*
|
|
12117
|
+
* Supported reasons:
|
|
12118
|
+
* - 'manual': user clicked logout, or admin terminated a session
|
|
12119
|
+
* - 'idle': inactivity timeout cleanup
|
|
12120
|
+
* - 'expired': maxSessionAgeDays exceeded
|
|
12121
|
+
* - 'blocked': the owning user was marked blocked
|
|
12122
|
+
*
|
|
12123
|
+
* For backwards compatibility `terminatedManually` is still set true
|
|
12124
|
+
* only when reason === 'manual'; idle/expired/blocked paths set it
|
|
12125
|
+
* false so reporting dashboards that queried that boolean continue
|
|
12126
|
+
* to work, while new code relies on `terminationReason`.
|
|
12127
|
+
*
|
|
11956
12128
|
* @param {Object} params
|
|
11957
12129
|
* @param {string} [params.sessionId]
|
|
11958
12130
|
* @param {string|number} [params.userId]
|
|
12131
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
11959
12132
|
* @returns {Promise<void>}
|
|
11960
12133
|
*/
|
|
11961
|
-
async terminateSession({ sessionId, userId }) {
|
|
12134
|
+
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
11962
12135
|
try {
|
|
11963
12136
|
const now = /* @__PURE__ */ new Date();
|
|
12137
|
+
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
12138
|
+
const finalReason = validReasons.includes(reason) ? reason : "manual";
|
|
12139
|
+
const updateData = {
|
|
12140
|
+
isActive: false,
|
|
12141
|
+
terminatedManually: finalReason === "manual",
|
|
12142
|
+
terminationReason: finalReason,
|
|
12143
|
+
logoutTime: now
|
|
12144
|
+
};
|
|
11964
12145
|
if (sessionId) {
|
|
11965
12146
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11966
12147
|
documentId: sessionId,
|
|
@@ -11972,9 +12153,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11972
12153
|
}
|
|
11973
12154
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11974
12155
|
documentId: sessionId,
|
|
11975
|
-
data:
|
|
12156
|
+
data: updateData
|
|
11976
12157
|
});
|
|
11977
|
-
log.info(`Session ${sessionId} terminated (
|
|
12158
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
11978
12159
|
} else if (userId) {
|
|
11979
12160
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11980
12161
|
if (!userDocumentId) return;
|
|
@@ -11986,10 +12167,10 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11986
12167
|
for (const session2 of activeSessions) {
|
|
11987
12168
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11988
12169
|
documentId: session2.documentId,
|
|
11989
|
-
data:
|
|
12170
|
+
data: updateData
|
|
11990
12171
|
});
|
|
11991
12172
|
}
|
|
11992
|
-
log.info(`All sessions terminated
|
|
12173
|
+
log.info(`All sessions terminated for user ${userDocumentId} (reason: ${finalReason})`);
|
|
11993
12174
|
}
|
|
11994
12175
|
} catch (err) {
|
|
11995
12176
|
log.error("Error terminating session:", err);
|
|
@@ -12074,7 +12255,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12074
12255
|
async touch({ userId, sessionId, token }) {
|
|
12075
12256
|
try {
|
|
12076
12257
|
const now = /* @__PURE__ */ new Date();
|
|
12077
|
-
const settings2 = await getPluginSettings$
|
|
12258
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12078
12259
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12079
12260
|
let session2 = null;
|
|
12080
12261
|
let sessionDocId = sessionId;
|
|
@@ -12143,7 +12324,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12143
12324
|
*/
|
|
12144
12325
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12145
12326
|
try {
|
|
12146
|
-
const settings2 = await getPluginSettings$
|
|
12327
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12147
12328
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12148
12329
|
const now = /* @__PURE__ */ new Date();
|
|
12149
12330
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12156,7 +12337,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12156
12337
|
});
|
|
12157
12338
|
}).update({
|
|
12158
12339
|
is_active: false,
|
|
12159
|
-
terminated_manually:
|
|
12340
|
+
terminated_manually: false,
|
|
12341
|
+
termination_reason: "idle",
|
|
12160
12342
|
logout_time: now
|
|
12161
12343
|
});
|
|
12162
12344
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12197,7 +12379,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12197
12379
|
documentId,
|
|
12198
12380
|
data: {
|
|
12199
12381
|
isActive: false,
|
|
12200
|
-
terminatedManually:
|
|
12382
|
+
terminatedManually: false,
|
|
12383
|
+
terminationReason: "idle",
|
|
12201
12384
|
logoutTime: now
|
|
12202
12385
|
}
|
|
12203
12386
|
});
|
|
@@ -12243,7 +12426,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12243
12426
|
*/
|
|
12244
12427
|
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12245
12428
|
try {
|
|
12246
|
-
const settings2 = await getPluginSettings$
|
|
12429
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12247
12430
|
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12248
12431
|
if (effectiveDays === -1) {
|
|
12249
12432
|
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
@@ -12348,7 +12531,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12348
12531
|
}
|
|
12349
12532
|
};
|
|
12350
12533
|
};
|
|
12351
|
-
const version$1 = "4.5.
|
|
12534
|
+
const version$1 = "4.5.2";
|
|
12352
12535
|
const require$$2 = {
|
|
12353
12536
|
version: version$1
|
|
12354
12537
|
};
|
|
@@ -13306,6 +13489,7 @@ var services$1 = {
|
|
|
13306
13489
|
geolocation,
|
|
13307
13490
|
notifications
|
|
13308
13491
|
};
|
|
13492
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13309
13493
|
const buckets = /* @__PURE__ */ new Map();
|
|
13310
13494
|
const prune = (now) => {
|
|
13311
13495
|
for (const [key, entry] of buckets) {
|
|
@@ -13319,10 +13503,45 @@ const callerKey = (ctx) => {
|
|
|
13319
13503
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13320
13504
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13321
13505
|
};
|
|
13506
|
+
const RESOLVED_TTL_MS = 3e4;
|
|
13507
|
+
let resolvedCache = null;
|
|
13508
|
+
let resolvedAt = 0;
|
|
13509
|
+
async function resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) {
|
|
13510
|
+
const now = Date.now();
|
|
13511
|
+
if (resolvedCache && now - resolvedAt < RESOLVED_TTL_MS) {
|
|
13512
|
+
const p = resolvedCache[profile];
|
|
13513
|
+
if (p) {
|
|
13514
|
+
return { max: p.max, windowMs: p.windowMs };
|
|
13515
|
+
}
|
|
13516
|
+
}
|
|
13517
|
+
let settings2 = {};
|
|
13518
|
+
try {
|
|
13519
|
+
settings2 = await getPluginSettings$1(strapi2);
|
|
13520
|
+
} catch {
|
|
13521
|
+
settings2 = {};
|
|
13522
|
+
}
|
|
13523
|
+
const windowSec = Number.isFinite(settings2.rateLimitWindowSeconds) ? settings2.rateLimitWindowSeconds : Math.round(routeWindowMs / 1e3);
|
|
13524
|
+
const windowMs = Math.max(1e4, windowSec * 1e3);
|
|
13525
|
+
const resolvedWrite = {
|
|
13526
|
+
max: Math.min(routeMax, Number.isFinite(settings2.rateLimitWriteMax) ? settings2.rateLimitWriteMax : routeMax),
|
|
13527
|
+
windowMs
|
|
13528
|
+
};
|
|
13529
|
+
const resolvedRead = {
|
|
13530
|
+
max: Math.max(routeMax, Number.isFinite(settings2.rateLimitReadMax) ? settings2.rateLimitReadMax : routeMax),
|
|
13531
|
+
windowMs
|
|
13532
|
+
};
|
|
13533
|
+
resolvedCache = { read: resolvedRead, write: resolvedWrite };
|
|
13534
|
+
resolvedAt = now;
|
|
13535
|
+
if (profile === "read") return resolvedRead;
|
|
13536
|
+
if (profile === "write") return resolvedWrite;
|
|
13537
|
+
return { max: routeMax, windowMs };
|
|
13538
|
+
}
|
|
13322
13539
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13323
|
-
const
|
|
13324
|
-
const
|
|
13540
|
+
const routeMax = Number.isFinite(cfg.max) ? cfg.max : 30;
|
|
13541
|
+
const routeWindowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
|
|
13542
|
+
const profile = cfg.profile === "read" || cfg.profile === "write" ? cfg.profile : null;
|
|
13325
13543
|
return async (ctx, next) => {
|
|
13544
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13326
13545
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13327
13546
|
const now = Date.now();
|
|
13328
13547
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13356,7 +13575,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13356
13575
|
var rateLimit_1 = rateLimit;
|
|
13357
13576
|
var middlewares$1 = {
|
|
13358
13577
|
"last-seen": lastSeen,
|
|
13359
|
-
"rate-limit": rateLimit_1
|
|
13578
|
+
"rate-limit": rateLimit_1,
|
|
13579
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13360
13580
|
};
|
|
13361
13581
|
var lodashExports = requireLodash();
|
|
13362
13582
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51437,18 +51657,18 @@ const CSP_DEFAULTS = {
|
|
|
51437
51657
|
"blob:"
|
|
51438
51658
|
]
|
|
51439
51659
|
};
|
|
51440
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51660
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51441
51661
|
return middlewares2.map((currentMiddleware) => {
|
|
51442
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51443
|
-
return
|
|
51662
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51663
|
+
return middleware2;
|
|
51444
51664
|
}
|
|
51445
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51665
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51446
51666
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51447
51667
|
if (Array.isArray(objValue)) {
|
|
51448
51668
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51449
51669
|
}
|
|
51450
51670
|
return void 0;
|
|
51451
|
-
}, currentMiddleware,
|
|
51671
|
+
}, currentMiddleware, middleware2);
|
|
51452
51672
|
}
|
|
51453
51673
|
return currentMiddleware;
|
|
51454
51674
|
});
|