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.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;
|
|
551
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.");
|
|
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([
|
|
@@ -9584,37 +9683,71 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9584
9683
|
}
|
|
9585
9684
|
}, 3e3);
|
|
9586
9685
|
const sessionService = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9686
|
+
if (!strapi2.sessionManagerIntervals) {
|
|
9687
|
+
strapi2.sessionManagerIntervals = {};
|
|
9688
|
+
}
|
|
9587
9689
|
log.info("Running initial session cleanup...");
|
|
9588
9690
|
try {
|
|
9589
|
-
const settings2 = await getPluginSettings$
|
|
9691
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9590
9692
|
await sessionService.cleanupInactiveSessions({
|
|
9591
9693
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9592
9694
|
});
|
|
9593
9695
|
} catch (cleanupErr) {
|
|
9594
9696
|
log.warn("Initial cleanup failed:", cleanupErr.message);
|
|
9595
9697
|
}
|
|
9596
|
-
const
|
|
9597
|
-
|
|
9698
|
+
const scheduleIdleCleanup = async () => {
|
|
9699
|
+
let intervalMs = 30 * 60 * 1e3;
|
|
9700
|
+
let useDbDirect = false;
|
|
9598
9701
|
try {
|
|
9599
|
-
const
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
});
|
|
9604
|
-
} catch (err) {
|
|
9605
|
-
log.error("Periodic cleanup error:", err);
|
|
9702
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9703
|
+
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9704
|
+
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9705
|
+
} catch {
|
|
9606
9706
|
}
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9707
|
+
const handle = setTimeout(async () => {
|
|
9708
|
+
try {
|
|
9709
|
+
const service = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9710
|
+
await service.cleanupInactiveSessions({ useDbDirect });
|
|
9711
|
+
} catch (err) {
|
|
9712
|
+
log.error("Periodic cleanup error:", err);
|
|
9713
|
+
}
|
|
9714
|
+
scheduleIdleCleanup();
|
|
9715
|
+
}, intervalMs);
|
|
9716
|
+
strapi2.sessionManagerIntervals.cleanupTimeout = handle;
|
|
9717
|
+
};
|
|
9718
|
+
await scheduleIdleCleanup();
|
|
9719
|
+
const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9720
|
+
const scheduleRetention = async () => {
|
|
9721
|
+
let useDbDirect = false;
|
|
9722
|
+
try {
|
|
9723
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9724
|
+
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9725
|
+
} catch {
|
|
9726
|
+
}
|
|
9727
|
+
const handle = setTimeout(async () => {
|
|
9728
|
+
try {
|
|
9729
|
+
const service = strapi2.plugin("magic-sessionmanager").service("session");
|
|
9730
|
+
await service.deleteOldSessions({ useDbDirect });
|
|
9731
|
+
} catch (err) {
|
|
9732
|
+
log.error("Retention cleanup error:", err);
|
|
9733
|
+
}
|
|
9734
|
+
scheduleRetention();
|
|
9735
|
+
}, RETENTION_INTERVAL_MS);
|
|
9736
|
+
strapi2.sessionManagerIntervals.retentionTimeout = handle;
|
|
9737
|
+
};
|
|
9738
|
+
strapi2.sessionManagerIntervals.retentionStartup = setTimeout(() => {
|
|
9739
|
+
scheduleRetention();
|
|
9740
|
+
}, 5 * 60 * 1e3);
|
|
9741
|
+
log.info("[TIME] Dynamic cleanup + retention scheduled");
|
|
9613
9742
|
mountPreLoginGeoGuard({ strapi: strapi2, log });
|
|
9614
9743
|
mountFailedLoginLockout({ strapi: strapi2, log });
|
|
9615
9744
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9616
9745
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9617
9746
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9747
|
+
strapi2.server.use(
|
|
9748
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9749
|
+
);
|
|
9750
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9618
9751
|
strapi2.server.use(
|
|
9619
9752
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9620
9753
|
);
|
|
@@ -9633,7 +9766,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9633
9766
|
}
|
|
9634
9767
|
let settings2 = {};
|
|
9635
9768
|
try {
|
|
9636
|
-
settings2 = await getPluginSettings$
|
|
9769
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9637
9770
|
} catch {
|
|
9638
9771
|
settings2 = {};
|
|
9639
9772
|
}
|
|
@@ -9809,7 +9942,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9809
9942
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9810
9943
|
let maxFailed = 0;
|
|
9811
9944
|
try {
|
|
9812
|
-
const settings2 = await getPluginSettings$
|
|
9945
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9813
9946
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9814
9947
|
} catch {
|
|
9815
9948
|
maxFailed = 0;
|
|
@@ -9888,7 +10021,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9888
10021
|
}
|
|
9889
10022
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9890
10023
|
try {
|
|
9891
|
-
const settings2 = await getPluginSettings$
|
|
10024
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9892
10025
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9893
10026
|
return;
|
|
9894
10027
|
}
|
|
@@ -10205,7 +10338,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10205
10338
|
}
|
|
10206
10339
|
let settings2;
|
|
10207
10340
|
try {
|
|
10208
|
-
settings2 = await getPluginSettings$
|
|
10341
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10209
10342
|
} catch {
|
|
10210
10343
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10211
10344
|
}
|
|
@@ -10230,6 +10363,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10230
10363
|
strapi2.log.info(
|
|
10231
10364
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10232
10365
|
);
|
|
10366
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10233
10367
|
return null;
|
|
10234
10368
|
}
|
|
10235
10369
|
} catch {
|
|
@@ -10240,7 +10374,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10240
10374
|
user: { documentId: userDocId },
|
|
10241
10375
|
tokenHash: tokenHashValue
|
|
10242
10376
|
},
|
|
10243
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10377
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10244
10378
|
});
|
|
10245
10379
|
if (thisSession) {
|
|
10246
10380
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10249,15 +10383,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10249
10383
|
);
|
|
10250
10384
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10251
10385
|
documentId: thisSession.documentId,
|
|
10252
|
-
data: {
|
|
10386
|
+
data: {
|
|
10387
|
+
isActive: false,
|
|
10388
|
+
terminatedManually: false,
|
|
10389
|
+
terminationReason: "expired",
|
|
10390
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10391
|
+
}
|
|
10253
10392
|
});
|
|
10393
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10254
10394
|
return null;
|
|
10255
10395
|
}
|
|
10256
|
-
if (thisSession.
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
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
|
+
}
|
|
10261
10405
|
}
|
|
10262
10406
|
if (thisSession.isActive) {
|
|
10263
10407
|
resetErrorCounter();
|
|
@@ -10272,8 +10416,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10272
10416
|
);
|
|
10273
10417
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10274
10418
|
documentId: thisSession.documentId,
|
|
10275
|
-
data: {
|
|
10419
|
+
data: {
|
|
10420
|
+
terminatedManually: false,
|
|
10421
|
+
terminationReason: "idle",
|
|
10422
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10423
|
+
}
|
|
10276
10424
|
});
|
|
10425
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10277
10426
|
return null;
|
|
10278
10427
|
}
|
|
10279
10428
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10341,9 +10490,10 @@ var destroy$1 = async ({ strapi: strapi2 }) => {
|
|
|
10341
10490
|
if (!handle) continue;
|
|
10342
10491
|
try {
|
|
10343
10492
|
clearInterval(handle);
|
|
10344
|
-
|
|
10493
|
+
clearTimeout(handle);
|
|
10494
|
+
log.info(`[STOP] ${name} timer stopped`);
|
|
10345
10495
|
} catch (err) {
|
|
10346
|
-
log.warn(`Failed to stop ${name}
|
|
10496
|
+
log.warn(`Failed to stop ${name} timer:`, err.message);
|
|
10347
10497
|
}
|
|
10348
10498
|
}
|
|
10349
10499
|
strapi2.sessionManagerIntervals = {};
|
|
@@ -10474,6 +10624,16 @@ const attributes = {
|
|
|
10474
10624
|
"default": false,
|
|
10475
10625
|
required: false
|
|
10476
10626
|
},
|
|
10627
|
+
terminationReason: {
|
|
10628
|
+
type: "enumeration",
|
|
10629
|
+
"enum": [
|
|
10630
|
+
"manual",
|
|
10631
|
+
"idle",
|
|
10632
|
+
"expired",
|
|
10633
|
+
"blocked"
|
|
10634
|
+
],
|
|
10635
|
+
required: false
|
|
10636
|
+
},
|
|
10477
10637
|
geoLocation: {
|
|
10478
10638
|
type: "json"
|
|
10479
10639
|
},
|
|
@@ -10507,10 +10667,16 @@ var contentTypes$2 = {
|
|
|
10507
10667
|
}
|
|
10508
10668
|
};
|
|
10509
10669
|
const writeRateLimit = [
|
|
10510
|
-
{
|
|
10670
|
+
{
|
|
10671
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10672
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10673
|
+
}
|
|
10511
10674
|
];
|
|
10512
10675
|
const readRateLimit = [
|
|
10513
|
-
{
|
|
10676
|
+
{
|
|
10677
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10678
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10679
|
+
}
|
|
10514
10680
|
];
|
|
10515
10681
|
var contentApi$1 = {
|
|
10516
10682
|
type: "content-api",
|
|
@@ -10913,15 +11079,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10913
11079
|
const { hashToken: hashToken$2 } = encryption;
|
|
10914
11080
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10915
11081
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10916
|
-
const { getPluginSettings: getPluginSettings$
|
|
11082
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10917
11083
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10918
11084
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10919
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
|
+
}
|
|
10920
11094
|
var session$3 = {
|
|
10921
11095
|
/**
|
|
10922
11096
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10923
11097
|
* @route GET /magic-sessionmanager/sessions
|
|
10924
|
-
* @returns {object} `{ data, meta }`
|
|
10925
11098
|
*/
|
|
10926
11099
|
async getAllSessionsAdmin(ctx) {
|
|
10927
11100
|
try {
|
|
@@ -10943,7 +11116,6 @@ var session$3 = {
|
|
|
10943
11116
|
/**
|
|
10944
11117
|
* Lists currently-active sessions only.
|
|
10945
11118
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10946
|
-
* @returns {object} `{ data, meta }`
|
|
10947
11119
|
*/
|
|
10948
11120
|
async getActiveSessions(ctx) {
|
|
10949
11121
|
try {
|
|
@@ -10959,35 +11131,33 @@ var session$3 = {
|
|
|
10959
11131
|
}
|
|
10960
11132
|
},
|
|
10961
11133
|
/**
|
|
10962
|
-
* Returns the authenticated user's own sessions,
|
|
10963
|
-
* flagged via `isCurrentSession`.
|
|
10964
|
-
*
|
|
11134
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10965
11135
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10966
|
-
* @returns {object} `{ data, meta }`
|
|
10967
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10968
11136
|
*/
|
|
10969
11137
|
async getOwnSessions(ctx) {
|
|
10970
11138
|
try {
|
|
10971
|
-
const
|
|
11139
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11140
|
+
if (!userDocId) {
|
|
11141
|
+
return ctx.unauthorized("Authentication required");
|
|
11142
|
+
}
|
|
10972
11143
|
const currentToken = extractBearerToken$1(ctx);
|
|
10973
11144
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
10974
|
-
if (!userId) {
|
|
10975
|
-
return ctx.throw(401, "Unauthorized");
|
|
10976
|
-
}
|
|
10977
11145
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
10978
|
-
filters: { user: { documentId:
|
|
11146
|
+
filters: { user: { documentId: userDocId } },
|
|
10979
11147
|
sort: { loginTime: "desc" },
|
|
10980
|
-
limit:
|
|
11148
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
10981
11149
|
});
|
|
10982
|
-
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);
|
|
10983
11153
|
const enhanceOpts = {
|
|
10984
11154
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
10985
11155
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
10986
11156
|
strapi
|
|
10987
11157
|
};
|
|
10988
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11158
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
10989
11159
|
for (const s3 of sessionsWithCurrent) {
|
|
10990
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11160
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
10991
11161
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
10992
11162
|
));
|
|
10993
11163
|
}
|
|
@@ -11000,7 +11170,9 @@ var session$3 = {
|
|
|
11000
11170
|
data: sessionsWithCurrent,
|
|
11001
11171
|
meta: {
|
|
11002
11172
|
count: sessionsWithCurrent.length,
|
|
11003
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11173
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11174
|
+
hasMore,
|
|
11175
|
+
limit: OWN_SESSIONS_LIMIT
|
|
11004
11176
|
}
|
|
11005
11177
|
};
|
|
11006
11178
|
} catch (err) {
|
|
@@ -11009,18 +11181,14 @@ var session$3 = {
|
|
|
11009
11181
|
}
|
|
11010
11182
|
},
|
|
11011
11183
|
/**
|
|
11012
|
-
* Get a specific user's sessions. Admins
|
|
11184
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11013
11185
|
* users can only query themselves.
|
|
11014
|
-
*
|
|
11015
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11016
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11017
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11018
11186
|
*/
|
|
11019
11187
|
async getUserSessions(ctx) {
|
|
11020
11188
|
try {
|
|
11021
11189
|
const { userId } = ctx.params;
|
|
11022
11190
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11023
|
-
const requestingUserDocId = ctx
|
|
11191
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11024
11192
|
if (!isAdminRequest) {
|
|
11025
11193
|
if (!requestingUserDocId) {
|
|
11026
11194
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11049,26 +11217,34 @@ var session$3 = {
|
|
|
11049
11217
|
*/
|
|
11050
11218
|
async logout(ctx) {
|
|
11051
11219
|
try {
|
|
11052
|
-
const
|
|
11220
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11053
11221
|
const token = extractBearerToken$1(ctx);
|
|
11054
|
-
if (!
|
|
11055
|
-
return ctx.
|
|
11222
|
+
if (!userDocId || !token) {
|
|
11223
|
+
return ctx.unauthorized("Authentication required");
|
|
11056
11224
|
}
|
|
11057
11225
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11058
11226
|
const currentTokenHash = hashToken$2(token);
|
|
11059
11227
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11060
11228
|
filters: {
|
|
11061
|
-
user: { documentId:
|
|
11229
|
+
user: { documentId: userDocId },
|
|
11062
11230
|
tokenHash: currentTokenHash,
|
|
11063
11231
|
isActive: true
|
|
11064
11232
|
},
|
|
11065
11233
|
fields: ["documentId"]
|
|
11066
11234
|
});
|
|
11235
|
+
let terminated = false;
|
|
11067
11236
|
if (matchingSession) {
|
|
11068
|
-
await sessionService.terminateSession({
|
|
11069
|
-
|
|
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})`);
|
|
11070
11243
|
}
|
|
11071
|
-
ctx.body = {
|
|
11244
|
+
ctx.body = {
|
|
11245
|
+
message: "Logged out successfully",
|
|
11246
|
+
terminated
|
|
11247
|
+
};
|
|
11072
11248
|
} catch (err) {
|
|
11073
11249
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11074
11250
|
ctx.throw(500, "Error during logout");
|
|
@@ -11080,13 +11256,13 @@ var session$3 = {
|
|
|
11080
11256
|
*/
|
|
11081
11257
|
async logoutAll(ctx) {
|
|
11082
11258
|
try {
|
|
11083
|
-
const
|
|
11084
|
-
if (!
|
|
11085
|
-
return ctx.
|
|
11259
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11260
|
+
if (!userDocId) {
|
|
11261
|
+
return ctx.unauthorized("Authentication required");
|
|
11086
11262
|
}
|
|
11087
11263
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11088
|
-
await sessionService.terminateSession({ userId });
|
|
11089
|
-
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`);
|
|
11090
11266
|
ctx.body = { message: "Logged out from all devices successfully" };
|
|
11091
11267
|
} catch (err) {
|
|
11092
11268
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
@@ -11095,27 +11271,48 @@ var session$3 = {
|
|
|
11095
11271
|
},
|
|
11096
11272
|
/**
|
|
11097
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
|
+
*
|
|
11098
11280
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11099
11281
|
*/
|
|
11100
11282
|
async getCurrentSession(ctx) {
|
|
11101
11283
|
try {
|
|
11102
|
-
const
|
|
11284
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11103
11285
|
const token = extractBearerToken$1(ctx);
|
|
11104
|
-
if (!
|
|
11105
|
-
return ctx.
|
|
11286
|
+
if (!userDocId || !token) {
|
|
11287
|
+
return ctx.unauthorized("Authentication required");
|
|
11106
11288
|
}
|
|
11107
11289
|
const currentTokenHash = hashToken$2(token);
|
|
11108
11290
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11109
11291
|
filters: {
|
|
11110
|
-
user: { documentId:
|
|
11292
|
+
user: { documentId: userDocId },
|
|
11111
11293
|
tokenHash: currentTokenHash,
|
|
11112
11294
|
isActive: true
|
|
11113
11295
|
}
|
|
11114
11296
|
});
|
|
11115
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
|
+
}
|
|
11116
11313
|
return ctx.notFound("Current session not found");
|
|
11117
11314
|
}
|
|
11118
|
-
const settings2 = await getPluginSettings$
|
|
11315
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11119
11316
|
const enhanced = await enhanceSession(currentSession, {
|
|
11120
11317
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11121
11318
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11131,17 +11328,17 @@ var session$3 = {
|
|
|
11131
11328
|
}
|
|
11132
11329
|
},
|
|
11133
11330
|
/**
|
|
11134
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11331
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11135
11332
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11136
11333
|
*/
|
|
11137
11334
|
async terminateOwnSession(ctx) {
|
|
11138
11335
|
try {
|
|
11139
|
-
const
|
|
11336
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11140
11337
|
const { sessionId } = ctx.params;
|
|
11141
11338
|
const currentToken = extractBearerToken$1(ctx);
|
|
11142
11339
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11143
|
-
if (!
|
|
11144
|
-
return ctx.
|
|
11340
|
+
if (!userDocId) {
|
|
11341
|
+
return ctx.unauthorized("Authentication required");
|
|
11145
11342
|
}
|
|
11146
11343
|
if (!sessionId) {
|
|
11147
11344
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11154,19 +11351,23 @@ var session$3 = {
|
|
|
11154
11351
|
return ctx.notFound("Session not found");
|
|
11155
11352
|
}
|
|
11156
11353
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11157
|
-
if (sessionUserId !==
|
|
11158
|
-
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}`);
|
|
11159
11356
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11160
11357
|
}
|
|
11161
11358
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11162
|
-
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}`);
|
|
11163
11366
|
}
|
|
11164
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11165
|
-
await sessionService.terminateSession({ sessionId });
|
|
11166
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11167
11367
|
ctx.body = {
|
|
11168
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11169
|
-
success: true
|
|
11368
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11369
|
+
success: true,
|
|
11370
|
+
alreadyTerminated
|
|
11170
11371
|
};
|
|
11171
11372
|
} catch (err) {
|
|
11172
11373
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11174,9 +11375,7 @@ var session$3 = {
|
|
|
11174
11375
|
}
|
|
11175
11376
|
},
|
|
11176
11377
|
/**
|
|
11177
|
-
*
|
|
11178
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11179
|
-
*
|
|
11378
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11180
11379
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11181
11380
|
*/
|
|
11182
11381
|
async simulateTimeout(ctx) {
|
|
@@ -11195,13 +11394,16 @@ var session$3 = {
|
|
|
11195
11394
|
}
|
|
11196
11395
|
await strapi.documents(SESSION_UID$2).update({
|
|
11197
11396
|
documentId: sessionId,
|
|
11198
|
-
data: {
|
|
11397
|
+
data: {
|
|
11398
|
+
isActive: false,
|
|
11399
|
+
terminatedManually: false,
|
|
11400
|
+
terminationReason: "idle"
|
|
11401
|
+
}
|
|
11199
11402
|
});
|
|
11200
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11403
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11201
11404
|
ctx.body = {
|
|
11202
|
-
message: `Session ${sessionId} marked as timed out
|
|
11203
|
-
success: true
|
|
11204
|
-
terminatedManually: false
|
|
11405
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11406
|
+
success: true
|
|
11205
11407
|
};
|
|
11206
11408
|
} catch (err) {
|
|
11207
11409
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11210,13 +11412,12 @@ var session$3 = {
|
|
|
11210
11412
|
},
|
|
11211
11413
|
/**
|
|
11212
11414
|
* Terminates a specific session (admin action).
|
|
11213
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11214
11415
|
*/
|
|
11215
11416
|
async terminateSingleSession(ctx) {
|
|
11216
11417
|
try {
|
|
11217
11418
|
const { sessionId } = ctx.params;
|
|
11218
11419
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11219
|
-
await sessionService.terminateSession({ sessionId });
|
|
11420
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11220
11421
|
ctx.body = {
|
|
11221
11422
|
message: `Session ${sessionId} terminated`,
|
|
11222
11423
|
success: true
|
|
@@ -11228,13 +11429,12 @@ var session$3 = {
|
|
|
11228
11429
|
},
|
|
11229
11430
|
/**
|
|
11230
11431
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11231
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11232
11432
|
*/
|
|
11233
11433
|
async terminateAllUserSessions(ctx) {
|
|
11234
11434
|
try {
|
|
11235
11435
|
const { userId } = ctx.params;
|
|
11236
11436
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11237
|
-
await sessionService.terminateSession({ userId });
|
|
11437
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11238
11438
|
ctx.body = {
|
|
11239
11439
|
message: `All sessions terminated for user ${userId}`,
|
|
11240
11440
|
success: true
|
|
@@ -11246,9 +11446,6 @@ var session$3 = {
|
|
|
11246
11446
|
},
|
|
11247
11447
|
/**
|
|
11248
11448
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11249
|
-
*
|
|
11250
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11251
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11252
11449
|
*/
|
|
11253
11450
|
async getIpGeolocation(ctx) {
|
|
11254
11451
|
try {
|
|
@@ -11275,10 +11472,7 @@ var session$3 = {
|
|
|
11275
11472
|
return ctx.badRequest("Invalid IP address format");
|
|
11276
11473
|
}
|
|
11277
11474
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11278
|
-
const pluginStore = strapi.store({
|
|
11279
|
-
type: "plugin",
|
|
11280
|
-
name: "magic-sessionmanager"
|
|
11281
|
-
});
|
|
11475
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11282
11476
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11283
11477
|
if (!licenseKey) {
|
|
11284
11478
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11300,7 +11494,6 @@ var session$3 = {
|
|
|
11300
11494
|
},
|
|
11301
11495
|
/**
|
|
11302
11496
|
* Permanently deletes a session (admin action).
|
|
11303
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11304
11497
|
*/
|
|
11305
11498
|
async deleteSession(ctx) {
|
|
11306
11499
|
try {
|
|
@@ -11318,7 +11511,6 @@ var session$3 = {
|
|
|
11318
11511
|
},
|
|
11319
11512
|
/**
|
|
11320
11513
|
* Deletes all inactive sessions (admin action).
|
|
11321
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11322
11514
|
*/
|
|
11323
11515
|
async cleanInactiveSessions(ctx) {
|
|
11324
11516
|
try {
|
|
@@ -11336,9 +11528,6 @@ var session$3 = {
|
|
|
11336
11528
|
},
|
|
11337
11529
|
/**
|
|
11338
11530
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11339
|
-
*
|
|
11340
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11341
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11342
11531
|
*/
|
|
11343
11532
|
async toggleUserBlock(ctx) {
|
|
11344
11533
|
try {
|
|
@@ -11354,11 +11543,11 @@ var session$3 = {
|
|
|
11354
11543
|
}
|
|
11355
11544
|
}
|
|
11356
11545
|
if (!userDocumentId) {
|
|
11357
|
-
return ctx.
|
|
11546
|
+
return ctx.notFound("User not found");
|
|
11358
11547
|
}
|
|
11359
11548
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11360
11549
|
if (!user) {
|
|
11361
|
-
return ctx.
|
|
11550
|
+
return ctx.notFound("User not found");
|
|
11362
11551
|
}
|
|
11363
11552
|
const newBlockedStatus = !user.blocked;
|
|
11364
11553
|
await strapi.documents(USER_UID).update({
|
|
@@ -11367,7 +11556,7 @@ var session$3 = {
|
|
|
11367
11556
|
});
|
|
11368
11557
|
if (newBlockedStatus) {
|
|
11369
11558
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11370
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11559
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11371
11560
|
}
|
|
11372
11561
|
ctx.body = {
|
|
11373
11562
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11859,13 +12048,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11859
12048
|
const { parseUserAgent } = userAgentParser;
|
|
11860
12049
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11861
12050
|
const { enhanceSessions } = enhanceSession_1;
|
|
11862
|
-
const { getPluginSettings: getPluginSettings$
|
|
12051
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11863
12052
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11864
12053
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11865
12054
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11866
12055
|
const log = createLogger$1(strapi2);
|
|
11867
12056
|
async function getEnhanceOpts() {
|
|
11868
|
-
const settings2 = await getPluginSettings$
|
|
12057
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11869
12058
|
return {
|
|
11870
12059
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11871
12060
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11934,15 +12123,38 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11934
12123
|
}
|
|
11935
12124
|
},
|
|
11936
12125
|
/**
|
|
11937
|
-
* 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
|
+
*
|
|
11938
12141
|
* @param {Object} params
|
|
11939
12142
|
* @param {string} [params.sessionId]
|
|
11940
12143
|
* @param {string|number} [params.userId]
|
|
12144
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
11941
12145
|
* @returns {Promise<void>}
|
|
11942
12146
|
*/
|
|
11943
|
-
async terminateSession({ sessionId, userId }) {
|
|
12147
|
+
async terminateSession({ sessionId, userId, reason = "manual" }) {
|
|
11944
12148
|
try {
|
|
11945
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
|
+
};
|
|
11946
12158
|
if (sessionId) {
|
|
11947
12159
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11948
12160
|
documentId: sessionId,
|
|
@@ -11954,9 +12166,9 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11954
12166
|
}
|
|
11955
12167
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11956
12168
|
documentId: sessionId,
|
|
11957
|
-
data:
|
|
12169
|
+
data: updateData
|
|
11958
12170
|
});
|
|
11959
|
-
log.info(`Session ${sessionId} terminated (
|
|
12171
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
11960
12172
|
} else if (userId) {
|
|
11961
12173
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11962
12174
|
if (!userDocumentId) return;
|
|
@@ -11968,10 +12180,10 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11968
12180
|
for (const session2 of activeSessions) {
|
|
11969
12181
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11970
12182
|
documentId: session2.documentId,
|
|
11971
|
-
data:
|
|
12183
|
+
data: updateData
|
|
11972
12184
|
});
|
|
11973
12185
|
}
|
|
11974
|
-
log.info(`All sessions terminated
|
|
12186
|
+
log.info(`All sessions terminated for user ${userDocumentId} (reason: ${finalReason})`);
|
|
11975
12187
|
}
|
|
11976
12188
|
} catch (err) {
|
|
11977
12189
|
log.error("Error terminating session:", err);
|
|
@@ -12056,7 +12268,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12056
12268
|
async touch({ userId, sessionId, token }) {
|
|
12057
12269
|
try {
|
|
12058
12270
|
const now = /* @__PURE__ */ new Date();
|
|
12059
|
-
const settings2 = await getPluginSettings$
|
|
12271
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12060
12272
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12061
12273
|
let session2 = null;
|
|
12062
12274
|
let sessionDocId = sessionId;
|
|
@@ -12125,7 +12337,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12125
12337
|
*/
|
|
12126
12338
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12127
12339
|
try {
|
|
12128
|
-
const settings2 = await getPluginSettings$
|
|
12340
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12129
12341
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12130
12342
|
const now = /* @__PURE__ */ new Date();
|
|
12131
12343
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12138,7 +12350,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12138
12350
|
});
|
|
12139
12351
|
}).update({
|
|
12140
12352
|
is_active: false,
|
|
12141
|
-
terminated_manually:
|
|
12353
|
+
terminated_manually: false,
|
|
12354
|
+
termination_reason: "idle",
|
|
12142
12355
|
logout_time: now
|
|
12143
12356
|
});
|
|
12144
12357
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12179,7 +12392,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12179
12392
|
documentId,
|
|
12180
12393
|
data: {
|
|
12181
12394
|
isActive: false,
|
|
12182
|
-
terminatedManually:
|
|
12395
|
+
terminatedManually: false,
|
|
12396
|
+
terminationReason: "idle",
|
|
12183
12397
|
logoutTime: now
|
|
12184
12398
|
}
|
|
12185
12399
|
});
|
|
@@ -12210,6 +12424,81 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12210
12424
|
throw err;
|
|
12211
12425
|
}
|
|
12212
12426
|
},
|
|
12427
|
+
/**
|
|
12428
|
+
* Permanently deletes sessions that have been inactive past the
|
|
12429
|
+
* configured retention window. Distinct from `deleteInactiveSessions`
|
|
12430
|
+
* (which deletes ALL inactive sessions) — this only drops rows older
|
|
12431
|
+
* than `retentionDays`, so recently-terminated sessions stay queryable
|
|
12432
|
+
* for audits.
|
|
12433
|
+
*
|
|
12434
|
+
* @param {Object} [options]
|
|
12435
|
+
* @param {number} [options.retentionDays] Overrides the stored setting.
|
|
12436
|
+
* @param {boolean} [options.useDbDirect] Fast-path via single SQL
|
|
12437
|
+
* DELETE. Bypasses lifecycle hooks; use only when necessary.
|
|
12438
|
+
* @returns {Promise<number>} Number of sessions deleted
|
|
12439
|
+
*/
|
|
12440
|
+
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12441
|
+
try {
|
|
12442
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12443
|
+
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12444
|
+
if (effectiveDays === -1) {
|
|
12445
|
+
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
12446
|
+
return 0;
|
|
12447
|
+
}
|
|
12448
|
+
const cutoffDate = new Date(Date.now() - effectiveDays * 24 * 60 * 60 * 1e3);
|
|
12449
|
+
const wantDbDirect = useDbDirect ?? settings2.cleanupUseDbDirect === true;
|
|
12450
|
+
log.info(`[RETENTION] Deleting inactive sessions older than ${effectiveDays} days (before ${cutoffDate.toISOString()})`);
|
|
12451
|
+
if (wantDbDirect) {
|
|
12452
|
+
try {
|
|
12453
|
+
const deleted = await strapi2.db.connection("magic_sessions").where("is_active", false).andWhere(function whereOldEnough() {
|
|
12454
|
+
this.where("logout_time", "<", cutoffDate).orWhere(function whereNullLogout() {
|
|
12455
|
+
this.whereNull("logout_time").andWhere(function whereOldByActivity() {
|
|
12456
|
+
this.where("last_active", "<", cutoffDate).orWhere(function whereNullActivity() {
|
|
12457
|
+
this.whereNull("last_active").andWhere("login_time", "<", cutoffDate);
|
|
12458
|
+
});
|
|
12459
|
+
});
|
|
12460
|
+
});
|
|
12461
|
+
}).del();
|
|
12462
|
+
log.info(`[SUCCESS] Retention (db-direct) deleted ${deleted} old session(s)`);
|
|
12463
|
+
return deleted;
|
|
12464
|
+
} catch (err) {
|
|
12465
|
+
log.warn("[RETENTION] DB-direct delete failed, falling back to Document Service:", err.message);
|
|
12466
|
+
}
|
|
12467
|
+
}
|
|
12468
|
+
let deletedCount = 0;
|
|
12469
|
+
const BATCH = 200;
|
|
12470
|
+
while (true) {
|
|
12471
|
+
const batch = await strapi2.documents(SESSION_UID$1).findMany({
|
|
12472
|
+
filters: {
|
|
12473
|
+
isActive: false,
|
|
12474
|
+
$or: [
|
|
12475
|
+
{ logoutTime: { $lt: cutoffDate } },
|
|
12476
|
+
{ logoutTime: { $null: true }, lastActive: { $lt: cutoffDate } },
|
|
12477
|
+
{ logoutTime: { $null: true }, lastActive: { $null: true }, loginTime: { $lt: cutoffDate } }
|
|
12478
|
+
]
|
|
12479
|
+
},
|
|
12480
|
+
fields: ["documentId"],
|
|
12481
|
+
sort: { loginTime: "asc" },
|
|
12482
|
+
limit: BATCH
|
|
12483
|
+
});
|
|
12484
|
+
if (!batch || batch.length === 0) break;
|
|
12485
|
+
for (const session2 of batch) {
|
|
12486
|
+
try {
|
|
12487
|
+
await strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId });
|
|
12488
|
+
deletedCount++;
|
|
12489
|
+
} catch (err) {
|
|
12490
|
+
log.debug(`[RETENTION] Failed to delete session ${session2.documentId}:`, err.message);
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
if (batch.length < BATCH) break;
|
|
12494
|
+
}
|
|
12495
|
+
log.info(`[SUCCESS] Retention deleted ${deletedCount} old session(s)`);
|
|
12496
|
+
return deletedCount;
|
|
12497
|
+
} catch (err) {
|
|
12498
|
+
log.error("Error in retention cleanup:", err);
|
|
12499
|
+
return 0;
|
|
12500
|
+
}
|
|
12501
|
+
},
|
|
12213
12502
|
/**
|
|
12214
12503
|
* Permanently deletes all inactive sessions.
|
|
12215
12504
|
* Uses an inner scan loop that tolerates partial failures.
|
|
@@ -12255,7 +12544,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12255
12544
|
}
|
|
12256
12545
|
};
|
|
12257
12546
|
};
|
|
12258
|
-
const version$1 = "4.5.
|
|
12547
|
+
const version$1 = "4.5.2";
|
|
12259
12548
|
const require$$2 = {
|
|
12260
12549
|
version: version$1
|
|
12261
12550
|
};
|
|
@@ -13213,6 +13502,7 @@ var services$1 = {
|
|
|
13213
13502
|
geolocation,
|
|
13214
13503
|
notifications
|
|
13215
13504
|
};
|
|
13505
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13216
13506
|
const buckets = /* @__PURE__ */ new Map();
|
|
13217
13507
|
const prune = (now) => {
|
|
13218
13508
|
for (const [key, entry] of buckets) {
|
|
@@ -13226,10 +13516,45 @@ const callerKey = (ctx) => {
|
|
|
13226
13516
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13227
13517
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13228
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
|
+
}
|
|
13229
13552
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13230
|
-
const
|
|
13231
|
-
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;
|
|
13232
13556
|
return async (ctx, next) => {
|
|
13557
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13233
13558
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13234
13559
|
const now = Date.now();
|
|
13235
13560
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13263,7 +13588,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13263
13588
|
var rateLimit_1 = rateLimit;
|
|
13264
13589
|
var middlewares$1 = {
|
|
13265
13590
|
"last-seen": lastSeen,
|
|
13266
|
-
"rate-limit": rateLimit_1
|
|
13591
|
+
"rate-limit": rateLimit_1,
|
|
13592
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13267
13593
|
};
|
|
13268
13594
|
var lodashExports = requireLodash();
|
|
13269
13595
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51344,18 +51670,18 @@ const CSP_DEFAULTS = {
|
|
|
51344
51670
|
"blob:"
|
|
51345
51671
|
]
|
|
51346
51672
|
};
|
|
51347
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51673
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51348
51674
|
return middlewares2.map((currentMiddleware) => {
|
|
51349
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51350
|
-
return
|
|
51675
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51676
|
+
return middleware2;
|
|
51351
51677
|
}
|
|
51352
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51678
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51353
51679
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51354
51680
|
if (Array.isArray(objValue)) {
|
|
51355
51681
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51356
51682
|
}
|
|
51357
51683
|
return void 0;
|
|
51358
|
-
}, currentMiddleware,
|
|
51684
|
+
}, currentMiddleware, middleware2);
|
|
51359
51685
|
}
|
|
51360
51686
|
return currentMiddleware;
|
|
51361
51687
|
});
|