strapi-plugin-magic-sessionmanager 4.2.9 → 4.2.11

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/README.md CHANGED
@@ -390,30 +390,204 @@ Trigger a suspicious login (e.g., use a VPN) and check if the email arrives!
390
390
 
391
391
  ---
392
392
 
393
- ## 📋 Simple API Guide
393
+ ## 📋 Content-API Endpoints (For Frontend/Apps)
394
394
 
395
- ### Get Sessions
395
+ All Content-API endpoints require a valid JWT token in the `Authorization` header.
396
+ Users can only access their **own** sessions.
397
+
398
+ ### Get My Sessions
399
+
400
+ Returns all sessions for the authenticated user.
401
+
402
+ ```bash
403
+ GET /api/magic-sessionmanager/my-sessions
404
+ Authorization: Bearer <JWT>
405
+ ```
406
+
407
+ **Response:**
408
+ ```json
409
+ {
410
+ "data": [
411
+ {
412
+ "id": 41,
413
+ "documentId": "abc123xyz",
414
+ "sessionId": "sess_m5k2h_8a3b1c2d_f9e8d7c6",
415
+ "ipAddress": "192.168.1.100",
416
+ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
417
+ "loginTime": "2026-01-02T10:30:00.000Z",
418
+ "lastActive": "2026-01-02T13:45:00.000Z",
419
+ "logoutTime": null,
420
+ "isActive": true,
421
+ "deviceType": "desktop",
422
+ "browserName": "Chrome 143",
423
+ "osName": "macOS 10.15.7",
424
+ "geoLocation": null,
425
+ "securityScore": null,
426
+ "isCurrentSession": true,
427
+ "isTrulyActive": true,
428
+ "minutesSinceActive": 2
429
+ },
430
+ {
431
+ "id": 40,
432
+ "documentId": "def456uvw",
433
+ "sessionId": "sess_m5k1g_7b2a0c1d_e8d7c6b5",
434
+ "ipAddress": "10.0.0.50",
435
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...",
436
+ "loginTime": "2026-01-01T08:15:00.000Z",
437
+ "lastActive": "2026-01-01T12:00:00.000Z",
438
+ "logoutTime": null,
439
+ "isActive": true,
440
+ "deviceType": "mobile",
441
+ "browserName": "Safari",
442
+ "osName": "iOS 17",
443
+ "geoLocation": null,
444
+ "securityScore": null,
445
+ "isCurrentSession": false,
446
+ "isTrulyActive": false,
447
+ "minutesSinceActive": 1545
448
+ }
449
+ ],
450
+ "meta": {
451
+ "count": 2,
452
+ "active": 1
453
+ }
454
+ }
455
+ ```
456
+
457
+ ### Get Current Session
458
+
459
+ Returns only the session associated with the current JWT token.
460
+
461
+ ```bash
462
+ GET /api/magic-sessionmanager/current-session
463
+ Authorization: Bearer <JWT>
464
+ ```
465
+
466
+ **Response:**
467
+ ```json
468
+ {
469
+ "data": {
470
+ "id": 41,
471
+ "documentId": "abc123xyz",
472
+ "sessionId": "sess_m5k2h_8a3b1c2d_f9e8d7c6",
473
+ "ipAddress": "192.168.1.100",
474
+ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
475
+ "loginTime": "2026-01-02T10:30:00.000Z",
476
+ "lastActive": "2026-01-02T13:45:00.000Z",
477
+ "logoutTime": null,
478
+ "isActive": true,
479
+ "deviceType": "desktop",
480
+ "browserName": "Chrome 143",
481
+ "osName": "macOS 10.15.7",
482
+ "geoLocation": null,
483
+ "securityScore": null,
484
+ "isCurrentSession": true,
485
+ "isTrulyActive": true,
486
+ "minutesSinceActive": 2
487
+ }
488
+ }
489
+ ```
490
+
491
+ ### Logout (Current Session)
492
+
493
+ Terminates only the current session.
494
+
495
+ ```bash
496
+ POST /api/magic-sessionmanager/logout
497
+ Authorization: Bearer <JWT>
498
+ ```
499
+
500
+ **Response:**
501
+ ```json
502
+ {
503
+ "message": "Logged out successfully"
504
+ }
505
+ ```
506
+
507
+ ### Logout All Devices
508
+
509
+ Terminates ALL sessions for the authenticated user (logs out everywhere).
510
+
511
+ ```bash
512
+ POST /api/magic-sessionmanager/logout-all
513
+ Authorization: Bearer <JWT>
514
+ ```
515
+
516
+ **Response:**
517
+ ```json
518
+ {
519
+ "message": "Logged out from all devices successfully"
520
+ }
521
+ ```
522
+
523
+ ### Terminate Specific Session
524
+
525
+ Terminates a specific session (not the current one). Useful for "Log out other devices".
526
+
527
+ ```bash
528
+ DELETE /api/magic-sessionmanager/my-sessions/:sessionId
529
+ Authorization: Bearer <JWT>
530
+ ```
531
+
532
+ **Response:**
533
+ ```json
534
+ {
535
+ "message": "Session abc123xyz terminated successfully",
536
+ "success": true
537
+ }
538
+ ```
539
+
540
+ **Error (trying to terminate current session):**
541
+ ```json
542
+ {
543
+ "error": {
544
+ "status": 400,
545
+ "message": "Cannot terminate current session. Use /logout instead."
546
+ }
547
+ }
548
+ ```
549
+
550
+ ---
551
+
552
+ ## 📋 Admin-API Endpoints (For Admin Panel)
553
+
554
+ These endpoints require admin authentication.
555
+
556
+ ### Get All Sessions
396
557
 
397
558
  ```bash
398
- # Get all active sessions
399
559
  GET /magic-sessionmanager/sessions
400
560
  ```
401
561
 
402
- ### Logout
562
+ ### Get Active Sessions Only
403
563
 
404
564
  ```bash
405
- # Logout current user
406
- POST /api/auth/logout
565
+ GET /magic-sessionmanager/sessions/active
407
566
  ```
408
567
 
409
- ### Force Logout
568
+ ### Force Terminate Session
410
569
 
411
570
  ```bash
412
- # Admin force-logout a session
413
571
  POST /magic-sessionmanager/sessions/:sessionId/terminate
414
572
  ```
415
573
 
416
- **That's all you need to know!**
574
+ ### Terminate All User Sessions
575
+
576
+ ```bash
577
+ POST /magic-sessionmanager/user/:userId/terminate-all
578
+ ```
579
+
580
+ ### Block/Unblock User
581
+
582
+ ```bash
583
+ POST /magic-sessionmanager/user/:userId/toggle-block
584
+ ```
585
+
586
+ ### Clean Inactive Sessions
587
+
588
+ ```bash
589
+ POST /magic-sessionmanager/sessions/clean-inactive
590
+ ```
417
591
 
418
592
  ---
419
593
 
@@ -206,7 +206,7 @@ function generateSessionId$1(userId) {
206
206
  const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
207
207
  return `sess_${timestamp}_${userHash}_${randomBytes}`;
208
208
  }
209
- function hashToken$3(token) {
209
+ function hashToken$4(token) {
210
210
  if (!token) return null;
211
211
  return crypto$1.createHash("sha256").update(token).digest("hex");
212
212
  }
@@ -214,10 +214,10 @@ var encryption = {
214
214
  encryptToken: encryptToken$2,
215
215
  decryptToken: decryptToken$3,
216
216
  generateSessionId: generateSessionId$1,
217
- hashToken: hashToken$3
217
+ hashToken: hashToken$4
218
218
  };
219
219
  const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
220
- const { hashToken: hashToken$2 } = encryption;
220
+ const { hashToken: hashToken$3 } = encryption;
221
221
  const lastTouchCache = /* @__PURE__ */ new Map();
222
222
  var lastSeen = ({ strapi: strapi2 }) => {
223
223
  return async (ctx, next) => {
@@ -233,7 +233,7 @@ var lastSeen = ({ strapi: strapi2 }) => {
233
233
  }
234
234
  let matchingSession = null;
235
235
  try {
236
- const currentTokenHash = hashToken$2(currentToken);
236
+ const currentTokenHash = hashToken$3(currentToken);
237
237
  matchingSession = await strapi2.documents(SESSION_UID$3).findFirst({
238
238
  filters: {
239
239
  tokenHash: currentTokenHash,
@@ -278,7 +278,7 @@ var lastSeen = ({ strapi: strapi2 }) => {
278
278
  };
279
279
  };
280
280
  const getClientIp = getClientIp_1;
281
- const { encryptToken: encryptToken$1, decryptToken: decryptToken$2, hashToken: hashToken$1 } = encryption;
281
+ const { encryptToken: encryptToken$1, decryptToken: decryptToken$2, hashToken: hashToken$2 } = encryption;
282
282
  const { createLogger: createLogger$3 } = logger;
283
283
  const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
284
284
  const USER_UID$2 = "plugin::users-permissions.user";
@@ -286,6 +286,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
286
286
  const log = createLogger$3(strapi2);
287
287
  log.info("[START] Bootstrap starting...");
288
288
  try {
289
+ await ensureTokenHashIndex(strapi2, log);
289
290
  const licenseGuardService = strapi2.plugin("magic-sessionmanager").service("license-guard");
290
291
  setTimeout(async () => {
291
292
  const licenseStatus = await licenseGuardService.initialize();
@@ -562,8 +563,8 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
562
563
  if (matchingSession) {
563
564
  const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
564
565
  const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
565
- const newTokenHash = newAccessToken ? hashToken$1(newAccessToken) : matchingSession.tokenHash;
566
- const newRefreshTokenHash = newRefreshToken ? hashToken$1(newRefreshToken) : matchingSession.refreshTokenHash;
566
+ const newTokenHash = newAccessToken ? hashToken$2(newAccessToken) : matchingSession.tokenHash;
567
+ const newRefreshTokenHash = newRefreshToken ? hashToken$2(newRefreshToken) : matchingSession.refreshTokenHash;
567
568
  await strapi2.documents(SESSION_UID$2).update({
568
569
  documentId: matchingSession.documentId,
569
570
  data: {
@@ -642,6 +643,46 @@ async function ensureContentApiPermissions(strapi2, log) {
642
643
  log.warn("Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated");
643
644
  }
644
645
  }
646
+ async function ensureTokenHashIndex(strapi2, log) {
647
+ try {
648
+ const knex = strapi2.db.connection;
649
+ const tableName = "magic_sessions";
650
+ const indexName = "idx_magic_sessions_token_hash";
651
+ const hasIndex = await knex.schema.hasTable(tableName).then(async (exists) => {
652
+ if (!exists) return false;
653
+ const dialect = strapi2.db.dialect.client;
654
+ if (dialect === "postgres") {
655
+ const result = await knex.raw(`
656
+ SELECT indexname FROM pg_indexes
657
+ WHERE tablename = ? AND indexname = ?
658
+ `, [tableName, indexName]);
659
+ return result.rows.length > 0;
660
+ } else if (dialect === "mysql" || dialect === "mysql2") {
661
+ const result = await knex.raw(`
662
+ SHOW INDEX FROM ${tableName} WHERE Key_name = ?
663
+ `, [indexName]);
664
+ return result[0].length > 0;
665
+ } else if (dialect === "sqlite" || dialect === "better-sqlite3") {
666
+ const result = await knex.raw(`
667
+ SELECT name FROM sqlite_master
668
+ WHERE type='index' AND name = ?
669
+ `, [indexName]);
670
+ return result.length > 0;
671
+ }
672
+ return false;
673
+ });
674
+ if (hasIndex) {
675
+ log.debug("[INDEX] tokenHash index already exists");
676
+ return;
677
+ }
678
+ await knex.schema.alterTable(tableName, (table) => {
679
+ table.index(["token_hash", "is_active"], indexName);
680
+ });
681
+ log.info("[INDEX] Created tokenHash index for O(1) session lookup");
682
+ } catch (err) {
683
+ log.debug("[INDEX] Could not create tokenHash index (will retry on next startup):", err.message);
684
+ }
685
+ }
645
686
  const { createLogger: createLogger$2 } = logger;
646
687
  var destroy$1 = async ({ strapi: strapi2 }) => {
647
688
  const log = createLogger$2(strapi2);
@@ -722,7 +763,8 @@ const attributes = {
722
763
  },
723
764
  tokenHash: {
724
765
  type: "string",
725
- configurable: false
766
+ configurable: false,
767
+ unique: false
726
768
  },
727
769
  refreshToken: {
728
770
  type: "text",
@@ -1000,7 +1042,91 @@ var routes$1 = {
1000
1042
  admin,
1001
1043
  "content-api": contentApi
1002
1044
  };
1003
- const { decryptToken: decryptToken$1 } = encryption;
1045
+ function parseUserAgent$2(userAgent) {
1046
+ if (!userAgent) {
1047
+ return {
1048
+ deviceType: "unknown",
1049
+ browserName: "unknown",
1050
+ browserVersion: null,
1051
+ osName: "unknown",
1052
+ osVersion: null
1053
+ };
1054
+ }
1055
+ userAgent.toLowerCase();
1056
+ let deviceType = "desktop";
1057
+ if (/mobile|android.*mobile|iphone|ipod|blackberry|iemobile|opera mini|opera mobi/i.test(userAgent)) {
1058
+ deviceType = "mobile";
1059
+ } else if (/tablet|ipad|android(?!.*mobile)|kindle|silk/i.test(userAgent)) {
1060
+ deviceType = "tablet";
1061
+ } else if (/bot|crawl|spider|slurp|mediapartners/i.test(userAgent)) {
1062
+ deviceType = "bot";
1063
+ }
1064
+ let browserName = "unknown";
1065
+ let browserVersion = null;
1066
+ if (/edg\//i.test(userAgent)) {
1067
+ browserName = "Edge";
1068
+ browserVersion = extractVersion(userAgent, /edg\/(\d+[\.\d]*)/i);
1069
+ } else if (/opr\//i.test(userAgent) || /opera/i.test(userAgent)) {
1070
+ browserName = "Opera";
1071
+ browserVersion = extractVersion(userAgent, /(?:opr|opera)[\s\/](\d+[\.\d]*)/i);
1072
+ } else if (/chrome|crios/i.test(userAgent) && !/edg/i.test(userAgent)) {
1073
+ browserName = "Chrome";
1074
+ browserVersion = extractVersion(userAgent, /(?:chrome|crios)\/(\d+[\.\d]*)/i);
1075
+ } else if (/firefox|fxios/i.test(userAgent)) {
1076
+ browserName = "Firefox";
1077
+ browserVersion = extractVersion(userAgent, /(?:firefox|fxios)\/(\d+[\.\d]*)/i);
1078
+ } else if (/safari/i.test(userAgent) && !/chrome|chromium/i.test(userAgent)) {
1079
+ browserName = "Safari";
1080
+ browserVersion = extractVersion(userAgent, /version\/(\d+[\.\d]*)/i);
1081
+ } else if (/msie|trident/i.test(userAgent)) {
1082
+ browserName = "Internet Explorer";
1083
+ browserVersion = extractVersion(userAgent, /(?:msie |rv:)(\d+[\.\d]*)/i);
1084
+ }
1085
+ let osName = "unknown";
1086
+ let osVersion = null;
1087
+ if (/windows nt/i.test(userAgent)) {
1088
+ osName = "Windows";
1089
+ const winVersion = extractVersion(userAgent, /windows nt (\d+[\.\d]*)/i);
1090
+ const winVersionMap = {
1091
+ "10.0": "10/11",
1092
+ "6.3": "8.1",
1093
+ "6.2": "8",
1094
+ "6.1": "7",
1095
+ "6.0": "Vista",
1096
+ "5.1": "XP"
1097
+ };
1098
+ osVersion = winVersionMap[winVersion] || winVersion;
1099
+ } else if (/mac os x/i.test(userAgent)) {
1100
+ osName = "macOS";
1101
+ osVersion = extractVersion(userAgent, /mac os x (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1102
+ } else if (/iphone|ipad|ipod/i.test(userAgent)) {
1103
+ osName = "iOS";
1104
+ osVersion = extractVersion(userAgent, /os (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1105
+ } else if (/android/i.test(userAgent)) {
1106
+ osName = "Android";
1107
+ osVersion = extractVersion(userAgent, /android (\d+[\.\d]*)/i);
1108
+ } else if (/linux/i.test(userAgent)) {
1109
+ osName = "Linux";
1110
+ } else if (/cros/i.test(userAgent)) {
1111
+ osName = "Chrome OS";
1112
+ }
1113
+ return {
1114
+ deviceType,
1115
+ browserName,
1116
+ browserVersion,
1117
+ osName,
1118
+ osVersion
1119
+ };
1120
+ }
1121
+ function extractVersion(userAgent, regex) {
1122
+ const match = userAgent.match(regex);
1123
+ return match ? match[1] : null;
1124
+ }
1125
+ var userAgentParser = {
1126
+ parseUserAgent: parseUserAgent$2
1127
+ };
1128
+ const { decryptToken: decryptToken$1, hashToken: hashToken$1 } = encryption;
1129
+ const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
1004
1130
  const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
1005
1131
  const USER_UID$1 = "plugin::users-permissions.user";
1006
1132
  var session$3 = {
@@ -1046,12 +1172,13 @@ var session$3 = {
1046
1172
  * Get own sessions (authenticated user)
1047
1173
  * GET /api/magic-sessionmanager/my-sessions
1048
1174
  * Automatically uses the authenticated user's documentId
1049
- * Marks which session is the current one (based on JWT token)
1175
+ * Marks which session is the current one (based on JWT token hash)
1050
1176
  */
1051
1177
  async getOwnSessions(ctx) {
1052
1178
  try {
1053
1179
  const userId = ctx.state.user?.documentId;
1054
1180
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1181
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1055
1182
  if (!userId) {
1056
1183
  return ctx.throw(401, "Unauthorized");
1057
1184
  }
@@ -1066,17 +1193,26 @@ var session$3 = {
1066
1193
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1067
1194
  const timeSinceActive = now - lastActiveTime;
1068
1195
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1069
- let isCurrentSession = false;
1070
- if (session2.token && currentToken) {
1071
- try {
1072
- const decrypted = decryptToken$1(session2.token);
1073
- isCurrentSession = decrypted === currentToken;
1074
- } catch (err) {
1075
- }
1076
- }
1077
- const { token, refreshToken, ...sessionWithoutTokens } = session2;
1196
+ const isCurrentSession = currentTokenHash && session2.tokenHash === currentTokenHash;
1197
+ const parsedUA = parseUserAgent$1(session2.userAgent);
1198
+ const deviceType = session2.deviceType || parsedUA.deviceType;
1199
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1200
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1201
+ const {
1202
+ token,
1203
+ tokenHash,
1204
+ refreshToken,
1205
+ refreshTokenHash,
1206
+ locale,
1207
+ publishedAt,
1208
+ // Remove Strapi internal fields
1209
+ ...sessionWithoutTokens
1210
+ } = session2;
1078
1211
  return {
1079
1212
  ...sessionWithoutTokens,
1213
+ deviceType,
1214
+ browserName,
1215
+ osName,
1080
1216
  isCurrentSession,
1081
1217
  isTrulyActive,
1082
1218
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1138,21 +1274,14 @@ var session$3 = {
1138
1274
  return ctx.throw(401, "Unauthorized");
1139
1275
  }
1140
1276
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1141
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1277
+ const currentTokenHash = hashToken$1(token);
1278
+ const matchingSession = await strapi.documents(SESSION_UID$1).findFirst({
1142
1279
  filters: {
1143
1280
  user: { documentId: userId },
1281
+ tokenHash: currentTokenHash,
1144
1282
  isActive: true
1145
1283
  }
1146
1284
  });
1147
- const matchingSession = sessions.find((session2) => {
1148
- if (!session2.token) return false;
1149
- try {
1150
- const decrypted = decryptToken$1(session2.token);
1151
- return decrypted === token;
1152
- } catch (err) {
1153
- return false;
1154
- }
1155
- });
1156
1285
  if (matchingSession) {
1157
1286
  await sessionService.terminateSession({ sessionId: matchingSession.documentId });
1158
1287
  strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
@@ -1187,7 +1316,7 @@ var session$3 = {
1187
1316
  }
1188
1317
  },
1189
1318
  /**
1190
- * Get current session info based on JWT token
1319
+ * Get current session info based on JWT token hash
1191
1320
  * GET /api/magic-sessionmanager/current-session
1192
1321
  * Returns the session associated with the current JWT token
1193
1322
  */
@@ -1198,21 +1327,14 @@ var session$3 = {
1198
1327
  if (!userId || !token) {
1199
1328
  return ctx.throw(401, "Unauthorized");
1200
1329
  }
1201
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1330
+ const currentTokenHash = hashToken$1(token);
1331
+ const currentSession = await strapi.documents(SESSION_UID$1).findFirst({
1202
1332
  filters: {
1203
1333
  user: { documentId: userId },
1334
+ tokenHash: currentTokenHash,
1204
1335
  isActive: true
1205
1336
  }
1206
1337
  });
1207
- const currentSession = sessions.find((session2) => {
1208
- if (!session2.token) return false;
1209
- try {
1210
- const decrypted = decryptToken$1(session2.token);
1211
- return decrypted === token;
1212
- } catch (err) {
1213
- return false;
1214
- }
1215
- });
1216
1338
  if (!currentSession) {
1217
1339
  return ctx.notFound("Current session not found");
1218
1340
  }
@@ -1221,10 +1343,25 @@ var session$3 = {
1221
1343
  const now = /* @__PURE__ */ new Date();
1222
1344
  const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
1223
1345
  const timeSinceActive = now - lastActiveTime;
1224
- const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
1346
+ const parsedUA = parseUserAgent$1(currentSession.userAgent);
1347
+ const deviceType = currentSession.deviceType || parsedUA.deviceType;
1348
+ const browserName = currentSession.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1349
+ const osName = currentSession.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1350
+ const {
1351
+ token: _,
1352
+ tokenHash: _th,
1353
+ refreshToken: __,
1354
+ refreshTokenHash: _rth,
1355
+ locale: _l,
1356
+ publishedAt: _p,
1357
+ ...sessionWithoutTokens
1358
+ } = currentSession;
1225
1359
  ctx.body = {
1226
1360
  data: {
1227
1361
  ...sessionWithoutTokens,
1362
+ deviceType,
1363
+ browserName,
1364
+ osName,
1228
1365
  isCurrentSession: true,
1229
1366
  isTrulyActive: timeSinceActive < inactivityTimeout,
1230
1367
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1245,6 +1382,7 @@ var session$3 = {
1245
1382
  const userId = ctx.state.user?.documentId;
1246
1383
  const { sessionId } = ctx.params;
1247
1384
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1385
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1248
1386
  if (!userId) {
1249
1387
  return ctx.throw(401, "Unauthorized");
1250
1388
  }
@@ -1263,14 +1401,8 @@ var session$3 = {
1263
1401
  strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
1264
1402
  return ctx.forbidden("You can only terminate your own sessions");
1265
1403
  }
1266
- if (sessionToTerminate.token && currentToken) {
1267
- try {
1268
- const decrypted = decryptToken$1(sessionToTerminate.token);
1269
- if (decrypted === currentToken) {
1270
- return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1271
- }
1272
- } catch (err) {
1273
- }
1404
+ if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
1405
+ return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1274
1406
  }
1275
1407
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1276
1408
  await sessionService.terminateSession({ sessionId });
@@ -1765,6 +1897,7 @@ var controllers$1 = {
1765
1897
  };
1766
1898
  const { encryptToken, decryptToken, generateSessionId, hashToken } = encryption;
1767
1899
  const { createLogger: createLogger$1 } = logger;
1900
+ const { parseUserAgent } = userAgentParser;
1768
1901
  const SESSION_UID = "plugin::magic-sessionmanager.session";
1769
1902
  const USER_UID = "plugin::users-permissions.user";
1770
1903
  var session$1 = ({ strapi: strapi2 }) => {
@@ -1879,9 +2012,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1879
2012
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1880
2013
  const timeSinceActive = now - lastActiveTime;
1881
2014
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1882
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2015
+ const parsedUA = parseUserAgent(session2.userAgent);
2016
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2017
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2018
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2019
+ const {
2020
+ token,
2021
+ tokenHash,
2022
+ refreshToken,
2023
+ refreshTokenHash,
2024
+ locale,
2025
+ publishedAt,
2026
+ ...safeSession
2027
+ } = session2;
1883
2028
  return {
1884
2029
  ...safeSession,
2030
+ deviceType,
2031
+ browserName,
2032
+ osName,
1885
2033
  isTrulyActive,
1886
2034
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1887
2035
  };
@@ -1910,9 +2058,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1910
2058
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1911
2059
  const timeSinceActive = now - lastActiveTime;
1912
2060
  const isTrulyActive = timeSinceActive < inactivityTimeout;
1913
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2061
+ const parsedUA = parseUserAgent(session2.userAgent);
2062
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2063
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2064
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2065
+ const {
2066
+ token,
2067
+ tokenHash,
2068
+ refreshToken,
2069
+ refreshTokenHash,
2070
+ locale,
2071
+ publishedAt,
2072
+ ...safeSession
2073
+ } = session2;
1914
2074
  return {
1915
2075
  ...safeSession,
2076
+ deviceType,
2077
+ browserName,
2078
+ osName,
1916
2079
  isTrulyActive,
1917
2080
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1918
2081
  };
@@ -1949,9 +2112,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1949
2112
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1950
2113
  const timeSinceActive = now - lastActiveTime;
1951
2114
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1952
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2115
+ const parsedUA = parseUserAgent(session2.userAgent);
2116
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2117
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2118
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2119
+ const {
2120
+ token,
2121
+ tokenHash,
2122
+ refreshToken,
2123
+ refreshTokenHash,
2124
+ locale,
2125
+ publishedAt,
2126
+ ...safeSession
2127
+ } = session2;
1953
2128
  return {
1954
2129
  ...safeSession,
2130
+ deviceType,
2131
+ browserName,
2132
+ osName,
1955
2133
  isTrulyActive,
1956
2134
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1957
2135
  };
@@ -2068,7 +2246,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2068
2246
  }
2069
2247
  };
2070
2248
  };
2071
- const version = "4.2.8";
2249
+ const version = "4.2.10";
2072
2250
  const require$$2 = {
2073
2251
  version
2074
2252
  };
@@ -202,7 +202,7 @@ function generateSessionId$1(userId) {
202
202
  const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
203
203
  return `sess_${timestamp}_${userHash}_${randomBytes}`;
204
204
  }
205
- function hashToken$3(token) {
205
+ function hashToken$4(token) {
206
206
  if (!token) return null;
207
207
  return crypto$1.createHash("sha256").update(token).digest("hex");
208
208
  }
@@ -210,10 +210,10 @@ var encryption = {
210
210
  encryptToken: encryptToken$2,
211
211
  decryptToken: decryptToken$3,
212
212
  generateSessionId: generateSessionId$1,
213
- hashToken: hashToken$3
213
+ hashToken: hashToken$4
214
214
  };
215
215
  const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
216
- const { hashToken: hashToken$2 } = encryption;
216
+ const { hashToken: hashToken$3 } = encryption;
217
217
  const lastTouchCache = /* @__PURE__ */ new Map();
218
218
  var lastSeen = ({ strapi: strapi2 }) => {
219
219
  return async (ctx, next) => {
@@ -229,7 +229,7 @@ var lastSeen = ({ strapi: strapi2 }) => {
229
229
  }
230
230
  let matchingSession = null;
231
231
  try {
232
- const currentTokenHash = hashToken$2(currentToken);
232
+ const currentTokenHash = hashToken$3(currentToken);
233
233
  matchingSession = await strapi2.documents(SESSION_UID$3).findFirst({
234
234
  filters: {
235
235
  tokenHash: currentTokenHash,
@@ -274,7 +274,7 @@ var lastSeen = ({ strapi: strapi2 }) => {
274
274
  };
275
275
  };
276
276
  const getClientIp = getClientIp_1;
277
- const { encryptToken: encryptToken$1, decryptToken: decryptToken$2, hashToken: hashToken$1 } = encryption;
277
+ const { encryptToken: encryptToken$1, decryptToken: decryptToken$2, hashToken: hashToken$2 } = encryption;
278
278
  const { createLogger: createLogger$3 } = logger;
279
279
  const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
280
280
  const USER_UID$2 = "plugin::users-permissions.user";
@@ -282,6 +282,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
282
282
  const log = createLogger$3(strapi2);
283
283
  log.info("[START] Bootstrap starting...");
284
284
  try {
285
+ await ensureTokenHashIndex(strapi2, log);
285
286
  const licenseGuardService = strapi2.plugin("magic-sessionmanager").service("license-guard");
286
287
  setTimeout(async () => {
287
288
  const licenseStatus = await licenseGuardService.initialize();
@@ -558,8 +559,8 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
558
559
  if (matchingSession) {
559
560
  const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
560
561
  const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
561
- const newTokenHash = newAccessToken ? hashToken$1(newAccessToken) : matchingSession.tokenHash;
562
- const newRefreshTokenHash = newRefreshToken ? hashToken$1(newRefreshToken) : matchingSession.refreshTokenHash;
562
+ const newTokenHash = newAccessToken ? hashToken$2(newAccessToken) : matchingSession.tokenHash;
563
+ const newRefreshTokenHash = newRefreshToken ? hashToken$2(newRefreshToken) : matchingSession.refreshTokenHash;
563
564
  await strapi2.documents(SESSION_UID$2).update({
564
565
  documentId: matchingSession.documentId,
565
566
  data: {
@@ -638,6 +639,46 @@ async function ensureContentApiPermissions(strapi2, log) {
638
639
  log.warn("Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated");
639
640
  }
640
641
  }
642
+ async function ensureTokenHashIndex(strapi2, log) {
643
+ try {
644
+ const knex = strapi2.db.connection;
645
+ const tableName = "magic_sessions";
646
+ const indexName = "idx_magic_sessions_token_hash";
647
+ const hasIndex = await knex.schema.hasTable(tableName).then(async (exists) => {
648
+ if (!exists) return false;
649
+ const dialect = strapi2.db.dialect.client;
650
+ if (dialect === "postgres") {
651
+ const result = await knex.raw(`
652
+ SELECT indexname FROM pg_indexes
653
+ WHERE tablename = ? AND indexname = ?
654
+ `, [tableName, indexName]);
655
+ return result.rows.length > 0;
656
+ } else if (dialect === "mysql" || dialect === "mysql2") {
657
+ const result = await knex.raw(`
658
+ SHOW INDEX FROM ${tableName} WHERE Key_name = ?
659
+ `, [indexName]);
660
+ return result[0].length > 0;
661
+ } else if (dialect === "sqlite" || dialect === "better-sqlite3") {
662
+ const result = await knex.raw(`
663
+ SELECT name FROM sqlite_master
664
+ WHERE type='index' AND name = ?
665
+ `, [indexName]);
666
+ return result.length > 0;
667
+ }
668
+ return false;
669
+ });
670
+ if (hasIndex) {
671
+ log.debug("[INDEX] tokenHash index already exists");
672
+ return;
673
+ }
674
+ await knex.schema.alterTable(tableName, (table) => {
675
+ table.index(["token_hash", "is_active"], indexName);
676
+ });
677
+ log.info("[INDEX] Created tokenHash index for O(1) session lookup");
678
+ } catch (err) {
679
+ log.debug("[INDEX] Could not create tokenHash index (will retry on next startup):", err.message);
680
+ }
681
+ }
641
682
  const { createLogger: createLogger$2 } = logger;
642
683
  var destroy$1 = async ({ strapi: strapi2 }) => {
643
684
  const log = createLogger$2(strapi2);
@@ -718,7 +759,8 @@ const attributes = {
718
759
  },
719
760
  tokenHash: {
720
761
  type: "string",
721
- configurable: false
762
+ configurable: false,
763
+ unique: false
722
764
  },
723
765
  refreshToken: {
724
766
  type: "text",
@@ -996,7 +1038,91 @@ var routes$1 = {
996
1038
  admin,
997
1039
  "content-api": contentApi
998
1040
  };
999
- const { decryptToken: decryptToken$1 } = encryption;
1041
+ function parseUserAgent$2(userAgent) {
1042
+ if (!userAgent) {
1043
+ return {
1044
+ deviceType: "unknown",
1045
+ browserName: "unknown",
1046
+ browserVersion: null,
1047
+ osName: "unknown",
1048
+ osVersion: null
1049
+ };
1050
+ }
1051
+ userAgent.toLowerCase();
1052
+ let deviceType = "desktop";
1053
+ if (/mobile|android.*mobile|iphone|ipod|blackberry|iemobile|opera mini|opera mobi/i.test(userAgent)) {
1054
+ deviceType = "mobile";
1055
+ } else if (/tablet|ipad|android(?!.*mobile)|kindle|silk/i.test(userAgent)) {
1056
+ deviceType = "tablet";
1057
+ } else if (/bot|crawl|spider|slurp|mediapartners/i.test(userAgent)) {
1058
+ deviceType = "bot";
1059
+ }
1060
+ let browserName = "unknown";
1061
+ let browserVersion = null;
1062
+ if (/edg\//i.test(userAgent)) {
1063
+ browserName = "Edge";
1064
+ browserVersion = extractVersion(userAgent, /edg\/(\d+[\.\d]*)/i);
1065
+ } else if (/opr\//i.test(userAgent) || /opera/i.test(userAgent)) {
1066
+ browserName = "Opera";
1067
+ browserVersion = extractVersion(userAgent, /(?:opr|opera)[\s\/](\d+[\.\d]*)/i);
1068
+ } else if (/chrome|crios/i.test(userAgent) && !/edg/i.test(userAgent)) {
1069
+ browserName = "Chrome";
1070
+ browserVersion = extractVersion(userAgent, /(?:chrome|crios)\/(\d+[\.\d]*)/i);
1071
+ } else if (/firefox|fxios/i.test(userAgent)) {
1072
+ browserName = "Firefox";
1073
+ browserVersion = extractVersion(userAgent, /(?:firefox|fxios)\/(\d+[\.\d]*)/i);
1074
+ } else if (/safari/i.test(userAgent) && !/chrome|chromium/i.test(userAgent)) {
1075
+ browserName = "Safari";
1076
+ browserVersion = extractVersion(userAgent, /version\/(\d+[\.\d]*)/i);
1077
+ } else if (/msie|trident/i.test(userAgent)) {
1078
+ browserName = "Internet Explorer";
1079
+ browserVersion = extractVersion(userAgent, /(?:msie |rv:)(\d+[\.\d]*)/i);
1080
+ }
1081
+ let osName = "unknown";
1082
+ let osVersion = null;
1083
+ if (/windows nt/i.test(userAgent)) {
1084
+ osName = "Windows";
1085
+ const winVersion = extractVersion(userAgent, /windows nt (\d+[\.\d]*)/i);
1086
+ const winVersionMap = {
1087
+ "10.0": "10/11",
1088
+ "6.3": "8.1",
1089
+ "6.2": "8",
1090
+ "6.1": "7",
1091
+ "6.0": "Vista",
1092
+ "5.1": "XP"
1093
+ };
1094
+ osVersion = winVersionMap[winVersion] || winVersion;
1095
+ } else if (/mac os x/i.test(userAgent)) {
1096
+ osName = "macOS";
1097
+ osVersion = extractVersion(userAgent, /mac os x (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1098
+ } else if (/iphone|ipad|ipod/i.test(userAgent)) {
1099
+ osName = "iOS";
1100
+ osVersion = extractVersion(userAgent, /os (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1101
+ } else if (/android/i.test(userAgent)) {
1102
+ osName = "Android";
1103
+ osVersion = extractVersion(userAgent, /android (\d+[\.\d]*)/i);
1104
+ } else if (/linux/i.test(userAgent)) {
1105
+ osName = "Linux";
1106
+ } else if (/cros/i.test(userAgent)) {
1107
+ osName = "Chrome OS";
1108
+ }
1109
+ return {
1110
+ deviceType,
1111
+ browserName,
1112
+ browserVersion,
1113
+ osName,
1114
+ osVersion
1115
+ };
1116
+ }
1117
+ function extractVersion(userAgent, regex) {
1118
+ const match = userAgent.match(regex);
1119
+ return match ? match[1] : null;
1120
+ }
1121
+ var userAgentParser = {
1122
+ parseUserAgent: parseUserAgent$2
1123
+ };
1124
+ const { decryptToken: decryptToken$1, hashToken: hashToken$1 } = encryption;
1125
+ const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
1000
1126
  const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
1001
1127
  const USER_UID$1 = "plugin::users-permissions.user";
1002
1128
  var session$3 = {
@@ -1042,12 +1168,13 @@ var session$3 = {
1042
1168
  * Get own sessions (authenticated user)
1043
1169
  * GET /api/magic-sessionmanager/my-sessions
1044
1170
  * Automatically uses the authenticated user's documentId
1045
- * Marks which session is the current one (based on JWT token)
1171
+ * Marks which session is the current one (based on JWT token hash)
1046
1172
  */
1047
1173
  async getOwnSessions(ctx) {
1048
1174
  try {
1049
1175
  const userId = ctx.state.user?.documentId;
1050
1176
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1177
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1051
1178
  if (!userId) {
1052
1179
  return ctx.throw(401, "Unauthorized");
1053
1180
  }
@@ -1062,17 +1189,26 @@ var session$3 = {
1062
1189
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1063
1190
  const timeSinceActive = now - lastActiveTime;
1064
1191
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1065
- let isCurrentSession = false;
1066
- if (session2.token && currentToken) {
1067
- try {
1068
- const decrypted = decryptToken$1(session2.token);
1069
- isCurrentSession = decrypted === currentToken;
1070
- } catch (err) {
1071
- }
1072
- }
1073
- const { token, refreshToken, ...sessionWithoutTokens } = session2;
1192
+ const isCurrentSession = currentTokenHash && session2.tokenHash === currentTokenHash;
1193
+ const parsedUA = parseUserAgent$1(session2.userAgent);
1194
+ const deviceType = session2.deviceType || parsedUA.deviceType;
1195
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1196
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1197
+ const {
1198
+ token,
1199
+ tokenHash,
1200
+ refreshToken,
1201
+ refreshTokenHash,
1202
+ locale,
1203
+ publishedAt,
1204
+ // Remove Strapi internal fields
1205
+ ...sessionWithoutTokens
1206
+ } = session2;
1074
1207
  return {
1075
1208
  ...sessionWithoutTokens,
1209
+ deviceType,
1210
+ browserName,
1211
+ osName,
1076
1212
  isCurrentSession,
1077
1213
  isTrulyActive,
1078
1214
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1134,21 +1270,14 @@ var session$3 = {
1134
1270
  return ctx.throw(401, "Unauthorized");
1135
1271
  }
1136
1272
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1137
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1273
+ const currentTokenHash = hashToken$1(token);
1274
+ const matchingSession = await strapi.documents(SESSION_UID$1).findFirst({
1138
1275
  filters: {
1139
1276
  user: { documentId: userId },
1277
+ tokenHash: currentTokenHash,
1140
1278
  isActive: true
1141
1279
  }
1142
1280
  });
1143
- const matchingSession = sessions.find((session2) => {
1144
- if (!session2.token) return false;
1145
- try {
1146
- const decrypted = decryptToken$1(session2.token);
1147
- return decrypted === token;
1148
- } catch (err) {
1149
- return false;
1150
- }
1151
- });
1152
1281
  if (matchingSession) {
1153
1282
  await sessionService.terminateSession({ sessionId: matchingSession.documentId });
1154
1283
  strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
@@ -1183,7 +1312,7 @@ var session$3 = {
1183
1312
  }
1184
1313
  },
1185
1314
  /**
1186
- * Get current session info based on JWT token
1315
+ * Get current session info based on JWT token hash
1187
1316
  * GET /api/magic-sessionmanager/current-session
1188
1317
  * Returns the session associated with the current JWT token
1189
1318
  */
@@ -1194,21 +1323,14 @@ var session$3 = {
1194
1323
  if (!userId || !token) {
1195
1324
  return ctx.throw(401, "Unauthorized");
1196
1325
  }
1197
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1326
+ const currentTokenHash = hashToken$1(token);
1327
+ const currentSession = await strapi.documents(SESSION_UID$1).findFirst({
1198
1328
  filters: {
1199
1329
  user: { documentId: userId },
1330
+ tokenHash: currentTokenHash,
1200
1331
  isActive: true
1201
1332
  }
1202
1333
  });
1203
- const currentSession = sessions.find((session2) => {
1204
- if (!session2.token) return false;
1205
- try {
1206
- const decrypted = decryptToken$1(session2.token);
1207
- return decrypted === token;
1208
- } catch (err) {
1209
- return false;
1210
- }
1211
- });
1212
1334
  if (!currentSession) {
1213
1335
  return ctx.notFound("Current session not found");
1214
1336
  }
@@ -1217,10 +1339,25 @@ var session$3 = {
1217
1339
  const now = /* @__PURE__ */ new Date();
1218
1340
  const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
1219
1341
  const timeSinceActive = now - lastActiveTime;
1220
- const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
1342
+ const parsedUA = parseUserAgent$1(currentSession.userAgent);
1343
+ const deviceType = currentSession.deviceType || parsedUA.deviceType;
1344
+ const browserName = currentSession.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1345
+ const osName = currentSession.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1346
+ const {
1347
+ token: _,
1348
+ tokenHash: _th,
1349
+ refreshToken: __,
1350
+ refreshTokenHash: _rth,
1351
+ locale: _l,
1352
+ publishedAt: _p,
1353
+ ...sessionWithoutTokens
1354
+ } = currentSession;
1221
1355
  ctx.body = {
1222
1356
  data: {
1223
1357
  ...sessionWithoutTokens,
1358
+ deviceType,
1359
+ browserName,
1360
+ osName,
1224
1361
  isCurrentSession: true,
1225
1362
  isTrulyActive: timeSinceActive < inactivityTimeout,
1226
1363
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1241,6 +1378,7 @@ var session$3 = {
1241
1378
  const userId = ctx.state.user?.documentId;
1242
1379
  const { sessionId } = ctx.params;
1243
1380
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1381
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1244
1382
  if (!userId) {
1245
1383
  return ctx.throw(401, "Unauthorized");
1246
1384
  }
@@ -1259,14 +1397,8 @@ var session$3 = {
1259
1397
  strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
1260
1398
  return ctx.forbidden("You can only terminate your own sessions");
1261
1399
  }
1262
- if (sessionToTerminate.token && currentToken) {
1263
- try {
1264
- const decrypted = decryptToken$1(sessionToTerminate.token);
1265
- if (decrypted === currentToken) {
1266
- return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1267
- }
1268
- } catch (err) {
1269
- }
1400
+ if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
1401
+ return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1270
1402
  }
1271
1403
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1272
1404
  await sessionService.terminateSession({ sessionId });
@@ -1761,6 +1893,7 @@ var controllers$1 = {
1761
1893
  };
1762
1894
  const { encryptToken, decryptToken, generateSessionId, hashToken } = encryption;
1763
1895
  const { createLogger: createLogger$1 } = logger;
1896
+ const { parseUserAgent } = userAgentParser;
1764
1897
  const SESSION_UID = "plugin::magic-sessionmanager.session";
1765
1898
  const USER_UID = "plugin::users-permissions.user";
1766
1899
  var session$1 = ({ strapi: strapi2 }) => {
@@ -1875,9 +2008,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1875
2008
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1876
2009
  const timeSinceActive = now - lastActiveTime;
1877
2010
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1878
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2011
+ const parsedUA = parseUserAgent(session2.userAgent);
2012
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2013
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2014
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2015
+ const {
2016
+ token,
2017
+ tokenHash,
2018
+ refreshToken,
2019
+ refreshTokenHash,
2020
+ locale,
2021
+ publishedAt,
2022
+ ...safeSession
2023
+ } = session2;
1879
2024
  return {
1880
2025
  ...safeSession,
2026
+ deviceType,
2027
+ browserName,
2028
+ osName,
1881
2029
  isTrulyActive,
1882
2030
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1883
2031
  };
@@ -1906,9 +2054,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1906
2054
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1907
2055
  const timeSinceActive = now - lastActiveTime;
1908
2056
  const isTrulyActive = timeSinceActive < inactivityTimeout;
1909
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2057
+ const parsedUA = parseUserAgent(session2.userAgent);
2058
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2059
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2060
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2061
+ const {
2062
+ token,
2063
+ tokenHash,
2064
+ refreshToken,
2065
+ refreshTokenHash,
2066
+ locale,
2067
+ publishedAt,
2068
+ ...safeSession
2069
+ } = session2;
1910
2070
  return {
1911
2071
  ...safeSession,
2072
+ deviceType,
2073
+ browserName,
2074
+ osName,
1912
2075
  isTrulyActive,
1913
2076
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1914
2077
  };
@@ -1945,9 +2108,24 @@ var session$1 = ({ strapi: strapi2 }) => {
1945
2108
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1946
2109
  const timeSinceActive = now - lastActiveTime;
1947
2110
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1948
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2111
+ const parsedUA = parseUserAgent(session2.userAgent);
2112
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2113
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2114
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2115
+ const {
2116
+ token,
2117
+ tokenHash,
2118
+ refreshToken,
2119
+ refreshTokenHash,
2120
+ locale,
2121
+ publishedAt,
2122
+ ...safeSession
2123
+ } = session2;
1949
2124
  return {
1950
2125
  ...safeSession,
2126
+ deviceType,
2127
+ browserName,
2128
+ osName,
1951
2129
  isTrulyActive,
1952
2130
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1953
2131
  };
@@ -2064,7 +2242,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2064
2242
  }
2065
2243
  };
2066
2244
  };
2067
- const version = "4.2.8";
2245
+ const version = "4.2.10";
2068
2246
  const require$$2 = {
2069
2247
  version
2070
2248
  };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.2.9",
2
+ "version": "4.2.11",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",