strapi-plugin-magic-sessionmanager 4.5.2 → 4.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.js +560 -212
- package/dist/server/index.mjs +560 -212
- 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
|
-
|
|
565
|
+
return next();
|
|
566
|
+
}
|
|
567
|
+
if (!ctx.state.user) {
|
|
568
|
+
return next();
|
|
482
569
|
}
|
|
483
|
-
|
|
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) {
|
|
589
|
+
try {
|
|
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();
|
|
533
605
|
try {
|
|
534
|
-
await sessionService.touch({
|
|
606
|
+
await sessionService.touch({
|
|
607
|
+
userId: userDocId,
|
|
608
|
+
sessionId: thisSession.documentId
|
|
609
|
+
});
|
|
535
610
|
} catch (err) {
|
|
536
611
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
537
612
|
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (strictMode) {
|
|
616
|
+
const iat = ctx.state.user?.iat;
|
|
617
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
618
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
619
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
620
|
+
ctx.state.userDocumentId = userDocId;
|
|
621
|
+
return next();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
strapi2.log.info(
|
|
625
|
+
`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId.substring(0, 8)}..., strictMode)`
|
|
626
|
+
);
|
|
627
|
+
return ctx.unauthorized("No valid session. Please login again.");
|
|
538
628
|
}
|
|
629
|
+
strapi2.log.debug(
|
|
630
|
+
`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId.substring(0, 8)}...) - allowing in non-strict mode`
|
|
631
|
+
);
|
|
632
|
+
ctx.state.userDocumentId = userDocId;
|
|
633
|
+
return next();
|
|
539
634
|
};
|
|
540
635
|
};
|
|
541
636
|
var jsonwebtoken = { exports: {} };
|
|
@@ -9502,8 +9597,12 @@ const getClientIp = getClientIp_1;
|
|
|
9502
9597
|
const { encryptToken: encryptToken$1, hashToken: hashToken$3 } = encryption;
|
|
9503
9598
|
const { createLogger: createLogger$3 } = logger;
|
|
9504
9599
|
const { resolveUserDocumentId: resolveUserDocumentId$3 } = resolveUser;
|
|
9505
|
-
const { getPluginSettings: getPluginSettings$
|
|
9600
|
+
const { getPluginSettings: getPluginSettings$4 } = settingsLoader;
|
|
9506
9601
|
const { extractBearerToken: extractBearerToken$2 } = extractToken;
|
|
9602
|
+
const {
|
|
9603
|
+
setSessionRejectionReason,
|
|
9604
|
+
consumeSessionRejectionReason
|
|
9605
|
+
} = rejectionCache;
|
|
9507
9606
|
const SESSION_UID$4 = "plugin::magic-sessionmanager.session";
|
|
9508
9607
|
const JWT_WRAPPED_FLAG = Symbol.for("magic-sessionmanager.jwt.wrapped");
|
|
9509
9608
|
const LOGIN_PATHS = /* @__PURE__ */ new Set([
|
|
@@ -9576,7 +9675,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9576
9675
|
}
|
|
9577
9676
|
log.info("Running initial session cleanup...");
|
|
9578
9677
|
try {
|
|
9579
|
-
const settings2 = await getPluginSettings$
|
|
9678
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9580
9679
|
await sessionService.cleanupInactiveSessions({
|
|
9581
9680
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9582
9681
|
});
|
|
@@ -9587,7 +9686,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9587
9686
|
let intervalMs = 30 * 60 * 1e3;
|
|
9588
9687
|
let useDbDirect = false;
|
|
9589
9688
|
try {
|
|
9590
|
-
const settings2 = await getPluginSettings$
|
|
9689
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9591
9690
|
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9592
9691
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9593
9692
|
} catch {
|
|
@@ -9608,7 +9707,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9608
9707
|
const scheduleRetention = async () => {
|
|
9609
9708
|
let useDbDirect = false;
|
|
9610
9709
|
try {
|
|
9611
|
-
const settings2 = await getPluginSettings$
|
|
9710
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9612
9711
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9613
9712
|
} catch {
|
|
9614
9713
|
}
|
|
@@ -9632,6 +9731,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9632
9731
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9633
9732
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9634
9733
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9734
|
+
strapi2.server.use(
|
|
9735
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9736
|
+
);
|
|
9737
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9635
9738
|
strapi2.server.use(
|
|
9636
9739
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9637
9740
|
);
|
|
@@ -9650,7 +9753,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9650
9753
|
}
|
|
9651
9754
|
let settings2 = {};
|
|
9652
9755
|
try {
|
|
9653
|
-
settings2 = await getPluginSettings$
|
|
9756
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9654
9757
|
} catch {
|
|
9655
9758
|
settings2 = {};
|
|
9656
9759
|
}
|
|
@@ -9826,7 +9929,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9826
9929
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9827
9930
|
let maxFailed = 0;
|
|
9828
9931
|
try {
|
|
9829
|
-
const settings2 = await getPluginSettings$
|
|
9932
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9830
9933
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9831
9934
|
} catch {
|
|
9832
9935
|
maxFailed = 0;
|
|
@@ -9865,17 +9968,28 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9865
9968
|
});
|
|
9866
9969
|
log.info("[SUCCESS] Failed-login lockout middleware mounted");
|
|
9867
9970
|
}
|
|
9971
|
+
function isJwtIssuingPath(path2, method) {
|
|
9972
|
+
if (!path2) return false;
|
|
9973
|
+
const get2 = method === "GET";
|
|
9974
|
+
const post = method === "POST";
|
|
9975
|
+
if (post && path2 === "/api/auth/local") return true;
|
|
9976
|
+
if (post && path2 === "/api/auth/local/register") return true;
|
|
9977
|
+
if (post && path2 === "/api/auth/reset-password") return true;
|
|
9978
|
+
if ((get2 || post) && path2 === "/api/auth/email-confirmation") return true;
|
|
9979
|
+
if (get2 && /^\/api\/auth\/[a-z0-9-]+\/callback$/i.test(path2)) return true;
|
|
9980
|
+
if ((get2 || post) && path2.startsWith("/api/magic-link/login")) return true;
|
|
9981
|
+
if (post && path2 === "/api/magic-link/verify-mfa-totp") return true;
|
|
9982
|
+
if (post && path2 === "/api/magic-link/otp/verify") return true;
|
|
9983
|
+
if (post && path2 === "/api/magic-link/login-totp") return true;
|
|
9984
|
+
if ((get2 || post) && path2.startsWith("/api/passwordless/")) return true;
|
|
9985
|
+
return false;
|
|
9986
|
+
}
|
|
9868
9987
|
function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
9869
9988
|
strapi2.server.use(async (ctx, next) => {
|
|
9870
9989
|
await next();
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
const isMagicLinkOTP = ctx.path.includes("/magic-link/otp/verify") && ctx.method === "POST";
|
|
9875
|
-
const isMagicLink = isMagicLinkLogin || isMagicLinkMFA || isMagicLinkOTP;
|
|
9876
|
-
if (!((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.jwt && ctx.body.user)) {
|
|
9877
|
-
return;
|
|
9878
|
-
}
|
|
9990
|
+
if (!isJwtIssuingPath(ctx.path, ctx.method)) return;
|
|
9991
|
+
if (ctx.status !== 200) return;
|
|
9992
|
+
if (!ctx.body || !ctx.body.jwt || !ctx.body.user) return;
|
|
9879
9993
|
try {
|
|
9880
9994
|
const user = ctx.body.user;
|
|
9881
9995
|
const ip = getClientIp(ctx);
|
|
@@ -9905,7 +10019,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9905
10019
|
}
|
|
9906
10020
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9907
10021
|
try {
|
|
9908
|
-
const settings2 = await getPluginSettings$
|
|
10022
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9909
10023
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9910
10024
|
return;
|
|
9911
10025
|
}
|
|
@@ -10076,11 +10190,19 @@ function mountRefreshTokenInterceptor({ strapi: strapi2, log }) {
|
|
|
10076
10190
|
log.info("[SUCCESS] Refresh token interceptor middleware mounted");
|
|
10077
10191
|
}
|
|
10078
10192
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
10193
|
+
const PERMISSIONS_VERSION = 2;
|
|
10079
10194
|
try {
|
|
10080
10195
|
const pluginStore = strapi2.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
10081
|
-
const
|
|
10082
|
-
if (
|
|
10083
|
-
|
|
10196
|
+
const storedVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10197
|
+
if (storedVersion === void 0) {
|
|
10198
|
+
const legacyFlag = await pluginStore.get({ key: "contentApiPermissionsInitialized" });
|
|
10199
|
+
if (legacyFlag === true) {
|
|
10200
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: 1 });
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
const effectiveVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10204
|
+
if (effectiveVersion >= PERMISSIONS_VERSION) {
|
|
10205
|
+
log.debug("Content-API permissions already at current version (skipping auto-setup)");
|
|
10084
10206
|
return;
|
|
10085
10207
|
}
|
|
10086
10208
|
const ROLE_UID = "plugin::users-permissions.role";
|
|
@@ -10098,6 +10220,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10098
10220
|
const requiredActions = [
|
|
10099
10221
|
"plugin::magic-sessionmanager.session.logout",
|
|
10100
10222
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
10223
|
+
"plugin::magic-sessionmanager.session.logoutOthers",
|
|
10101
10224
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
10102
10225
|
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
10103
10226
|
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
@@ -10114,7 +10237,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10114
10237
|
const existingActions = existingPermissions.map((p) => p.action);
|
|
10115
10238
|
const missingActions = requiredActions.filter((action) => !existingActions.includes(action));
|
|
10116
10239
|
if (missingActions.length === 0) {
|
|
10117
|
-
await pluginStore.set({ key: "
|
|
10240
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10118
10241
|
log.debug("Content-API permissions already configured");
|
|
10119
10242
|
return;
|
|
10120
10243
|
}
|
|
@@ -10124,7 +10247,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10124
10247
|
});
|
|
10125
10248
|
log.info(`[PERMISSION] Enabled ${action} for authenticated users`);
|
|
10126
10249
|
}
|
|
10127
|
-
await pluginStore.set({ key: "
|
|
10250
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10128
10251
|
log.info("[SUCCESS] Content-API permissions configured for authenticated users");
|
|
10129
10252
|
} catch (err) {
|
|
10130
10253
|
log.warn("Could not auto-configure permissions:", err.message);
|
|
@@ -10222,7 +10345,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10222
10345
|
}
|
|
10223
10346
|
let settings2;
|
|
10224
10347
|
try {
|
|
10225
|
-
settings2 = await getPluginSettings$
|
|
10348
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10226
10349
|
} catch {
|
|
10227
10350
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10228
10351
|
}
|
|
@@ -10247,6 +10370,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10247
10370
|
strapi2.log.info(
|
|
10248
10371
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10249
10372
|
);
|
|
10373
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10250
10374
|
return null;
|
|
10251
10375
|
}
|
|
10252
10376
|
} catch {
|
|
@@ -10257,7 +10381,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10257
10381
|
user: { documentId: userDocId },
|
|
10258
10382
|
tokenHash: tokenHashValue
|
|
10259
10383
|
},
|
|
10260
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10384
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10261
10385
|
});
|
|
10262
10386
|
if (thisSession) {
|
|
10263
10387
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10266,15 +10390,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10266
10390
|
);
|
|
10267
10391
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10268
10392
|
documentId: thisSession.documentId,
|
|
10269
|
-
data: {
|
|
10393
|
+
data: {
|
|
10394
|
+
isActive: false,
|
|
10395
|
+
terminatedManually: false,
|
|
10396
|
+
terminationReason: "expired",
|
|
10397
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10398
|
+
}
|
|
10270
10399
|
});
|
|
10400
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10271
10401
|
return null;
|
|
10272
10402
|
}
|
|
10273
|
-
if (thisSession.
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10403
|
+
if (thisSession.isActive === false) {
|
|
10404
|
+
const reason = thisSession.terminationReason || (thisSession.terminatedManually === true ? "manual" : null);
|
|
10405
|
+
if (reason) {
|
|
10406
|
+
strapi2.log.info(
|
|
10407
|
+
`[magic-sessionmanager] [JWT-REJECTED] Session inactive (reason: ${reason}) for user ${userDocId.substring(0, 8)}...`
|
|
10408
|
+
);
|
|
10409
|
+
setSessionRejectionReason(tokenHashValue, reason);
|
|
10410
|
+
return null;
|
|
10411
|
+
}
|
|
10278
10412
|
}
|
|
10279
10413
|
if (thisSession.isActive) {
|
|
10280
10414
|
resetErrorCounter();
|
|
@@ -10289,8 +10423,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10289
10423
|
);
|
|
10290
10424
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10291
10425
|
documentId: thisSession.documentId,
|
|
10292
|
-
data: {
|
|
10426
|
+
data: {
|
|
10427
|
+
terminatedManually: false,
|
|
10428
|
+
terminationReason: "idle",
|
|
10429
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10430
|
+
}
|
|
10293
10431
|
});
|
|
10432
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10294
10433
|
return null;
|
|
10295
10434
|
}
|
|
10296
10435
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10492,6 +10631,16 @@ const attributes = {
|
|
|
10492
10631
|
"default": false,
|
|
10493
10632
|
required: false
|
|
10494
10633
|
},
|
|
10634
|
+
terminationReason: {
|
|
10635
|
+
type: "enumeration",
|
|
10636
|
+
"enum": [
|
|
10637
|
+
"manual",
|
|
10638
|
+
"idle",
|
|
10639
|
+
"expired",
|
|
10640
|
+
"blocked"
|
|
10641
|
+
],
|
|
10642
|
+
required: false
|
|
10643
|
+
},
|
|
10495
10644
|
geoLocation: {
|
|
10496
10645
|
type: "json"
|
|
10497
10646
|
},
|
|
@@ -10525,10 +10674,16 @@ var contentTypes$2 = {
|
|
|
10525
10674
|
}
|
|
10526
10675
|
};
|
|
10527
10676
|
const writeRateLimit = [
|
|
10528
|
-
{
|
|
10677
|
+
{
|
|
10678
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10679
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10680
|
+
}
|
|
10529
10681
|
];
|
|
10530
10682
|
const readRateLimit = [
|
|
10531
|
-
{
|
|
10683
|
+
{
|
|
10684
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10685
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10686
|
+
}
|
|
10532
10687
|
];
|
|
10533
10688
|
var contentApi$1 = {
|
|
10534
10689
|
type: "content-api",
|
|
@@ -10551,7 +10706,17 @@ var contentApi$1 = {
|
|
|
10551
10706
|
config: {
|
|
10552
10707
|
auth: { strategies: ["users-permissions"] },
|
|
10553
10708
|
middlewares: writeRateLimit,
|
|
10554
|
-
description: "Logout from
|
|
10709
|
+
description: "Logout from ALL devices including the current one (requires JWT)"
|
|
10710
|
+
}
|
|
10711
|
+
},
|
|
10712
|
+
{
|
|
10713
|
+
method: "POST",
|
|
10714
|
+
path: "/logout-others",
|
|
10715
|
+
handler: "session.logoutOthers",
|
|
10716
|
+
config: {
|
|
10717
|
+
auth: { strategies: ["users-permissions"] },
|
|
10718
|
+
middlewares: writeRateLimit,
|
|
10719
|
+
description: "Logout from all OTHER devices, keep current session alive (requires JWT)"
|
|
10555
10720
|
}
|
|
10556
10721
|
},
|
|
10557
10722
|
// ================== SESSION QUERIES ==================
|
|
@@ -10931,15 +11096,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10931
11096
|
const { hashToken: hashToken$2 } = encryption;
|
|
10932
11097
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10933
11098
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10934
|
-
const { getPluginSettings: getPluginSettings$
|
|
11099
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10935
11100
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10936
11101
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10937
11102
|
const USER_UID = "plugin::users-permissions.user";
|
|
11103
|
+
const OWN_SESSIONS_LIMIT = 200;
|
|
11104
|
+
async function resolveAuthUserDocId(ctx) {
|
|
11105
|
+
const u2 = ctx.state.user;
|
|
11106
|
+
if (!u2) return null;
|
|
11107
|
+
if (u2.documentId) return u2.documentId;
|
|
11108
|
+
if (u2.id) return resolveUserDocumentId$2(strapi, u2.id);
|
|
11109
|
+
return null;
|
|
11110
|
+
}
|
|
10938
11111
|
var session$3 = {
|
|
10939
11112
|
/**
|
|
10940
11113
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10941
11114
|
* @route GET /magic-sessionmanager/sessions
|
|
10942
|
-
* @returns {object} `{ data, meta }`
|
|
10943
11115
|
*/
|
|
10944
11116
|
async getAllSessionsAdmin(ctx) {
|
|
10945
11117
|
try {
|
|
@@ -10961,7 +11133,6 @@ var session$3 = {
|
|
|
10961
11133
|
/**
|
|
10962
11134
|
* Lists currently-active sessions only.
|
|
10963
11135
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10964
|
-
* @returns {object} `{ data, meta }`
|
|
10965
11136
|
*/
|
|
10966
11137
|
async getActiveSessions(ctx) {
|
|
10967
11138
|
try {
|
|
@@ -10977,35 +11148,33 @@ var session$3 = {
|
|
|
10977
11148
|
}
|
|
10978
11149
|
},
|
|
10979
11150
|
/**
|
|
10980
|
-
* Returns the authenticated user's own sessions,
|
|
10981
|
-
* flagged via `isCurrentSession`.
|
|
10982
|
-
*
|
|
11151
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10983
11152
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10984
|
-
* @returns {object} `{ data, meta }`
|
|
10985
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10986
11153
|
*/
|
|
10987
11154
|
async getOwnSessions(ctx) {
|
|
10988
11155
|
try {
|
|
10989
|
-
const
|
|
11156
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11157
|
+
if (!userDocId) {
|
|
11158
|
+
return ctx.unauthorized("Authentication required");
|
|
11159
|
+
}
|
|
10990
11160
|
const currentToken = extractBearerToken$1(ctx);
|
|
10991
11161
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
10992
|
-
if (!userId) {
|
|
10993
|
-
return ctx.throw(401, "Unauthorized");
|
|
10994
|
-
}
|
|
10995
11162
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
10996
|
-
filters: { user: { documentId:
|
|
11163
|
+
filters: { user: { documentId: userDocId } },
|
|
10997
11164
|
sort: { loginTime: "desc" },
|
|
10998
|
-
limit:
|
|
11165
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
10999
11166
|
});
|
|
11000
|
-
const
|
|
11167
|
+
const hasMore = allSessions.length > OWN_SESSIONS_LIMIT;
|
|
11168
|
+
const paged = hasMore ? allSessions.slice(0, OWN_SESSIONS_LIMIT) : allSessions;
|
|
11169
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11001
11170
|
const enhanceOpts = {
|
|
11002
11171
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11003
11172
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
11004
11173
|
strapi
|
|
11005
11174
|
};
|
|
11006
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11175
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
11007
11176
|
for (const s3 of sessionsWithCurrent) {
|
|
11008
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11177
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
11009
11178
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
11010
11179
|
));
|
|
11011
11180
|
}
|
|
@@ -11018,7 +11187,9 @@ var session$3 = {
|
|
|
11018
11187
|
data: sessionsWithCurrent,
|
|
11019
11188
|
meta: {
|
|
11020
11189
|
count: sessionsWithCurrent.length,
|
|
11021
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11190
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11191
|
+
hasMore,
|
|
11192
|
+
limit: OWN_SESSIONS_LIMIT
|
|
11022
11193
|
}
|
|
11023
11194
|
};
|
|
11024
11195
|
} catch (err) {
|
|
@@ -11027,18 +11198,14 @@ var session$3 = {
|
|
|
11027
11198
|
}
|
|
11028
11199
|
},
|
|
11029
11200
|
/**
|
|
11030
|
-
* Get a specific user's sessions. Admins
|
|
11201
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11031
11202
|
* users can only query themselves.
|
|
11032
|
-
*
|
|
11033
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11034
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11035
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11036
11203
|
*/
|
|
11037
11204
|
async getUserSessions(ctx) {
|
|
11038
11205
|
try {
|
|
11039
11206
|
const { userId } = ctx.params;
|
|
11040
11207
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11041
|
-
const requestingUserDocId = ctx
|
|
11208
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11042
11209
|
if (!isAdminRequest) {
|
|
11043
11210
|
if (!requestingUserDocId) {
|
|
11044
11211
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11067,73 +11234,177 @@ var session$3 = {
|
|
|
11067
11234
|
*/
|
|
11068
11235
|
async logout(ctx) {
|
|
11069
11236
|
try {
|
|
11070
|
-
const
|
|
11237
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11071
11238
|
const token = extractBearerToken$1(ctx);
|
|
11072
|
-
if (!
|
|
11073
|
-
return ctx.
|
|
11239
|
+
if (!userDocId || !token) {
|
|
11240
|
+
return ctx.unauthorized("Authentication required");
|
|
11074
11241
|
}
|
|
11075
11242
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11076
11243
|
const currentTokenHash = hashToken$2(token);
|
|
11077
11244
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11078
11245
|
filters: {
|
|
11079
|
-
user: { documentId:
|
|
11246
|
+
user: { documentId: userDocId },
|
|
11080
11247
|
tokenHash: currentTokenHash,
|
|
11081
11248
|
isActive: true
|
|
11082
11249
|
},
|
|
11083
11250
|
fields: ["documentId"]
|
|
11084
11251
|
});
|
|
11252
|
+
let terminated = false;
|
|
11085
11253
|
if (matchingSession) {
|
|
11086
|
-
await sessionService.terminateSession({
|
|
11087
|
-
|
|
11254
|
+
await sessionService.terminateSession({
|
|
11255
|
+
sessionId: matchingSession.documentId,
|
|
11256
|
+
reason: "manual"
|
|
11257
|
+
});
|
|
11258
|
+
terminated = true;
|
|
11259
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out (session ${matchingSession.documentId})`);
|
|
11088
11260
|
}
|
|
11089
|
-
ctx.body = {
|
|
11261
|
+
ctx.body = {
|
|
11262
|
+
message: "Logged out successfully",
|
|
11263
|
+
terminated
|
|
11264
|
+
};
|
|
11090
11265
|
} catch (err) {
|
|
11091
11266
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11092
11267
|
ctx.throw(500, "Error during logout");
|
|
11093
11268
|
}
|
|
11094
11269
|
},
|
|
11095
11270
|
/**
|
|
11096
|
-
* Terminates
|
|
11271
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11272
|
+
* current one. After this call the caller will also be logged out on
|
|
11273
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11274
|
+
* with reason=manual).
|
|
11275
|
+
*
|
|
11276
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11277
|
+
* `/logout-others` below.
|
|
11278
|
+
*
|
|
11097
11279
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11098
11280
|
*/
|
|
11099
11281
|
async logoutAll(ctx) {
|
|
11100
11282
|
try {
|
|
11101
|
-
const
|
|
11102
|
-
if (!
|
|
11103
|
-
return ctx.
|
|
11283
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11284
|
+
if (!userDocId) {
|
|
11285
|
+
return ctx.unauthorized("Authentication required");
|
|
11104
11286
|
}
|
|
11105
11287
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11106
|
-
await sessionService.terminateSession({
|
|
11107
|
-
|
|
11108
|
-
|
|
11288
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11289
|
+
userId: userDocId,
|
|
11290
|
+
reason: "manual"
|
|
11291
|
+
});
|
|
11292
|
+
strapi.log.info(
|
|
11293
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11294
|
+
);
|
|
11295
|
+
ctx.body = {
|
|
11296
|
+
message: "Logged out from all devices successfully",
|
|
11297
|
+
terminatedCount
|
|
11298
|
+
};
|
|
11109
11299
|
} catch (err) {
|
|
11110
11300
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11111
11301
|
ctx.throw(500, "Error during logout");
|
|
11112
11302
|
}
|
|
11113
11303
|
},
|
|
11304
|
+
/**
|
|
11305
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11306
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11307
|
+
* caller stays logged in on the device they are using.
|
|
11308
|
+
*
|
|
11309
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11310
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11311
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11312
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11313
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11314
|
+
* its success message.
|
|
11315
|
+
*
|
|
11316
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11317
|
+
*/
|
|
11318
|
+
async logoutOthers(ctx) {
|
|
11319
|
+
try {
|
|
11320
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11321
|
+
const token = extractBearerToken$1(ctx);
|
|
11322
|
+
if (!userDocId || !token) {
|
|
11323
|
+
return ctx.unauthorized("Authentication required");
|
|
11324
|
+
}
|
|
11325
|
+
const currentTokenHash = hashToken$2(token);
|
|
11326
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11327
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11328
|
+
fields: ["documentId"]
|
|
11329
|
+
});
|
|
11330
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11331
|
+
if (!currentSession) {
|
|
11332
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11333
|
+
userId: userDocId,
|
|
11334
|
+
reason: "manual"
|
|
11335
|
+
});
|
|
11336
|
+
strapi.log.warn(
|
|
11337
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11338
|
+
);
|
|
11339
|
+
ctx.body = {
|
|
11340
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11341
|
+
terminatedCount: terminatedCount2,
|
|
11342
|
+
fellBackToLogoutAll: true
|
|
11343
|
+
};
|
|
11344
|
+
return;
|
|
11345
|
+
}
|
|
11346
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11347
|
+
userId: userDocId,
|
|
11348
|
+
exceptSessionId: currentSession.documentId,
|
|
11349
|
+
reason: "manual"
|
|
11350
|
+
});
|
|
11351
|
+
strapi.log.info(
|
|
11352
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11353
|
+
);
|
|
11354
|
+
ctx.body = {
|
|
11355
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11356
|
+
terminatedCount,
|
|
11357
|
+
currentSessionPreserved: true
|
|
11358
|
+
};
|
|
11359
|
+
} catch (err) {
|
|
11360
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11361
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11362
|
+
}
|
|
11363
|
+
},
|
|
11114
11364
|
/**
|
|
11115
11365
|
* Returns the session associated with the current JWT.
|
|
11366
|
+
*
|
|
11367
|
+
* During the post-login grace window the session-create write may not
|
|
11368
|
+
* yet be visible. In that case we return 202 Accepted with
|
|
11369
|
+
* `{ pending: true }` so the client knows to retry shortly instead of
|
|
11370
|
+
* interpreting a 404 as "no session at all".
|
|
11371
|
+
*
|
|
11116
11372
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11117
11373
|
*/
|
|
11118
11374
|
async getCurrentSession(ctx) {
|
|
11119
11375
|
try {
|
|
11120
|
-
const
|
|
11376
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11121
11377
|
const token = extractBearerToken$1(ctx);
|
|
11122
|
-
if (!
|
|
11123
|
-
return ctx.
|
|
11378
|
+
if (!userDocId || !token) {
|
|
11379
|
+
return ctx.unauthorized("Authentication required");
|
|
11124
11380
|
}
|
|
11125
11381
|
const currentTokenHash = hashToken$2(token);
|
|
11126
11382
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11127
11383
|
filters: {
|
|
11128
|
-
user: { documentId:
|
|
11384
|
+
user: { documentId: userDocId },
|
|
11129
11385
|
tokenHash: currentTokenHash,
|
|
11130
11386
|
isActive: true
|
|
11131
11387
|
}
|
|
11132
11388
|
});
|
|
11133
11389
|
if (!currentSession) {
|
|
11390
|
+
const settings3 = await getPluginSettings$3(strapi);
|
|
11391
|
+
const gracePeriodMs = Math.max(0, Number(settings3.sessionCreationGraceMs) || 5e3);
|
|
11392
|
+
const iat = ctx.state.user?.iat || ctx.state.auth?.credentials?.iat || null;
|
|
11393
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
11394
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
11395
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
11396
|
+
ctx.status = 202;
|
|
11397
|
+
ctx.body = {
|
|
11398
|
+
data: null,
|
|
11399
|
+
meta: { pending: true, retryAfterMs: gracePeriodMs - ageMs },
|
|
11400
|
+
message: "Session is still being created — please retry shortly."
|
|
11401
|
+
};
|
|
11402
|
+
return;
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11134
11405
|
return ctx.notFound("Current session not found");
|
|
11135
11406
|
}
|
|
11136
|
-
const settings2 = await getPluginSettings$
|
|
11407
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11137
11408
|
const enhanced = await enhanceSession(currentSession, {
|
|
11138
11409
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11139
11410
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11149,17 +11420,17 @@ var session$3 = {
|
|
|
11149
11420
|
}
|
|
11150
11421
|
},
|
|
11151
11422
|
/**
|
|
11152
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11423
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11153
11424
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11154
11425
|
*/
|
|
11155
11426
|
async terminateOwnSession(ctx) {
|
|
11156
11427
|
try {
|
|
11157
|
-
const
|
|
11428
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11158
11429
|
const { sessionId } = ctx.params;
|
|
11159
11430
|
const currentToken = extractBearerToken$1(ctx);
|
|
11160
11431
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11161
|
-
if (!
|
|
11162
|
-
return ctx.
|
|
11432
|
+
if (!userDocId) {
|
|
11433
|
+
return ctx.unauthorized("Authentication required");
|
|
11163
11434
|
}
|
|
11164
11435
|
if (!sessionId) {
|
|
11165
11436
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11172,19 +11443,23 @@ var session$3 = {
|
|
|
11172
11443
|
return ctx.notFound("Session not found");
|
|
11173
11444
|
}
|
|
11174
11445
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11175
|
-
if (sessionUserId !==
|
|
11176
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
11446
|
+
if (sessionUserId !== userDocId) {
|
|
11447
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userDocId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
11177
11448
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11178
11449
|
}
|
|
11179
11450
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11180
|
-
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
11451
|
+
return ctx.badRequest("Cannot terminate the current session. Use /logout instead.");
|
|
11452
|
+
}
|
|
11453
|
+
const alreadyTerminated = sessionToTerminate.isActive === false;
|
|
11454
|
+
if (!alreadyTerminated) {
|
|
11455
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11456
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11457
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} terminated own session ${sessionId}`);
|
|
11181
11458
|
}
|
|
11182
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11183
|
-
await sessionService.terminateSession({ sessionId });
|
|
11184
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11185
11459
|
ctx.body = {
|
|
11186
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11187
|
-
success: true
|
|
11460
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11461
|
+
success: true,
|
|
11462
|
+
alreadyTerminated
|
|
11188
11463
|
};
|
|
11189
11464
|
} catch (err) {
|
|
11190
11465
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11192,9 +11467,7 @@ var session$3 = {
|
|
|
11192
11467
|
}
|
|
11193
11468
|
},
|
|
11194
11469
|
/**
|
|
11195
|
-
*
|
|
11196
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11197
|
-
*
|
|
11470
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11198
11471
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11199
11472
|
*/
|
|
11200
11473
|
async simulateTimeout(ctx) {
|
|
@@ -11213,13 +11486,16 @@ var session$3 = {
|
|
|
11213
11486
|
}
|
|
11214
11487
|
await strapi.documents(SESSION_UID$2).update({
|
|
11215
11488
|
documentId: sessionId,
|
|
11216
|
-
data: {
|
|
11489
|
+
data: {
|
|
11490
|
+
isActive: false,
|
|
11491
|
+
terminatedManually: false,
|
|
11492
|
+
terminationReason: "idle"
|
|
11493
|
+
}
|
|
11217
11494
|
});
|
|
11218
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11495
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11219
11496
|
ctx.body = {
|
|
11220
|
-
message: `Session ${sessionId} marked as timed out
|
|
11221
|
-
success: true
|
|
11222
|
-
terminatedManually: false
|
|
11497
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11498
|
+
success: true
|
|
11223
11499
|
};
|
|
11224
11500
|
} catch (err) {
|
|
11225
11501
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11228,13 +11504,12 @@ var session$3 = {
|
|
|
11228
11504
|
},
|
|
11229
11505
|
/**
|
|
11230
11506
|
* Terminates a specific session (admin action).
|
|
11231
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11232
11507
|
*/
|
|
11233
11508
|
async terminateSingleSession(ctx) {
|
|
11234
11509
|
try {
|
|
11235
11510
|
const { sessionId } = ctx.params;
|
|
11236
11511
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11237
|
-
await sessionService.terminateSession({ sessionId });
|
|
11512
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11238
11513
|
ctx.body = {
|
|
11239
11514
|
message: `Session ${sessionId} terminated`,
|
|
11240
11515
|
success: true
|
|
@@ -11246,13 +11521,12 @@ var session$3 = {
|
|
|
11246
11521
|
},
|
|
11247
11522
|
/**
|
|
11248
11523
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11249
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11250
11524
|
*/
|
|
11251
11525
|
async terminateAllUserSessions(ctx) {
|
|
11252
11526
|
try {
|
|
11253
11527
|
const { userId } = ctx.params;
|
|
11254
11528
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11255
|
-
await sessionService.terminateSession({ userId });
|
|
11529
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11256
11530
|
ctx.body = {
|
|
11257
11531
|
message: `All sessions terminated for user ${userId}`,
|
|
11258
11532
|
success: true
|
|
@@ -11264,9 +11538,6 @@ var session$3 = {
|
|
|
11264
11538
|
},
|
|
11265
11539
|
/**
|
|
11266
11540
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11267
|
-
*
|
|
11268
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11269
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11270
11541
|
*/
|
|
11271
11542
|
async getIpGeolocation(ctx) {
|
|
11272
11543
|
try {
|
|
@@ -11293,10 +11564,7 @@ var session$3 = {
|
|
|
11293
11564
|
return ctx.badRequest("Invalid IP address format");
|
|
11294
11565
|
}
|
|
11295
11566
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11296
|
-
const pluginStore = strapi.store({
|
|
11297
|
-
type: "plugin",
|
|
11298
|
-
name: "magic-sessionmanager"
|
|
11299
|
-
});
|
|
11567
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11300
11568
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11301
11569
|
if (!licenseKey) {
|
|
11302
11570
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11318,7 +11586,6 @@ var session$3 = {
|
|
|
11318
11586
|
},
|
|
11319
11587
|
/**
|
|
11320
11588
|
* Permanently deletes a session (admin action).
|
|
11321
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11322
11589
|
*/
|
|
11323
11590
|
async deleteSession(ctx) {
|
|
11324
11591
|
try {
|
|
@@ -11336,7 +11603,6 @@ var session$3 = {
|
|
|
11336
11603
|
},
|
|
11337
11604
|
/**
|
|
11338
11605
|
* Deletes all inactive sessions (admin action).
|
|
11339
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11340
11606
|
*/
|
|
11341
11607
|
async cleanInactiveSessions(ctx) {
|
|
11342
11608
|
try {
|
|
@@ -11354,9 +11620,6 @@ var session$3 = {
|
|
|
11354
11620
|
},
|
|
11355
11621
|
/**
|
|
11356
11622
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11357
|
-
*
|
|
11358
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11359
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11360
11623
|
*/
|
|
11361
11624
|
async toggleUserBlock(ctx) {
|
|
11362
11625
|
try {
|
|
@@ -11372,11 +11635,11 @@ var session$3 = {
|
|
|
11372
11635
|
}
|
|
11373
11636
|
}
|
|
11374
11637
|
if (!userDocumentId) {
|
|
11375
|
-
return ctx.
|
|
11638
|
+
return ctx.notFound("User not found");
|
|
11376
11639
|
}
|
|
11377
11640
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11378
11641
|
if (!user) {
|
|
11379
|
-
return ctx.
|
|
11642
|
+
return ctx.notFound("User not found");
|
|
11380
11643
|
}
|
|
11381
11644
|
const newBlockedStatus = !user.blocked;
|
|
11382
11645
|
await strapi.documents(USER_UID).update({
|
|
@@ -11385,7 +11648,7 @@ var session$3 = {
|
|
|
11385
11648
|
});
|
|
11386
11649
|
if (newBlockedStatus) {
|
|
11387
11650
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11388
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11651
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11389
11652
|
}
|
|
11390
11653
|
ctx.body = {
|
|
11391
11654
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11877,13 +12140,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11877
12140
|
const { parseUserAgent } = userAgentParser;
|
|
11878
12141
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11879
12142
|
const { enhanceSessions } = enhanceSession_1;
|
|
11880
|
-
const { getPluginSettings: getPluginSettings$
|
|
12143
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11881
12144
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11882
12145
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11883
12146
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11884
12147
|
const log = createLogger$1(strapi2);
|
|
11885
12148
|
async function getEnhanceOpts() {
|
|
11886
|
-
const settings2 = await getPluginSettings$
|
|
12149
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11887
12150
|
return {
|
|
11888
12151
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11889
12152
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11952,15 +12215,44 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11952
12215
|
}
|
|
11953
12216
|
},
|
|
11954
12217
|
/**
|
|
11955
|
-
* Terminates a single session
|
|
12218
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12219
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12220
|
+
* can communicate the cause to the client.
|
|
12221
|
+
*
|
|
12222
|
+
* Supported reasons:
|
|
12223
|
+
* - 'manual': user clicked logout, or admin terminated a session
|
|
12224
|
+
* - 'idle': inactivity timeout cleanup
|
|
12225
|
+
* - 'expired': maxSessionAgeDays exceeded
|
|
12226
|
+
* - 'blocked': the owning user was marked blocked
|
|
12227
|
+
*
|
|
12228
|
+
* For backwards compatibility `terminatedManually` is still set true
|
|
12229
|
+
* only when reason === 'manual'; idle/expired/blocked paths set it
|
|
12230
|
+
* false so reporting dashboards that queried that boolean continue
|
|
12231
|
+
* to work, while new code relies on `terminationReason`.
|
|
12232
|
+
*
|
|
11956
12233
|
* @param {Object} params
|
|
11957
|
-
* @param {string} [params.sessionId]
|
|
11958
|
-
* @param {string|number} [params.userId]
|
|
11959
|
-
*
|
|
12234
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12235
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12236
|
+
* of this user …
|
|
12237
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12238
|
+
* meaningful together with
|
|
12239
|
+
* `userId`. Used by
|
|
12240
|
+
* /logout-other-devices so
|
|
12241
|
+
* the caller stays logged in.
|
|
12242
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12243
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
11960
12244
|
*/
|
|
11961
|
-
async terminateSession({ sessionId, userId }) {
|
|
12245
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
11962
12246
|
try {
|
|
11963
12247
|
const now = /* @__PURE__ */ new Date();
|
|
12248
|
+
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
12249
|
+
const finalReason = validReasons.includes(reason) ? reason : "manual";
|
|
12250
|
+
const updateData = {
|
|
12251
|
+
isActive: false,
|
|
12252
|
+
terminatedManually: finalReason === "manual",
|
|
12253
|
+
terminationReason: finalReason,
|
|
12254
|
+
logoutTime: now
|
|
12255
|
+
};
|
|
11964
12256
|
if (sessionId) {
|
|
11965
12257
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11966
12258
|
documentId: sessionId,
|
|
@@ -11968,29 +12260,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11968
12260
|
});
|
|
11969
12261
|
if (!existing) {
|
|
11970
12262
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
11971
|
-
return;
|
|
12263
|
+
return { terminatedCount: 0 };
|
|
11972
12264
|
}
|
|
11973
12265
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11974
12266
|
documentId: sessionId,
|
|
11975
|
-
data:
|
|
12267
|
+
data: updateData
|
|
11976
12268
|
});
|
|
11977
|
-
log.info(`Session ${sessionId} terminated (
|
|
11978
|
-
|
|
12269
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12270
|
+
return { terminatedCount: 1 };
|
|
12271
|
+
}
|
|
12272
|
+
if (userId) {
|
|
11979
12273
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11980
|
-
if (!userDocumentId) return;
|
|
12274
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12275
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12276
|
+
if (exceptSessionId) {
|
|
12277
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12278
|
+
}
|
|
11981
12279
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
11982
|
-
filters:
|
|
12280
|
+
filters: filters2,
|
|
11983
12281
|
fields: ["documentId"],
|
|
11984
12282
|
limit: MAX_SESSIONS_QUERY
|
|
11985
12283
|
});
|
|
12284
|
+
let terminatedCount = 0;
|
|
11986
12285
|
for (const session2 of activeSessions) {
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
12286
|
+
try {
|
|
12287
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12288
|
+
documentId: session2.documentId,
|
|
12289
|
+
data: updateData
|
|
12290
|
+
});
|
|
12291
|
+
terminatedCount++;
|
|
12292
|
+
} catch (err) {
|
|
12293
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12294
|
+
}
|
|
11991
12295
|
}
|
|
11992
|
-
|
|
12296
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12297
|
+
log.info(
|
|
12298
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12299
|
+
);
|
|
12300
|
+
return { terminatedCount };
|
|
11993
12301
|
}
|
|
12302
|
+
return { terminatedCount: 0 };
|
|
11994
12303
|
} catch (err) {
|
|
11995
12304
|
log.error("Error terminating session:", err);
|
|
11996
12305
|
throw err;
|
|
@@ -12074,7 +12383,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12074
12383
|
async touch({ userId, sessionId, token }) {
|
|
12075
12384
|
try {
|
|
12076
12385
|
const now = /* @__PURE__ */ new Date();
|
|
12077
|
-
const settings2 = await getPluginSettings$
|
|
12386
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12078
12387
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12079
12388
|
let session2 = null;
|
|
12080
12389
|
let sessionDocId = sessionId;
|
|
@@ -12143,7 +12452,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12143
12452
|
*/
|
|
12144
12453
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12145
12454
|
try {
|
|
12146
|
-
const settings2 = await getPluginSettings$
|
|
12455
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12147
12456
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12148
12457
|
const now = /* @__PURE__ */ new Date();
|
|
12149
12458
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12156,7 +12465,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12156
12465
|
});
|
|
12157
12466
|
}).update({
|
|
12158
12467
|
is_active: false,
|
|
12159
|
-
terminated_manually:
|
|
12468
|
+
terminated_manually: false,
|
|
12469
|
+
termination_reason: "idle",
|
|
12160
12470
|
logout_time: now
|
|
12161
12471
|
});
|
|
12162
12472
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12197,7 +12507,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12197
12507
|
documentId,
|
|
12198
12508
|
data: {
|
|
12199
12509
|
isActive: false,
|
|
12200
|
-
terminatedManually:
|
|
12510
|
+
terminatedManually: false,
|
|
12511
|
+
terminationReason: "idle",
|
|
12201
12512
|
logoutTime: now
|
|
12202
12513
|
}
|
|
12203
12514
|
});
|
|
@@ -12243,7 +12554,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12243
12554
|
*/
|
|
12244
12555
|
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12245
12556
|
try {
|
|
12246
|
-
const settings2 = await getPluginSettings$
|
|
12557
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12247
12558
|
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12248
12559
|
if (effectiveDays === -1) {
|
|
12249
12560
|
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
@@ -12348,7 +12659,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12348
12659
|
}
|
|
12349
12660
|
};
|
|
12350
12661
|
};
|
|
12351
|
-
const version$1 = "4.5.
|
|
12662
|
+
const version$1 = "4.5.3";
|
|
12352
12663
|
const require$$2 = {
|
|
12353
12664
|
version: version$1
|
|
12354
12665
|
};
|
|
@@ -13306,6 +13617,7 @@ var services$1 = {
|
|
|
13306
13617
|
geolocation,
|
|
13307
13618
|
notifications
|
|
13308
13619
|
};
|
|
13620
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13309
13621
|
const buckets = /* @__PURE__ */ new Map();
|
|
13310
13622
|
const prune = (now) => {
|
|
13311
13623
|
for (const [key, entry] of buckets) {
|
|
@@ -13319,10 +13631,45 @@ const callerKey = (ctx) => {
|
|
|
13319
13631
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13320
13632
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13321
13633
|
};
|
|
13634
|
+
const RESOLVED_TTL_MS = 3e4;
|
|
13635
|
+
let resolvedCache = null;
|
|
13636
|
+
let resolvedAt = 0;
|
|
13637
|
+
async function resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) {
|
|
13638
|
+
const now = Date.now();
|
|
13639
|
+
if (resolvedCache && now - resolvedAt < RESOLVED_TTL_MS) {
|
|
13640
|
+
const p = resolvedCache[profile];
|
|
13641
|
+
if (p) {
|
|
13642
|
+
return { max: p.max, windowMs: p.windowMs };
|
|
13643
|
+
}
|
|
13644
|
+
}
|
|
13645
|
+
let settings2 = {};
|
|
13646
|
+
try {
|
|
13647
|
+
settings2 = await getPluginSettings$1(strapi2);
|
|
13648
|
+
} catch {
|
|
13649
|
+
settings2 = {};
|
|
13650
|
+
}
|
|
13651
|
+
const windowSec = Number.isFinite(settings2.rateLimitWindowSeconds) ? settings2.rateLimitWindowSeconds : Math.round(routeWindowMs / 1e3);
|
|
13652
|
+
const windowMs = Math.max(1e4, windowSec * 1e3);
|
|
13653
|
+
const resolvedWrite = {
|
|
13654
|
+
max: Math.min(routeMax, Number.isFinite(settings2.rateLimitWriteMax) ? settings2.rateLimitWriteMax : routeMax),
|
|
13655
|
+
windowMs
|
|
13656
|
+
};
|
|
13657
|
+
const resolvedRead = {
|
|
13658
|
+
max: Math.max(routeMax, Number.isFinite(settings2.rateLimitReadMax) ? settings2.rateLimitReadMax : routeMax),
|
|
13659
|
+
windowMs
|
|
13660
|
+
};
|
|
13661
|
+
resolvedCache = { read: resolvedRead, write: resolvedWrite };
|
|
13662
|
+
resolvedAt = now;
|
|
13663
|
+
if (profile === "read") return resolvedRead;
|
|
13664
|
+
if (profile === "write") return resolvedWrite;
|
|
13665
|
+
return { max: routeMax, windowMs };
|
|
13666
|
+
}
|
|
13322
13667
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13323
|
-
const
|
|
13324
|
-
const
|
|
13668
|
+
const routeMax = Number.isFinite(cfg.max) ? cfg.max : 30;
|
|
13669
|
+
const routeWindowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
|
|
13670
|
+
const profile = cfg.profile === "read" || cfg.profile === "write" ? cfg.profile : null;
|
|
13325
13671
|
return async (ctx, next) => {
|
|
13672
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13326
13673
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13327
13674
|
const now = Date.now();
|
|
13328
13675
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13356,7 +13703,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13356
13703
|
var rateLimit_1 = rateLimit;
|
|
13357
13704
|
var middlewares$1 = {
|
|
13358
13705
|
"last-seen": lastSeen,
|
|
13359
|
-
"rate-limit": rateLimit_1
|
|
13706
|
+
"rate-limit": rateLimit_1,
|
|
13707
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13360
13708
|
};
|
|
13361
13709
|
var lodashExports = requireLodash();
|
|
13362
13710
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51437,18 +51785,18 @@ const CSP_DEFAULTS = {
|
|
|
51437
51785
|
"blob:"
|
|
51438
51786
|
]
|
|
51439
51787
|
};
|
|
51440
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51788
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51441
51789
|
return middlewares2.map((currentMiddleware) => {
|
|
51442
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51443
|
-
return
|
|
51790
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51791
|
+
return middleware2;
|
|
51444
51792
|
}
|
|
51445
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51793
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51446
51794
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51447
51795
|
if (Array.isArray(objValue)) {
|
|
51448
51796
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51449
51797
|
}
|
|
51450
51798
|
return void 0;
|
|
51451
|
-
}, currentMiddleware,
|
|
51799
|
+
}, currentMiddleware, middleware2);
|
|
51452
51800
|
}
|
|
51453
51801
|
return currentMiddleware;
|
|
51454
51802
|
});
|