strapi-plugin-magic-sessionmanager 4.5.2 → 4.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: { isActive: false, terminatedManually: true, logoutTime: now }
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$5(token) {
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$5
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$5(strapi2) {
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$5,
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$4(ctx) {
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$4 };
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$4 } = settingsLoader;
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
- await next();
481
- return;
565
+ return next();
482
566
  }
483
- if (ctx.state.user) {
567
+ if (!ctx.state.user) {
568
+ return next();
569
+ }
570
+ let userDocId = ctx.state.user.documentId;
571
+ if (!userDocId && ctx.state.user.id) {
484
572
  try {
485
- let userDocId2 = ctx.state.user.documentId;
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] Error checking active sessions:", err.message);
575
+ strapi2.log.debug("[magic-sessionmanager] user doc-id lookup failed:", err.message);
576
+ return next();
527
577
  }
528
578
  }
529
- await next();
530
- const userDocId = ctx.state.userDocumentId || ctx.state.user?.documentId;
531
- const sessionId = ctx.state.__magicSessionId;
532
- if (userDocId && sessionId) {
579
+ if (!userDocId) {
580
+ return next();
581
+ }
582
+ const settings2 = await getPluginSettings$5(strapi2).catch(() => ({}));
583
+ const strictMode = settings2.strictSessionEnforcement === true;
584
+ const gracePeriodMs = Math.max(0, Number(settings2.sessionCreationGraceMs) || 5e3);
585
+ const token = extractBearerToken$3(ctx);
586
+ const tokenHashValue = token ? hashToken$4(token) : null;
587
+ let thisSession = null;
588
+ if (tokenHashValue) {
533
589
  try {
534
- await sessionService.touch({ userId: userDocId, sessionId });
590
+ thisSession = await strapi2.documents(SESSION_UID$5).findFirst({
591
+ filters: { user: { documentId: userDocId }, tokenHash: tokenHashValue },
592
+ fields: ["documentId", "isActive", "terminatedManually", "terminationReason"]
593
+ });
594
+ } catch (err) {
595
+ strapi2.log.debug("[magic-sessionmanager] session lookup failed:", err.message);
596
+ }
597
+ }
598
+ if (thisSession) {
599
+ if (thisSession.isActive === false) {
600
+ return ctx.unauthorized("Session terminated. Please login again.");
601
+ }
602
+ ctx.state.userDocumentId = userDocId;
603
+ ctx.state.__magicSessionId = thisSession.documentId;
604
+ await next();
605
+ try {
606
+ await sessionService.touch({
607
+ userId: userDocId,
608
+ sessionId: thisSession.documentId
609
+ });
535
610
  } catch (err) {
536
611
  strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
537
612
  }
613
+ return;
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$3 } = settingsLoader;
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$3(strapi2);
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$3(strapi2);
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$3(strapi2);
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$3(strapi2);
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$3(strapi2);
9932
+ const settings2 = await getPluginSettings$4(strapi2);
9830
9933
  maxFailed = Number(settings2.maxFailedLogins) || 0;
9831
9934
  } catch {
9832
9935
  maxFailed = 0;
@@ -9905,7 +10008,7 @@ function mountLoginInterceptor({ strapi: strapi2, log, sessionService }) {
9905
10008
  }
9906
10009
  log.info(`[SUCCESS] Session ${newSession.documentId} created for user ${userDocId} (IP: ${ip})`);
9907
10010
  try {
9908
- const settings2 = await getPluginSettings$3(strapi2);
10011
+ const settings2 = await getPluginSettings$4(strapi2);
9909
10012
  if (!geoData || !(settings2.enableEmailAlerts || settings2.enableWebhooks)) {
9910
10013
  return;
9911
10014
  }
@@ -10222,7 +10325,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
10222
10325
  }
10223
10326
  let settings2;
10224
10327
  try {
10225
- settings2 = await getPluginSettings$3(strapi2);
10328
+ settings2 = await getPluginSettings$4(strapi2);
10226
10329
  } catch {
10227
10330
  settings2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
10228
10331
  }
@@ -10247,6 +10350,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
10247
10350
  strapi2.log.info(
10248
10351
  `[magic-sessionmanager] [JWT-BLOCKED] User is blocked (user: ${userDocId.substring(0, 8)}...)`
10249
10352
  );
10353
+ setSessionRejectionReason(hashToken$3(token), "blocked");
10250
10354
  return null;
10251
10355
  }
10252
10356
  } catch {
@@ -10257,7 +10361,7 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
10257
10361
  user: { documentId: userDocId },
10258
10362
  tokenHash: tokenHashValue
10259
10363
  },
10260
- fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
10364
+ fields: ["documentId", "isActive", "terminatedManually", "terminationReason", "lastActive", "loginTime"]
10261
10365
  });
10262
10366
  if (thisSession) {
10263
10367
  if (isSessionExpired(thisSession, maxSessionAgeDays)) {
@@ -10266,15 +10370,25 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
10266
10370
  );
10267
10371
  await strapi2.documents(SESSION_UID$4).update({
10268
10372
  documentId: thisSession.documentId,
10269
- data: { isActive: false, terminatedManually: true, logoutTime: /* @__PURE__ */ new Date() }
10373
+ data: {
10374
+ isActive: false,
10375
+ terminatedManually: false,
10376
+ terminationReason: "expired",
10377
+ logoutTime: /* @__PURE__ */ new Date()
10378
+ }
10270
10379
  });
10380
+ setSessionRejectionReason(tokenHashValue, "expired");
10271
10381
  return null;
10272
10382
  }
10273
- if (thisSession.terminatedManually === true) {
10274
- strapi2.log.info(
10275
- `[magic-sessionmanager] [JWT-BLOCKED] Session was manually terminated (user: ${userDocId.substring(0, 8)}...)`
10276
- );
10277
- return null;
10383
+ if (thisSession.isActive === false) {
10384
+ const reason = thisSession.terminationReason || (thisSession.terminatedManually === true ? "manual" : null);
10385
+ if (reason) {
10386
+ strapi2.log.info(
10387
+ `[magic-sessionmanager] [JWT-REJECTED] Session inactive (reason: ${reason}) for user ${userDocId.substring(0, 8)}...`
10388
+ );
10389
+ setSessionRejectionReason(tokenHashValue, reason);
10390
+ return null;
10391
+ }
10278
10392
  }
10279
10393
  if (thisSession.isActive) {
10280
10394
  resetErrorCounter();
@@ -10289,8 +10403,13 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
10289
10403
  );
10290
10404
  await strapi2.documents(SESSION_UID$4).update({
10291
10405
  documentId: thisSession.documentId,
10292
- data: { terminatedManually: true, logoutTime: /* @__PURE__ */ new Date() }
10406
+ data: {
10407
+ terminatedManually: false,
10408
+ terminationReason: "idle",
10409
+ logoutTime: /* @__PURE__ */ new Date()
10410
+ }
10293
10411
  });
10412
+ setSessionRejectionReason(tokenHashValue, "idle");
10294
10413
  return null;
10295
10414
  }
10296
10415
  await strapi2.documents(SESSION_UID$4).update({
@@ -10492,6 +10611,16 @@ const attributes = {
10492
10611
  "default": false,
10493
10612
  required: false
10494
10613
  },
10614
+ terminationReason: {
10615
+ type: "enumeration",
10616
+ "enum": [
10617
+ "manual",
10618
+ "idle",
10619
+ "expired",
10620
+ "blocked"
10621
+ ],
10622
+ required: false
10623
+ },
10495
10624
  geoLocation: {
10496
10625
  type: "json"
10497
10626
  },
@@ -10525,10 +10654,16 @@ var contentTypes$2 = {
10525
10654
  }
10526
10655
  };
10527
10656
  const writeRateLimit = [
10528
- { name: "plugin::magic-sessionmanager.rate-limit", config: { max: 10, window: 6e4 } }
10657
+ {
10658
+ name: "plugin::magic-sessionmanager.rate-limit",
10659
+ config: { profile: "write", max: 10, window: 6e4 }
10660
+ }
10529
10661
  ];
10530
10662
  const readRateLimit = [
10531
- { name: "plugin::magic-sessionmanager.rate-limit", config: { max: 120, window: 6e4 } }
10663
+ {
10664
+ name: "plugin::magic-sessionmanager.rate-limit",
10665
+ config: { profile: "read", max: 120, window: 6e4 }
10666
+ }
10532
10667
  ];
10533
10668
  var contentApi$1 = {
10534
10669
  type: "content-api",
@@ -10931,15 +11066,22 @@ var enhanceSession_1 = { enhanceSession: enhanceSession$1, enhanceSessions: enha
10931
11066
  const { hashToken: hashToken$2 } = encryption;
10932
11067
  const { enhanceSessions: enhanceSessions$1, enhanceSession } = enhanceSession_1;
10933
11068
  const { resolveUserDocumentId: resolveUserDocumentId$2 } = resolveUser;
10934
- const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
11069
+ const { getPluginSettings: getPluginSettings$3 } = settingsLoader;
10935
11070
  const { extractBearerToken: extractBearerToken$1 } = extractToken;
10936
11071
  const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
10937
11072
  const USER_UID = "plugin::users-permissions.user";
11073
+ const OWN_SESSIONS_LIMIT = 200;
11074
+ async function resolveAuthUserDocId(ctx) {
11075
+ const u2 = ctx.state.user;
11076
+ if (!u2) return null;
11077
+ if (u2.documentId) return u2.documentId;
11078
+ if (u2.id) return resolveUserDocumentId$2(strapi, u2.id);
11079
+ return null;
11080
+ }
10938
11081
  var session$3 = {
10939
11082
  /**
10940
11083
  * Lists all sessions (active + inactive) for admin overviews.
10941
11084
  * @route GET /magic-sessionmanager/sessions
10942
- * @returns {object} `{ data, meta }`
10943
11085
  */
10944
11086
  async getAllSessionsAdmin(ctx) {
10945
11087
  try {
@@ -10961,7 +11103,6 @@ var session$3 = {
10961
11103
  /**
10962
11104
  * Lists currently-active sessions only.
10963
11105
  * @route GET /magic-sessionmanager/sessions/active
10964
- * @returns {object} `{ data, meta }`
10965
11106
  */
10966
11107
  async getActiveSessions(ctx) {
10967
11108
  try {
@@ -10977,35 +11118,33 @@ var session$3 = {
10977
11118
  }
10978
11119
  },
10979
11120
  /**
10980
- * Returns the authenticated user's own sessions, with the current session
10981
- * flagged via `isCurrentSession`.
10982
- *
11121
+ * Returns the authenticated user's own sessions, current session flagged.
10983
11122
  * @route GET /api/magic-sessionmanager/my-sessions
10984
- * @returns {object} `{ data, meta }`
10985
- * @throws {UnauthorizedError} When user is not authenticated
10986
11123
  */
10987
11124
  async getOwnSessions(ctx) {
10988
11125
  try {
10989
- const userId = ctx.state.user?.documentId;
11126
+ const userDocId = await resolveAuthUserDocId(ctx);
11127
+ if (!userDocId) {
11128
+ return ctx.unauthorized("Authentication required");
11129
+ }
10990
11130
  const currentToken = extractBearerToken$1(ctx);
10991
11131
  const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
10992
- if (!userId) {
10993
- return ctx.throw(401, "Unauthorized");
10994
- }
10995
11132
  const allSessions = await strapi.documents(SESSION_UID$2).findMany({
10996
- filters: { user: { documentId: userId } },
11133
+ filters: { user: { documentId: userDocId } },
10997
11134
  sort: { loginTime: "desc" },
10998
- limit: 200
11135
+ limit: OWN_SESSIONS_LIMIT + 1
10999
11136
  });
11000
- const settings2 = await getPluginSettings$2(strapi);
11137
+ const hasMore = allSessions.length > OWN_SESSIONS_LIMIT;
11138
+ const paged = hasMore ? allSessions.slice(0, OWN_SESSIONS_LIMIT) : allSessions;
11139
+ const settings2 = await getPluginSettings$3(strapi);
11001
11140
  const enhanceOpts = {
11002
11141
  inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
11003
11142
  geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
11004
11143
  strapi
11005
11144
  };
11006
- const sessionsWithCurrent = await enhanceSessions$1(allSessions, enhanceOpts, 20);
11145
+ const sessionsWithCurrent = await enhanceSessions$1(paged, enhanceOpts, 20);
11007
11146
  for (const s3 of sessionsWithCurrent) {
11008
- s3.isCurrentSession = !!(currentTokenHash && allSessions.find(
11147
+ s3.isCurrentSession = !!(currentTokenHash && paged.find(
11009
11148
  (raw) => raw.documentId === s3.documentId && raw.tokenHash === currentTokenHash
11010
11149
  ));
11011
11150
  }
@@ -11018,7 +11157,9 @@ var session$3 = {
11018
11157
  data: sessionsWithCurrent,
11019
11158
  meta: {
11020
11159
  count: sessionsWithCurrent.length,
11021
- active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length
11160
+ active: sessionsWithCurrent.filter((s3) => s3.isTrulyActive).length,
11161
+ hasMore,
11162
+ limit: OWN_SESSIONS_LIMIT
11022
11163
  }
11023
11164
  };
11024
11165
  } catch (err) {
@@ -11027,18 +11168,14 @@ var session$3 = {
11027
11168
  }
11028
11169
  },
11029
11170
  /**
11030
- * Get a specific user's sessions. Admins may query any user; Content-API
11171
+ * Get a specific user's sessions. Admins can query any user; content-api
11031
11172
  * 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
11173
  */
11037
11174
  async getUserSessions(ctx) {
11038
11175
  try {
11039
11176
  const { userId } = ctx.params;
11040
11177
  const isAdminRequest = !!(ctx.state.userAbility || ctx.state.admin);
11041
- const requestingUserDocId = ctx.state.user?.documentId;
11178
+ const requestingUserDocId = await resolveAuthUserDocId(ctx);
11042
11179
  if (!isAdminRequest) {
11043
11180
  if (!requestingUserDocId) {
11044
11181
  strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
@@ -11067,26 +11204,34 @@ var session$3 = {
11067
11204
  */
11068
11205
  async logout(ctx) {
11069
11206
  try {
11070
- const userId = ctx.state.user?.documentId;
11207
+ const userDocId = await resolveAuthUserDocId(ctx);
11071
11208
  const token = extractBearerToken$1(ctx);
11072
- if (!userId || !token) {
11073
- return ctx.throw(401, "Unauthorized");
11209
+ if (!userDocId || !token) {
11210
+ return ctx.unauthorized("Authentication required");
11074
11211
  }
11075
11212
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11076
11213
  const currentTokenHash = hashToken$2(token);
11077
11214
  const matchingSession = await strapi.documents(SESSION_UID$2).findFirst({
11078
11215
  filters: {
11079
- user: { documentId: userId },
11216
+ user: { documentId: userDocId },
11080
11217
  tokenHash: currentTokenHash,
11081
11218
  isActive: true
11082
11219
  },
11083
11220
  fields: ["documentId"]
11084
11221
  });
11222
+ let terminated = false;
11085
11223
  if (matchingSession) {
11086
- await sessionService.terminateSession({ sessionId: matchingSession.documentId });
11087
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
11224
+ await sessionService.terminateSession({
11225
+ sessionId: matchingSession.documentId,
11226
+ reason: "manual"
11227
+ });
11228
+ terminated = true;
11229
+ strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out (session ${matchingSession.documentId})`);
11088
11230
  }
11089
- ctx.body = { message: "Logged out successfully" };
11231
+ ctx.body = {
11232
+ message: "Logged out successfully",
11233
+ terminated
11234
+ };
11090
11235
  } catch (err) {
11091
11236
  strapi.log.error("[magic-sessionmanager] Logout error:", err);
11092
11237
  ctx.throw(500, "Error during logout");
@@ -11098,13 +11243,13 @@ var session$3 = {
11098
11243
  */
11099
11244
  async logoutAll(ctx) {
11100
11245
  try {
11101
- const userId = ctx.state.user?.documentId;
11102
- if (!userId) {
11103
- return ctx.throw(401, "Unauthorized");
11246
+ const userDocId = await resolveAuthUserDocId(ctx);
11247
+ if (!userDocId) {
11248
+ return ctx.unauthorized("Authentication required");
11104
11249
  }
11105
11250
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11106
- await sessionService.terminateSession({ userId });
11107
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out from all devices`);
11251
+ await sessionService.terminateSession({ userId: userDocId, reason: "manual" });
11252
+ strapi.log.info(`[magic-sessionmanager] User ${userDocId} logged out from all devices`);
11108
11253
  ctx.body = { message: "Logged out from all devices successfully" };
11109
11254
  } catch (err) {
11110
11255
  strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
@@ -11113,27 +11258,48 @@ var session$3 = {
11113
11258
  },
11114
11259
  /**
11115
11260
  * Returns the session associated with the current JWT.
11261
+ *
11262
+ * During the post-login grace window the session-create write may not
11263
+ * yet be visible. In that case we return 202 Accepted with
11264
+ * `{ pending: true }` so the client knows to retry shortly instead of
11265
+ * interpreting a 404 as "no session at all".
11266
+ *
11116
11267
  * @route GET /api/magic-sessionmanager/current-session
11117
11268
  */
11118
11269
  async getCurrentSession(ctx) {
11119
11270
  try {
11120
- const userId = ctx.state.user?.documentId;
11271
+ const userDocId = await resolveAuthUserDocId(ctx);
11121
11272
  const token = extractBearerToken$1(ctx);
11122
- if (!userId || !token) {
11123
- return ctx.throw(401, "Unauthorized");
11273
+ if (!userDocId || !token) {
11274
+ return ctx.unauthorized("Authentication required");
11124
11275
  }
11125
11276
  const currentTokenHash = hashToken$2(token);
11126
11277
  const currentSession = await strapi.documents(SESSION_UID$2).findFirst({
11127
11278
  filters: {
11128
- user: { documentId: userId },
11279
+ user: { documentId: userDocId },
11129
11280
  tokenHash: currentTokenHash,
11130
11281
  isActive: true
11131
11282
  }
11132
11283
  });
11133
11284
  if (!currentSession) {
11285
+ const settings3 = await getPluginSettings$3(strapi);
11286
+ const gracePeriodMs = Math.max(0, Number(settings3.sessionCreationGraceMs) || 5e3);
11287
+ const iat = ctx.state.user?.iat || ctx.state.auth?.credentials?.iat || null;
11288
+ if (gracePeriodMs > 0 && typeof iat === "number") {
11289
+ const ageMs = Date.now() - iat * 1e3;
11290
+ if (ageMs >= 0 && ageMs < gracePeriodMs) {
11291
+ ctx.status = 202;
11292
+ ctx.body = {
11293
+ data: null,
11294
+ meta: { pending: true, retryAfterMs: gracePeriodMs - ageMs },
11295
+ message: "Session is still being created — please retry shortly."
11296
+ };
11297
+ return;
11298
+ }
11299
+ }
11134
11300
  return ctx.notFound("Current session not found");
11135
11301
  }
11136
- const settings2 = await getPluginSettings$2(strapi);
11302
+ const settings2 = await getPluginSettings$3(strapi);
11137
11303
  const enhanced = await enhanceSession(currentSession, {
11138
11304
  inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
11139
11305
  geolocationService: strapi.plugin("magic-sessionmanager").service("geolocation"),
@@ -11149,17 +11315,17 @@ var session$3 = {
11149
11315
  }
11150
11316
  },
11151
11317
  /**
11152
- * Terminates one of the authenticated user's OWN sessions (not the current one).
11318
+ * Terminates one of the authenticated user's OWN sessions (not current).
11153
11319
  * @route DELETE /api/magic-sessionmanager/my-sessions/:sessionId
11154
11320
  */
11155
11321
  async terminateOwnSession(ctx) {
11156
11322
  try {
11157
- const userId = ctx.state.user?.documentId;
11323
+ const userDocId = await resolveAuthUserDocId(ctx);
11158
11324
  const { sessionId } = ctx.params;
11159
11325
  const currentToken = extractBearerToken$1(ctx);
11160
11326
  const currentTokenHash = currentToken ? hashToken$2(currentToken) : null;
11161
- if (!userId) {
11162
- return ctx.throw(401, "Unauthorized");
11327
+ if (!userDocId) {
11328
+ return ctx.unauthorized("Authentication required");
11163
11329
  }
11164
11330
  if (!sessionId) {
11165
11331
  return ctx.badRequest("Session ID is required");
@@ -11172,19 +11338,23 @@ var session$3 = {
11172
11338
  return ctx.notFound("Session not found");
11173
11339
  }
11174
11340
  const sessionUserId = sessionToTerminate.user?.documentId;
11175
- if (sessionUserId !== userId) {
11176
- strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
11341
+ if (sessionUserId !== userDocId) {
11342
+ strapi.log.warn(`[magic-sessionmanager] Security: User ${userDocId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
11177
11343
  return ctx.forbidden("You can only terminate your own sessions");
11178
11344
  }
11179
11345
  if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
11180
- return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
11346
+ return ctx.badRequest("Cannot terminate the current session. Use /logout instead.");
11347
+ }
11348
+ const alreadyTerminated = sessionToTerminate.isActive === false;
11349
+ if (!alreadyTerminated) {
11350
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11351
+ await sessionService.terminateSession({ sessionId, reason: "manual" });
11352
+ strapi.log.info(`[magic-sessionmanager] User ${userDocId} terminated own session ${sessionId}`);
11181
11353
  }
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
11354
  ctx.body = {
11186
- message: `Session ${sessionId} terminated successfully`,
11187
- success: true
11355
+ message: alreadyTerminated ? `Session ${sessionId} was already terminated` : `Session ${sessionId} terminated successfully`,
11356
+ success: true,
11357
+ alreadyTerminated
11188
11358
  };
11189
11359
  } catch (err) {
11190
11360
  strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
@@ -11192,9 +11362,7 @@ var session$3 = {
11192
11362
  }
11193
11363
  },
11194
11364
  /**
11195
- * Sets isActive:false + terminatedManually:false on a session, simulating
11196
- * a cleanup timeout. Available only outside of production/staging.
11197
- *
11365
+ * Simulates an inactivity timeout on a session. Dev-only.
11198
11366
  * @route POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
11199
11367
  */
11200
11368
  async simulateTimeout(ctx) {
@@ -11213,13 +11381,16 @@ var session$3 = {
11213
11381
  }
11214
11382
  await strapi.documents(SESSION_UID$2).update({
11215
11383
  documentId: sessionId,
11216
- data: { isActive: false, terminatedManually: false }
11384
+ data: {
11385
+ isActive: false,
11386
+ terminatedManually: false,
11387
+ terminationReason: "idle"
11388
+ }
11217
11389
  });
11218
- strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout (terminatedManually: false)`);
11390
+ strapi.log.info(`[magic-sessionmanager] [TEST] Session ${sessionId} simulated timeout`);
11219
11391
  ctx.body = {
11220
- message: `Session ${sessionId} marked as timed out (reactivatable)`,
11221
- success: true,
11222
- terminatedManually: false
11392
+ message: `Session ${sessionId} marked as timed out`,
11393
+ success: true
11223
11394
  };
11224
11395
  } catch (err) {
11225
11396
  strapi.log.error("[magic-sessionmanager] Error simulating timeout:", err);
@@ -11228,13 +11399,12 @@ var session$3 = {
11228
11399
  },
11229
11400
  /**
11230
11401
  * Terminates a specific session (admin action).
11231
- * @route POST /magic-sessionmanager/sessions/:sessionId/terminate
11232
11402
  */
11233
11403
  async terminateSingleSession(ctx) {
11234
11404
  try {
11235
11405
  const { sessionId } = ctx.params;
11236
11406
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11237
- await sessionService.terminateSession({ sessionId });
11407
+ await sessionService.terminateSession({ sessionId, reason: "manual" });
11238
11408
  ctx.body = {
11239
11409
  message: `Session ${sessionId} terminated`,
11240
11410
  success: true
@@ -11246,13 +11416,12 @@ var session$3 = {
11246
11416
  },
11247
11417
  /**
11248
11418
  * Terminates ALL sessions for a specific user (admin action).
11249
- * @route POST /magic-sessionmanager/user/:userId/terminate-all
11250
11419
  */
11251
11420
  async terminateAllUserSessions(ctx) {
11252
11421
  try {
11253
11422
  const { userId } = ctx.params;
11254
11423
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11255
- await sessionService.terminateSession({ userId });
11424
+ await sessionService.terminateSession({ userId, reason: "manual" });
11256
11425
  ctx.body = {
11257
11426
  message: `All sessions terminated for user ${userId}`,
11258
11427
  success: true
@@ -11264,9 +11433,6 @@ var session$3 = {
11264
11433
  },
11265
11434
  /**
11266
11435
  * 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
11436
  */
11271
11437
  async getIpGeolocation(ctx) {
11272
11438
  try {
@@ -11293,10 +11459,7 @@ var session$3 = {
11293
11459
  return ctx.badRequest("Invalid IP address format");
11294
11460
  }
11295
11461
  const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
11296
- const pluginStore = strapi.store({
11297
- type: "plugin",
11298
- name: "magic-sessionmanager"
11299
- });
11462
+ const pluginStore = strapi.store({ type: "plugin", name: "magic-sessionmanager" });
11300
11463
  const licenseKey = await pluginStore.get({ key: "licenseKey" });
11301
11464
  if (!licenseKey) {
11302
11465
  return ctx.forbidden("Premium license required for geolocation features");
@@ -11318,7 +11481,6 @@ var session$3 = {
11318
11481
  },
11319
11482
  /**
11320
11483
  * Permanently deletes a session (admin action).
11321
- * @route DELETE /magic-sessionmanager/sessions/:sessionId
11322
11484
  */
11323
11485
  async deleteSession(ctx) {
11324
11486
  try {
@@ -11336,7 +11498,6 @@ var session$3 = {
11336
11498
  },
11337
11499
  /**
11338
11500
  * Deletes all inactive sessions (admin action).
11339
- * @route POST /magic-sessionmanager/sessions/clean-inactive
11340
11501
  */
11341
11502
  async cleanInactiveSessions(ctx) {
11342
11503
  try {
@@ -11354,9 +11515,6 @@ var session$3 = {
11354
11515
  },
11355
11516
  /**
11356
11517
  * 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
11518
  */
11361
11519
  async toggleUserBlock(ctx) {
11362
11520
  try {
@@ -11372,11 +11530,11 @@ var session$3 = {
11372
11530
  }
11373
11531
  }
11374
11532
  if (!userDocumentId) {
11375
- return ctx.throw(404, "User not found");
11533
+ return ctx.notFound("User not found");
11376
11534
  }
11377
11535
  const user = await strapi.documents(USER_UID).findOne({ documentId: userDocumentId });
11378
11536
  if (!user) {
11379
- return ctx.throw(404, "User not found");
11537
+ return ctx.notFound("User not found");
11380
11538
  }
11381
11539
  const newBlockedStatus = !user.blocked;
11382
11540
  await strapi.documents(USER_UID).update({
@@ -11385,7 +11543,7 @@ var session$3 = {
11385
11543
  });
11386
11544
  if (newBlockedStatus) {
11387
11545
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
11388
- await sessionService.terminateSession({ userId: userDocumentId });
11546
+ await sessionService.terminateSession({ userId: userDocumentId, reason: "blocked" });
11389
11547
  }
11390
11548
  ctx.body = {
11391
11549
  message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
@@ -11877,13 +12035,13 @@ const { createLogger: createLogger$1 } = logger;
11877
12035
  const { parseUserAgent } = userAgentParser;
11878
12036
  const { resolveUserDocumentId: resolveUserDocumentId$1 } = resolveUser;
11879
12037
  const { enhanceSessions } = enhanceSession_1;
11880
- const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
12038
+ const { getPluginSettings: getPluginSettings$2 } = settingsLoader;
11881
12039
  const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
11882
12040
  const MAX_SESSIONS_QUERY = 1e3;
11883
12041
  var session$1 = ({ strapi: strapi2 }) => {
11884
12042
  const log = createLogger$1(strapi2);
11885
12043
  async function getEnhanceOpts() {
11886
- const settings2 = await getPluginSettings$1(strapi2);
12044
+ const settings2 = await getPluginSettings$2(strapi2);
11887
12045
  return {
11888
12046
  inactivityTimeout: settings2.inactivityTimeout || 15 * 60 * 1e3,
11889
12047
  geolocationService: strapi2.plugin("magic-sessionmanager").service("geolocation"),
@@ -11952,15 +12110,38 @@ var session$1 = ({ strapi: strapi2 }) => {
11952
12110
  }
11953
12111
  },
11954
12112
  /**
11955
- * Terminates a single session or all sessions of a user.
12113
+ * Terminates a single session or all sessions of a user with a typed
12114
+ * reason so the JWT-verify wrapper and downstream middleware can
12115
+ * communicate the cause to the client (and we can show sensible UI).
12116
+ *
12117
+ * Supported reasons:
12118
+ * - 'manual': user clicked logout, or admin terminated a session
12119
+ * - 'idle': inactivity timeout cleanup
12120
+ * - 'expired': maxSessionAgeDays exceeded
12121
+ * - 'blocked': the owning user was marked blocked
12122
+ *
12123
+ * For backwards compatibility `terminatedManually` is still set true
12124
+ * only when reason === 'manual'; idle/expired/blocked paths set it
12125
+ * false so reporting dashboards that queried that boolean continue
12126
+ * to work, while new code relies on `terminationReason`.
12127
+ *
11956
12128
  * @param {Object} params
11957
12129
  * @param {string} [params.sessionId]
11958
12130
  * @param {string|number} [params.userId]
12131
+ * @param {'manual'|'idle'|'expired'|'blocked'} [params.reason='manual']
11959
12132
  * @returns {Promise<void>}
11960
12133
  */
11961
- async terminateSession({ sessionId, userId }) {
12134
+ async terminateSession({ sessionId, userId, reason = "manual" }) {
11962
12135
  try {
11963
12136
  const now = /* @__PURE__ */ new Date();
12137
+ const validReasons = ["manual", "idle", "expired", "blocked"];
12138
+ const finalReason = validReasons.includes(reason) ? reason : "manual";
12139
+ const updateData = {
12140
+ isActive: false,
12141
+ terminatedManually: finalReason === "manual",
12142
+ terminationReason: finalReason,
12143
+ logoutTime: now
12144
+ };
11964
12145
  if (sessionId) {
11965
12146
  const existing = await strapi2.documents(SESSION_UID$1).findOne({
11966
12147
  documentId: sessionId,
@@ -11972,9 +12153,9 @@ var session$1 = ({ strapi: strapi2 }) => {
11972
12153
  }
11973
12154
  await strapi2.documents(SESSION_UID$1).update({
11974
12155
  documentId: sessionId,
11975
- data: { isActive: false, terminatedManually: true, logoutTime: now }
12156
+ data: updateData
11976
12157
  });
11977
- log.info(`Session ${sessionId} terminated (manual)`);
12158
+ log.info(`Session ${sessionId} terminated (reason: ${finalReason})`);
11978
12159
  } else if (userId) {
11979
12160
  const userDocumentId = await resolveUserDocumentId$1(strapi2, userId);
11980
12161
  if (!userDocumentId) return;
@@ -11986,10 +12167,10 @@ var session$1 = ({ strapi: strapi2 }) => {
11986
12167
  for (const session2 of activeSessions) {
11987
12168
  await strapi2.documents(SESSION_UID$1).update({
11988
12169
  documentId: session2.documentId,
11989
- data: { isActive: false, terminatedManually: true, logoutTime: now }
12170
+ data: updateData
11990
12171
  });
11991
12172
  }
11992
- log.info(`All sessions terminated (manual) for user ${userDocumentId}`);
12173
+ log.info(`All sessions terminated for user ${userDocumentId} (reason: ${finalReason})`);
11993
12174
  }
11994
12175
  } catch (err) {
11995
12176
  log.error("Error terminating session:", err);
@@ -12074,7 +12255,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12074
12255
  async touch({ userId, sessionId, token }) {
12075
12256
  try {
12076
12257
  const now = /* @__PURE__ */ new Date();
12077
- const settings2 = await getPluginSettings$1(strapi2);
12258
+ const settings2 = await getPluginSettings$2(strapi2);
12078
12259
  const rateLimit2 = settings2.lastSeenRateLimit || 3e4;
12079
12260
  let session2 = null;
12080
12261
  let sessionDocId = sessionId;
@@ -12143,7 +12324,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12143
12324
  */
12144
12325
  async cleanupInactiveSessions({ useDbDirect = false } = {}) {
12145
12326
  try {
12146
- const settings2 = await getPluginSettings$1(strapi2);
12327
+ const settings2 = await getPluginSettings$2(strapi2);
12147
12328
  const inactivityTimeout = settings2.inactivityTimeout || 15 * 60 * 1e3;
12148
12329
  const now = /* @__PURE__ */ new Date();
12149
12330
  const cutoffTime = new Date(now.getTime() - inactivityTimeout);
@@ -12156,7 +12337,8 @@ var session$1 = ({ strapi: strapi2 }) => {
12156
12337
  });
12157
12338
  }).update({
12158
12339
  is_active: false,
12159
- terminated_manually: true,
12340
+ terminated_manually: false,
12341
+ termination_reason: "idle",
12160
12342
  logout_time: now
12161
12343
  });
12162
12344
  log.info(`[SUCCESS] Cleanup (db-direct) complete: ${deactivated} sessions deactivated`);
@@ -12197,7 +12379,8 @@ var session$1 = ({ strapi: strapi2 }) => {
12197
12379
  documentId,
12198
12380
  data: {
12199
12381
  isActive: false,
12200
- terminatedManually: true,
12382
+ terminatedManually: false,
12383
+ terminationReason: "idle",
12201
12384
  logoutTime: now
12202
12385
  }
12203
12386
  });
@@ -12243,7 +12426,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12243
12426
  */
12244
12427
  async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
12245
12428
  try {
12246
- const settings2 = await getPluginSettings$1(strapi2);
12429
+ const settings2 = await getPluginSettings$2(strapi2);
12247
12430
  const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
12248
12431
  if (effectiveDays === -1) {
12249
12432
  log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
@@ -12348,7 +12531,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12348
12531
  }
12349
12532
  };
12350
12533
  };
12351
- const version$1 = "4.5.1";
12534
+ const version$1 = "4.5.2";
12352
12535
  const require$$2 = {
12353
12536
  version: version$1
12354
12537
  };
@@ -13306,6 +13489,7 @@ var services$1 = {
13306
13489
  geolocation,
13307
13490
  notifications
13308
13491
  };
13492
+ const { getPluginSettings: getPluginSettings$1 } = settingsLoader;
13309
13493
  const buckets = /* @__PURE__ */ new Map();
13310
13494
  const prune = (now) => {
13311
13495
  for (const [key, entry] of buckets) {
@@ -13319,10 +13503,45 @@ const callerKey = (ctx) => {
13319
13503
  if (tokenId) return `t:${String(tokenId).slice(-16)}`;
13320
13504
  return `ip:${ctx.request.ip || ctx.ip || "unknown"}`;
13321
13505
  };
13506
+ const RESOLVED_TTL_MS = 3e4;
13507
+ let resolvedCache = null;
13508
+ let resolvedAt = 0;
13509
+ async function resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) {
13510
+ const now = Date.now();
13511
+ if (resolvedCache && now - resolvedAt < RESOLVED_TTL_MS) {
13512
+ const p = resolvedCache[profile];
13513
+ if (p) {
13514
+ return { max: p.max, windowMs: p.windowMs };
13515
+ }
13516
+ }
13517
+ let settings2 = {};
13518
+ try {
13519
+ settings2 = await getPluginSettings$1(strapi2);
13520
+ } catch {
13521
+ settings2 = {};
13522
+ }
13523
+ const windowSec = Number.isFinite(settings2.rateLimitWindowSeconds) ? settings2.rateLimitWindowSeconds : Math.round(routeWindowMs / 1e3);
13524
+ const windowMs = Math.max(1e4, windowSec * 1e3);
13525
+ const resolvedWrite = {
13526
+ max: Math.min(routeMax, Number.isFinite(settings2.rateLimitWriteMax) ? settings2.rateLimitWriteMax : routeMax),
13527
+ windowMs
13528
+ };
13529
+ const resolvedRead = {
13530
+ max: Math.max(routeMax, Number.isFinite(settings2.rateLimitReadMax) ? settings2.rateLimitReadMax : routeMax),
13531
+ windowMs
13532
+ };
13533
+ resolvedCache = { read: resolvedRead, write: resolvedWrite };
13534
+ resolvedAt = now;
13535
+ if (profile === "read") return resolvedRead;
13536
+ if (profile === "write") return resolvedWrite;
13537
+ return { max: routeMax, windowMs };
13538
+ }
13322
13539
  const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
13323
- const max = Number.isFinite(cfg.max) ? cfg.max : 30;
13324
- const windowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
13540
+ const routeMax = Number.isFinite(cfg.max) ? cfg.max : 30;
13541
+ const routeWindowMs = Number.isFinite(cfg.window) ? cfg.window : 6e4;
13542
+ const profile = cfg.profile === "read" || cfg.profile === "write" ? cfg.profile : null;
13325
13543
  return async (ctx, next) => {
13544
+ const { max, windowMs } = profile ? await resolveLimits({ profile, routeMax, routeWindowMs, strapi: strapi2 }) : { max: routeMax, windowMs: routeWindowMs };
13326
13545
  const key = `${ctx.path}::${callerKey(ctx)}`;
13327
13546
  const now = Date.now();
13328
13547
  if (buckets.size > 5e3) prune(now);
@@ -13356,7 +13575,8 @@ const rateLimit = (cfg = {}, { strapi: strapi2 }) => {
13356
13575
  var rateLimit_1 = rateLimit;
13357
13576
  var middlewares$1 = {
13358
13577
  "last-seen": lastSeen,
13359
- "rate-limit": rateLimit_1
13578
+ "rate-limit": rateLimit_1,
13579
+ "session-rejection-headers": sessionRejectionHeaders
13360
13580
  };
13361
13581
  var lodashExports = requireLodash();
13362
13582
  const ___default = /* @__PURE__ */ getDefaultExportFromCjs(lodashExports);
@@ -51437,18 +51657,18 @@ const CSP_DEFAULTS = {
51437
51657
  "blob:"
51438
51658
  ]
51439
51659
  };
51440
- const extendMiddlewareConfiguration = (middlewares2, middleware) => {
51660
+ const extendMiddlewareConfiguration = (middlewares2, middleware2) => {
51441
51661
  return middlewares2.map((currentMiddleware) => {
51442
- if (typeof currentMiddleware === "string" && currentMiddleware === middleware.name) {
51443
- return middleware;
51662
+ if (typeof currentMiddleware === "string" && currentMiddleware === middleware2.name) {
51663
+ return middleware2;
51444
51664
  }
51445
- if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware.name) {
51665
+ if (typeof currentMiddleware === "object" && currentMiddleware.name === middleware2.name) {
51446
51666
  return fp.mergeWith((objValue, srcValue) => {
51447
51667
  if (Array.isArray(objValue)) {
51448
51668
  return Array.from(new Set(objValue.concat(srcValue)));
51449
51669
  }
51450
51670
  return void 0;
51451
- }, currentMiddleware, middleware);
51671
+ }, currentMiddleware, middleware2);
51452
51672
  }
51453
51673
  return currentMiddleware;
51454
51674
  });