strapi-plugin-magic-sessionmanager 4.5.1 → 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/_chunks/{Analytics-DQyamHg6.mjs → Analytics--qB2cKuD.mjs} +2 -2
- package/dist/_chunks/{Analytics-Cp-WjeV3.js → Analytics-DM4_UffY.js} +2 -2
- package/dist/_chunks/{App-CN4bKWV9.mjs → App-CL5q29Mi.mjs} +2 -2
- package/dist/_chunks/{App-BvYLeEbF.js → App-DF-VsbDW.js} +2 -2
- package/dist/_chunks/{License-Bgcm630d.mjs → License-D9piCp34.mjs} +1 -1
- package/dist/_chunks/{License-DPyeZQWz.js → License-Dmm1d3fQ.js} +1 -1
- package/dist/_chunks/{OnlineUsersWidget-E2FA7rGi.mjs → OnlineUsersWidget-DHsthkt0.mjs} +1 -1
- package/dist/_chunks/{OnlineUsersWidget-BYnqhN3O.js → OnlineUsersWidget-KchZ_ScS.js} +1 -1
- package/dist/_chunks/{Settings-Bt6wy131.js → Settings-CUNaxDWk.js} +255 -2
- package/dist/_chunks/{Settings-D9N7m11p.mjs → Settings-D6ILgR9X.mjs} +255 -2
- package/dist/_chunks/{UpgradePage-DM23ZYVa.mjs → UpgradePage-D697BVWo.mjs} +1 -1
- package/dist/_chunks/{UpgradePage-Bh21Lg-G.js → UpgradePage-Dwrv7g8L.js} +1 -1
- package/dist/_chunks/{index-BRESWp1b.js → index-CTxGMDHr.js} +6 -6
- package/dist/_chunks/{index-BbbrBv3t.mjs → index-CwxKazpc.mjs} +6 -6
- package/dist/_chunks/{useLicense-Bszkymz3.mjs → useLicense-B9WW9s_d.mjs} +1 -1
- package/dist/_chunks/{useLicense-D_wQcoMn.js → useLicense-BEbtA_Zo.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +526 -200
- package/dist/server/index.mjs +526 -200
- 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;
|
|
538
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.");
|
|
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([
|
|
@@ -9571,37 +9670,71 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9571
9670
|
}
|
|
9572
9671
|
}, 3e3);
|
|
9573
9672
|
const sessionService = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9673
|
+
if (!strapi2.sessionManagerIntervals) {
|
|
9674
|
+
strapi2.sessionManagerIntervals = {};
|
|
9675
|
+
}
|
|
9574
9676
|
log.info("Running initial session cleanup...");
|
|
9575
9677
|
try {
|
|
9576
|
-
const settings2 = await getPluginSettings$
|
|
9678
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9577
9679
|
await sessionService.cleanupInactiveSessions({
|
|
9578
9680
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9579
9681
|
});
|
|
9580
9682
|
} catch (cleanupErr) {
|
|
9581
9683
|
log.warn("Initial cleanup failed:", cleanupErr.message);
|
|
9582
9684
|
}
|
|
9583
|
-
const
|
|
9584
|
-
|
|
9685
|
+
const scheduleIdleCleanup = async () => {
|
|
9686
|
+
let intervalMs = 30 * 60 * 1e3;
|
|
9687
|
+
let useDbDirect = false;
|
|
9585
9688
|
try {
|
|
9586
|
-
const
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
});
|
|
9591
|
-
} catch (err) {
|
|
9592
|
-
log.error("Periodic cleanup error:", err);
|
|
9689
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9690
|
+
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9691
|
+
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9692
|
+
} catch {
|
|
9593
9693
|
}
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9694
|
+
const handle = setTimeout(async () => {
|
|
9695
|
+
try {
|
|
9696
|
+
const service = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9697
|
+
await service.cleanupInactiveSessions({ useDbDirect });
|
|
9698
|
+
} catch (err) {
|
|
9699
|
+
log.error("Periodic cleanup error:", err);
|
|
9700
|
+
}
|
|
9701
|
+
scheduleIdleCleanup();
|
|
9702
|
+
}, intervalMs);
|
|
9703
|
+
strapi2.sessionManagerIntervals.cleanupTimeout = handle;
|
|
9704
|
+
};
|
|
9705
|
+
await scheduleIdleCleanup();
|
|
9706
|
+
const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9707
|
+
const scheduleRetention = async () => {
|
|
9708
|
+
let useDbDirect = false;
|
|
9709
|
+
try {
|
|
9710
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9711
|
+
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9712
|
+
} catch {
|
|
9713
|
+
}
|
|
9714
|
+
const handle = setTimeout(async () => {
|
|
9715
|
+
try {
|
|
9716
|
+
const service = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9717
|
+
await service.deleteOldSessions({ useDbDirect });
|
|
9718
|
+
} catch (err) {
|
|
9719
|
+
log.error("Retention cleanup error:", err);
|
|
9720
|
+
}
|
|
9721
|
+
scheduleRetention();
|
|
9722
|
+
}, RETENTION_INTERVAL_MS);
|
|
9723
|
+
strapi2.sessionManagerIntervals.retentionTimeout = handle;
|
|
9724
|
+
};
|
|
9725
|
+
strapi2.sessionManagerIntervals.retentionStartup = setTimeout(() => {
|
|
9726
|
+
scheduleRetention();
|
|
9727
|
+
}, 5 * 60 * 1e3);
|
|
9728
|
+
log.info("[TIME] Dynamic cleanup + retention scheduled");
|
|
9600
9729
|
mountPreLoginGeoGuard({ strapi: strapi2, log });
|
|
9601
9730
|
mountFailedLoginLockout({ strapi: strapi2, log });
|
|
9602
9731
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9603
9732
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9604
9733
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9734
|
+
strapi2.server.use(
|
|
9735
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9736
|
+
);
|
|
9737
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9605
9738
|
strapi2.server.use(
|
|
9606
9739
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9607
9740
|
);
|
|
@@ -9620,7 +9753,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9620
9753
|
}
|
|
9621
9754
|
let settings2 = {};
|
|
9622
9755
|
try {
|
|
9623
|
-
settings2 = await getPluginSettings$
|
|
9756
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9624
9757
|
} catch {
|
|
9625
9758
|
settings2 = {};
|
|
9626
9759
|
}
|
|
@@ -9796,7 +9929,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9796
9929
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9797
9930
|
let maxFailed = 0;
|
|
9798
9931
|
try {
|
|
9799
|
-
const settings2 = await getPluginSettings$
|
|
9932
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9800
9933
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9801
9934
|
} catch {
|
|
9802
9935
|
maxFailed = 0;
|
|
@@ -9875,7 +10008,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9875
10008
|
}
|
|
9876
10009
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9877
10010
|
try {
|
|
9878
|
-
const settings2 = await getPluginSettings$
|
|
10011
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9879
10012
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9880
10013
|
return;
|
|
9881
10014
|
}
|
|
@@ -10192,7 +10325,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10192
10325
|
}
|
|
10193
10326
|
let settings2;
|
|
10194
10327
|
try {
|
|
10195
|
-
settings2 = await getPluginSettings$
|
|
10328
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10196
10329
|
} catch {
|
|
10197
10330
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10198
10331
|
}
|
|
@@ -10217,6 +10350,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10217
10350
|
strapi2.log.info(
|
|
10218
10351
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10219
10352
|
);
|
|
10353
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10220
10354
|
return null;
|
|
10221
10355
|
}
|
|
10222
10356
|
} catch {
|
|
@@ -10227,7 +10361,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10227
10361
|
user: { documentId: userDocId },
|
|
10228
10362
|
tokenHash: tokenHashValue
|
|
10229
10363
|
},
|
|
10230
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10364
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10231
10365
|
});
|
|
10232
10366
|
if (thisSession) {
|
|
10233
10367
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10236,15 +10370,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10236
10370
|
);
|
|
10237
10371
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10238
10372
|
documentId: thisSession.documentId,
|
|
10239
|
-
data: {
|
|
10373
|
+
data: {
|
|
10374
|
+
isActive: false,
|
|
10375
|
+
terminatedManually: false,
|
|
10376
|
+
terminationReason: "expired",
|
|
10377
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10378
|
+
}
|
|
10240
10379
|
});
|
|
10380
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10241
10381
|
return null;
|
|
10242
10382
|
}
|
|
10243
|
-
if (thisSession.
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
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
|
+
}
|
|
10248
10392
|
}
|
|
10249
10393
|
if (thisSession.isActive) {
|
|
10250
10394
|
resetErrorCounter();
|
|
@@ -10259,8 +10403,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10259
10403
|
);
|
|
10260
10404
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10261
10405
|
documentId: thisSession.documentId,
|
|
10262
|
-
data: {
|
|
10406
|
+
data: {
|
|
10407
|
+
terminatedManually: false,
|
|
10408
|
+
terminationReason: "idle",
|
|
10409
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10410
|
+
}
|
|
10263
10411
|
});
|
|
10412
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10264
10413
|
return null;
|
|
10265
10414
|
}
|
|
10266
10415
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10328,9 +10477,10 @@ var destroy$1 = async ({ strapi: strapi2 }) => {
|
|
|
10328
10477
|
if (!handle) continue;
|
|
10329
10478
|
try {
|
|
10330
10479
|
clearInterval(handle);
|
|
10331
|
-
|
|
10480
|
+
clearTimeout(handle);
|
|
10481
|
+
log.info(`[STOP] ${name} timer stopped`);
|
|
10332
10482
|
} catch (err) {
|
|
10333
|
-
log.warn(`Failed to stop ${name}
|
|
10483
|
+
log.warn(`Failed to stop ${name} timer:`, err.message);
|
|
10334
10484
|
}
|
|
10335
10485
|
}
|
|
10336
10486
|
strapi2.sessionManagerIntervals = {};
|
|
@@ -10461,6 +10611,16 @@ const attributes = {
|
|
|
10461
10611
|
"default": false,
|
|
10462
10612
|
required: false
|
|
10463
10613
|
},
|
|
10614
|
+
terminationReason: {
|
|
10615
|
+
type: "enumeration",
|
|
10616
|
+
"enum": [
|
|
10617
|
+
"manual",
|
|
10618
|
+
"idle",
|
|
10619
|
+
"expired",
|
|
10620
|
+
"blocked"
|
|
10621
|
+
],
|
|
10622
|
+
required: false
|
|
10623
|
+
},
|
|
10464
10624
|
geoLocation: {
|
|
10465
10625
|
type: "json"
|
|
10466
10626
|
},
|
|
@@ -10494,10 +10654,16 @@ var contentTypes$2 = {
|
|
|
10494
10654
|
}
|
|
10495
10655
|
};
|
|
10496
10656
|
const writeRateLimit = [
|
|
10497
|
-
{
|
|
10657
|
+
{
|
|
10658
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10659
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10660
|
+
}
|
|
10498
10661
|
];
|
|
10499
10662
|
const readRateLimit = [
|
|
10500
|
-
{
|
|
10663
|
+
{
|
|
10664
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10665
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10666
|
+
}
|
|
10501
10667
|
];
|
|
10502
10668
|
var contentApi$1 = {
|
|
10503
10669
|
type: "content-api",
|
|
@@ -10900,15 +11066,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10900
11066
|
const { hashToken: hashToken$2 } = encryption;
|
|
10901
11067
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10902
11068
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10903
|
-
const { getPluginSettings: getPluginSettings$
|
|
11069
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10904
11070
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10905
11071
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10906
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
|
+
}
|
|
10907
11081
|
var session$3 = {
|
|
10908
11082
|
/**
|
|
10909
11083
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10910
11084
|
* @route GET /magic-sessionmanager/sessions
|
|
10911
|
-
* @returns {object} `{ data, meta }`
|
|
10912
11085
|
*/
|
|
10913
11086
|
async getAllSessionsAdmin(ctx) {
|
|
10914
11087
|
try {
|
|
@@ -10930,7 +11103,6 @@ var session$3 = {
|
|
|
10930
11103
|
/**
|
|
10931
11104
|
* Lists currently-active sessions only.
|
|
10932
11105
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10933
|
-
* @returns {object} `{ data, meta }`
|
|
10934
11106
|
*/
|
|
10935
11107
|
async getActiveSessions(ctx) {
|
|
10936
11108
|
try {
|
|
@@ -10946,35 +11118,33 @@ var session$3 = {
|
|
|
10946
11118
|
}
|
|
10947
11119
|
},
|
|
10948
11120
|
/**
|
|
10949
|
-
* Returns the authenticated user's own sessions,
|
|
10950
|
-
* flagged via `isCurrentSession`.
|
|
10951
|
-
*
|
|
11121
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10952
11122
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10953
|
-
* @returns {object} `{ data, meta }`
|
|
10954
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10955
11123
|
*/
|
|
10956
11124
|
async getOwnSessions(ctx) {
|
|
10957
11125
|
try {
|
|
10958
|
-
const
|
|
11126
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11127
|
+
if (!userDocId) {
|
|
11128
|
+
return ctx.unauthorized("Authentication required");
|
|
11129
|
+
}
|
|
10959
11130
|
const currentToken = extractBearerToken$1(ctx);
|
|
10960
11131
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
10961
|
-
if (!userId) {
|
|
10962
|
-
return ctx.throw(401, "Unauthorized");
|
|
10963
|
-
}
|
|
10964
11132
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
10965
|
-
filters: { user: { documentId:
|
|
11133
|
+
filters: { user: { documentId: userDocId } },
|
|
10966
11134
|
sort: { loginTime: "desc" },
|
|
10967
|
-
limit:
|
|
11135
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
10968
11136
|
});
|
|
10969
|
-
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);
|
|
10970
11140
|
const enhanceOpts = {
|
|
10971
11141
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
10972
11142
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
10973
11143
|
strapi
|
|
10974
11144
|
};
|
|
10975
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11145
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
10976
11146
|
for (const s3 of sessionsWithCurrent) {
|
|
10977
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11147
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
10978
11148
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
10979
11149
|
));
|
|
10980
11150
|
}
|
|
@@ -10987,7 +11157,9 @@ var session$3 = {
|
|
|
10987
11157
|
data: sessionsWithCurrent,
|
|
10988
11158
|
meta: {
|
|
10989
11159
|
count: sessionsWithCurrent.length,
|
|
10990
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11160
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11161
|
+
hasMore,
|
|
11162
|
+
limit: OWN_SESSIONS_LIMIT
|
|
10991
11163
|
}
|
|
10992
11164
|
};
|
|
10993
11165
|
} catch (err) {
|
|
@@ -10996,18 +11168,14 @@ var session$3 = {
|
|
|
10996
11168
|
}
|
|
10997
11169
|
},
|
|
10998
11170
|
/**
|
|
10999
|
-
* Get a specific user's sessions. Admins
|
|
11171
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11000
11172
|
* users can only query themselves.
|
|
11001
|
-
*
|
|
11002
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11003
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11004
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11005
11173
|
*/
|
|
11006
11174
|
async getUserSessions(ctx) {
|
|
11007
11175
|
try {
|
|
11008
11176
|
const { userId } = ctx.params;
|
|
11009
11177
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11010
|
-
const requestingUserDocId = ctx
|
|
11178
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11011
11179
|
if (!isAdminRequest) {
|
|
11012
11180
|
if (!requestingUserDocId) {
|
|
11013
11181
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11036,26 +11204,34 @@ var session$3 = {
|
|
|
11036
11204
|
*/
|
|
11037
11205
|
async logout(ctx) {
|
|
11038
11206
|
try {
|
|
11039
|
-
const
|
|
11207
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11040
11208
|
const token = extractBearerToken$1(ctx);
|
|
11041
|
-
if (!
|
|
11042
|
-
return ctx.
|
|
11209
|
+
if (!userDocId || !token) {
|
|
11210
|
+
return ctx.unauthorized("Authentication required");
|
|
11043
11211
|
}
|
|
11044
11212
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11045
11213
|
const currentTokenHash = hashToken$2(token);
|
|
11046
11214
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11047
11215
|
filters: {
|
|
11048
|
-
user: { documentId:
|
|
11216
|
+
user: { documentId: userDocId },
|
|
11049
11217
|
tokenHash: currentTokenHash,
|
|
11050
11218
|
isActive: true
|
|
11051
11219
|
},
|
|
11052
11220
|
fields: ["documentId"]
|
|
11053
11221
|
});
|
|
11222
|
+
let terminated = false;
|
|
11054
11223
|
if (matchingSession) {
|
|
11055
|
-
await sessionService.terminateSession({
|
|
11056
|
-
|
|
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})`);
|
|
11057
11230
|
}
|
|
11058
|
-
ctx.body = {
|
|
11231
|
+
ctx.body = {
|
|
11232
|
+
message: "Logged out successfully",
|
|
11233
|
+
terminated
|
|
11234
|
+
};
|
|
11059
11235
|
} catch (err) {
|
|
11060
11236
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11061
11237
|
ctx.throw(500, "Error during logout");
|
|
@@ -11067,13 +11243,13 @@ var session$3 = {
|
|
|
11067
11243
|
*/
|
|
11068
11244
|
async logoutAll(ctx) {
|
|
11069
11245
|
try {
|
|
11070
|
-
const
|
|
11071
|
-
if (!
|
|
11072
|
-
return ctx.
|
|
11246
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11247
|
+
if (!userDocId) {
|
|
11248
|
+
return ctx.unauthorized("Authentication required");
|
|
11073
11249
|
}
|
|
11074
11250
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11075
|
-
await sessionService.terminateSession({ userId });
|
|
11076
|
-
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`);
|
|
11077
11253
|
ctx.body = { message: "Logged out from all devices successfully" };
|
|
11078
11254
|
} catch (err) {
|
|
11079
11255
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
@@ -11082,27 +11258,48 @@ var session$3 = {
|
|
|
11082
11258
|
},
|
|
11083
11259
|
/**
|
|
11084
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
|
+
*
|
|
11085
11267
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11086
11268
|
*/
|
|
11087
11269
|
async getCurrentSession(ctx) {
|
|
11088
11270
|
try {
|
|
11089
|
-
const
|
|
11271
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11090
11272
|
const token = extractBearerToken$1(ctx);
|
|
11091
|
-
if (!
|
|
11092
|
-
return ctx.
|
|
11273
|
+
if (!userDocId || !token) {
|
|
11274
|
+
return ctx.unauthorized("Authentication required");
|
|
11093
11275
|
}
|
|
11094
11276
|
const currentTokenHash = hashToken$2(token);
|
|
11095
11277
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11096
11278
|
filters: {
|
|
11097
|
-
user: { documentId:
|
|
11279
|
+
user: { documentId: userDocId },
|
|
11098
11280
|
tokenHash: currentTokenHash,
|
|
11099
11281
|
isActive: true
|
|
11100
11282
|
}
|
|
11101
11283
|
});
|
|
11102
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
|
+
}
|
|
11103
11300
|
return ctx.notFound("Current session not found");
|
|
11104
11301
|
}
|
|
11105
|
-
const settings2 = await getPluginSettings$
|
|
11302
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11106
11303
|
const enhanced = await enhanceSession(currentSession, {
|
|
11107
11304
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11108
11305
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11118,17 +11315,17 @@ var session$3 = {
|
|
|
11118
11315
|
}
|
|
11119
11316
|
},
|
|
11120
11317
|
/**
|
|
11121
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11318
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11122
11319
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11123
11320
|
*/
|
|
11124
11321
|
async terminateOwnSession(ctx) {
|
|
11125
11322
|
try {
|
|
11126
|
-
const
|
|
11323
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11127
11324
|
const { sessionId } = ctx.params;
|
|
11128
11325
|
const currentToken = extractBearerToken$1(ctx);
|
|
11129
11326
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11130
|
-
if (!
|
|
11131
|
-
return ctx.
|
|
11327
|
+
if (!userDocId) {
|
|
11328
|
+
return ctx.unauthorized("Authentication required");
|
|
11132
11329
|
}
|
|
11133
11330
|
if (!sessionId) {
|
|
11134
11331
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11141,19 +11338,23 @@ var session$3 = {
|
|
|
11141
11338
|
return ctx.notFound("Session not found");
|
|
11142
11339
|
}
|
|
11143
11340
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11144
|
-
if (sessionUserId !==
|
|
11145
|
-
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}`);
|
|
11146
11343
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11147
11344
|
}
|
|
11148
11345
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11149
|
-
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}`);
|
|
11150
11353
|
}
|
|
11151
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11152
|
-
await sessionService.terminateSession({ sessionId });
|
|
11153
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11154
11354
|
ctx.body = {
|
|
11155
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11156
|
-
success: true
|
|
11355
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11356
|
+
success: true,
|
|
11357
|
+
alreadyTerminated
|
|
11157
11358
|
};
|
|
11158
11359
|
} catch (err) {
|
|
11159
11360
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11161,9 +11362,7 @@ var session$3 = {
|
|
|
11161
11362
|
}
|
|
11162
11363
|
},
|
|
11163
11364
|
/**
|
|
11164
|
-
*
|
|
11165
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11166
|
-
*
|
|
11365
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11167
11366
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11168
11367
|
*/
|
|
11169
11368
|
async simulateTimeout(ctx) {
|
|
@@ -11182,13 +11381,16 @@ var session$3 = {
|
|
|
11182
11381
|
}
|
|
11183
11382
|
await strapi.documents(SESSION_UID$2).update({
|
|
11184
11383
|
documentId: sessionId,
|
|
11185
|
-
data: {
|
|
11384
|
+
data: {
|
|
11385
|
+
isActive: false,
|
|
11386
|
+
terminatedManually: false,
|
|
11387
|
+
terminationReason: "idle"
|
|
11388
|
+
}
|
|
11186
11389
|
});
|
|
11187
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11390
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11188
11391
|
ctx.body = {
|
|
11189
|
-
message: `Session ${sessionId} marked as timed out
|
|
11190
|
-
success: true
|
|
11191
|
-
terminatedManually: false
|
|
11392
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11393
|
+
success: true
|
|
11192
11394
|
};
|
|
11193
11395
|
} catch (err) {
|
|
11194
11396
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11197,13 +11399,12 @@ var session$3 = {
|
|
|
11197
11399
|
},
|
|
11198
11400
|
/**
|
|
11199
11401
|
* Terminates a specific session (admin action).
|
|
11200
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11201
11402
|
*/
|
|
11202
11403
|
async terminateSingleSession(ctx) {
|
|
11203
11404
|
try {
|
|
11204
11405
|
const { sessionId } = ctx.params;
|
|
11205
11406
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11206
|
-
await sessionService.terminateSession({ sessionId });
|
|
11407
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11207
11408
|
ctx.body = {
|
|
11208
11409
|
message: `Session ${sessionId} terminated`,
|
|
11209
11410
|
success: true
|
|
@@ -11215,13 +11416,12 @@ var session$3 = {
|
|
|
11215
11416
|
},
|
|
11216
11417
|
/**
|
|
11217
11418
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11218
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11219
11419
|
*/
|
|
11220
11420
|
async terminateAllUserSessions(ctx) {
|
|
11221
11421
|
try {
|
|
11222
11422
|
const { userId } = ctx.params;
|
|
11223
11423
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11224
|
-
await sessionService.terminateSession({ userId });
|
|
11424
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11225
11425
|
ctx.body = {
|
|
11226
11426
|
message: `All sessions terminated for user ${userId}`,
|
|
11227
11427
|
success: true
|
|
@@ -11233,9 +11433,6 @@ var session$3 = {
|
|
|
11233
11433
|
},
|
|
11234
11434
|
/**
|
|
11235
11435
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11236
|
-
*
|
|
11237
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11238
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11239
11436
|
*/
|
|
11240
11437
|
async getIpGeolocation(ctx) {
|
|
11241
11438
|
try {
|
|
@@ -11262,10 +11459,7 @@ var session$3 = {
|
|
|
11262
11459
|
return ctx.badRequest("Invalid IP address format");
|
|
11263
11460
|
}
|
|
11264
11461
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11265
|
-
const pluginStore = strapi.store({
|
|
11266
|
-
type: "plugin",
|
|
11267
|
-
name: "magic-sessionmanager"
|
|
11268
|
-
});
|
|
11462
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11269
11463
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11270
11464
|
if (!licenseKey) {
|
|
11271
11465
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11287,7 +11481,6 @@ var session$3 = {
|
|
|
11287
11481
|
},
|
|
11288
11482
|
/**
|
|
11289
11483
|
* Permanently deletes a session (admin action).
|
|
11290
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11291
11484
|
*/
|
|
11292
11485
|
async deleteSession(ctx) {
|
|
11293
11486
|
try {
|
|
@@ -11305,7 +11498,6 @@ var session$3 = {
|
|
|
11305
11498
|
},
|
|
11306
11499
|
/**
|
|
11307
11500
|
* Deletes all inactive sessions (admin action).
|
|
11308
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11309
11501
|
*/
|
|
11310
11502
|
async cleanInactiveSessions(ctx) {
|
|
11311
11503
|
try {
|
|
@@ -11323,9 +11515,6 @@ var session$3 = {
|
|
|
11323
11515
|
},
|
|
11324
11516
|
/**
|
|
11325
11517
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11326
|
-
*
|
|
11327
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11328
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11329
11518
|
*/
|
|
11330
11519
|
async toggleUserBlock(ctx) {
|
|
11331
11520
|
try {
|
|
@@ -11341,11 +11530,11 @@ var session$3 = {
|
|
|
11341
11530
|
}
|
|
11342
11531
|
}
|
|
11343
11532
|
if (!userDocumentId) {
|
|
11344
|
-
return ctx.
|
|
11533
|
+
return ctx.notFound("User not found");
|
|
11345
11534
|
}
|
|
11346
11535
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11347
11536
|
if (!user) {
|
|
11348
|
-
return ctx.
|
|
11537
|
+
return ctx.notFound("User not found");
|
|
11349
11538
|
}
|
|
11350
11539
|
const newBlockedStatus = !user.blocked;
|
|
11351
11540
|
await strapi.documents(USER_UID).update({
|
|
@@ -11354,7 +11543,7 @@ var session$3 = {
|
|
|
11354
11543
|
});
|
|
11355
11544
|
if (newBlockedStatus) {
|
|
11356
11545
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11357
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11546
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11358
11547
|
}
|
|
11359
11548
|
ctx.body = {
|
|
11360
11549
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11846,13 +12035,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11846
12035
|
const { parseUserAgent } = userAgentParser;
|
|
11847
12036
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11848
12037
|
const { enhanceSessions } = enhanceSession_1;
|
|
11849
|
-
const { getPluginSettings: getPluginSettings$
|
|
12038
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11850
12039
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11851
12040
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11852
12041
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11853
12042
|
const log = createLogger$1(strapi2);
|
|
11854
12043
|
async function getEnhanceOpts() {
|
|
11855
|
-
const settings2 = await getPluginSettings$
|
|
12044
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11856
12045
|
return {
|
|
11857
12046
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11858
12047
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11921,15 +12110,38 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11921
12110
|
}
|
|
11922
12111
|
},
|
|
11923
12112
|
/**
|
|
11924
|
-
* 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
|
+
*
|
|
11925
12128
|
* @param {Object} params
|
|
11926
12129
|
* @param {string} [params.sessionId]
|
|
11927
12130
|
* @param {string|number} [params.userId]
|
|
12131
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
11928
12132
|
* @returns {Promise<void>}
|
|
11929
12133
|
*/
|
|
11930
|
-
async terminateSession({ sessionId, userId }) {
|
|
12134
|
+
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
11931
12135
|
try {
|
|
11932
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
|
+
};
|
|
11933
12145
|
if (sessionId) {
|
|
11934
12146
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11935
12147
|
documentId: sessionId,
|
|
@@ -11941,9 +12153,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11941
12153
|
}
|
|
11942
12154
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11943
12155
|
documentId: sessionId,
|
|
11944
|
-
data:
|
|
12156
|
+
data: updateData
|
|
11945
12157
|
});
|
|
11946
|
-
log.info(`Session ${sessionId} terminated (
|
|
12158
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
11947
12159
|
} else if (userId) {
|
|
11948
12160
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11949
12161
|
if (!userDocumentId) return;
|
|
@@ -11955,10 +12167,10 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11955
12167
|
for (const session2 of activeSessions) {
|
|
11956
12168
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11957
12169
|
documentId: session2.documentId,
|
|
11958
|
-
data:
|
|
12170
|
+
data: updateData
|
|
11959
12171
|
});
|
|
11960
12172
|
}
|
|
11961
|
-
log.info(`All sessions terminated
|
|
12173
|
+
log.info(`All sessions terminated for user ${userDocumentId} (reason: ${finalReason})`);
|
|
11962
12174
|
}
|
|
11963
12175
|
} catch (err) {
|
|
11964
12176
|
log.error("Error terminating session:", err);
|
|
@@ -12043,7 +12255,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12043
12255
|
async touch({ userId, sessionId, token }) {
|
|
12044
12256
|
try {
|
|
12045
12257
|
const now = /* @__PURE__ */ new Date();
|
|
12046
|
-
const settings2 = await getPluginSettings$
|
|
12258
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12047
12259
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12048
12260
|
let session2 = null;
|
|
12049
12261
|
let sessionDocId = sessionId;
|
|
@@ -12112,7 +12324,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12112
12324
|
*/
|
|
12113
12325
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12114
12326
|
try {
|
|
12115
|
-
const settings2 = await getPluginSettings$
|
|
12327
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12116
12328
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12117
12329
|
const now = /* @__PURE__ */ new Date();
|
|
12118
12330
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12125,7 +12337,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12125
12337
|
});
|
|
12126
12338
|
}).update({
|
|
12127
12339
|
is_active: false,
|
|
12128
|
-
terminated_manually:
|
|
12340
|
+
terminated_manually: false,
|
|
12341
|
+
termination_reason: "idle",
|
|
12129
12342
|
logout_time: now
|
|
12130
12343
|
});
|
|
12131
12344
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12166,7 +12379,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12166
12379
|
documentId,
|
|
12167
12380
|
data: {
|
|
12168
12381
|
isActive: false,
|
|
12169
|
-
terminatedManually:
|
|
12382
|
+
terminatedManually: false,
|
|
12383
|
+
terminationReason: "idle",
|
|
12170
12384
|
logoutTime: now
|
|
12171
12385
|
}
|
|
12172
12386
|
});
|
|
@@ -12197,6 +12411,81 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12197
12411
|
throw err;
|
|
12198
12412
|
}
|
|
12199
12413
|
},
|
|
12414
|
+
/**
|
|
12415
|
+
* Permanently deletes sessions that have been inactive past the
|
|
12416
|
+
* configured retention window. Distinct from `deleteInactiveSessions`
|
|
12417
|
+
* (which deletes ALL inactive sessions) — this only drops rows older
|
|
12418
|
+
* than `retentionDays`, so recently-terminated sessions stay queryable
|
|
12419
|
+
* for audits.
|
|
12420
|
+
*
|
|
12421
|
+
* @param {Object} [options]
|
|
12422
|
+
* @param {number} [options.retentionDays] Overrides the stored setting.
|
|
12423
|
+
* @param {boolean} [options.useDbDirect] Fast-path via single SQL
|
|
12424
|
+
* DELETE. Bypasses lifecycle hooks; use only when necessary.
|
|
12425
|
+
* @returns {Promise<number>} Number of sessions deleted
|
|
12426
|
+
*/
|
|
12427
|
+
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12428
|
+
try {
|
|
12429
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12430
|
+
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12431
|
+
if (effectiveDays === -1) {
|
|
12432
|
+
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
12433
|
+
return 0;
|
|
12434
|
+
}
|
|
12435
|
+
const cutoffDate = new Date(Date.now() - effectiveDays * 24 * 60 * 60 * 1e3);
|
|
12436
|
+
const wantDbDirect = useDbDirect ?? settings2.cleanupUseDbDirect === true;
|
|
12437
|
+
log.info(`[RETENTION] Deleting inactive sessions older than ${effectiveDays} days (before ${cutoffDate.toISOString()})`);
|
|
12438
|
+
if (wantDbDirect) {
|
|
12439
|
+
try {
|
|
12440
|
+
const deleted = await strapi2.db.connection("magic_sessions").where("is_active", false).andWhere(function whereOldEnough() {
|
|
12441
|
+
this.where("logout_time", "<", cutoffDate).orWhere(function whereNullLogout() {
|
|
12442
|
+
this.whereNull("logout_time").andWhere(function whereOldByActivity() {
|
|
12443
|
+
this.where("last_active", "<", cutoffDate).orWhere(function whereNullActivity() {
|
|
12444
|
+
this.whereNull("last_active").andWhere("login_time", "<", cutoffDate);
|
|
12445
|
+
});
|
|
12446
|
+
});
|
|
12447
|
+
});
|
|
12448
|
+
}).del();
|
|
12449
|
+
log.info(`[SUCCESS] Retention (db-direct) deleted ${deleted} old session(s)`);
|
|
12450
|
+
return deleted;
|
|
12451
|
+
} catch (err) {
|
|
12452
|
+
log.warn("[RETENTION] DB-direct delete failed, falling back to Document Service:", err.message);
|
|
12453
|
+
}
|
|
12454
|
+
}
|
|
12455
|
+
let deletedCount = 0;
|
|
12456
|
+
const BATCH = 200;
|
|
12457
|
+
while (true) {
|
|
12458
|
+
const batch = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12459
|
+
filters: {
|
|
12460
|
+
isActive: false,
|
|
12461
|
+
$or: [
|
|
12462
|
+
{ logoutTime: { $lt: cutoffDate } },
|
|
12463
|
+
{ logoutTime: { $null: true }, lastActive: { $lt: cutoffDate } },
|
|
12464
|
+
{ logoutTime: { $null: true }, lastActive: { $null: true }, loginTime: { $lt: cutoffDate } }
|
|
12465
|
+
]
|
|
12466
|
+
},
|
|
12467
|
+
fields: ["documentId"],
|
|
12468
|
+
sort: { loginTime: "asc" },
|
|
12469
|
+
limit: BATCH
|
|
12470
|
+
});
|
|
12471
|
+
if (!batch || batch.length === 0) break;
|
|
12472
|
+
for (const session2 of batch) {
|
|
12473
|
+
try {
|
|
12474
|
+
await strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId });
|
|
12475
|
+
deletedCount++;
|
|
12476
|
+
} catch (err) {
|
|
12477
|
+
log.debug(`[RETENTION] Failed to delete session ${session2.documentId}:`, err.message);
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12480
|
+
if (batch.length < BATCH) break;
|
|
12481
|
+
}
|
|
12482
|
+
log.info(`[SUCCESS] Retention deleted ${deletedCount} old session(s)`);
|
|
12483
|
+
return deletedCount;
|
|
12484
|
+
} catch (err) {
|
|
12485
|
+
log.error("Error in retention cleanup:", err);
|
|
12486
|
+
return 0;
|
|
12487
|
+
}
|
|
12488
|
+
},
|
|
12200
12489
|
/**
|
|
12201
12490
|
* Permanently deletes all inactive sessions.
|
|
12202
12491
|
* Uses an inner scan loop that tolerates partial failures.
|
|
@@ -12242,7 +12531,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12242
12531
|
}
|
|
12243
12532
|
};
|
|
12244
12533
|
};
|
|
12245
|
-
const version$1 = "4.5.
|
|
12534
|
+
const version$1 = "4.5.2";
|
|
12246
12535
|
const require$$2 = {
|
|
12247
12536
|
version: version$1
|
|
12248
12537
|
};
|
|
@@ -13200,6 +13489,7 @@ var services$1 = {
|
|
|
13200
13489
|
geolocation,
|
|
13201
13490
|
notifications
|
|
13202
13491
|
};
|
|
13492
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13203
13493
|
const buckets = /* @__PURE__ */ new Map();
|
|
13204
13494
|
const prune = (now) => {
|
|
13205
13495
|
for (const [key, entry] of buckets) {
|
|
@@ -13213,10 +13503,45 @@ const callerKey = (ctx) => {
|
|
|
13213
13503
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13214
13504
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13215
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
|
+
}
|
|
13216
13539
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13217
|
-
const
|
|
13218
|
-
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;
|
|
13219
13543
|
return async (ctx, next) => {
|
|
13544
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13220
13545
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13221
13546
|
const now = Date.now();
|
|
13222
13547
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13250,7 +13575,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13250
13575
|
var rateLimit_1 = rateLimit;
|
|
13251
13576
|
var middlewares$1 = {
|
|
13252
13577
|
"last-seen": lastSeen,
|
|
13253
|
-
"rate-limit": rateLimit_1
|
|
13578
|
+
"rate-limit": rateLimit_1,
|
|
13579
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13254
13580
|
};
|
|
13255
13581
|
var lodashExports = requireLodash();
|
|
13256
13582
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51331,18 +51657,18 @@ const CSP_DEFAULTS = {
|
|
|
51331
51657
|
"blob:"
|
|
51332
51658
|
]
|
|
51333
51659
|
};
|
|
51334
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51660
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51335
51661
|
return middlewares2.map((currentMiddleware) => {
|
|
51336
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51337
|
-
return
|
|
51662
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51663
|
+
return middleware2;
|
|
51338
51664
|
}
|
|
51339
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51665
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51340
51666
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51341
51667
|
if (Array.isArray(objValue)) {
|
|
51342
51668
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51343
51669
|
}
|
|
51344
51670
|
return void 0;
|
|
51345
|
-
}, currentMiddleware,
|
|
51671
|
+
}, currentMiddleware, middleware2);
|
|
51346
51672
|
}
|
|
51347
51673
|
return currentMiddleware;
|
|
51348
51674
|
});
|