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.js
CHANGED
|
@@ -174,7 +174,16 @@ var register$1 = async ({ strapi: strapi2 }) => {
|
|
|
174
174
|
try {
|
|
175
175
|
await strapi2.documents(SESSION_UID$6).update({
|
|
176
176
|
documentId: session2.documentId,
|
|
177
|
-
data: {
|
|
177
|
+
data: {
|
|
178
|
+
isActive: false,
|
|
179
|
+
// Blocked users are NOT a "manual" self-logout — mark
|
|
180
|
+
// this distinctly so the client receives a different
|
|
181
|
+
// error message ("account blocked") rather than
|
|
182
|
+
// "session terminated".
|
|
183
|
+
terminatedManually: false,
|
|
184
|
+
terminationReason: "blocked",
|
|
185
|
+
logoutTime: now
|
|
186
|
+
}
|
|
178
187
|
});
|
|
179
188
|
terminated++;
|
|
180
189
|
} catch (updateErr) {
|
|
@@ -304,27 +313,27 @@ function generateSessionId$1(userId) {
|
|
|
304
313
|
const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
|
|
305
314
|
return `sess_${timestamp2}_${userHash}_${randomBytes}`;
|
|
306
315
|
}
|
|
307
|
-
function hashToken$
|
|
316
|
+
function hashToken$6(token) {
|
|
308
317
|
if (!token) return null;
|
|
309
318
|
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
310
319
|
}
|
|
311
320
|
var encryption = {
|
|
312
321
|
encryptToken: encryptToken$2,
|
|
313
322
|
generateSessionId: generateSessionId$1,
|
|
314
|
-
hashToken: hashToken$
|
|
323
|
+
hashToken: hashToken$6
|
|
315
324
|
};
|
|
316
325
|
const USER_UID$1 = "plugin::users-permissions.user";
|
|
317
|
-
const cache = /* @__PURE__ */ new Map();
|
|
326
|
+
const cache$1 = /* @__PURE__ */ new Map();
|
|
318
327
|
const CACHE_TTL = 5 * 60 * 1e3;
|
|
319
328
|
const CACHE_MAX_SIZE = 1e3;
|
|
320
329
|
function evict() {
|
|
321
330
|
const now = Date.now();
|
|
322
|
-
for (const [key, value] of cache) {
|
|
323
|
-
if (now - value.ts >= CACHE_TTL) cache.delete(key);
|
|
331
|
+
for (const [key, value] of cache$1) {
|
|
332
|
+
if (now - value.ts >= CACHE_TTL) cache$1.delete(key);
|
|
324
333
|
}
|
|
325
|
-
if (cache.size >= CACHE_MAX_SIZE) {
|
|
326
|
-
const keysToDelete = [...cache.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
327
|
-
keysToDelete.forEach((k2) => cache.delete(k2));
|
|
334
|
+
if (cache$1.size >= CACHE_MAX_SIZE) {
|
|
335
|
+
const keysToDelete = [...cache$1.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
336
|
+
keysToDelete.forEach((k2) => cache$1.delete(k2));
|
|
328
337
|
}
|
|
329
338
|
}
|
|
330
339
|
async function resolveUserDocumentId$5(strapi2, userId) {
|
|
@@ -334,17 +343,17 @@ async function resolveUserDocumentId$5(strapi2, userId) {
|
|
|
334
343
|
}
|
|
335
344
|
const numericId = typeof userId === "number" ? userId : parseInt(userId, 10);
|
|
336
345
|
const cacheKey = `u_${numericId}`;
|
|
337
|
-
const cached2 = cache.get(cacheKey);
|
|
346
|
+
const cached2 = cache$1.get(cacheKey);
|
|
338
347
|
if (cached2 && Date.now() - cached2.ts < CACHE_TTL) {
|
|
339
348
|
return cached2.documentId;
|
|
340
349
|
}
|
|
341
|
-
if (cache.size >= CACHE_MAX_SIZE) evict();
|
|
350
|
+
if (cache$1.size >= CACHE_MAX_SIZE) evict();
|
|
342
351
|
try {
|
|
343
352
|
const user = await strapi2.entityService.findOne(USER_UID$1, numericId, {
|
|
344
353
|
fields: ["documentId"]
|
|
345
354
|
});
|
|
346
355
|
if (user?.documentId) {
|
|
347
|
-
cache.set(cacheKey, { documentId: user.documentId, ts: Date.now() });
|
|
356
|
+
cache$1.set(cacheKey, { documentId: user.documentId, ts: Date.now() });
|
|
348
357
|
return user.documentId;
|
|
349
358
|
}
|
|
350
359
|
} catch {
|
|
@@ -405,6 +414,15 @@ function normalizeStoredSettings(stored) {
|
|
|
405
414
|
if (stored.sessionCreationGraceMs !== void 0) {
|
|
406
415
|
out.sessionCreationGraceMs = toIntInRange(stored.sessionCreationGraceMs, 5e3, 0, 3e4);
|
|
407
416
|
}
|
|
417
|
+
if (stored.rateLimitWriteMax !== void 0) {
|
|
418
|
+
out.rateLimitWriteMax = toIntInRange(stored.rateLimitWriteMax, 10, 1, 1e3);
|
|
419
|
+
}
|
|
420
|
+
if (stored.rateLimitReadMax !== void 0) {
|
|
421
|
+
out.rateLimitReadMax = toIntInRange(stored.rateLimitReadMax, 120, 1, 1e4);
|
|
422
|
+
}
|
|
423
|
+
if (stored.rateLimitWindowSeconds !== void 0) {
|
|
424
|
+
out.rateLimitWindowSeconds = toIntInRange(stored.rateLimitWindowSeconds, 60, 10, 3600);
|
|
425
|
+
}
|
|
408
426
|
for (const key of passthroughBooleans) {
|
|
409
427
|
if (stored[key] !== void 0) out[key] = !!stored[key];
|
|
410
428
|
}
|
|
@@ -428,7 +446,7 @@ function normalizeStoredSettings(stored) {
|
|
|
428
446
|
}
|
|
429
447
|
return out;
|
|
430
448
|
}
|
|
431
|
-
async function getPluginSettings$
|
|
449
|
+
async function getPluginSettings$6(strapi2) {
|
|
432
450
|
const now = Date.now();
|
|
433
451
|
if (cached$1 && now - cachedAt < CACHE_TTL_MS) {
|
|
434
452
|
return cached$1;
|
|
@@ -452,12 +470,12 @@ function invalidateSettingsCache$1() {
|
|
|
452
470
|
cachedAt = 0;
|
|
453
471
|
}
|
|
454
472
|
var settingsLoader = {
|
|
455
|
-
getPluginSettings: getPluginSettings$
|
|
473
|
+
getPluginSettings: getPluginSettings$6,
|
|
456
474
|
invalidateSettingsCache: invalidateSettingsCache$1
|
|
457
475
|
};
|
|
458
476
|
const MIN_TOKEN_LENGTH = 40;
|
|
459
477
|
const MAX_TOKEN_LENGTH = 8192;
|
|
460
|
-
function extractBearerToken$
|
|
478
|
+
function extractBearerToken$5(ctx) {
|
|
461
479
|
const headers = ctx?.request?.headers || ctx?.request?.header || {};
|
|
462
480
|
const raw = headers.authorization || headers.Authorization;
|
|
463
481
|
if (!raw || typeof raw !== "string") return null;
|
|
@@ -467,9 +485,76 @@ function extractBearerToken$4(ctx) {
|
|
|
467
485
|
if (!token || token.length < MIN_TOKEN_LENGTH || token.length > MAX_TOKEN_LENGTH) return null;
|
|
468
486
|
return token;
|
|
469
487
|
}
|
|
470
|
-
var extractToken = { extractBearerToken: extractBearerToken$
|
|
488
|
+
var extractToken = { extractBearerToken: extractBearerToken$5 };
|
|
489
|
+
const TTL_MS = 60 * 1e3;
|
|
490
|
+
const MAX_ENTRIES = 1e4;
|
|
491
|
+
const cache = /* @__PURE__ */ new Map();
|
|
492
|
+
const prune$1 = () => {
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
for (const [k2, v] of cache) {
|
|
495
|
+
if (v.expiresAt <= now) cache.delete(k2);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
function setSessionRejectionReason$1(tokenHash, reason) {
|
|
499
|
+
if (!tokenHash || !reason) return;
|
|
500
|
+
if (cache.size >= MAX_ENTRIES) prune$1();
|
|
501
|
+
cache.set(tokenHash, { reason, expiresAt: Date.now() + TTL_MS });
|
|
502
|
+
}
|
|
503
|
+
function consumeSessionRejectionReason$2(tokenHash) {
|
|
504
|
+
if (!tokenHash) return null;
|
|
505
|
+
const entry = cache.get(tokenHash);
|
|
506
|
+
if (!entry) return null;
|
|
507
|
+
cache.delete(tokenHash);
|
|
508
|
+
if (entry.expiresAt <= Date.now()) return null;
|
|
509
|
+
return entry.reason;
|
|
510
|
+
}
|
|
511
|
+
var rejectionCache = {
|
|
512
|
+
setSessionRejectionReason: setSessionRejectionReason$1,
|
|
513
|
+
consumeSessionRejectionReason: consumeSessionRejectionReason$2
|
|
514
|
+
};
|
|
515
|
+
const { extractBearerToken: extractBearerToken$4 } = extractToken;
|
|
516
|
+
const { hashToken: hashToken$5 } = encryption;
|
|
517
|
+
const { consumeSessionRejectionReason: consumeSessionRejectionReason$1 } = rejectionCache;
|
|
518
|
+
const HEADER = "X-Session-Terminated-Reason";
|
|
519
|
+
const REASON_MESSAGES = {
|
|
520
|
+
manual: "Your session was terminated. Please log in again.",
|
|
521
|
+
idle: "Your session expired due to inactivity. Please log in again.",
|
|
522
|
+
expired: "Your session has reached its maximum age. Please log in again.",
|
|
523
|
+
blocked: "Your account has been blocked. Contact support."
|
|
524
|
+
};
|
|
525
|
+
const middleware = () => async (ctx, next) => {
|
|
526
|
+
await next();
|
|
527
|
+
if (ctx.status !== 401) return;
|
|
528
|
+
const token = extractBearerToken$4(ctx);
|
|
529
|
+
if (!token) return;
|
|
530
|
+
const reason = consumeSessionRejectionReason$1(hashToken$5(token));
|
|
531
|
+
if (!reason) return;
|
|
532
|
+
ctx.set(HEADER, reason);
|
|
533
|
+
const existing = ctx.body;
|
|
534
|
+
const friendlyMessage = REASON_MESSAGES[reason] || "Session invalid. Please log in again.";
|
|
535
|
+
if (existing && typeof existing === "object" && existing.error) {
|
|
536
|
+
existing.error.details = existing.error.details || {};
|
|
537
|
+
if (!existing.error.details.reason) {
|
|
538
|
+
existing.error.details.reason = reason;
|
|
539
|
+
}
|
|
540
|
+
if (!existing.error.message || existing.error.message === "Unauthorized") {
|
|
541
|
+
existing.error.message = friendlyMessage;
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
ctx.body = {
|
|
546
|
+
data: null,
|
|
547
|
+
error: {
|
|
548
|
+
status: 401,
|
|
549
|
+
name: "UnauthorizedError",
|
|
550
|
+
message: friendlyMessage,
|
|
551
|
+
details: { reason }
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
};
|
|
555
|
+
var sessionRejectionHeaders = middleware;
|
|
471
556
|
const { resolveUserDocumentId: resolveUserDocumentId$4 } = resolveUser;
|
|
472
|
-
const { getPluginSettings: getPluginSettings$
|
|
557
|
+
const { getPluginSettings: getPluginSettings$5 } = settingsLoader;
|
|
473
558
|
const { extractBearerToken: extractBearerToken$3 } = extractToken;
|
|
474
559
|
const { hashToken: hashToken$4 } = encryption;
|
|
475
560
|
const SESSION_UID$5 = "plugin::magic-sessionmanager.session";
|
|
@@ -490,65 +575,75 @@ function isAuthEndpoint(path2) {
|
|
|
490
575
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
491
576
|
return async (ctx, next) => {
|
|
492
577
|
if (isAuthEndpoint(ctx.path)) {
|
|
493
|
-
|
|
494
|
-
return;
|
|
578
|
+
return next();
|
|
495
579
|
}
|
|
496
|
-
if (ctx.state.user) {
|
|
580
|
+
if (!ctx.state.user) {
|
|
581
|
+
return next();
|
|
582
|
+
}
|
|
583
|
+
let userDocId = ctx.state.user.documentId;
|
|
584
|
+
if (!userDocId && ctx.state.user.id) {
|
|
497
585
|
try {
|
|
498
|
-
|
|
499
|
-
if (!userDocId2 && ctx.state.user.id) {
|
|
500
|
-
userDocId2 = await resolveUserDocumentId$4(strapi2, ctx.state.user.id);
|
|
501
|
-
}
|
|
502
|
-
if (userDocId2) {
|
|
503
|
-
const settings2 = await getPluginSettings$4(strapi2);
|
|
504
|
-
const strictMode = settings2.strictSessionEnforcement === true;
|
|
505
|
-
const token = extractBearerToken$3(ctx);
|
|
506
|
-
const tokenHashValue = token ? hashToken$4(token) : null;
|
|
507
|
-
const thisSession = tokenHashValue ? await strapi2.documents(SESSION_UID$5).findFirst({
|
|
508
|
-
filters: { user: { documentId: userDocId2 }, tokenHash: tokenHashValue },
|
|
509
|
-
fields: ["documentId", "isActive", "terminatedManually"]
|
|
510
|
-
}) : null;
|
|
511
|
-
if (thisSession) {
|
|
512
|
-
if (thisSession.terminatedManually === true) {
|
|
513
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session was manually terminated (user: ${userDocId2.substring(0, 8)}...)`);
|
|
514
|
-
return ctx.unauthorized("Session terminated. Please login again.");
|
|
515
|
-
}
|
|
516
|
-
ctx.state.userDocumentId = userDocId2;
|
|
517
|
-
ctx.state.__magicSessionId = thisSession.documentId;
|
|
518
|
-
await next();
|
|
519
|
-
if (thisSession.isActive) {
|
|
520
|
-
try {
|
|
521
|
-
await sessionService.touch({
|
|
522
|
-
userId: userDocId2,
|
|
523
|
-
sessionId: thisSession.documentId
|
|
524
|
-
});
|
|
525
|
-
} catch (err) {
|
|
526
|
-
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
if (strictMode) {
|
|
532
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId2.substring(0, 8)}..., strictMode)`);
|
|
533
|
-
return ctx.unauthorized("No valid session. Please login again.");
|
|
534
|
-
}
|
|
535
|
-
strapi2.log.warn(`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId2.substring(0, 8)}...) - allowing in non-strict mode`);
|
|
536
|
-
ctx.state.userDocumentId = userDocId2;
|
|
537
|
-
}
|
|
586
|
+
userDocId = await resolveUserDocumentId$4(strapi2, ctx.state.user.id);
|
|
538
587
|
} catch (err) {
|
|
539
|
-
strapi2.log.debug("[magic-sessionmanager]
|
|
588
|
+
strapi2.log.debug("[magic-sessionmanager] user doc-id lookup failed:", err.message);
|
|
589
|
+
return next();
|
|
540
590
|
}
|
|
541
591
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
592
|
+
if (!userDocId) {
|
|
593
|
+
return next();
|
|
594
|
+
}
|
|
595
|
+
const settings2 = await getPluginSettings$5(strapi2).catch(() => ({}));
|
|
596
|
+
const strictMode = settings2.strictSessionEnforcement === true;
|
|
597
|
+
const gracePeriodMs = Math.max(0, Number(settings2.sessionCreationGraceMs) || 5e3);
|
|
598
|
+
const token = extractBearerToken$3(ctx);
|
|
599
|
+
const tokenHashValue = token ? hashToken$4(token) : null;
|
|
600
|
+
let thisSession = null;
|
|
601
|
+
if (tokenHashValue) {
|
|
546
602
|
try {
|
|
547
|
-
await
|
|
603
|
+
thisSession = await strapi2.documents(SESSION_UID$5).findFirst({
|
|
604
|
+
filters: { user: { documentId: userDocId }, tokenHash: tokenHashValue },
|
|
605
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason"]
|
|
606
|
+
});
|
|
607
|
+
} catch (err) {
|
|
608
|
+
strapi2.log.debug("[magic-sessionmanager] session lookup failed:", err.message);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (thisSession) {
|
|
612
|
+
if (thisSession.isActive === false) {
|
|
613
|
+
return ctx.unauthorized("Session terminated. Please login again.");
|
|
614
|
+
}
|
|
615
|
+
ctx.state.userDocumentId = userDocId;
|
|
616
|
+
ctx.state.__magicSessionId = thisSession.documentId;
|
|
617
|
+
await next();
|
|
618
|
+
try {
|
|
619
|
+
await sessionService.touch({
|
|
620
|
+
userId: userDocId,
|
|
621
|
+
sessionId: thisSession.documentId
|
|
622
|
+
});
|
|
548
623
|
} catch (err) {
|
|
549
624
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
550
625
|
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (strictMode) {
|
|
629
|
+
const iat = ctx.state.user?.iat;
|
|
630
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
631
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
632
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
633
|
+
ctx.state.userDocumentId = userDocId;
|
|
634
|
+
return next();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
strapi2.log.info(
|
|
638
|
+
`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId.substring(0, 8)}..., strictMode)`
|
|
639
|
+
);
|
|
640
|
+
return ctx.unauthorized("No valid session. Please login again.");
|
|
551
641
|
}
|
|
642
|
+
strapi2.log.debug(
|
|
643
|
+
`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId.substring(0, 8)}...) - allowing in non-strict mode`
|
|
644
|
+
);
|
|
645
|
+
ctx.state.userDocumentId = userDocId;
|
|
646
|
+
return next();
|
|
552
647
|
};
|
|
553
648
|
};
|
|
554
649
|
var jsonwebtoken = { exports: {} };
|
|
@@ -9515,8 +9610,12 @@ const getClientIp = getClientIp_1;
|
|
|
9515
9610
|
const { encryptToken: encryptToken$1, hashToken: hashToken$3 } = encryption;
|
|
9516
9611
|
const { createLogger: createLogger$3 } = logger;
|
|
9517
9612
|
const { resolveUserDocumentId: resolveUserDocumentId$3 } = resolveUser;
|
|
9518
|
-
const { getPluginSettings: getPluginSettings$
|
|
9613
|
+
const { getPluginSettings: getPluginSettings$4 } = settingsLoader;
|
|
9519
9614
|
const { extractBearerToken: extractBearerToken$2 } = extractToken;
|
|
9615
|
+
const {
|
|
9616
|
+
setSessionRejectionReason,
|
|
9617
|
+
consumeSessionRejectionReason
|
|
9618
|
+
} = rejectionCache;
|
|
9520
9619
|
const SESSION_UID$4 = "plugin::magic-sessionmanager.session";
|
|
9521
9620
|
const JWT_WRAPPED_FLAG = Symbol.for("magic-sessionmanager.jwt.wrapped");
|
|
9522
9621
|
const LOGIN_PATHS = /* @__PURE__ */ new Set([
|
|
@@ -9589,7 +9688,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9589
9688
|
}
|
|
9590
9689
|
log.info("Running initial session cleanup...");
|
|
9591
9690
|
try {
|
|
9592
|
-
const settings2 = await getPluginSettings$
|
|
9691
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9593
9692
|
await sessionService.cleanupInactiveSessions({
|
|
9594
9693
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9595
9694
|
});
|
|
@@ -9600,7 +9699,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9600
9699
|
let intervalMs = 30 * 60 * 1e3;
|
|
9601
9700
|
let useDbDirect = false;
|
|
9602
9701
|
try {
|
|
9603
|
-
const settings2 = await getPluginSettings$
|
|
9702
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9604
9703
|
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9605
9704
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9606
9705
|
} catch {
|
|
@@ -9621,7 +9720,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9621
9720
|
const scheduleRetention = async () => {
|
|
9622
9721
|
let useDbDirect = false;
|
|
9623
9722
|
try {
|
|
9624
|
-
const settings2 = await getPluginSettings$
|
|
9723
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9625
9724
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9626
9725
|
} catch {
|
|
9627
9726
|
}
|
|
@@ -9645,6 +9744,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9645
9744
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9646
9745
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9647
9746
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9747
|
+
strapi2.server.use(
|
|
9748
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9749
|
+
);
|
|
9750
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9648
9751
|
strapi2.server.use(
|
|
9649
9752
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9650
9753
|
);
|
|
@@ -9663,7 +9766,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9663
9766
|
}
|
|
9664
9767
|
let settings2 = {};
|
|
9665
9768
|
try {
|
|
9666
|
-
settings2 = await getPluginSettings$
|
|
9769
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9667
9770
|
} catch {
|
|
9668
9771
|
settings2 = {};
|
|
9669
9772
|
}
|
|
@@ -9839,7 +9942,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9839
9942
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9840
9943
|
let maxFailed = 0;
|
|
9841
9944
|
try {
|
|
9842
|
-
const settings2 = await getPluginSettings$
|
|
9945
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9843
9946
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9844
9947
|
} catch {
|
|
9845
9948
|
maxFailed = 0;
|
|
@@ -9918,7 +10021,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9918
10021
|
}
|
|
9919
10022
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9920
10023
|
try {
|
|
9921
|
-
const settings2 = await getPluginSettings$
|
|
10024
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9922
10025
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9923
10026
|
return;
|
|
9924
10027
|
}
|
|
@@ -10235,7 +10338,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10235
10338
|
}
|
|
10236
10339
|
let settings2;
|
|
10237
10340
|
try {
|
|
10238
|
-
settings2 = await getPluginSettings$
|
|
10341
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10239
10342
|
} catch {
|
|
10240
10343
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10241
10344
|
}
|
|
@@ -10260,6 +10363,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10260
10363
|
strapi2.log.info(
|
|
10261
10364
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10262
10365
|
);
|
|
10366
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10263
10367
|
return null;
|
|
10264
10368
|
}
|
|
10265
10369
|
} catch {
|
|
@@ -10270,7 +10374,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10270
10374
|
user: { documentId: userDocId },
|
|
10271
10375
|
tokenHash: tokenHashValue
|
|
10272
10376
|
},
|
|
10273
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10377
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10274
10378
|
});
|
|
10275
10379
|
if (thisSession) {
|
|
10276
10380
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10279,15 +10383,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10279
10383
|
);
|
|
10280
10384
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10281
10385
|
documentId: thisSession.documentId,
|
|
10282
|
-
data: {
|
|
10386
|
+
data: {
|
|
10387
|
+
isActive: false,
|
|
10388
|
+
terminatedManually: false,
|
|
10389
|
+
terminationReason: "expired",
|
|
10390
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10391
|
+
}
|
|
10283
10392
|
});
|
|
10393
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10284
10394
|
return null;
|
|
10285
10395
|
}
|
|
10286
|
-
if (thisSession.
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10396
|
+
if (thisSession.isActive === false) {
|
|
10397
|
+
const reason = thisSession.terminationReason || (thisSession.terminatedManually === true ? "manual" : null);
|
|
10398
|
+
if (reason) {
|
|
10399
|
+
strapi2.log.info(
|
|
10400
|
+
`[magic-sessionmanager] [JWT-REJECTED] Session inactive (reason: ${reason}) for user ${userDocId.substring(0, 8)}...`
|
|
10401
|
+
);
|
|
10402
|
+
setSessionRejectionReason(tokenHashValue, reason);
|
|
10403
|
+
return null;
|
|
10404
|
+
}
|
|
10291
10405
|
}
|
|
10292
10406
|
if (thisSession.isActive) {
|
|
10293
10407
|
resetErrorCounter();
|
|
@@ -10302,8 +10416,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10302
10416
|
);
|
|
10303
10417
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10304
10418
|
documentId: thisSession.documentId,
|
|
10305
|
-
data: {
|
|
10419
|
+
data: {
|
|
10420
|
+
terminatedManually: false,
|
|
10421
|
+
terminationReason: "idle",
|
|
10422
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10423
|
+
}
|
|
10306
10424
|
});
|
|
10425
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10307
10426
|
return null;
|
|
10308
10427
|
}
|
|
10309
10428
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10505,6 +10624,16 @@ const attributes = {
|
|
|
10505
10624
|
"default": false,
|
|
10506
10625
|
required: false
|
|
10507
10626
|
},
|
|
10627
|
+
terminationReason: {
|
|
10628
|
+
type: "enumeration",
|
|
10629
|
+
"enum": [
|
|
10630
|
+
"manual",
|
|
10631
|
+
"idle",
|
|
10632
|
+
"expired",
|
|
10633
|
+
"blocked"
|
|
10634
|
+
],
|
|
10635
|
+
required: false
|
|
10636
|
+
},
|
|
10508
10637
|
geoLocation: {
|
|
10509
10638
|
type: "json"
|
|
10510
10639
|
},
|
|
@@ -10538,10 +10667,16 @@ var contentTypes$2 = {
|
|
|
10538
10667
|
}
|
|
10539
10668
|
};
|
|
10540
10669
|
const writeRateLimit = [
|
|
10541
|
-
{
|
|
10670
|
+
{
|
|
10671
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10672
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10673
|
+
}
|
|
10542
10674
|
];
|
|
10543
10675
|
const readRateLimit = [
|
|
10544
|
-
{
|
|
10676
|
+
{
|
|
10677
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10678
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10679
|
+
}
|
|
10545
10680
|
];
|
|
10546
10681
|
var contentApi$1 = {
|
|
10547
10682
|
type: "content-api",
|
|
@@ -10944,15 +11079,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10944
11079
|
const { hashToken: hashToken$2 } = encryption;
|
|
10945
11080
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10946
11081
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10947
|
-
const { getPluginSettings: getPluginSettings$
|
|
11082
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10948
11083
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10949
11084
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10950
11085
|
const USER_UID = "plugin::users-permissions.user";
|
|
11086
|
+
const OWN_SESSIONS_LIMIT = 200;
|
|
11087
|
+
async function resolveAuthUserDocId(ctx) {
|
|
11088
|
+
const u2 = ctx.state.user;
|
|
11089
|
+
if (!u2) return null;
|
|
11090
|
+
if (u2.documentId) return u2.documentId;
|
|
11091
|
+
if (u2.id) return resolveUserDocumentId$2(strapi, u2.id);
|
|
11092
|
+
return null;
|
|
11093
|
+
}
|
|
10951
11094
|
var session$3 = {
|
|
10952
11095
|
/**
|
|
10953
11096
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10954
11097
|
* @route GET /magic-sessionmanager/sessions
|
|
10955
|
-
* @returns {object} `{ data, meta }`
|
|
10956
11098
|
*/
|
|
10957
11099
|
async getAllSessionsAdmin(ctx) {
|
|
10958
11100
|
try {
|
|
@@ -10974,7 +11116,6 @@ var session$3 = {
|
|
|
10974
11116
|
/**
|
|
10975
11117
|
* Lists currently-active sessions only.
|
|
10976
11118
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10977
|
-
* @returns {object} `{ data, meta }`
|
|
10978
11119
|
*/
|
|
10979
11120
|
async getActiveSessions(ctx) {
|
|
10980
11121
|
try {
|
|
@@ -10990,35 +11131,33 @@ var session$3 = {
|
|
|
10990
11131
|
}
|
|
10991
11132
|
},
|
|
10992
11133
|
/**
|
|
10993
|
-
* Returns the authenticated user's own sessions,
|
|
10994
|
-
* flagged via `isCurrentSession`.
|
|
10995
|
-
*
|
|
11134
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10996
11135
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10997
|
-
* @returns {object} `{ data, meta }`
|
|
10998
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10999
11136
|
*/
|
|
11000
11137
|
async getOwnSessions(ctx) {
|
|
11001
11138
|
try {
|
|
11002
|
-
const
|
|
11139
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11140
|
+
if (!userDocId) {
|
|
11141
|
+
return ctx.unauthorized("Authentication required");
|
|
11142
|
+
}
|
|
11003
11143
|
const currentToken = extractBearerToken$1(ctx);
|
|
11004
11144
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11005
|
-
if (!userId) {
|
|
11006
|
-
return ctx.throw(401, "Unauthorized");
|
|
11007
|
-
}
|
|
11008
11145
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
11009
|
-
filters: { user: { documentId:
|
|
11146
|
+
filters: { user: { documentId: userDocId } },
|
|
11010
11147
|
sort: { loginTime: "desc" },
|
|
11011
|
-
limit:
|
|
11148
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
11012
11149
|
});
|
|
11013
|
-
const
|
|
11150
|
+
const hasMore = allSessions.length > OWN_SESSIONS_LIMIT;
|
|
11151
|
+
const paged = hasMore ? allSessions.slice(0, OWN_SESSIONS_LIMIT) : allSessions;
|
|
11152
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11014
11153
|
const enhanceOpts = {
|
|
11015
11154
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11016
11155
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
11017
11156
|
strapi
|
|
11018
11157
|
};
|
|
11019
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11158
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
11020
11159
|
for (const s3 of sessionsWithCurrent) {
|
|
11021
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11160
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
11022
11161
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
11023
11162
|
));
|
|
11024
11163
|
}
|
|
@@ -11031,7 +11170,9 @@ var session$3 = {
|
|
|
11031
11170
|
data: sessionsWithCurrent,
|
|
11032
11171
|
meta: {
|
|
11033
11172
|
count: sessionsWithCurrent.length,
|
|
11034
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11173
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11174
|
+
hasMore,
|
|
11175
|
+
limit: OWN_SESSIONS_LIMIT
|
|
11035
11176
|
}
|
|
11036
11177
|
};
|
|
11037
11178
|
} catch (err) {
|
|
@@ -11040,18 +11181,14 @@ var session$3 = {
|
|
|
11040
11181
|
}
|
|
11041
11182
|
},
|
|
11042
11183
|
/**
|
|
11043
|
-
* Get a specific user's sessions. Admins
|
|
11184
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11044
11185
|
* users can only query themselves.
|
|
11045
|
-
*
|
|
11046
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11047
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11048
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11049
11186
|
*/
|
|
11050
11187
|
async getUserSessions(ctx) {
|
|
11051
11188
|
try {
|
|
11052
11189
|
const { userId } = ctx.params;
|
|
11053
11190
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11054
|
-
const requestingUserDocId = ctx
|
|
11191
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11055
11192
|
if (!isAdminRequest) {
|
|
11056
11193
|
if (!requestingUserDocId) {
|
|
11057
11194
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11080,26 +11217,34 @@ var session$3 = {
|
|
|
11080
11217
|
*/
|
|
11081
11218
|
async logout(ctx) {
|
|
11082
11219
|
try {
|
|
11083
|
-
const
|
|
11220
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11084
11221
|
const token = extractBearerToken$1(ctx);
|
|
11085
|
-
if (!
|
|
11086
|
-
return ctx.
|
|
11222
|
+
if (!userDocId || !token) {
|
|
11223
|
+
return ctx.unauthorized("Authentication required");
|
|
11087
11224
|
}
|
|
11088
11225
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11089
11226
|
const currentTokenHash = hashToken$2(token);
|
|
11090
11227
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11091
11228
|
filters: {
|
|
11092
|
-
user: { documentId:
|
|
11229
|
+
user: { documentId: userDocId },
|
|
11093
11230
|
tokenHash: currentTokenHash,
|
|
11094
11231
|
isActive: true
|
|
11095
11232
|
},
|
|
11096
11233
|
fields: ["documentId"]
|
|
11097
11234
|
});
|
|
11235
|
+
let terminated = false;
|
|
11098
11236
|
if (matchingSession) {
|
|
11099
|
-
await sessionService.terminateSession({
|
|
11100
|
-
|
|
11237
|
+
await sessionService.terminateSession({
|
|
11238
|
+
sessionId: matchingSession.documentId,
|
|
11239
|
+
reason: "manual"
|
|
11240
|
+
});
|
|
11241
|
+
terminated = true;
|
|
11242
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out (session ${matchingSession.documentId})`);
|
|
11101
11243
|
}
|
|
11102
|
-
ctx.body = {
|
|
11244
|
+
ctx.body = {
|
|
11245
|
+
message: "Logged out successfully",
|
|
11246
|
+
terminated
|
|
11247
|
+
};
|
|
11103
11248
|
} catch (err) {
|
|
11104
11249
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11105
11250
|
ctx.throw(500, "Error during logout");
|
|
@@ -11111,13 +11256,13 @@ var session$3 = {
|
|
|
11111
11256
|
*/
|
|
11112
11257
|
async logoutAll(ctx) {
|
|
11113
11258
|
try {
|
|
11114
|
-
const
|
|
11115
|
-
if (!
|
|
11116
|
-
return ctx.
|
|
11259
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11260
|
+
if (!userDocId) {
|
|
11261
|
+
return ctx.unauthorized("Authentication required");
|
|
11117
11262
|
}
|
|
11118
11263
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11119
|
-
await sessionService.terminateSession({ userId });
|
|
11120
|
-
strapi.log.info(`[magic-sessionmanager] User ${
|
|
11264
|
+
await sessionService.terminateSession({ userId: userDocId, reason: "manual" });
|
|
11265
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out from all devices`);
|
|
11121
11266
|
ctx.body = { message: "Logged out from all devices successfully" };
|
|
11122
11267
|
} catch (err) {
|
|
11123
11268
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
@@ -11126,27 +11271,48 @@ var session$3 = {
|
|
|
11126
11271
|
},
|
|
11127
11272
|
/**
|
|
11128
11273
|
* Returns the session associated with the current JWT.
|
|
11274
|
+
*
|
|
11275
|
+
* During the post-login grace window the session-create write may not
|
|
11276
|
+
* yet be visible. In that case we return 202 Accepted with
|
|
11277
|
+
* `{ pending: true }` so the client knows to retry shortly instead of
|
|
11278
|
+
* interpreting a 404 as "no session at all".
|
|
11279
|
+
*
|
|
11129
11280
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11130
11281
|
*/
|
|
11131
11282
|
async getCurrentSession(ctx) {
|
|
11132
11283
|
try {
|
|
11133
|
-
const
|
|
11284
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11134
11285
|
const token = extractBearerToken$1(ctx);
|
|
11135
|
-
if (!
|
|
11136
|
-
return ctx.
|
|
11286
|
+
if (!userDocId || !token) {
|
|
11287
|
+
return ctx.unauthorized("Authentication required");
|
|
11137
11288
|
}
|
|
11138
11289
|
const currentTokenHash = hashToken$2(token);
|
|
11139
11290
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11140
11291
|
filters: {
|
|
11141
|
-
user: { documentId:
|
|
11292
|
+
user: { documentId: userDocId },
|
|
11142
11293
|
tokenHash: currentTokenHash,
|
|
11143
11294
|
isActive: true
|
|
11144
11295
|
}
|
|
11145
11296
|
});
|
|
11146
11297
|
if (!currentSession) {
|
|
11298
|
+
const settings3 = await getPluginSettings$3(strapi);
|
|
11299
|
+
const gracePeriodMs = Math.max(0, Number(settings3.sessionCreationGraceMs) || 5e3);
|
|
11300
|
+
const iat = ctx.state.user?.iat || ctx.state.auth?.credentials?.iat || null;
|
|
11301
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
11302
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
11303
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
11304
|
+
ctx.status = 202;
|
|
11305
|
+
ctx.body = {
|
|
11306
|
+
data: null,
|
|
11307
|
+
meta: { pending: true, retryAfterMs: gracePeriodMs - ageMs },
|
|
11308
|
+
message: "Session is still being created — please retry shortly."
|
|
11309
|
+
};
|
|
11310
|
+
return;
|
|
11311
|
+
}
|
|
11312
|
+
}
|
|
11147
11313
|
return ctx.notFound("Current session not found");
|
|
11148
11314
|
}
|
|
11149
|
-
const settings2 = await getPluginSettings$
|
|
11315
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11150
11316
|
const enhanced = await enhanceSession(currentSession, {
|
|
11151
11317
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11152
11318
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11162,17 +11328,17 @@ var session$3 = {
|
|
|
11162
11328
|
}
|
|
11163
11329
|
},
|
|
11164
11330
|
/**
|
|
11165
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11331
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11166
11332
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11167
11333
|
*/
|
|
11168
11334
|
async terminateOwnSession(ctx) {
|
|
11169
11335
|
try {
|
|
11170
|
-
const
|
|
11336
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11171
11337
|
const { sessionId } = ctx.params;
|
|
11172
11338
|
const currentToken = extractBearerToken$1(ctx);
|
|
11173
11339
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11174
|
-
if (!
|
|
11175
|
-
return ctx.
|
|
11340
|
+
if (!userDocId) {
|
|
11341
|
+
return ctx.unauthorized("Authentication required");
|
|
11176
11342
|
}
|
|
11177
11343
|
if (!sessionId) {
|
|
11178
11344
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11185,19 +11351,23 @@ var session$3 = {
|
|
|
11185
11351
|
return ctx.notFound("Session not found");
|
|
11186
11352
|
}
|
|
11187
11353
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11188
|
-
if (sessionUserId !==
|
|
11189
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
11354
|
+
if (sessionUserId !== userDocId) {
|
|
11355
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userDocId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
11190
11356
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11191
11357
|
}
|
|
11192
11358
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11193
|
-
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
11359
|
+
return ctx.badRequest("Cannot terminate the current session. Use /logout instead.");
|
|
11360
|
+
}
|
|
11361
|
+
const alreadyTerminated = sessionToTerminate.isActive === false;
|
|
11362
|
+
if (!alreadyTerminated) {
|
|
11363
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11364
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11365
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} terminated own session ${sessionId}`);
|
|
11194
11366
|
}
|
|
11195
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11196
|
-
await sessionService.terminateSession({ sessionId });
|
|
11197
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11198
11367
|
ctx.body = {
|
|
11199
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11200
|
-
success: true
|
|
11368
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11369
|
+
success: true,
|
|
11370
|
+
alreadyTerminated
|
|
11201
11371
|
};
|
|
11202
11372
|
} catch (err) {
|
|
11203
11373
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11205,9 +11375,7 @@ var session$3 = {
|
|
|
11205
11375
|
}
|
|
11206
11376
|
},
|
|
11207
11377
|
/**
|
|
11208
|
-
*
|
|
11209
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11210
|
-
*
|
|
11378
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11211
11379
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11212
11380
|
*/
|
|
11213
11381
|
async simulateTimeout(ctx) {
|
|
@@ -11226,13 +11394,16 @@ var session$3 = {
|
|
|
11226
11394
|
}
|
|
11227
11395
|
await strapi.documents(SESSION_UID$2).update({
|
|
11228
11396
|
documentId: sessionId,
|
|
11229
|
-
data: {
|
|
11397
|
+
data: {
|
|
11398
|
+
isActive: false,
|
|
11399
|
+
terminatedManually: false,
|
|
11400
|
+
terminationReason: "idle"
|
|
11401
|
+
}
|
|
11230
11402
|
});
|
|
11231
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11403
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11232
11404
|
ctx.body = {
|
|
11233
|
-
message: `Session ${sessionId} marked as timed out
|
|
11234
|
-
success: true
|
|
11235
|
-
terminatedManually: false
|
|
11405
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11406
|
+
success: true
|
|
11236
11407
|
};
|
|
11237
11408
|
} catch (err) {
|
|
11238
11409
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11241,13 +11412,12 @@ var session$3 = {
|
|
|
11241
11412
|
},
|
|
11242
11413
|
/**
|
|
11243
11414
|
* Terminates a specific session (admin action).
|
|
11244
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11245
11415
|
*/
|
|
11246
11416
|
async terminateSingleSession(ctx) {
|
|
11247
11417
|
try {
|
|
11248
11418
|
const { sessionId } = ctx.params;
|
|
11249
11419
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11250
|
-
await sessionService.terminateSession({ sessionId });
|
|
11420
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11251
11421
|
ctx.body = {
|
|
11252
11422
|
message: `Session ${sessionId} terminated`,
|
|
11253
11423
|
success: true
|
|
@@ -11259,13 +11429,12 @@ var session$3 = {
|
|
|
11259
11429
|
},
|
|
11260
11430
|
/**
|
|
11261
11431
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11262
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11263
11432
|
*/
|
|
11264
11433
|
async terminateAllUserSessions(ctx) {
|
|
11265
11434
|
try {
|
|
11266
11435
|
const { userId } = ctx.params;
|
|
11267
11436
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11268
|
-
await sessionService.terminateSession({ userId });
|
|
11437
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11269
11438
|
ctx.body = {
|
|
11270
11439
|
message: `All sessions terminated for user ${userId}`,
|
|
11271
11440
|
success: true
|
|
@@ -11277,9 +11446,6 @@ var session$3 = {
|
|
|
11277
11446
|
},
|
|
11278
11447
|
/**
|
|
11279
11448
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11280
|
-
*
|
|
11281
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11282
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11283
11449
|
*/
|
|
11284
11450
|
async getIpGeolocation(ctx) {
|
|
11285
11451
|
try {
|
|
@@ -11306,10 +11472,7 @@ var session$3 = {
|
|
|
11306
11472
|
return ctx.badRequest("Invalid IP address format");
|
|
11307
11473
|
}
|
|
11308
11474
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11309
|
-
const pluginStore = strapi.store({
|
|
11310
|
-
type: "plugin",
|
|
11311
|
-
name: "magic-sessionmanager"
|
|
11312
|
-
});
|
|
11475
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11313
11476
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11314
11477
|
if (!licenseKey) {
|
|
11315
11478
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11331,7 +11494,6 @@ var session$3 = {
|
|
|
11331
11494
|
},
|
|
11332
11495
|
/**
|
|
11333
11496
|
* Permanently deletes a session (admin action).
|
|
11334
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11335
11497
|
*/
|
|
11336
11498
|
async deleteSession(ctx) {
|
|
11337
11499
|
try {
|
|
@@ -11349,7 +11511,6 @@ var session$3 = {
|
|
|
11349
11511
|
},
|
|
11350
11512
|
/**
|
|
11351
11513
|
* Deletes all inactive sessions (admin action).
|
|
11352
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11353
11514
|
*/
|
|
11354
11515
|
async cleanInactiveSessions(ctx) {
|
|
11355
11516
|
try {
|
|
@@ -11367,9 +11528,6 @@ var session$3 = {
|
|
|
11367
11528
|
},
|
|
11368
11529
|
/**
|
|
11369
11530
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11370
|
-
*
|
|
11371
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11372
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11373
11531
|
*/
|
|
11374
11532
|
async toggleUserBlock(ctx) {
|
|
11375
11533
|
try {
|
|
@@ -11385,11 +11543,11 @@ var session$3 = {
|
|
|
11385
11543
|
}
|
|
11386
11544
|
}
|
|
11387
11545
|
if (!userDocumentId) {
|
|
11388
|
-
return ctx.
|
|
11546
|
+
return ctx.notFound("User not found");
|
|
11389
11547
|
}
|
|
11390
11548
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11391
11549
|
if (!user) {
|
|
11392
|
-
return ctx.
|
|
11550
|
+
return ctx.notFound("User not found");
|
|
11393
11551
|
}
|
|
11394
11552
|
const newBlockedStatus = !user.blocked;
|
|
11395
11553
|
await strapi.documents(USER_UID).update({
|
|
@@ -11398,7 +11556,7 @@ var session$3 = {
|
|
|
11398
11556
|
});
|
|
11399
11557
|
if (newBlockedStatus) {
|
|
11400
11558
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11401
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11559
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11402
11560
|
}
|
|
11403
11561
|
ctx.body = {
|
|
11404
11562
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11890,13 +12048,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11890
12048
|
const { parseUserAgent } = userAgentParser;
|
|
11891
12049
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11892
12050
|
const { enhanceSessions } = enhanceSession_1;
|
|
11893
|
-
const { getPluginSettings: getPluginSettings$
|
|
12051
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11894
12052
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11895
12053
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11896
12054
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11897
12055
|
const log = createLogger$1(strapi2);
|
|
11898
12056
|
async function getEnhanceOpts() {
|
|
11899
|
-
const settings2 = await getPluginSettings$
|
|
12057
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11900
12058
|
return {
|
|
11901
12059
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11902
12060
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11965,15 +12123,38 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11965
12123
|
}
|
|
11966
12124
|
},
|
|
11967
12125
|
/**
|
|
11968
|
-
* Terminates a single session or all sessions of a user
|
|
12126
|
+
* Terminates a single session or all sessions of a user with a typed
|
|
12127
|
+
* reason so the JWT-verify wrapper and downstream middleware can
|
|
12128
|
+
* communicate the cause to the client (and we can show sensible UI).
|
|
12129
|
+
*
|
|
12130
|
+
* Supported reasons:
|
|
12131
|
+
* - 'manual': user clicked logout, or admin terminated a session
|
|
12132
|
+
* - 'idle': inactivity timeout cleanup
|
|
12133
|
+
* - 'expired': maxSessionAgeDays exceeded
|
|
12134
|
+
* - 'blocked': the owning user was marked blocked
|
|
12135
|
+
*
|
|
12136
|
+
* For backwards compatibility `terminatedManually` is still set true
|
|
12137
|
+
* only when reason === 'manual'; idle/expired/blocked paths set it
|
|
12138
|
+
* false so reporting dashboards that queried that boolean continue
|
|
12139
|
+
* to work, while new code relies on `terminationReason`.
|
|
12140
|
+
*
|
|
11969
12141
|
* @param {Object} params
|
|
11970
12142
|
* @param {string} [params.sessionId]
|
|
11971
12143
|
* @param {string|number} [params.userId]
|
|
12144
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
11972
12145
|
* @returns {Promise<void>}
|
|
11973
12146
|
*/
|
|
11974
|
-
async terminateSession({ sessionId, userId }) {
|
|
12147
|
+
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
11975
12148
|
try {
|
|
11976
12149
|
const now = /* @__PURE__ */ new Date();
|
|
12150
|
+
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
12151
|
+
const finalReason = validReasons.includes(reason) ? reason : "manual";
|
|
12152
|
+
const updateData = {
|
|
12153
|
+
isActive: false,
|
|
12154
|
+
terminatedManually: finalReason === "manual",
|
|
12155
|
+
terminationReason: finalReason,
|
|
12156
|
+
logoutTime: now
|
|
12157
|
+
};
|
|
11977
12158
|
if (sessionId) {
|
|
11978
12159
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11979
12160
|
documentId: sessionId,
|
|
@@ -11985,9 +12166,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11985
12166
|
}
|
|
11986
12167
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11987
12168
|
documentId: sessionId,
|
|
11988
|
-
data:
|
|
12169
|
+
data: updateData
|
|
11989
12170
|
});
|
|
11990
|
-
log.info(`Session ${sessionId} terminated (
|
|
12171
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
11991
12172
|
} else if (userId) {
|
|
11992
12173
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11993
12174
|
if (!userDocumentId) return;
|
|
@@ -11999,10 +12180,10 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11999
12180
|
for (const session2 of activeSessions) {
|
|
12000
12181
|
await strapi2.documents(SESSION_UID$1).update({
|
|
12001
12182
|
documentId: session2.documentId,
|
|
12002
|
-
data:
|
|
12183
|
+
data: updateData
|
|
12003
12184
|
});
|
|
12004
12185
|
}
|
|
12005
|
-
log.info(`All sessions terminated
|
|
12186
|
+
log.info(`All sessions terminated for user ${userDocumentId} (reason: ${finalReason})`);
|
|
12006
12187
|
}
|
|
12007
12188
|
} catch (err) {
|
|
12008
12189
|
log.error("Error terminating session:", err);
|
|
@@ -12087,7 +12268,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12087
12268
|
async touch({ userId, sessionId, token }) {
|
|
12088
12269
|
try {
|
|
12089
12270
|
const now = /* @__PURE__ */ new Date();
|
|
12090
|
-
const settings2 = await getPluginSettings$
|
|
12271
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12091
12272
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12092
12273
|
let session2 = null;
|
|
12093
12274
|
let sessionDocId = sessionId;
|
|
@@ -12156,7 +12337,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12156
12337
|
*/
|
|
12157
12338
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12158
12339
|
try {
|
|
12159
|
-
const settings2 = await getPluginSettings$
|
|
12340
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12160
12341
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12161
12342
|
const now = /* @__PURE__ */ new Date();
|
|
12162
12343
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12169,7 +12350,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12169
12350
|
});
|
|
12170
12351
|
}).update({
|
|
12171
12352
|
is_active: false,
|
|
12172
|
-
terminated_manually:
|
|
12353
|
+
terminated_manually: false,
|
|
12354
|
+
termination_reason: "idle",
|
|
12173
12355
|
logout_time: now
|
|
12174
12356
|
});
|
|
12175
12357
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12210,7 +12392,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12210
12392
|
documentId,
|
|
12211
12393
|
data: {
|
|
12212
12394
|
isActive: false,
|
|
12213
|
-
terminatedManually:
|
|
12395
|
+
terminatedManually: false,
|
|
12396
|
+
terminationReason: "idle",
|
|
12214
12397
|
logoutTime: now
|
|
12215
12398
|
}
|
|
12216
12399
|
});
|
|
@@ -12256,7 +12439,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12256
12439
|
*/
|
|
12257
12440
|
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12258
12441
|
try {
|
|
12259
|
-
const settings2 = await getPluginSettings$
|
|
12442
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12260
12443
|
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12261
12444
|
if (effectiveDays === -1) {
|
|
12262
12445
|
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
@@ -12361,7 +12544,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12361
12544
|
}
|
|
12362
12545
|
};
|
|
12363
12546
|
};
|
|
12364
|
-
const version$1 = "4.5.
|
|
12547
|
+
const version$1 = "4.5.2";
|
|
12365
12548
|
const require$$2 = {
|
|
12366
12549
|
version: version$1
|
|
12367
12550
|
};
|
|
@@ -13319,6 +13502,7 @@ var services$1 = {
|
|
|
13319
13502
|
geolocation,
|
|
13320
13503
|
notifications
|
|
13321
13504
|
};
|
|
13505
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13322
13506
|
const buckets = /* @__PURE__ */ new Map();
|
|
13323
13507
|
const prune = (now) => {
|
|
13324
13508
|
for (const [key, entry] of buckets) {
|
|
@@ -13332,10 +13516,45 @@ const callerKey = (ctx) => {
|
|
|
13332
13516
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13333
13517
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13334
13518
|
};
|
|
13519
|
+
const RESOLVED_TTL_MS = 3e4;
|
|
13520
|
+
let resolvedCache = null;
|
|
13521
|
+
let resolvedAt = 0;
|
|
13522
|
+
async function resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) {
|
|
13523
|
+
const now = Date.now();
|
|
13524
|
+
if (resolvedCache && now - resolvedAt < RESOLVED_TTL_MS) {
|
|
13525
|
+
const p = resolvedCache[profile];
|
|
13526
|
+
if (p) {
|
|
13527
|
+
return { max: p.max, windowMs: p.windowMs };
|
|
13528
|
+
}
|
|
13529
|
+
}
|
|
13530
|
+
let settings2 = {};
|
|
13531
|
+
try {
|
|
13532
|
+
settings2 = await getPluginSettings$1(strapi2);
|
|
13533
|
+
} catch {
|
|
13534
|
+
settings2 = {};
|
|
13535
|
+
}
|
|
13536
|
+
const windowSec = Number.isFinite(settings2.rateLimitWindowSeconds) ? settings2.rateLimitWindowSeconds : Math.round(routeWindowMs / 1e3);
|
|
13537
|
+
const windowMs = Math.max(1e4, windowSec * 1e3);
|
|
13538
|
+
const resolvedWrite = {
|
|
13539
|
+
max: Math.min(routeMax, Number.isFinite(settings2.rateLimitWriteMax) ? settings2.rateLimitWriteMax : routeMax),
|
|
13540
|
+
windowMs
|
|
13541
|
+
};
|
|
13542
|
+
const resolvedRead = {
|
|
13543
|
+
max: Math.max(routeMax, Number.isFinite(settings2.rateLimitReadMax) ? settings2.rateLimitReadMax : routeMax),
|
|
13544
|
+
windowMs
|
|
13545
|
+
};
|
|
13546
|
+
resolvedCache = { read: resolvedRead, write: resolvedWrite };
|
|
13547
|
+
resolvedAt = now;
|
|
13548
|
+
if (profile === "read") return resolvedRead;
|
|
13549
|
+
if (profile === "write") return resolvedWrite;
|
|
13550
|
+
return { max: routeMax, windowMs };
|
|
13551
|
+
}
|
|
13335
13552
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13336
|
-
const
|
|
13337
|
-
const
|
|
13553
|
+
const routeMax = Number.isFinite(cfg.max) ? cfg.max : 30;
|
|
13554
|
+
const routeWindowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
|
|
13555
|
+
const profile = cfg.profile === "read" || cfg.profile === "write" ? cfg.profile : null;
|
|
13338
13556
|
return async (ctx, next) => {
|
|
13557
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13339
13558
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13340
13559
|
const now = Date.now();
|
|
13341
13560
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13369,7 +13588,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13369
13588
|
var rateLimit_1 = rateLimit;
|
|
13370
13589
|
var middlewares$1 = {
|
|
13371
13590
|
"last-seen": lastSeen,
|
|
13372
|
-
"rate-limit": rateLimit_1
|
|
13591
|
+
"rate-limit": rateLimit_1,
|
|
13592
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13373
13593
|
};
|
|
13374
13594
|
var lodashExports = requireLodash();
|
|
13375
13595
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51450,18 +51670,18 @@ const CSP_DEFAULTS = {
|
|
|
51450
51670
|
"blob:"
|
|
51451
51671
|
]
|
|
51452
51672
|
};
|
|
51453
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51673
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51454
51674
|
return middlewares2.map((currentMiddleware) => {
|
|
51455
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51456
|
-
return
|
|
51675
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51676
|
+
return middleware2;
|
|
51457
51677
|
}
|
|
51458
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51678
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51459
51679
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51460
51680
|
if (Array.isArray(objValue)) {
|
|
51461
51681
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51462
51682
|
}
|
|
51463
51683
|
return void 0;
|
|
51464
|
-
}, currentMiddleware,
|
|
51684
|
+
}, currentMiddleware, middleware2);
|
|
51465
51685
|
}
|
|
51466
51686
|
return currentMiddleware;
|
|
51467
51687
|
});
|