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.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
|
-
|
|
578
|
+
return next();
|
|
579
|
+
}
|
|
580
|
+
if (!ctx.state.user) {
|
|
581
|
+
return next();
|
|
495
582
|
}
|
|
496
|
-
|
|
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) {
|
|
602
|
+
try {
|
|
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();
|
|
546
618
|
try {
|
|
547
|
-
await sessionService.touch({
|
|
619
|
+
await sessionService.touch({
|
|
620
|
+
userId: userDocId,
|
|
621
|
+
sessionId: thisSession.documentId
|
|
622
|
+
});
|
|
548
623
|
} catch (err) {
|
|
549
624
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
550
625
|
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (strictMode) {
|
|
629
|
+
const iat = ctx.state.user?.iat;
|
|
630
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
631
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
632
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
633
|
+
ctx.state.userDocumentId = userDocId;
|
|
634
|
+
return next();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
strapi2.log.info(
|
|
638
|
+
`[magic-sessionmanager] [BLOCKED] No session matches this token (user: ${userDocId.substring(0, 8)}..., strictMode)`
|
|
639
|
+
);
|
|
640
|
+
return ctx.unauthorized("No valid session. Please login again.");
|
|
551
641
|
}
|
|
642
|
+
strapi2.log.debug(
|
|
643
|
+
`[magic-sessionmanager] [WARN] No session for token (user: ${userDocId.substring(0, 8)}...) - allowing in non-strict mode`
|
|
644
|
+
);
|
|
645
|
+
ctx.state.userDocumentId = userDocId;
|
|
646
|
+
return next();
|
|
552
647
|
};
|
|
553
648
|
};
|
|
554
649
|
var jsonwebtoken = { exports: {} };
|
|
@@ -9515,8 +9610,12 @@ const getClientIp = getClientIp_1;
|
|
|
9515
9610
|
const { encryptToken: encryptToken$1, hashToken: hashToken$3 } = encryption;
|
|
9516
9611
|
const { createLogger: createLogger$3 } = logger;
|
|
9517
9612
|
const { resolveUserDocumentId: resolveUserDocumentId$3 } = resolveUser;
|
|
9518
|
-
const { getPluginSettings: getPluginSettings$
|
|
9613
|
+
const { getPluginSettings: getPluginSettings$4 } = settingsLoader;
|
|
9519
9614
|
const { extractBearerToken: extractBearerToken$2 } = extractToken;
|
|
9615
|
+
const {
|
|
9616
|
+
setSessionRejectionReason,
|
|
9617
|
+
consumeSessionRejectionReason
|
|
9618
|
+
} = rejectionCache;
|
|
9520
9619
|
const SESSION_UID$4 = "plugin::magic-sessionmanager.session";
|
|
9521
9620
|
const JWT_WRAPPED_FLAG = Symbol.for("magic-sessionmanager.jwt.wrapped");
|
|
9522
9621
|
const LOGIN_PATHS = /* @__PURE__ */ new Set([
|
|
@@ -9589,7 +9688,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9589
9688
|
}
|
|
9590
9689
|
log.info("Running initial session cleanup...");
|
|
9591
9690
|
try {
|
|
9592
|
-
const settings2 = await getPluginSettings$
|
|
9691
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9593
9692
|
await sessionService.cleanupInactiveSessions({
|
|
9594
9693
|
useDbDirect: settings2.cleanupUseDbDirect === true
|
|
9595
9694
|
});
|
|
@@ -9600,7 +9699,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9600
9699
|
let intervalMs = 30 * 60 * 1e3;
|
|
9601
9700
|
let useDbDirect = false;
|
|
9602
9701
|
try {
|
|
9603
|
-
const settings2 = await getPluginSettings$
|
|
9702
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9604
9703
|
intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
|
|
9605
9704
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9606
9705
|
} catch {
|
|
@@ -9621,7 +9720,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9621
9720
|
const scheduleRetention = async () => {
|
|
9622
9721
|
let useDbDirect = false;
|
|
9623
9722
|
try {
|
|
9624
|
-
const settings2 = await getPluginSettings$
|
|
9723
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9625
9724
|
useDbDirect = settings2.cleanupUseDbDirect === true;
|
|
9626
9725
|
} catch {
|
|
9627
9726
|
}
|
|
@@ -9645,6 +9744,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
9645
9744
|
mountLogoutRoute({ strapi: strapi2, log, sessionService });
|
|
9646
9745
|
mountLoginInterceptor({ strapi: strapi2, log, sessionService });
|
|
9647
9746
|
mountRefreshTokenInterceptor({ strapi: strapi2, log });
|
|
9747
|
+
strapi2.server.use(
|
|
9748
|
+
sessionRejectionHeaders({}, { strapi: strapi2 })
|
|
9749
|
+
);
|
|
9750
|
+
log.info("[SUCCESS] Session-rejection-headers middleware mounted");
|
|
9648
9751
|
strapi2.server.use(
|
|
9649
9752
|
lastSeen({ strapi: strapi2, sessionService })
|
|
9650
9753
|
);
|
|
@@ -9663,7 +9766,7 @@ function mountPreLoginGeoGuard({ strapi: strapi2, log }) {
|
|
|
9663
9766
|
}
|
|
9664
9767
|
let settings2 = {};
|
|
9665
9768
|
try {
|
|
9666
|
-
settings2 = await getPluginSettings$
|
|
9769
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
9667
9770
|
} catch {
|
|
9668
9771
|
settings2 = {};
|
|
9669
9772
|
}
|
|
@@ -9839,7 +9942,7 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9839
9942
|
if (!isLoginPath(ctx.path, ctx.method)) return next();
|
|
9840
9943
|
let maxFailed = 0;
|
|
9841
9944
|
try {
|
|
9842
|
-
const settings2 = await getPluginSettings$
|
|
9945
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9843
9946
|
maxFailed = Number(settings2.maxFailedLogins) || 0;
|
|
9844
9947
|
} catch {
|
|
9845
9948
|
maxFailed = 0;
|
|
@@ -9878,17 +9981,28 @@ function mountFailedLoginLockout({ strapi: strapi2, log }) {
|
|
|
9878
9981
|
});
|
|
9879
9982
|
log.info("[SUCCESS] Failed-login lockout middleware mounted");
|
|
9880
9983
|
}
|
|
9984
|
+
function isJwtIssuingPath(path2, method) {
|
|
9985
|
+
if (!path2) return false;
|
|
9986
|
+
const get2 = method === "GET";
|
|
9987
|
+
const post = method === "POST";
|
|
9988
|
+
if (post && path2 === "/api/auth/local") return true;
|
|
9989
|
+
if (post && path2 === "/api/auth/local/register") return true;
|
|
9990
|
+
if (post && path2 === "/api/auth/reset-password") return true;
|
|
9991
|
+
if ((get2 || post) && path2 === "/api/auth/email-confirmation") return true;
|
|
9992
|
+
if (get2 && /^\/api\/auth\/[a-z0-9-]+\/callback$/i.test(path2)) return true;
|
|
9993
|
+
if ((get2 || post) && path2.startsWith("/api/magic-link/login")) return true;
|
|
9994
|
+
if (post && path2 === "/api/magic-link/verify-mfa-totp") return true;
|
|
9995
|
+
if (post && path2 === "/api/magic-link/otp/verify") return true;
|
|
9996
|
+
if (post && path2 === "/api/magic-link/login-totp") return true;
|
|
9997
|
+
if ((get2 || post) && path2.startsWith("/api/passwordless/")) return true;
|
|
9998
|
+
return false;
|
|
9999
|
+
}
|
|
9881
10000
|
function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
9882
10001
|
strapi2.server.use(async (ctx, next) => {
|
|
9883
10002
|
await next();
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
const isMagicLinkOTP = ctx.path.includes("/magic-link/otp/verify") && ctx.method === "POST";
|
|
9888
|
-
const isMagicLink = isMagicLinkLogin || isMagicLinkMFA || isMagicLinkOTP;
|
|
9889
|
-
if (!((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.jwt && ctx.body.user)) {
|
|
9890
|
-
return;
|
|
9891
|
-
}
|
|
10003
|
+
if (!isJwtIssuingPath(ctx.path, ctx.method)) return;
|
|
10004
|
+
if (ctx.status !== 200) return;
|
|
10005
|
+
if (!ctx.body || !ctx.body.jwt || !ctx.body.user) return;
|
|
9892
10006
|
try {
|
|
9893
10007
|
const user = ctx.body.user;
|
|
9894
10008
|
const ip = getClientIp(ctx);
|
|
@@ -9918,7 +10032,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
|
|
|
9918
10032
|
}
|
|
9919
10033
|
log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
|
|
9920
10034
|
try {
|
|
9921
|
-
const settings2 = await getPluginSettings$
|
|
10035
|
+
const settings2 = await getPluginSettings$4(strapi2);
|
|
9922
10036
|
if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
|
|
9923
10037
|
return;
|
|
9924
10038
|
}
|
|
@@ -10089,11 +10203,19 @@ function mountRefreshTokenInterceptor({ strapi: strapi2, log }) {
|
|
|
10089
10203
|
log.info("[SUCCESS] Refresh token interceptor middleware mounted");
|
|
10090
10204
|
}
|
|
10091
10205
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
10206
|
+
const PERMISSIONS_VERSION = 2;
|
|
10092
10207
|
try {
|
|
10093
10208
|
const pluginStore = strapi2.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
10094
|
-
const
|
|
10095
|
-
if (
|
|
10096
|
-
|
|
10209
|
+
const storedVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10210
|
+
if (storedVersion === void 0) {
|
|
10211
|
+
const legacyFlag = await pluginStore.get({ key: "contentApiPermissionsInitialized" });
|
|
10212
|
+
if (legacyFlag === true) {
|
|
10213
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: 1 });
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
const effectiveVersion = await pluginStore.get({ key: "contentApiPermissionsVersion" });
|
|
10217
|
+
if (effectiveVersion >= PERMISSIONS_VERSION) {
|
|
10218
|
+
log.debug("Content-API permissions already at current version (skipping auto-setup)");
|
|
10097
10219
|
return;
|
|
10098
10220
|
}
|
|
10099
10221
|
const ROLE_UID = "plugin::users-permissions.role";
|
|
@@ -10111,6 +10233,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10111
10233
|
const requiredActions = [
|
|
10112
10234
|
"plugin::magic-sessionmanager.session.logout",
|
|
10113
10235
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
10236
|
+
"plugin::magic-sessionmanager.session.logoutOthers",
|
|
10114
10237
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
10115
10238
|
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
10116
10239
|
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
@@ -10127,7 +10250,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10127
10250
|
const existingActions = existingPermissions.map((p) => p.action);
|
|
10128
10251
|
const missingActions = requiredActions.filter((action) => !existingActions.includes(action));
|
|
10129
10252
|
if (missingActions.length === 0) {
|
|
10130
|
-
await pluginStore.set({ key: "
|
|
10253
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10131
10254
|
log.debug("Content-API permissions already configured");
|
|
10132
10255
|
return;
|
|
10133
10256
|
}
|
|
@@ -10137,7 +10260,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
10137
10260
|
});
|
|
10138
10261
|
log.info(`[PERMISSION] Enabled ${action} for authenticated users`);
|
|
10139
10262
|
}
|
|
10140
|
-
await pluginStore.set({ key: "
|
|
10263
|
+
await pluginStore.set({ key: "contentApiPermissionsVersion", value: PERMISSIONS_VERSION });
|
|
10141
10264
|
log.info("[SUCCESS] Content-API permissions configured for authenticated users");
|
|
10142
10265
|
} catch (err) {
|
|
10143
10266
|
log.warn("Could not auto-configure permissions:", err.message);
|
|
@@ -10235,7 +10358,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10235
10358
|
}
|
|
10236
10359
|
let settings2;
|
|
10237
10360
|
try {
|
|
10238
|
-
settings2 = await getPluginSettings$
|
|
10361
|
+
settings2 = await getPluginSettings$4(strapi2);
|
|
10239
10362
|
} catch {
|
|
10240
10363
|
settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
10241
10364
|
}
|
|
@@ -10260,6 +10383,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10260
10383
|
strapi2.log.info(
|
|
10261
10384
|
`[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
|
|
10262
10385
|
);
|
|
10386
|
+
setSessionRejectionReason(hashToken$3(token), "blocked");
|
|
10263
10387
|
return null;
|
|
10264
10388
|
}
|
|
10265
10389
|
} catch {
|
|
@@ -10270,7 +10394,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10270
10394
|
user: { documentId: userDocId },
|
|
10271
10395
|
tokenHash: tokenHashValue
|
|
10272
10396
|
},
|
|
10273
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
10397
|
+
fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
|
|
10274
10398
|
});
|
|
10275
10399
|
if (thisSession) {
|
|
10276
10400
|
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
@@ -10279,15 +10403,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10279
10403
|
);
|
|
10280
10404
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10281
10405
|
documentId: thisSession.documentId,
|
|
10282
|
-
data: {
|
|
10406
|
+
data: {
|
|
10407
|
+
isActive: false,
|
|
10408
|
+
terminatedManually: false,
|
|
10409
|
+
terminationReason: "expired",
|
|
10410
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10411
|
+
}
|
|
10283
10412
|
});
|
|
10413
|
+
setSessionRejectionReason(tokenHashValue, "expired");
|
|
10284
10414
|
return null;
|
|
10285
10415
|
}
|
|
10286
|
-
if (thisSession.
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10416
|
+
if (thisSession.isActive === false) {
|
|
10417
|
+
const reason = thisSession.terminationReason || (thisSession.terminatedManually === true ? "manual" : null);
|
|
10418
|
+
if (reason) {
|
|
10419
|
+
strapi2.log.info(
|
|
10420
|
+
`[magic-sessionmanager] [JWT-REJECTED] Session inactive (reason: ${reason}) for user ${userDocId.substring(0, 8)}...`
|
|
10421
|
+
);
|
|
10422
|
+
setSessionRejectionReason(tokenHashValue, reason);
|
|
10423
|
+
return null;
|
|
10424
|
+
}
|
|
10291
10425
|
}
|
|
10292
10426
|
if (thisSession.isActive) {
|
|
10293
10427
|
resetErrorCounter();
|
|
@@ -10302,8 +10436,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
10302
10436
|
);
|
|
10303
10437
|
await strapi2.documents(SESSION_UID$4).update({
|
|
10304
10438
|
documentId: thisSession.documentId,
|
|
10305
|
-
data: {
|
|
10439
|
+
data: {
|
|
10440
|
+
terminatedManually: false,
|
|
10441
|
+
terminationReason: "idle",
|
|
10442
|
+
logoutTime: /* @__PURE__ */ new Date()
|
|
10443
|
+
}
|
|
10306
10444
|
});
|
|
10445
|
+
setSessionRejectionReason(tokenHashValue, "idle");
|
|
10307
10446
|
return null;
|
|
10308
10447
|
}
|
|
10309
10448
|
await strapi2.documents(SESSION_UID$4).update({
|
|
@@ -10505,6 +10644,16 @@ const attributes = {
|
|
|
10505
10644
|
"default": false,
|
|
10506
10645
|
required: false
|
|
10507
10646
|
},
|
|
10647
|
+
terminationReason: {
|
|
10648
|
+
type: "enumeration",
|
|
10649
|
+
"enum": [
|
|
10650
|
+
"manual",
|
|
10651
|
+
"idle",
|
|
10652
|
+
"expired",
|
|
10653
|
+
"blocked"
|
|
10654
|
+
],
|
|
10655
|
+
required: false
|
|
10656
|
+
},
|
|
10508
10657
|
geoLocation: {
|
|
10509
10658
|
type: "json"
|
|
10510
10659
|
},
|
|
@@ -10538,10 +10687,16 @@ var contentTypes$2 = {
|
|
|
10538
10687
|
}
|
|
10539
10688
|
};
|
|
10540
10689
|
const writeRateLimit = [
|
|
10541
|
-
{
|
|
10690
|
+
{
|
|
10691
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10692
|
+
config: { profile: "write", max: 10, window: 6e4 }
|
|
10693
|
+
}
|
|
10542
10694
|
];
|
|
10543
10695
|
const readRateLimit = [
|
|
10544
|
-
{
|
|
10696
|
+
{
|
|
10697
|
+
name: "plugin::magic-sessionmanager.rate-limit",
|
|
10698
|
+
config: { profile: "read", max: 120, window: 6e4 }
|
|
10699
|
+
}
|
|
10545
10700
|
];
|
|
10546
10701
|
var contentApi$1 = {
|
|
10547
10702
|
type: "content-api",
|
|
@@ -10564,7 +10719,17 @@ var contentApi$1 = {
|
|
|
10564
10719
|
config: {
|
|
10565
10720
|
auth: { strategies: ["users-permissions"] },
|
|
10566
10721
|
middlewares: writeRateLimit,
|
|
10567
|
-
description: "Logout from
|
|
10722
|
+
description: "Logout from ALL devices including the current one (requires JWT)"
|
|
10723
|
+
}
|
|
10724
|
+
},
|
|
10725
|
+
{
|
|
10726
|
+
method: "POST",
|
|
10727
|
+
path: "/logout-others",
|
|
10728
|
+
handler: "session.logoutOthers",
|
|
10729
|
+
config: {
|
|
10730
|
+
auth: { strategies: ["users-permissions"] },
|
|
10731
|
+
middlewares: writeRateLimit,
|
|
10732
|
+
description: "Logout from all OTHER devices, keep current session alive (requires JWT)"
|
|
10568
10733
|
}
|
|
10569
10734
|
},
|
|
10570
10735
|
// ================== SESSION QUERIES ==================
|
|
@@ -10944,15 +11109,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
|
|
|
10944
11109
|
const { hashToken: hashToken$2 } = encryption;
|
|
10945
11110
|
const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
|
|
10946
11111
|
const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
|
|
10947
|
-
const { getPluginSettings: getPluginSettings$
|
|
11112
|
+
const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
|
|
10948
11113
|
const { extractBearerToken: extractBearerToken$1 } = extractToken;
|
|
10949
11114
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
10950
11115
|
const USER_UID = "plugin::users-permissions.user";
|
|
11116
|
+
const OWN_SESSIONS_LIMIT = 200;
|
|
11117
|
+
async function resolveAuthUserDocId(ctx) {
|
|
11118
|
+
const u2 = ctx.state.user;
|
|
11119
|
+
if (!u2) return null;
|
|
11120
|
+
if (u2.documentId) return u2.documentId;
|
|
11121
|
+
if (u2.id) return resolveUserDocumentId$2(strapi, u2.id);
|
|
11122
|
+
return null;
|
|
11123
|
+
}
|
|
10951
11124
|
var session$3 = {
|
|
10952
11125
|
/**
|
|
10953
11126
|
* Lists all sessions (active + inactive) for admin overviews.
|
|
10954
11127
|
* @route GET /magic-sessionmanager/sessions
|
|
10955
|
-
* @returns {object} `{ data, meta }`
|
|
10956
11128
|
*/
|
|
10957
11129
|
async getAllSessionsAdmin(ctx) {
|
|
10958
11130
|
try {
|
|
@@ -10974,7 +11146,6 @@ var session$3 = {
|
|
|
10974
11146
|
/**
|
|
10975
11147
|
* Lists currently-active sessions only.
|
|
10976
11148
|
* @route GET /magic-sessionmanager/sessions/active
|
|
10977
|
-
* @returns {object} `{ data, meta }`
|
|
10978
11149
|
*/
|
|
10979
11150
|
async getActiveSessions(ctx) {
|
|
10980
11151
|
try {
|
|
@@ -10990,35 +11161,33 @@ var session$3 = {
|
|
|
10990
11161
|
}
|
|
10991
11162
|
},
|
|
10992
11163
|
/**
|
|
10993
|
-
* Returns the authenticated user's own sessions,
|
|
10994
|
-
* flagged via `isCurrentSession`.
|
|
10995
|
-
*
|
|
11164
|
+
* Returns the authenticated user's own sessions, current session flagged.
|
|
10996
11165
|
* @route GET /api/magic-sessionmanager/my-sessions
|
|
10997
|
-
* @returns {object} `{ data, meta }`
|
|
10998
|
-
* @throws {UnauthorizedError} When user is not authenticated
|
|
10999
11166
|
*/
|
|
11000
11167
|
async getOwnSessions(ctx) {
|
|
11001
11168
|
try {
|
|
11002
|
-
const
|
|
11169
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11170
|
+
if (!userDocId) {
|
|
11171
|
+
return ctx.unauthorized("Authentication required");
|
|
11172
|
+
}
|
|
11003
11173
|
const currentToken = extractBearerToken$1(ctx);
|
|
11004
11174
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11005
|
-
if (!userId) {
|
|
11006
|
-
return ctx.throw(401, "Unauthorized");
|
|
11007
|
-
}
|
|
11008
11175
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
11009
|
-
filters: { user: { documentId:
|
|
11176
|
+
filters: { user: { documentId: userDocId } },
|
|
11010
11177
|
sort: { loginTime: "desc" },
|
|
11011
|
-
limit:
|
|
11178
|
+
limit: OWN_SESSIONS_LIMIT + 1
|
|
11012
11179
|
});
|
|
11013
|
-
const
|
|
11180
|
+
const hasMore = allSessions.length > OWN_SESSIONS_LIMIT;
|
|
11181
|
+
const paged = hasMore ? allSessions.slice(0, OWN_SESSIONS_LIMIT) : allSessions;
|
|
11182
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11014
11183
|
const enhanceOpts = {
|
|
11015
11184
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11016
11185
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
11017
11186
|
strapi
|
|
11018
11187
|
};
|
|
11019
|
-
const sessionsWithCurrent = await enhanceSessions$1(
|
|
11188
|
+
const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
|
|
11020
11189
|
for (const s3 of sessionsWithCurrent) {
|
|
11021
|
-
s3.isCurrentSession = !!(currentTokenHash &&
|
|
11190
|
+
s3.isCurrentSession = !!(currentTokenHash && paged.find(
|
|
11022
11191
|
(raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
|
|
11023
11192
|
));
|
|
11024
11193
|
}
|
|
@@ -11031,7 +11200,9 @@ var session$3 = {
|
|
|
11031
11200
|
data: sessionsWithCurrent,
|
|
11032
11201
|
meta: {
|
|
11033
11202
|
count: sessionsWithCurrent.length,
|
|
11034
|
-
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
|
|
11203
|
+
active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
|
|
11204
|
+
hasMore,
|
|
11205
|
+
limit: OWN_SESSIONS_LIMIT
|
|
11035
11206
|
}
|
|
11036
11207
|
};
|
|
11037
11208
|
} catch (err) {
|
|
@@ -11040,18 +11211,14 @@ var session$3 = {
|
|
|
11040
11211
|
}
|
|
11041
11212
|
},
|
|
11042
11213
|
/**
|
|
11043
|
-
* Get a specific user's sessions. Admins
|
|
11214
|
+
* Get a specific user's sessions. Admins can query any user; content-api
|
|
11044
11215
|
* users can only query themselves.
|
|
11045
|
-
*
|
|
11046
|
-
* @route GET /magic-sessionmanager/user/:userId/sessions (admin)
|
|
11047
|
-
* @route GET /api/magic-sessionmanager/user/:userId/sessions (content-api)
|
|
11048
|
-
* @throws {ForbiddenError} When a non-admin requests another user's sessions
|
|
11049
11216
|
*/
|
|
11050
11217
|
async getUserSessions(ctx) {
|
|
11051
11218
|
try {
|
|
11052
11219
|
const { userId } = ctx.params;
|
|
11053
11220
|
const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
|
|
11054
|
-
const requestingUserDocId = ctx
|
|
11221
|
+
const requestingUserDocId = await resolveAuthUserDocId(ctx);
|
|
11055
11222
|
if (!isAdminRequest) {
|
|
11056
11223
|
if (!requestingUserDocId) {
|
|
11057
11224
|
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
@@ -11080,73 +11247,177 @@ var session$3 = {
|
|
|
11080
11247
|
*/
|
|
11081
11248
|
async logout(ctx) {
|
|
11082
11249
|
try {
|
|
11083
|
-
const
|
|
11250
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11084
11251
|
const token = extractBearerToken$1(ctx);
|
|
11085
|
-
if (!
|
|
11086
|
-
return ctx.
|
|
11252
|
+
if (!userDocId || !token) {
|
|
11253
|
+
return ctx.unauthorized("Authentication required");
|
|
11087
11254
|
}
|
|
11088
11255
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11089
11256
|
const currentTokenHash = hashToken$2(token);
|
|
11090
11257
|
const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11091
11258
|
filters: {
|
|
11092
|
-
user: { documentId:
|
|
11259
|
+
user: { documentId: userDocId },
|
|
11093
11260
|
tokenHash: currentTokenHash,
|
|
11094
11261
|
isActive: true
|
|
11095
11262
|
},
|
|
11096
11263
|
fields: ["documentId"]
|
|
11097
11264
|
});
|
|
11265
|
+
let terminated = false;
|
|
11098
11266
|
if (matchingSession) {
|
|
11099
|
-
await sessionService.terminateSession({
|
|
11100
|
-
|
|
11267
|
+
await sessionService.terminateSession({
|
|
11268
|
+
sessionId: matchingSession.documentId,
|
|
11269
|
+
reason: "manual"
|
|
11270
|
+
});
|
|
11271
|
+
terminated = true;
|
|
11272
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out (session ${matchingSession.documentId})`);
|
|
11101
11273
|
}
|
|
11102
|
-
ctx.body = {
|
|
11274
|
+
ctx.body = {
|
|
11275
|
+
message: "Logged out successfully",
|
|
11276
|
+
terminated
|
|
11277
|
+
};
|
|
11103
11278
|
} catch (err) {
|
|
11104
11279
|
strapi.log.error("[magic-sessionmanager] Logout error:", err);
|
|
11105
11280
|
ctx.throw(500, "Error during logout");
|
|
11106
11281
|
}
|
|
11107
11282
|
},
|
|
11108
11283
|
/**
|
|
11109
|
-
* Terminates
|
|
11284
|
+
* Terminates EVERY session of the authenticated user — including the
|
|
11285
|
+
* current one. After this call the caller will also be logged out on
|
|
11286
|
+
* the next request (their own JWT is rejected by the JWT-verify wrapper
|
|
11287
|
+
* with reason=manual).
|
|
11288
|
+
*
|
|
11289
|
+
* For the "log me out everywhere ELSE but keep me here" flow, use
|
|
11290
|
+
* `/logout-others` below.
|
|
11291
|
+
*
|
|
11110
11292
|
* @route POST /api/magic-sessionmanager/logout-all
|
|
11111
11293
|
*/
|
|
11112
11294
|
async logoutAll(ctx) {
|
|
11113
11295
|
try {
|
|
11114
|
-
const
|
|
11115
|
-
if (!
|
|
11116
|
-
return ctx.
|
|
11296
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11297
|
+
if (!userDocId) {
|
|
11298
|
+
return ctx.unauthorized("Authentication required");
|
|
11117
11299
|
}
|
|
11118
11300
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11119
|
-
await sessionService.terminateSession({
|
|
11120
|
-
|
|
11121
|
-
|
|
11301
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11302
|
+
userId: userDocId,
|
|
11303
|
+
reason: "manual"
|
|
11304
|
+
});
|
|
11305
|
+
strapi.log.info(
|
|
11306
|
+
`[magic-sessionmanager] User ${userDocId} logged out from all devices (${terminatedCount})`
|
|
11307
|
+
);
|
|
11308
|
+
ctx.body = {
|
|
11309
|
+
message: "Logged out from all devices successfully",
|
|
11310
|
+
terminatedCount
|
|
11311
|
+
};
|
|
11122
11312
|
} catch (err) {
|
|
11123
11313
|
strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
|
|
11124
11314
|
ctx.throw(500, "Error during logout");
|
|
11125
11315
|
}
|
|
11126
11316
|
},
|
|
11317
|
+
/**
|
|
11318
|
+
* Terminates every session of the authenticated user EXCEPT the current
|
|
11319
|
+
* one. This is the "kick everyone else off my account" flow — the
|
|
11320
|
+
* caller stays logged in on the device they are using.
|
|
11321
|
+
*
|
|
11322
|
+
* If no "current session" record can be located for the caller's JWT
|
|
11323
|
+
* (edge case: user logged in before the session-manager was installed,
|
|
11324
|
+
* or right at the end of the grace window) we fall back to terminating
|
|
11325
|
+
* ALL sessions so the user still gets the safety effect they asked for,
|
|
11326
|
+
* and we report `fellBackToLogoutAll: true` so the client can adjust
|
|
11327
|
+
* its success message.
|
|
11328
|
+
*
|
|
11329
|
+
* @route POST /api/magic-sessionmanager/logout-others
|
|
11330
|
+
*/
|
|
11331
|
+
async logoutOthers(ctx) {
|
|
11332
|
+
try {
|
|
11333
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11334
|
+
const token = extractBearerToken$1(ctx);
|
|
11335
|
+
if (!userDocId || !token) {
|
|
11336
|
+
return ctx.unauthorized("Authentication required");
|
|
11337
|
+
}
|
|
11338
|
+
const currentTokenHash = hashToken$2(token);
|
|
11339
|
+
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11340
|
+
filters: { user: { documentId: userDocId }, tokenHash: currentTokenHash },
|
|
11341
|
+
fields: ["documentId"]
|
|
11342
|
+
});
|
|
11343
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11344
|
+
if (!currentSession) {
|
|
11345
|
+
const { terminatedCount: terminatedCount2 } = await sessionService.terminateSession({
|
|
11346
|
+
userId: userDocId,
|
|
11347
|
+
reason: "manual"
|
|
11348
|
+
});
|
|
11349
|
+
strapi.log.warn(
|
|
11350
|
+
`[magic-sessionmanager] logoutOthers fell back to logoutAll for user ${userDocId} (no current session match)`
|
|
11351
|
+
);
|
|
11352
|
+
ctx.body = {
|
|
11353
|
+
message: "All sessions terminated (could not preserve current session)",
|
|
11354
|
+
terminatedCount: terminatedCount2,
|
|
11355
|
+
fellBackToLogoutAll: true
|
|
11356
|
+
};
|
|
11357
|
+
return;
|
|
11358
|
+
}
|
|
11359
|
+
const { terminatedCount } = await sessionService.terminateSession({
|
|
11360
|
+
userId: userDocId,
|
|
11361
|
+
exceptSessionId: currentSession.documentId,
|
|
11362
|
+
reason: "manual"
|
|
11363
|
+
});
|
|
11364
|
+
strapi.log.info(
|
|
11365
|
+
`[magic-sessionmanager] User ${userDocId} logged out ${terminatedCount} other device(s)`
|
|
11366
|
+
);
|
|
11367
|
+
ctx.body = {
|
|
11368
|
+
message: terminatedCount === 0 ? "No other active sessions to terminate" : `${terminatedCount} other session(s) terminated`,
|
|
11369
|
+
terminatedCount,
|
|
11370
|
+
currentSessionPreserved: true
|
|
11371
|
+
};
|
|
11372
|
+
} catch (err) {
|
|
11373
|
+
strapi.log.error("[magic-sessionmanager] Logout-others error:", err);
|
|
11374
|
+
ctx.throw(500, "Error terminating other sessions");
|
|
11375
|
+
}
|
|
11376
|
+
},
|
|
11127
11377
|
/**
|
|
11128
11378
|
* Returns the session associated with the current JWT.
|
|
11379
|
+
*
|
|
11380
|
+
* During the post-login grace window the session-create write may not
|
|
11381
|
+
* yet be visible. In that case we return 202 Accepted with
|
|
11382
|
+
* `{ pending: true }` so the client knows to retry shortly instead of
|
|
11383
|
+
* interpreting a 404 as "no session at all".
|
|
11384
|
+
*
|
|
11129
11385
|
* @route GET /api/magic-sessionmanager/current-session
|
|
11130
11386
|
*/
|
|
11131
11387
|
async getCurrentSession(ctx) {
|
|
11132
11388
|
try {
|
|
11133
|
-
const
|
|
11389
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11134
11390
|
const token = extractBearerToken$1(ctx);
|
|
11135
|
-
if (!
|
|
11136
|
-
return ctx.
|
|
11391
|
+
if (!userDocId || !token) {
|
|
11392
|
+
return ctx.unauthorized("Authentication required");
|
|
11137
11393
|
}
|
|
11138
11394
|
const currentTokenHash = hashToken$2(token);
|
|
11139
11395
|
const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
|
|
11140
11396
|
filters: {
|
|
11141
|
-
user: { documentId:
|
|
11397
|
+
user: { documentId: userDocId },
|
|
11142
11398
|
tokenHash: currentTokenHash,
|
|
11143
11399
|
isActive: true
|
|
11144
11400
|
}
|
|
11145
11401
|
});
|
|
11146
11402
|
if (!currentSession) {
|
|
11403
|
+
const settings3 = await getPluginSettings$3(strapi);
|
|
11404
|
+
const gracePeriodMs = Math.max(0, Number(settings3.sessionCreationGraceMs) || 5e3);
|
|
11405
|
+
const iat = ctx.state.user?.iat || ctx.state.auth?.credentials?.iat || null;
|
|
11406
|
+
if (gracePeriodMs > 0 && typeof iat === "number") {
|
|
11407
|
+
const ageMs = Date.now() - iat * 1e3;
|
|
11408
|
+
if (ageMs >= 0 && ageMs < gracePeriodMs) {
|
|
11409
|
+
ctx.status = 202;
|
|
11410
|
+
ctx.body = {
|
|
11411
|
+
data: null,
|
|
11412
|
+
meta: { pending: true, retryAfterMs: gracePeriodMs - ageMs },
|
|
11413
|
+
message: "Session is still being created — please retry shortly."
|
|
11414
|
+
};
|
|
11415
|
+
return;
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11147
11418
|
return ctx.notFound("Current session not found");
|
|
11148
11419
|
}
|
|
11149
|
-
const settings2 = await getPluginSettings$
|
|
11420
|
+
const settings2 = await getPluginSettings$3(strapi);
|
|
11150
11421
|
const enhanced = await enhanceSession(currentSession, {
|
|
11151
11422
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11152
11423
|
geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11162,17 +11433,17 @@ var session$3 = {
|
|
|
11162
11433
|
}
|
|
11163
11434
|
},
|
|
11164
11435
|
/**
|
|
11165
|
-
* Terminates one of the authenticated user's OWN sessions (not
|
|
11436
|
+
* Terminates one of the authenticated user's OWN sessions (not current).
|
|
11166
11437
|
* @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
11167
11438
|
*/
|
|
11168
11439
|
async terminateOwnSession(ctx) {
|
|
11169
11440
|
try {
|
|
11170
|
-
const
|
|
11441
|
+
const userDocId = await resolveAuthUserDocId(ctx);
|
|
11171
11442
|
const { sessionId } = ctx.params;
|
|
11172
11443
|
const currentToken = extractBearerToken$1(ctx);
|
|
11173
11444
|
const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
|
|
11174
|
-
if (!
|
|
11175
|
-
return ctx.
|
|
11445
|
+
if (!userDocId) {
|
|
11446
|
+
return ctx.unauthorized("Authentication required");
|
|
11176
11447
|
}
|
|
11177
11448
|
if (!sessionId) {
|
|
11178
11449
|
return ctx.badRequest("Session ID is required");
|
|
@@ -11185,19 +11456,23 @@ var session$3 = {
|
|
|
11185
11456
|
return ctx.notFound("Session not found");
|
|
11186
11457
|
}
|
|
11187
11458
|
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
11188
|
-
if (sessionUserId !==
|
|
11189
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
11459
|
+
if (sessionUserId !== userDocId) {
|
|
11460
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userDocId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
11190
11461
|
return ctx.forbidden("You can only terminate your own sessions");
|
|
11191
11462
|
}
|
|
11192
11463
|
if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
|
|
11193
|
-
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
11464
|
+
return ctx.badRequest("Cannot terminate the current session. Use /logout instead.");
|
|
11465
|
+
}
|
|
11466
|
+
const alreadyTerminated = sessionToTerminate.isActive === false;
|
|
11467
|
+
if (!alreadyTerminated) {
|
|
11468
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11469
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11470
|
+
strapi.log.info(`[magic-sessionmanager] User ${userDocId} terminated own session ${sessionId}`);
|
|
11194
11471
|
}
|
|
11195
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11196
|
-
await sessionService.terminateSession({ sessionId });
|
|
11197
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
11198
11472
|
ctx.body = {
|
|
11199
|
-
message: `Session ${sessionId} terminated successfully`,
|
|
11200
|
-
success: true
|
|
11473
|
+
message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
|
|
11474
|
+
success: true,
|
|
11475
|
+
alreadyTerminated
|
|
11201
11476
|
};
|
|
11202
11477
|
} catch (err) {
|
|
11203
11478
|
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
@@ -11205,9 +11480,7 @@ var session$3 = {
|
|
|
11205
11480
|
}
|
|
11206
11481
|
},
|
|
11207
11482
|
/**
|
|
11208
|
-
*
|
|
11209
|
-
* a cleanup timeout. Available only outside of production/staging.
|
|
11210
|
-
*
|
|
11483
|
+
* Simulates an inactivity timeout on a session. Dev-only.
|
|
11211
11484
|
* @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
11212
11485
|
*/
|
|
11213
11486
|
async simulateTimeout(ctx) {
|
|
@@ -11226,13 +11499,16 @@ var session$3 = {
|
|
|
11226
11499
|
}
|
|
11227
11500
|
await strapi.documents(SESSION_UID$2).update({
|
|
11228
11501
|
documentId: sessionId,
|
|
11229
|
-
data: {
|
|
11502
|
+
data: {
|
|
11503
|
+
isActive: false,
|
|
11504
|
+
terminatedManually: false,
|
|
11505
|
+
terminationReason: "idle"
|
|
11506
|
+
}
|
|
11230
11507
|
});
|
|
11231
|
-
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout
|
|
11508
|
+
strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
|
|
11232
11509
|
ctx.body = {
|
|
11233
|
-
message: `Session ${sessionId} marked as timed out
|
|
11234
|
-
success: true
|
|
11235
|
-
terminatedManually: false
|
|
11510
|
+
message: `Session ${sessionId} marked as timed out`,
|
|
11511
|
+
success: true
|
|
11236
11512
|
};
|
|
11237
11513
|
} catch (err) {
|
|
11238
11514
|
strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
|
|
@@ -11241,13 +11517,12 @@ var session$3 = {
|
|
|
11241
11517
|
},
|
|
11242
11518
|
/**
|
|
11243
11519
|
* Terminates a specific session (admin action).
|
|
11244
|
-
* @route POST /magic-sessionmanager/sessions/:sessionId/terminate
|
|
11245
11520
|
*/
|
|
11246
11521
|
async terminateSingleSession(ctx) {
|
|
11247
11522
|
try {
|
|
11248
11523
|
const { sessionId } = ctx.params;
|
|
11249
11524
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11250
|
-
await sessionService.terminateSession({ sessionId });
|
|
11525
|
+
await sessionService.terminateSession({ sessionId, reason: "manual" });
|
|
11251
11526
|
ctx.body = {
|
|
11252
11527
|
message: `Session ${sessionId} terminated`,
|
|
11253
11528
|
success: true
|
|
@@ -11259,13 +11534,12 @@ var session$3 = {
|
|
|
11259
11534
|
},
|
|
11260
11535
|
/**
|
|
11261
11536
|
* Terminates ALL sessions for a specific user (admin action).
|
|
11262
|
-
* @route POST /magic-sessionmanager/user/:userId/terminate-all
|
|
11263
11537
|
*/
|
|
11264
11538
|
async terminateAllUserSessions(ctx) {
|
|
11265
11539
|
try {
|
|
11266
11540
|
const { userId } = ctx.params;
|
|
11267
11541
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11268
|
-
await sessionService.terminateSession({ userId });
|
|
11542
|
+
await sessionService.terminateSession({ userId, reason: "manual" });
|
|
11269
11543
|
ctx.body = {
|
|
11270
11544
|
message: `All sessions terminated for user ${userId}`,
|
|
11271
11545
|
success: true
|
|
@@ -11277,9 +11551,6 @@ var session$3 = {
|
|
|
11277
11551
|
},
|
|
11278
11552
|
/**
|
|
11279
11553
|
* Returns geolocation data for a specific IP address (Premium feature).
|
|
11280
|
-
*
|
|
11281
|
-
* @route GET /magic-sessionmanager/geolocation/:ipAddress
|
|
11282
|
-
* @throws {ForbiddenError} When no premium license is active
|
|
11283
11554
|
*/
|
|
11284
11555
|
async getIpGeolocation(ctx) {
|
|
11285
11556
|
try {
|
|
@@ -11306,10 +11577,7 @@ var session$3 = {
|
|
|
11306
11577
|
return ctx.badRequest("Invalid IP address format");
|
|
11307
11578
|
}
|
|
11308
11579
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
11309
|
-
const pluginStore = strapi.store({
|
|
11310
|
-
type: "plugin",
|
|
11311
|
-
name: "magic-sessionmanager"
|
|
11312
|
-
});
|
|
11580
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
|
|
11313
11581
|
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
11314
11582
|
if (!licenseKey) {
|
|
11315
11583
|
return ctx.forbidden("Premium license required for geolocation features");
|
|
@@ -11331,7 +11599,6 @@ var session$3 = {
|
|
|
11331
11599
|
},
|
|
11332
11600
|
/**
|
|
11333
11601
|
* Permanently deletes a session (admin action).
|
|
11334
|
-
* @route DELETE /magic-sessionmanager/sessions/:sessionId
|
|
11335
11602
|
*/
|
|
11336
11603
|
async deleteSession(ctx) {
|
|
11337
11604
|
try {
|
|
@@ -11349,7 +11616,6 @@ var session$3 = {
|
|
|
11349
11616
|
},
|
|
11350
11617
|
/**
|
|
11351
11618
|
* Deletes all inactive sessions (admin action).
|
|
11352
|
-
* @route POST /magic-sessionmanager/sessions/clean-inactive
|
|
11353
11619
|
*/
|
|
11354
11620
|
async cleanInactiveSessions(ctx) {
|
|
11355
11621
|
try {
|
|
@@ -11367,9 +11633,6 @@ var session$3 = {
|
|
|
11367
11633
|
},
|
|
11368
11634
|
/**
|
|
11369
11635
|
* Toggles a user's blocked status and terminates their sessions on block.
|
|
11370
|
-
*
|
|
11371
|
-
* @route POST /magic-sessionmanager/user/:userId/toggle-block
|
|
11372
|
-
* @throws {NotFoundError} When the user cannot be found
|
|
11373
11636
|
*/
|
|
11374
11637
|
async toggleUserBlock(ctx) {
|
|
11375
11638
|
try {
|
|
@@ -11385,11 +11648,11 @@ var session$3 = {
|
|
|
11385
11648
|
}
|
|
11386
11649
|
}
|
|
11387
11650
|
if (!userDocumentId) {
|
|
11388
|
-
return ctx.
|
|
11651
|
+
return ctx.notFound("User not found");
|
|
11389
11652
|
}
|
|
11390
11653
|
const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
|
|
11391
11654
|
if (!user) {
|
|
11392
|
-
return ctx.
|
|
11655
|
+
return ctx.notFound("User not found");
|
|
11393
11656
|
}
|
|
11394
11657
|
const newBlockedStatus = !user.blocked;
|
|
11395
11658
|
await strapi.documents(USER_UID).update({
|
|
@@ -11398,7 +11661,7 @@ var session$3 = {
|
|
|
11398
11661
|
});
|
|
11399
11662
|
if (newBlockedStatus) {
|
|
11400
11663
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
11401
|
-
await sessionService.terminateSession({ userId: userDocumentId });
|
|
11664
|
+
await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
|
|
11402
11665
|
}
|
|
11403
11666
|
ctx.body = {
|
|
11404
11667
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -11890,13 +12153,13 @@ const { createLogger: createLogger$1 } = logger;
|
|
|
11890
12153
|
const { parseUserAgent } = userAgentParser;
|
|
11891
12154
|
const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
|
|
11892
12155
|
const { enhanceSessions } = enhanceSession_1;
|
|
11893
|
-
const { getPluginSettings: getPluginSettings$
|
|
12156
|
+
const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
|
|
11894
12157
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
11895
12158
|
const MAX_SESSIONS_QUERY = 1e3;
|
|
11896
12159
|
var session$1 = ({ strapi: strapi2 }) => {
|
|
11897
12160
|
const log = createLogger$1(strapi2);
|
|
11898
12161
|
async function getEnhanceOpts() {
|
|
11899
|
-
const settings2 = await getPluginSettings$
|
|
12162
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
11900
12163
|
return {
|
|
11901
12164
|
inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
|
|
11902
12165
|
geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
|
|
@@ -11965,15 +12228,44 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11965
12228
|
}
|
|
11966
12229
|
},
|
|
11967
12230
|
/**
|
|
11968
|
-
* Terminates a single session
|
|
12231
|
+
* Terminates a single session, all sessions of a user, or all sessions
|
|
12232
|
+
* of a user EXCEPT one with a typed reason so the JWT-verify wrapper
|
|
12233
|
+
* can communicate the cause to the client.
|
|
12234
|
+
*
|
|
12235
|
+
* Supported reasons:
|
|
12236
|
+
* - 'manual': user clicked logout, or admin terminated a session
|
|
12237
|
+
* - 'idle': inactivity timeout cleanup
|
|
12238
|
+
* - 'expired': maxSessionAgeDays exceeded
|
|
12239
|
+
* - 'blocked': the owning user was marked blocked
|
|
12240
|
+
*
|
|
12241
|
+
* For backwards compatibility `terminatedManually` is still set true
|
|
12242
|
+
* only when reason === 'manual'; idle/expired/blocked paths set it
|
|
12243
|
+
* false so reporting dashboards that queried that boolean continue
|
|
12244
|
+
* to work, while new code relies on `terminationReason`.
|
|
12245
|
+
*
|
|
11969
12246
|
* @param {Object} params
|
|
11970
|
-
* @param {string} [params.sessionId]
|
|
11971
|
-
* @param {string|number} [params.userId]
|
|
11972
|
-
*
|
|
12247
|
+
* @param {string} [params.sessionId] Terminate exactly this session
|
|
12248
|
+
* @param {string|number} [params.userId] Terminate every active session
|
|
12249
|
+
* of this user …
|
|
12250
|
+
* @param {string} [params.exceptSessionId] … except for this one. Only
|
|
12251
|
+
* meaningful together with
|
|
12252
|
+
* `userId`. Used by
|
|
12253
|
+
* /logout-other-devices so
|
|
12254
|
+
* the caller stays logged in.
|
|
12255
|
+
* @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
|
|
12256
|
+
* @returns {Promise<{terminatedCount: number}>}
|
|
11973
12257
|
*/
|
|
11974
|
-
async terminateSession({ sessionId, userId }) {
|
|
12258
|
+
async terminateSession({ sessionId, userId, exceptSessionId = null, reason = "manual" }) {
|
|
11975
12259
|
try {
|
|
11976
12260
|
const now = /* @__PURE__ */ new Date();
|
|
12261
|
+
const validReasons = ["manual", "idle", "expired", "blocked"];
|
|
12262
|
+
const finalReason = validReasons.includes(reason) ? reason : "manual";
|
|
12263
|
+
const updateData = {
|
|
12264
|
+
isActive: false,
|
|
12265
|
+
terminatedManually: finalReason === "manual",
|
|
12266
|
+
terminationReason: finalReason,
|
|
12267
|
+
logoutTime: now
|
|
12268
|
+
};
|
|
11977
12269
|
if (sessionId) {
|
|
11978
12270
|
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
11979
12271
|
documentId: sessionId,
|
|
@@ -11981,29 +12273,46 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
11981
12273
|
});
|
|
11982
12274
|
if (!existing) {
|
|
11983
12275
|
log.warn(`Session ${sessionId} not found for termination`);
|
|
11984
|
-
return;
|
|
12276
|
+
return { terminatedCount: 0 };
|
|
11985
12277
|
}
|
|
11986
12278
|
await strapi2.documents(SESSION_UID$1).update({
|
|
11987
12279
|
documentId: sessionId,
|
|
11988
|
-
data:
|
|
12280
|
+
data: updateData
|
|
11989
12281
|
});
|
|
11990
|
-
log.info(`Session ${sessionId} terminated (
|
|
11991
|
-
|
|
12282
|
+
log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
|
|
12283
|
+
return { terminatedCount: 1 };
|
|
12284
|
+
}
|
|
12285
|
+
if (userId) {
|
|
11992
12286
|
const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
|
|
11993
|
-
if (!userDocumentId) return;
|
|
12287
|
+
if (!userDocumentId) return { terminatedCount: 0 };
|
|
12288
|
+
const filters2 = { user: { documentId: userDocumentId }, isActive: true };
|
|
12289
|
+
if (exceptSessionId) {
|
|
12290
|
+
filters2.documentId = { $ne: exceptSessionId };
|
|
12291
|
+
}
|
|
11994
12292
|
const activeSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
11995
|
-
filters:
|
|
12293
|
+
filters: filters2,
|
|
11996
12294
|
fields: ["documentId"],
|
|
11997
12295
|
limit: MAX_SESSIONS_QUERY
|
|
11998
12296
|
});
|
|
12297
|
+
let terminatedCount = 0;
|
|
11999
12298
|
for (const session2 of activeSessions) {
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12299
|
+
try {
|
|
12300
|
+
await strapi2.documents(SESSION_UID$1).update({
|
|
12301
|
+
documentId: session2.documentId,
|
|
12302
|
+
data: updateData
|
|
12303
|
+
});
|
|
12304
|
+
terminatedCount++;
|
|
12305
|
+
} catch (err) {
|
|
12306
|
+
log.debug(`Failed to terminate session ${session2.documentId}:`, err.message);
|
|
12307
|
+
}
|
|
12004
12308
|
}
|
|
12005
|
-
|
|
12309
|
+
const label = exceptSessionId ? "OTHER sessions" : "ALL sessions";
|
|
12310
|
+
log.info(
|
|
12311
|
+
`${label} terminated for user ${userDocumentId} (reason: ${finalReason}, count: ${terminatedCount})`
|
|
12312
|
+
);
|
|
12313
|
+
return { terminatedCount };
|
|
12006
12314
|
}
|
|
12315
|
+
return { terminatedCount: 0 };
|
|
12007
12316
|
} catch (err) {
|
|
12008
12317
|
log.error("Error terminating session:", err);
|
|
12009
12318
|
throw err;
|
|
@@ -12087,7 +12396,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12087
12396
|
async touch({ userId, sessionId, token }) {
|
|
12088
12397
|
try {
|
|
12089
12398
|
const now = /* @__PURE__ */ new Date();
|
|
12090
|
-
const settings2 = await getPluginSettings$
|
|
12399
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12091
12400
|
const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
|
|
12092
12401
|
let session2 = null;
|
|
12093
12402
|
let sessionDocId = sessionId;
|
|
@@ -12156,7 +12465,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12156
12465
|
*/
|
|
12157
12466
|
async cleanupInactiveSessions({ useDbDirect = false } = {}) {
|
|
12158
12467
|
try {
|
|
12159
|
-
const settings2 = await getPluginSettings$
|
|
12468
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12160
12469
|
const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
|
|
12161
12470
|
const now = /* @__PURE__ */ new Date();
|
|
12162
12471
|
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
@@ -12169,7 +12478,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12169
12478
|
});
|
|
12170
12479
|
}).update({
|
|
12171
12480
|
is_active: false,
|
|
12172
|
-
terminated_manually:
|
|
12481
|
+
terminated_manually: false,
|
|
12482
|
+
termination_reason: "idle",
|
|
12173
12483
|
logout_time: now
|
|
12174
12484
|
});
|
|
12175
12485
|
log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
|
|
@@ -12210,7 +12520,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12210
12520
|
documentId,
|
|
12211
12521
|
data: {
|
|
12212
12522
|
isActive: false,
|
|
12213
|
-
terminatedManually:
|
|
12523
|
+
terminatedManually: false,
|
|
12524
|
+
terminationReason: "idle",
|
|
12214
12525
|
logoutTime: now
|
|
12215
12526
|
}
|
|
12216
12527
|
});
|
|
@@ -12256,7 +12567,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12256
12567
|
*/
|
|
12257
12568
|
async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
|
|
12258
12569
|
try {
|
|
12259
|
-
const settings2 = await getPluginSettings$
|
|
12570
|
+
const settings2 = await getPluginSettings$2(strapi2);
|
|
12260
12571
|
const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
|
|
12261
12572
|
if (effectiveDays === -1) {
|
|
12262
12573
|
log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
|
|
@@ -12361,7 +12672,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
12361
12672
|
}
|
|
12362
12673
|
};
|
|
12363
12674
|
};
|
|
12364
|
-
const version$1 = "4.5.
|
|
12675
|
+
const version$1 = "4.5.3";
|
|
12365
12676
|
const require$$2 = {
|
|
12366
12677
|
version: version$1
|
|
12367
12678
|
};
|
|
@@ -13319,6 +13630,7 @@ var services$1 = {
|
|
|
13319
13630
|
geolocation,
|
|
13320
13631
|
notifications
|
|
13321
13632
|
};
|
|
13633
|
+
const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
|
|
13322
13634
|
const buckets = /* @__PURE__ */ new Map();
|
|
13323
13635
|
const prune = (now) => {
|
|
13324
13636
|
for (const [key, entry] of buckets) {
|
|
@@ -13332,10 +13644,45 @@ const callerKey = (ctx) => {
|
|
|
13332
13644
|
if (tokenId) return `t:${String(tokenId).slice(-16)}`;
|
|
13333
13645
|
return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
|
|
13334
13646
|
};
|
|
13647
|
+
const RESOLVED_TTL_MS = 3e4;
|
|
13648
|
+
let resolvedCache = null;
|
|
13649
|
+
let resolvedAt = 0;
|
|
13650
|
+
async function resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) {
|
|
13651
|
+
const now = Date.now();
|
|
13652
|
+
if (resolvedCache && now - resolvedAt < RESOLVED_TTL_MS) {
|
|
13653
|
+
const p = resolvedCache[profile];
|
|
13654
|
+
if (p) {
|
|
13655
|
+
return { max: p.max, windowMs: p.windowMs };
|
|
13656
|
+
}
|
|
13657
|
+
}
|
|
13658
|
+
let settings2 = {};
|
|
13659
|
+
try {
|
|
13660
|
+
settings2 = await getPluginSettings$1(strapi2);
|
|
13661
|
+
} catch {
|
|
13662
|
+
settings2 = {};
|
|
13663
|
+
}
|
|
13664
|
+
const windowSec = Number.isFinite(settings2.rateLimitWindowSeconds) ? settings2.rateLimitWindowSeconds : Math.round(routeWindowMs / 1e3);
|
|
13665
|
+
const windowMs = Math.max(1e4, windowSec * 1e3);
|
|
13666
|
+
const resolvedWrite = {
|
|
13667
|
+
max: Math.min(routeMax, Number.isFinite(settings2.rateLimitWriteMax) ? settings2.rateLimitWriteMax : routeMax),
|
|
13668
|
+
windowMs
|
|
13669
|
+
};
|
|
13670
|
+
const resolvedRead = {
|
|
13671
|
+
max: Math.max(routeMax, Number.isFinite(settings2.rateLimitReadMax) ? settings2.rateLimitReadMax : routeMax),
|
|
13672
|
+
windowMs
|
|
13673
|
+
};
|
|
13674
|
+
resolvedCache = { read: resolvedRead, write: resolvedWrite };
|
|
13675
|
+
resolvedAt = now;
|
|
13676
|
+
if (profile === "read") return resolvedRead;
|
|
13677
|
+
if (profile === "write") return resolvedWrite;
|
|
13678
|
+
return { max: routeMax, windowMs };
|
|
13679
|
+
}
|
|
13335
13680
|
const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
13336
|
-
const
|
|
13337
|
-
const
|
|
13681
|
+
const routeMax = Number.isFinite(cfg.max) ? cfg.max : 30;
|
|
13682
|
+
const routeWindowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
|
|
13683
|
+
const profile = cfg.profile === "read" || cfg.profile === "write" ? cfg.profile : null;
|
|
13338
13684
|
return async (ctx, next) => {
|
|
13685
|
+
const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
|
|
13339
13686
|
const key = `${ctx.path}::${callerKey(ctx)}`;
|
|
13340
13687
|
const now = Date.now();
|
|
13341
13688
|
if (buckets.size > 5e3) prune(now);
|
|
@@ -13369,7 +13716,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
|
|
|
13369
13716
|
var rateLimit_1 = rateLimit;
|
|
13370
13717
|
var middlewares$1 = {
|
|
13371
13718
|
"last-seen": lastSeen,
|
|
13372
|
-
"rate-limit": rateLimit_1
|
|
13719
|
+
"rate-limit": rateLimit_1,
|
|
13720
|
+
"session-rejection-headers": sessionRejectionHeaders
|
|
13373
13721
|
};
|
|
13374
13722
|
var lodashExports = requireLodash();
|
|
13375
13723
|
const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
|
|
@@ -51450,18 +51798,18 @@ const CSP_DEFAULTS = {
|
|
|
51450
51798
|
"blob:"
|
|
51451
51799
|
]
|
|
51452
51800
|
};
|
|
51453
|
-
const extendMiddlewareConfiguration = (middlewares2,
|
|
51801
|
+
const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
|
|
51454
51802
|
return middlewares2.map((currentMiddleware) => {
|
|
51455
|
-
if (typeof currentMiddleware === "string" && currentMiddleware ===
|
|
51456
|
-
return
|
|
51803
|
+
if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
|
|
51804
|
+
return middleware2;
|
|
51457
51805
|
}
|
|
51458
|
-
if (typeof currentMiddleware === "object" && currentMiddleware.name ===
|
|
51806
|
+
if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
|
|
51459
51807
|
return fp.mergeWith((objValue, srcValue) => {
|
|
51460
51808
|
if (Array.isArray(objValue)) {
|
|
51461
51809
|
return Array.from(new Set(objValue.concat(srcValue)));
|
|
51462
51810
|
}
|
|
51463
51811
|
return void 0;
|
|
51464
|
-
}, currentMiddleware,
|
|
51812
|
+
}, currentMiddleware, middleware2);
|
|
51465
51813
|
}
|
|
51466
51814
|
return currentMiddleware;
|
|
51467
51815
|
});
|