strapi-plugin-magic-sessionmanager 4.2.10 → 4.2.12

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";
@@ -455,8 +455,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
455
455
  userAgent,
456
456
  token: ctx.body.jwt,
457
457
  // Store Access Token (encrypted)
458
- refreshToken: ctx.body.refreshToken
458
+ refreshToken: ctx.body.refreshToken,
459
459
  // Store Refresh Token (encrypted) if exists
460
+ geoData
461
+ // Store geolocation data if available
460
462
  });
461
463
  log.info(`[SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
462
464
  if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
@@ -563,8 +565,8 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
563
565
  if (matchingSession) {
564
566
  const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
565
567
  const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
566
- const newTokenHash = newAccessToken ? hashToken$1(newAccessToken) : matchingSession.tokenHash;
567
- const newRefreshTokenHash = newRefreshToken ? hashToken$1(newRefreshToken) : matchingSession.refreshTokenHash;
568
+ const newTokenHash = newAccessToken ? hashToken$2(newAccessToken) : matchingSession.tokenHash;
569
+ const newRefreshTokenHash = newRefreshToken ? hashToken$2(newRefreshToken) : matchingSession.refreshTokenHash;
568
570
  await strapi2.documents(SESSION_UID$2).update({
569
571
  documentId: matchingSession.documentId,
570
572
  data: {
@@ -1042,7 +1044,91 @@ var routes$1 = {
1042
1044
  admin,
1043
1045
  "content-api": contentApi
1044
1046
  };
1045
- const { decryptToken: decryptToken$1 } = encryption;
1047
+ function parseUserAgent$2(userAgent) {
1048
+ if (!userAgent) {
1049
+ return {
1050
+ deviceType: "unknown",
1051
+ browserName: "unknown",
1052
+ browserVersion: null,
1053
+ osName: "unknown",
1054
+ osVersion: null
1055
+ };
1056
+ }
1057
+ userAgent.toLowerCase();
1058
+ let deviceType = "desktop";
1059
+ if (/mobile|android.*mobile|iphone|ipod|blackberry|iemobile|opera mini|opera mobi/i.test(userAgent)) {
1060
+ deviceType = "mobile";
1061
+ } else if (/tablet|ipad|android(?!.*mobile)|kindle|silk/i.test(userAgent)) {
1062
+ deviceType = "tablet";
1063
+ } else if (/bot|crawl|spider|slurp|mediapartners/i.test(userAgent)) {
1064
+ deviceType = "bot";
1065
+ }
1066
+ let browserName = "unknown";
1067
+ let browserVersion = null;
1068
+ if (/edg\//i.test(userAgent)) {
1069
+ browserName = "Edge";
1070
+ browserVersion = extractVersion(userAgent, /edg\/(\d+[\.\d]*)/i);
1071
+ } else if (/opr\//i.test(userAgent) || /opera/i.test(userAgent)) {
1072
+ browserName = "Opera";
1073
+ browserVersion = extractVersion(userAgent, /(?:opr|opera)[\s\/](\d+[\.\d]*)/i);
1074
+ } else if (/chrome|crios/i.test(userAgent) && !/edg/i.test(userAgent)) {
1075
+ browserName = "Chrome";
1076
+ browserVersion = extractVersion(userAgent, /(?:chrome|crios)\/(\d+[\.\d]*)/i);
1077
+ } else if (/firefox|fxios/i.test(userAgent)) {
1078
+ browserName = "Firefox";
1079
+ browserVersion = extractVersion(userAgent, /(?:firefox|fxios)\/(\d+[\.\d]*)/i);
1080
+ } else if (/safari/i.test(userAgent) && !/chrome|chromium/i.test(userAgent)) {
1081
+ browserName = "Safari";
1082
+ browserVersion = extractVersion(userAgent, /version\/(\d+[\.\d]*)/i);
1083
+ } else if (/msie|trident/i.test(userAgent)) {
1084
+ browserName = "Internet Explorer";
1085
+ browserVersion = extractVersion(userAgent, /(?:msie |rv:)(\d+[\.\d]*)/i);
1086
+ }
1087
+ let osName = "unknown";
1088
+ let osVersion = null;
1089
+ if (/windows nt/i.test(userAgent)) {
1090
+ osName = "Windows";
1091
+ const winVersion = extractVersion(userAgent, /windows nt (\d+[\.\d]*)/i);
1092
+ const winVersionMap = {
1093
+ "10.0": "10/11",
1094
+ "6.3": "8.1",
1095
+ "6.2": "8",
1096
+ "6.1": "7",
1097
+ "6.0": "Vista",
1098
+ "5.1": "XP"
1099
+ };
1100
+ osVersion = winVersionMap[winVersion] || winVersion;
1101
+ } else if (/mac os x/i.test(userAgent)) {
1102
+ osName = "macOS";
1103
+ osVersion = extractVersion(userAgent, /mac os x (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1104
+ } else if (/iphone|ipad|ipod/i.test(userAgent)) {
1105
+ osName = "iOS";
1106
+ osVersion = extractVersion(userAgent, /os (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1107
+ } else if (/android/i.test(userAgent)) {
1108
+ osName = "Android";
1109
+ osVersion = extractVersion(userAgent, /android (\d+[\.\d]*)/i);
1110
+ } else if (/linux/i.test(userAgent)) {
1111
+ osName = "Linux";
1112
+ } else if (/cros/i.test(userAgent)) {
1113
+ osName = "Chrome OS";
1114
+ }
1115
+ return {
1116
+ deviceType,
1117
+ browserName,
1118
+ browserVersion,
1119
+ osName,
1120
+ osVersion
1121
+ };
1122
+ }
1123
+ function extractVersion(userAgent, regex) {
1124
+ const match = userAgent.match(regex);
1125
+ return match ? match[1] : null;
1126
+ }
1127
+ var userAgentParser = {
1128
+ parseUserAgent: parseUserAgent$2
1129
+ };
1130
+ const { decryptToken: decryptToken$1, hashToken: hashToken$1 } = encryption;
1131
+ const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
1046
1132
  const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
1047
1133
  const USER_UID$1 = "plugin::users-permissions.user";
1048
1134
  var session$3 = {
@@ -1088,12 +1174,13 @@ var session$3 = {
1088
1174
  * Get own sessions (authenticated user)
1089
1175
  * GET /api/magic-sessionmanager/my-sessions
1090
1176
  * Automatically uses the authenticated user's documentId
1091
- * Marks which session is the current one (based on JWT token)
1177
+ * Marks which session is the current one (based on JWT token hash)
1092
1178
  */
1093
1179
  async getOwnSessions(ctx) {
1094
1180
  try {
1095
1181
  const userId = ctx.state.user?.documentId;
1096
1182
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1183
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1097
1184
  if (!userId) {
1098
1185
  return ctx.throw(401, "Unauthorized");
1099
1186
  }
@@ -1108,17 +1195,38 @@ var session$3 = {
1108
1195
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1109
1196
  const timeSinceActive = now - lastActiveTime;
1110
1197
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1111
- let isCurrentSession = false;
1112
- if (session2.token && currentToken) {
1198
+ const isCurrentSession = currentTokenHash && session2.tokenHash === currentTokenHash;
1199
+ const parsedUA = parseUserAgent$1(session2.userAgent);
1200
+ const deviceType = session2.deviceType || parsedUA.deviceType;
1201
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1202
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1203
+ let geoLocation = session2.geoLocation;
1204
+ if (typeof geoLocation === "string") {
1113
1205
  try {
1114
- const decrypted = decryptToken$1(session2.token);
1115
- isCurrentSession = decrypted === currentToken;
1116
- } catch (err) {
1206
+ geoLocation = JSON.parse(geoLocation);
1207
+ } catch (e) {
1208
+ geoLocation = null;
1117
1209
  }
1118
1210
  }
1119
- const { token, refreshToken, ...sessionWithoutTokens } = session2;
1211
+ const {
1212
+ token,
1213
+ tokenHash,
1214
+ refreshToken,
1215
+ refreshTokenHash,
1216
+ locale,
1217
+ publishedAt,
1218
+ // Remove Strapi internal fields
1219
+ geoLocation: _geo,
1220
+ // Remove raw geoLocation
1221
+ ...sessionWithoutTokens
1222
+ } = session2;
1120
1223
  return {
1121
1224
  ...sessionWithoutTokens,
1225
+ deviceType,
1226
+ browserName,
1227
+ osName,
1228
+ geoLocation,
1229
+ // Parsed object or null
1122
1230
  isCurrentSession,
1123
1231
  isTrulyActive,
1124
1232
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1180,21 +1288,14 @@ var session$3 = {
1180
1288
  return ctx.throw(401, "Unauthorized");
1181
1289
  }
1182
1290
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1183
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1291
+ const currentTokenHash = hashToken$1(token);
1292
+ const matchingSession = await strapi.documents(SESSION_UID$1).findFirst({
1184
1293
  filters: {
1185
1294
  user: { documentId: userId },
1295
+ tokenHash: currentTokenHash,
1186
1296
  isActive: true
1187
1297
  }
1188
1298
  });
1189
- const matchingSession = sessions.find((session2) => {
1190
- if (!session2.token) return false;
1191
- try {
1192
- const decrypted = decryptToken$1(session2.token);
1193
- return decrypted === token;
1194
- } catch (err) {
1195
- return false;
1196
- }
1197
- });
1198
1299
  if (matchingSession) {
1199
1300
  await sessionService.terminateSession({ sessionId: matchingSession.documentId });
1200
1301
  strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
@@ -1229,7 +1330,7 @@ var session$3 = {
1229
1330
  }
1230
1331
  },
1231
1332
  /**
1232
- * Get current session info based on JWT token
1333
+ * Get current session info based on JWT token hash
1233
1334
  * GET /api/magic-sessionmanager/current-session
1234
1335
  * Returns the session associated with the current JWT token
1235
1336
  */
@@ -1240,21 +1341,14 @@ var session$3 = {
1240
1341
  if (!userId || !token) {
1241
1342
  return ctx.throw(401, "Unauthorized");
1242
1343
  }
1243
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1344
+ const currentTokenHash = hashToken$1(token);
1345
+ const currentSession = await strapi.documents(SESSION_UID$1).findFirst({
1244
1346
  filters: {
1245
1347
  user: { documentId: userId },
1348
+ tokenHash: currentTokenHash,
1246
1349
  isActive: true
1247
1350
  }
1248
1351
  });
1249
- const currentSession = sessions.find((session2) => {
1250
- if (!session2.token) return false;
1251
- try {
1252
- const decrypted = decryptToken$1(session2.token);
1253
- return decrypted === token;
1254
- } catch (err) {
1255
- return false;
1256
- }
1257
- });
1258
1352
  if (!currentSession) {
1259
1353
  return ctx.notFound("Current session not found");
1260
1354
  }
@@ -1263,10 +1357,36 @@ var session$3 = {
1263
1357
  const now = /* @__PURE__ */ new Date();
1264
1358
  const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
1265
1359
  const timeSinceActive = now - lastActiveTime;
1266
- const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
1360
+ const parsedUA = parseUserAgent$1(currentSession.userAgent);
1361
+ const deviceType = currentSession.deviceType || parsedUA.deviceType;
1362
+ const browserName = currentSession.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1363
+ const osName = currentSession.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1364
+ let geoLocation = currentSession.geoLocation;
1365
+ if (typeof geoLocation === "string") {
1366
+ try {
1367
+ geoLocation = JSON.parse(geoLocation);
1368
+ } catch (e) {
1369
+ geoLocation = null;
1370
+ }
1371
+ }
1372
+ const {
1373
+ token: _,
1374
+ tokenHash: _th,
1375
+ refreshToken: __,
1376
+ refreshTokenHash: _rth,
1377
+ locale: _l,
1378
+ publishedAt: _p,
1379
+ geoLocation: _geo,
1380
+ ...sessionWithoutTokens
1381
+ } = currentSession;
1267
1382
  ctx.body = {
1268
1383
  data: {
1269
1384
  ...sessionWithoutTokens,
1385
+ deviceType,
1386
+ browserName,
1387
+ osName,
1388
+ geoLocation,
1389
+ // Parsed object or null
1270
1390
  isCurrentSession: true,
1271
1391
  isTrulyActive: timeSinceActive < inactivityTimeout,
1272
1392
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1287,6 +1407,7 @@ var session$3 = {
1287
1407
  const userId = ctx.state.user?.documentId;
1288
1408
  const { sessionId } = ctx.params;
1289
1409
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1410
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1290
1411
  if (!userId) {
1291
1412
  return ctx.throw(401, "Unauthorized");
1292
1413
  }
@@ -1305,14 +1426,8 @@ var session$3 = {
1305
1426
  strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
1306
1427
  return ctx.forbidden("You can only terminate your own sessions");
1307
1428
  }
1308
- if (sessionToTerminate.token && currentToken) {
1309
- try {
1310
- const decrypted = decryptToken$1(sessionToTerminate.token);
1311
- if (decrypted === currentToken) {
1312
- return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1313
- }
1314
- } catch (err) {
1315
- }
1429
+ if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
1430
+ return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1316
1431
  }
1317
1432
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1318
1433
  await sessionService.terminateSession({ sessionId });
@@ -1807,6 +1922,7 @@ var controllers$1 = {
1807
1922
  };
1808
1923
  const { encryptToken, decryptToken, generateSessionId, hashToken } = encryption;
1809
1924
  const { createLogger: createLogger$1 } = logger;
1925
+ const { parseUserAgent } = userAgentParser;
1810
1926
  const SESSION_UID = "plugin::magic-sessionmanager.session";
1811
1927
  const USER_UID = "plugin::users-permissions.user";
1812
1928
  var session$1 = ({ strapi: strapi2 }) => {
@@ -1814,10 +1930,10 @@ var session$1 = ({ strapi: strapi2 }) => {
1814
1930
  return {
1815
1931
  /**
1816
1932
  * Create a new session record
1817
- * @param {Object} params - { userId, ip, userAgent, token, refreshToken }
1933
+ * @param {Object} params - { userId, ip, userAgent, token, refreshToken, geoData }
1818
1934
  * @returns {Promise<Object>} Created session
1819
1935
  */
1820
- async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
1936
+ async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken, geoData }) {
1821
1937
  try {
1822
1938
  const now = /* @__PURE__ */ new Date();
1823
1939
  const sessionId = generateSessionId(userId);
@@ -1825,6 +1941,7 @@ var session$1 = ({ strapi: strapi2 }) => {
1825
1941
  const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
1826
1942
  const tokenHashValue = token ? hashToken(token) : null;
1827
1943
  const refreshTokenHashValue = refreshToken ? hashToken(refreshToken) : null;
1944
+ const parsedUA = parseUserAgent(userAgent);
1828
1945
  const session2 = await strapi2.documents(SESSION_UID).create({
1829
1946
  data: {
1830
1947
  user: userId,
@@ -1842,8 +1959,22 @@ var session$1 = ({ strapi: strapi2 }) => {
1842
1959
  // Encrypted Refresh Token
1843
1960
  refreshTokenHash: refreshTokenHashValue,
1844
1961
  // SHA-256 hash for fast lookup
1845
- sessionId
1962
+ sessionId,
1846
1963
  // Unique identifier
1964
+ // Device info from User-Agent
1965
+ deviceType: parsedUA.deviceType,
1966
+ browserName: parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName,
1967
+ osName: parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName,
1968
+ // Geolocation data (if available from Premium features)
1969
+ geoLocation: geoData ? JSON.stringify({
1970
+ country: geoData.country,
1971
+ country_code: geoData.country_code,
1972
+ country_flag: geoData.country_flag,
1973
+ city: geoData.city,
1974
+ region: geoData.region,
1975
+ timezone: geoData.timezone
1976
+ }) : null,
1977
+ securityScore: geoData?.securityScore || null
1847
1978
  }
1848
1979
  });
1849
1980
  log.info(`[SUCCESS] Session ${session2.documentId} (${sessionId}) created for user ${userId}`);
@@ -1921,9 +2052,36 @@ var session$1 = ({ strapi: strapi2 }) => {
1921
2052
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1922
2053
  const timeSinceActive = now - lastActiveTime;
1923
2054
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1924
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2055
+ const parsedUA = parseUserAgent(session2.userAgent);
2056
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2057
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2058
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2059
+ let geoLocation = session2.geoLocation;
2060
+ if (typeof geoLocation === "string") {
2061
+ try {
2062
+ geoLocation = JSON.parse(geoLocation);
2063
+ } catch (e) {
2064
+ geoLocation = null;
2065
+ }
2066
+ }
2067
+ const {
2068
+ token,
2069
+ tokenHash,
2070
+ refreshToken,
2071
+ refreshTokenHash,
2072
+ locale,
2073
+ publishedAt,
2074
+ geoLocation: _geo,
2075
+ // Remove raw geoLocation, we use parsed version
2076
+ ...safeSession
2077
+ } = session2;
1925
2078
  return {
1926
2079
  ...safeSession,
2080
+ deviceType,
2081
+ browserName,
2082
+ osName,
2083
+ geoLocation,
2084
+ // Parsed object or null
1927
2085
  isTrulyActive,
1928
2086
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1929
2087
  };
@@ -1952,9 +2110,34 @@ var session$1 = ({ strapi: strapi2 }) => {
1952
2110
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1953
2111
  const timeSinceActive = now - lastActiveTime;
1954
2112
  const isTrulyActive = timeSinceActive < inactivityTimeout;
1955
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2113
+ const parsedUA = parseUserAgent(session2.userAgent);
2114
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2115
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2116
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2117
+ let geoLocation = session2.geoLocation;
2118
+ if (typeof geoLocation === "string") {
2119
+ try {
2120
+ geoLocation = JSON.parse(geoLocation);
2121
+ } catch (e) {
2122
+ geoLocation = null;
2123
+ }
2124
+ }
2125
+ const {
2126
+ token,
2127
+ tokenHash,
2128
+ refreshToken,
2129
+ refreshTokenHash,
2130
+ locale,
2131
+ publishedAt,
2132
+ geoLocation: _geo,
2133
+ ...safeSession
2134
+ } = session2;
1956
2135
  return {
1957
2136
  ...safeSession,
2137
+ deviceType,
2138
+ browserName,
2139
+ osName,
2140
+ geoLocation,
1958
2141
  isTrulyActive,
1959
2142
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1960
2143
  };
@@ -1991,9 +2174,34 @@ var session$1 = ({ strapi: strapi2 }) => {
1991
2174
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1992
2175
  const timeSinceActive = now - lastActiveTime;
1993
2176
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1994
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2177
+ const parsedUA = parseUserAgent(session2.userAgent);
2178
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2179
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2180
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2181
+ let geoLocation = session2.geoLocation;
2182
+ if (typeof geoLocation === "string") {
2183
+ try {
2184
+ geoLocation = JSON.parse(geoLocation);
2185
+ } catch (e) {
2186
+ geoLocation = null;
2187
+ }
2188
+ }
2189
+ const {
2190
+ token,
2191
+ tokenHash,
2192
+ refreshToken,
2193
+ refreshTokenHash,
2194
+ locale,
2195
+ publishedAt,
2196
+ geoLocation: _geo,
2197
+ ...safeSession
2198
+ } = session2;
1995
2199
  return {
1996
2200
  ...safeSession,
2201
+ deviceType,
2202
+ browserName,
2203
+ osName,
2204
+ geoLocation,
1997
2205
  isTrulyActive,
1998
2206
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1999
2207
  };
@@ -2110,7 +2318,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2110
2318
  }
2111
2319
  };
2112
2320
  };
2113
- const version = "4.2.9";
2321
+ const version = "4.2.11";
2114
2322
  const require$$2 = {
2115
2323
  version
2116
2324
  };
@@ -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";
@@ -451,8 +451,10 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
451
451
  userAgent,
452
452
  token: ctx.body.jwt,
453
453
  // Store Access Token (encrypted)
454
- refreshToken: ctx.body.refreshToken
454
+ refreshToken: ctx.body.refreshToken,
455
455
  // Store Refresh Token (encrypted) if exists
456
+ geoData
457
+ // Store geolocation data if available
456
458
  });
457
459
  log.info(`[SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
458
460
  if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
@@ -559,8 +561,8 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
559
561
  if (matchingSession) {
560
562
  const encryptedToken = newAccessToken ? encryptToken$1(newAccessToken) : matchingSession.token;
561
563
  const encryptedRefreshToken = newRefreshToken ? encryptToken$1(newRefreshToken) : matchingSession.refreshToken;
562
- const newTokenHash = newAccessToken ? hashToken$1(newAccessToken) : matchingSession.tokenHash;
563
- const newRefreshTokenHash = newRefreshToken ? hashToken$1(newRefreshToken) : matchingSession.refreshTokenHash;
564
+ const newTokenHash = newAccessToken ? hashToken$2(newAccessToken) : matchingSession.tokenHash;
565
+ const newRefreshTokenHash = newRefreshToken ? hashToken$2(newRefreshToken) : matchingSession.refreshTokenHash;
564
566
  await strapi2.documents(SESSION_UID$2).update({
565
567
  documentId: matchingSession.documentId,
566
568
  data: {
@@ -1038,7 +1040,91 @@ var routes$1 = {
1038
1040
  admin,
1039
1041
  "content-api": contentApi
1040
1042
  };
1041
- const { decryptToken: decryptToken$1 } = encryption;
1043
+ function parseUserAgent$2(userAgent) {
1044
+ if (!userAgent) {
1045
+ return {
1046
+ deviceType: "unknown",
1047
+ browserName: "unknown",
1048
+ browserVersion: null,
1049
+ osName: "unknown",
1050
+ osVersion: null
1051
+ };
1052
+ }
1053
+ userAgent.toLowerCase();
1054
+ let deviceType = "desktop";
1055
+ if (/mobile|android.*mobile|iphone|ipod|blackberry|iemobile|opera mini|opera mobi/i.test(userAgent)) {
1056
+ deviceType = "mobile";
1057
+ } else if (/tablet|ipad|android(?!.*mobile)|kindle|silk/i.test(userAgent)) {
1058
+ deviceType = "tablet";
1059
+ } else if (/bot|crawl|spider|slurp|mediapartners/i.test(userAgent)) {
1060
+ deviceType = "bot";
1061
+ }
1062
+ let browserName = "unknown";
1063
+ let browserVersion = null;
1064
+ if (/edg\//i.test(userAgent)) {
1065
+ browserName = "Edge";
1066
+ browserVersion = extractVersion(userAgent, /edg\/(\d+[\.\d]*)/i);
1067
+ } else if (/opr\//i.test(userAgent) || /opera/i.test(userAgent)) {
1068
+ browserName = "Opera";
1069
+ browserVersion = extractVersion(userAgent, /(?:opr|opera)[\s\/](\d+[\.\d]*)/i);
1070
+ } else if (/chrome|crios/i.test(userAgent) && !/edg/i.test(userAgent)) {
1071
+ browserName = "Chrome";
1072
+ browserVersion = extractVersion(userAgent, /(?:chrome|crios)\/(\d+[\.\d]*)/i);
1073
+ } else if (/firefox|fxios/i.test(userAgent)) {
1074
+ browserName = "Firefox";
1075
+ browserVersion = extractVersion(userAgent, /(?:firefox|fxios)\/(\d+[\.\d]*)/i);
1076
+ } else if (/safari/i.test(userAgent) && !/chrome|chromium/i.test(userAgent)) {
1077
+ browserName = "Safari";
1078
+ browserVersion = extractVersion(userAgent, /version\/(\d+[\.\d]*)/i);
1079
+ } else if (/msie|trident/i.test(userAgent)) {
1080
+ browserName = "Internet Explorer";
1081
+ browserVersion = extractVersion(userAgent, /(?:msie |rv:)(\d+[\.\d]*)/i);
1082
+ }
1083
+ let osName = "unknown";
1084
+ let osVersion = null;
1085
+ if (/windows nt/i.test(userAgent)) {
1086
+ osName = "Windows";
1087
+ const winVersion = extractVersion(userAgent, /windows nt (\d+[\.\d]*)/i);
1088
+ const winVersionMap = {
1089
+ "10.0": "10/11",
1090
+ "6.3": "8.1",
1091
+ "6.2": "8",
1092
+ "6.1": "7",
1093
+ "6.0": "Vista",
1094
+ "5.1": "XP"
1095
+ };
1096
+ osVersion = winVersionMap[winVersion] || winVersion;
1097
+ } else if (/mac os x/i.test(userAgent)) {
1098
+ osName = "macOS";
1099
+ osVersion = extractVersion(userAgent, /mac os x (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1100
+ } else if (/iphone|ipad|ipod/i.test(userAgent)) {
1101
+ osName = "iOS";
1102
+ osVersion = extractVersion(userAgent, /os (\d+[_\.\d]*)/i)?.replace(/_/g, ".");
1103
+ } else if (/android/i.test(userAgent)) {
1104
+ osName = "Android";
1105
+ osVersion = extractVersion(userAgent, /android (\d+[\.\d]*)/i);
1106
+ } else if (/linux/i.test(userAgent)) {
1107
+ osName = "Linux";
1108
+ } else if (/cros/i.test(userAgent)) {
1109
+ osName = "Chrome OS";
1110
+ }
1111
+ return {
1112
+ deviceType,
1113
+ browserName,
1114
+ browserVersion,
1115
+ osName,
1116
+ osVersion
1117
+ };
1118
+ }
1119
+ function extractVersion(userAgent, regex) {
1120
+ const match = userAgent.match(regex);
1121
+ return match ? match[1] : null;
1122
+ }
1123
+ var userAgentParser = {
1124
+ parseUserAgent: parseUserAgent$2
1125
+ };
1126
+ const { decryptToken: decryptToken$1, hashToken: hashToken$1 } = encryption;
1127
+ const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
1042
1128
  const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
1043
1129
  const USER_UID$1 = "plugin::users-permissions.user";
1044
1130
  var session$3 = {
@@ -1084,12 +1170,13 @@ var session$3 = {
1084
1170
  * Get own sessions (authenticated user)
1085
1171
  * GET /api/magic-sessionmanager/my-sessions
1086
1172
  * Automatically uses the authenticated user's documentId
1087
- * Marks which session is the current one (based on JWT token)
1173
+ * Marks which session is the current one (based on JWT token hash)
1088
1174
  */
1089
1175
  async getOwnSessions(ctx) {
1090
1176
  try {
1091
1177
  const userId = ctx.state.user?.documentId;
1092
1178
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1179
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1093
1180
  if (!userId) {
1094
1181
  return ctx.throw(401, "Unauthorized");
1095
1182
  }
@@ -1104,17 +1191,38 @@ var session$3 = {
1104
1191
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1105
1192
  const timeSinceActive = now - lastActiveTime;
1106
1193
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1107
- let isCurrentSession = false;
1108
- if (session2.token && currentToken) {
1194
+ const isCurrentSession = currentTokenHash && session2.tokenHash === currentTokenHash;
1195
+ const parsedUA = parseUserAgent$1(session2.userAgent);
1196
+ const deviceType = session2.deviceType || parsedUA.deviceType;
1197
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1198
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1199
+ let geoLocation = session2.geoLocation;
1200
+ if (typeof geoLocation === "string") {
1109
1201
  try {
1110
- const decrypted = decryptToken$1(session2.token);
1111
- isCurrentSession = decrypted === currentToken;
1112
- } catch (err) {
1202
+ geoLocation = JSON.parse(geoLocation);
1203
+ } catch (e) {
1204
+ geoLocation = null;
1113
1205
  }
1114
1206
  }
1115
- const { token, refreshToken, ...sessionWithoutTokens } = session2;
1207
+ const {
1208
+ token,
1209
+ tokenHash,
1210
+ refreshToken,
1211
+ refreshTokenHash,
1212
+ locale,
1213
+ publishedAt,
1214
+ // Remove Strapi internal fields
1215
+ geoLocation: _geo,
1216
+ // Remove raw geoLocation
1217
+ ...sessionWithoutTokens
1218
+ } = session2;
1116
1219
  return {
1117
1220
  ...sessionWithoutTokens,
1221
+ deviceType,
1222
+ browserName,
1223
+ osName,
1224
+ geoLocation,
1225
+ // Parsed object or null
1118
1226
  isCurrentSession,
1119
1227
  isTrulyActive,
1120
1228
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1176,21 +1284,14 @@ var session$3 = {
1176
1284
  return ctx.throw(401, "Unauthorized");
1177
1285
  }
1178
1286
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1179
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1287
+ const currentTokenHash = hashToken$1(token);
1288
+ const matchingSession = await strapi.documents(SESSION_UID$1).findFirst({
1180
1289
  filters: {
1181
1290
  user: { documentId: userId },
1291
+ tokenHash: currentTokenHash,
1182
1292
  isActive: true
1183
1293
  }
1184
1294
  });
1185
- const matchingSession = sessions.find((session2) => {
1186
- if (!session2.token) return false;
1187
- try {
1188
- const decrypted = decryptToken$1(session2.token);
1189
- return decrypted === token;
1190
- } catch (err) {
1191
- return false;
1192
- }
1193
- });
1194
1295
  if (matchingSession) {
1195
1296
  await sessionService.terminateSession({ sessionId: matchingSession.documentId });
1196
1297
  strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
@@ -1225,7 +1326,7 @@ var session$3 = {
1225
1326
  }
1226
1327
  },
1227
1328
  /**
1228
- * Get current session info based on JWT token
1329
+ * Get current session info based on JWT token hash
1229
1330
  * GET /api/magic-sessionmanager/current-session
1230
1331
  * Returns the session associated with the current JWT token
1231
1332
  */
@@ -1236,21 +1337,14 @@ var session$3 = {
1236
1337
  if (!userId || !token) {
1237
1338
  return ctx.throw(401, "Unauthorized");
1238
1339
  }
1239
- const sessions = await strapi.documents(SESSION_UID$1).findMany({
1340
+ const currentTokenHash = hashToken$1(token);
1341
+ const currentSession = await strapi.documents(SESSION_UID$1).findFirst({
1240
1342
  filters: {
1241
1343
  user: { documentId: userId },
1344
+ tokenHash: currentTokenHash,
1242
1345
  isActive: true
1243
1346
  }
1244
1347
  });
1245
- const currentSession = sessions.find((session2) => {
1246
- if (!session2.token) return false;
1247
- try {
1248
- const decrypted = decryptToken$1(session2.token);
1249
- return decrypted === token;
1250
- } catch (err) {
1251
- return false;
1252
- }
1253
- });
1254
1348
  if (!currentSession) {
1255
1349
  return ctx.notFound("Current session not found");
1256
1350
  }
@@ -1259,10 +1353,36 @@ var session$3 = {
1259
1353
  const now = /* @__PURE__ */ new Date();
1260
1354
  const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
1261
1355
  const timeSinceActive = now - lastActiveTime;
1262
- const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
1356
+ const parsedUA = parseUserAgent$1(currentSession.userAgent);
1357
+ const deviceType = currentSession.deviceType || parsedUA.deviceType;
1358
+ const browserName = currentSession.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
1359
+ const osName = currentSession.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
1360
+ let geoLocation = currentSession.geoLocation;
1361
+ if (typeof geoLocation === "string") {
1362
+ try {
1363
+ geoLocation = JSON.parse(geoLocation);
1364
+ } catch (e) {
1365
+ geoLocation = null;
1366
+ }
1367
+ }
1368
+ const {
1369
+ token: _,
1370
+ tokenHash: _th,
1371
+ refreshToken: __,
1372
+ refreshTokenHash: _rth,
1373
+ locale: _l,
1374
+ publishedAt: _p,
1375
+ geoLocation: _geo,
1376
+ ...sessionWithoutTokens
1377
+ } = currentSession;
1263
1378
  ctx.body = {
1264
1379
  data: {
1265
1380
  ...sessionWithoutTokens,
1381
+ deviceType,
1382
+ browserName,
1383
+ osName,
1384
+ geoLocation,
1385
+ // Parsed object or null
1266
1386
  isCurrentSession: true,
1267
1387
  isTrulyActive: timeSinceActive < inactivityTimeout,
1268
1388
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
@@ -1283,6 +1403,7 @@ var session$3 = {
1283
1403
  const userId = ctx.state.user?.documentId;
1284
1404
  const { sessionId } = ctx.params;
1285
1405
  const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
1406
+ const currentTokenHash = currentToken ? hashToken$1(currentToken) : null;
1286
1407
  if (!userId) {
1287
1408
  return ctx.throw(401, "Unauthorized");
1288
1409
  }
@@ -1301,14 +1422,8 @@ var session$3 = {
1301
1422
  strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
1302
1423
  return ctx.forbidden("You can only terminate your own sessions");
1303
1424
  }
1304
- if (sessionToTerminate.token && currentToken) {
1305
- try {
1306
- const decrypted = decryptToken$1(sessionToTerminate.token);
1307
- if (decrypted === currentToken) {
1308
- return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1309
- }
1310
- } catch (err) {
1311
- }
1425
+ if (currentTokenHash && sessionToTerminate.tokenHash === currentTokenHash) {
1426
+ return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
1312
1427
  }
1313
1428
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1314
1429
  await sessionService.terminateSession({ sessionId });
@@ -1803,6 +1918,7 @@ var controllers$1 = {
1803
1918
  };
1804
1919
  const { encryptToken, decryptToken, generateSessionId, hashToken } = encryption;
1805
1920
  const { createLogger: createLogger$1 } = logger;
1921
+ const { parseUserAgent } = userAgentParser;
1806
1922
  const SESSION_UID = "plugin::magic-sessionmanager.session";
1807
1923
  const USER_UID = "plugin::users-permissions.user";
1808
1924
  var session$1 = ({ strapi: strapi2 }) => {
@@ -1810,10 +1926,10 @@ var session$1 = ({ strapi: strapi2 }) => {
1810
1926
  return {
1811
1927
  /**
1812
1928
  * Create a new session record
1813
- * @param {Object} params - { userId, ip, userAgent, token, refreshToken }
1929
+ * @param {Object} params - { userId, ip, userAgent, token, refreshToken, geoData }
1814
1930
  * @returns {Promise<Object>} Created session
1815
1931
  */
1816
- async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken }) {
1932
+ async createSession({ userId, ip = "unknown", userAgent = "unknown", token, refreshToken, geoData }) {
1817
1933
  try {
1818
1934
  const now = /* @__PURE__ */ new Date();
1819
1935
  const sessionId = generateSessionId(userId);
@@ -1821,6 +1937,7 @@ var session$1 = ({ strapi: strapi2 }) => {
1821
1937
  const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
1822
1938
  const tokenHashValue = token ? hashToken(token) : null;
1823
1939
  const refreshTokenHashValue = refreshToken ? hashToken(refreshToken) : null;
1940
+ const parsedUA = parseUserAgent(userAgent);
1824
1941
  const session2 = await strapi2.documents(SESSION_UID).create({
1825
1942
  data: {
1826
1943
  user: userId,
@@ -1838,8 +1955,22 @@ var session$1 = ({ strapi: strapi2 }) => {
1838
1955
  // Encrypted Refresh Token
1839
1956
  refreshTokenHash: refreshTokenHashValue,
1840
1957
  // SHA-256 hash for fast lookup
1841
- sessionId
1958
+ sessionId,
1842
1959
  // Unique identifier
1960
+ // Device info from User-Agent
1961
+ deviceType: parsedUA.deviceType,
1962
+ browserName: parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName,
1963
+ osName: parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName,
1964
+ // Geolocation data (if available from Premium features)
1965
+ geoLocation: geoData ? JSON.stringify({
1966
+ country: geoData.country,
1967
+ country_code: geoData.country_code,
1968
+ country_flag: geoData.country_flag,
1969
+ city: geoData.city,
1970
+ region: geoData.region,
1971
+ timezone: geoData.timezone
1972
+ }) : null,
1973
+ securityScore: geoData?.securityScore || null
1843
1974
  }
1844
1975
  });
1845
1976
  log.info(`[SUCCESS] Session ${session2.documentId} (${sessionId}) created for user ${userId}`);
@@ -1917,9 +2048,36 @@ var session$1 = ({ strapi: strapi2 }) => {
1917
2048
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1918
2049
  const timeSinceActive = now - lastActiveTime;
1919
2050
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1920
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2051
+ const parsedUA = parseUserAgent(session2.userAgent);
2052
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2053
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2054
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2055
+ let geoLocation = session2.geoLocation;
2056
+ if (typeof geoLocation === "string") {
2057
+ try {
2058
+ geoLocation = JSON.parse(geoLocation);
2059
+ } catch (e) {
2060
+ geoLocation = null;
2061
+ }
2062
+ }
2063
+ const {
2064
+ token,
2065
+ tokenHash,
2066
+ refreshToken,
2067
+ refreshTokenHash,
2068
+ locale,
2069
+ publishedAt,
2070
+ geoLocation: _geo,
2071
+ // Remove raw geoLocation, we use parsed version
2072
+ ...safeSession
2073
+ } = session2;
1921
2074
  return {
1922
2075
  ...safeSession,
2076
+ deviceType,
2077
+ browserName,
2078
+ osName,
2079
+ geoLocation,
2080
+ // Parsed object or null
1923
2081
  isTrulyActive,
1924
2082
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1925
2083
  };
@@ -1948,9 +2106,34 @@ var session$1 = ({ strapi: strapi2 }) => {
1948
2106
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1949
2107
  const timeSinceActive = now - lastActiveTime;
1950
2108
  const isTrulyActive = timeSinceActive < inactivityTimeout;
1951
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2109
+ const parsedUA = parseUserAgent(session2.userAgent);
2110
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2111
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2112
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2113
+ let geoLocation = session2.geoLocation;
2114
+ if (typeof geoLocation === "string") {
2115
+ try {
2116
+ geoLocation = JSON.parse(geoLocation);
2117
+ } catch (e) {
2118
+ geoLocation = null;
2119
+ }
2120
+ }
2121
+ const {
2122
+ token,
2123
+ tokenHash,
2124
+ refreshToken,
2125
+ refreshTokenHash,
2126
+ locale,
2127
+ publishedAt,
2128
+ geoLocation: _geo,
2129
+ ...safeSession
2130
+ } = session2;
1952
2131
  return {
1953
2132
  ...safeSession,
2133
+ deviceType,
2134
+ browserName,
2135
+ osName,
2136
+ geoLocation,
1954
2137
  isTrulyActive,
1955
2138
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1956
2139
  };
@@ -1987,9 +2170,34 @@ var session$1 = ({ strapi: strapi2 }) => {
1987
2170
  const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1988
2171
  const timeSinceActive = now - lastActiveTime;
1989
2172
  const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1990
- const { token, tokenHash, refreshToken, refreshTokenHash, ...safeSession } = session2;
2173
+ const parsedUA = parseUserAgent(session2.userAgent);
2174
+ const deviceType = session2.deviceType || parsedUA.deviceType;
2175
+ const browserName = session2.browserName || (parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName);
2176
+ const osName = session2.osName || (parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName);
2177
+ let geoLocation = session2.geoLocation;
2178
+ if (typeof geoLocation === "string") {
2179
+ try {
2180
+ geoLocation = JSON.parse(geoLocation);
2181
+ } catch (e) {
2182
+ geoLocation = null;
2183
+ }
2184
+ }
2185
+ const {
2186
+ token,
2187
+ tokenHash,
2188
+ refreshToken,
2189
+ refreshTokenHash,
2190
+ locale,
2191
+ publishedAt,
2192
+ geoLocation: _geo,
2193
+ ...safeSession
2194
+ } = session2;
1991
2195
  return {
1992
2196
  ...safeSession,
2197
+ deviceType,
2198
+ browserName,
2199
+ osName,
2200
+ geoLocation,
1993
2201
  isTrulyActive,
1994
2202
  minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1995
2203
  };
@@ -2106,7 +2314,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2106
2314
  }
2107
2315
  };
2108
2316
  };
2109
- const version = "4.2.9";
2317
+ const version = "4.2.11";
2110
2318
  const require$$2 = {
2111
2319
  version
2112
2320
  };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.2.10",
2
+ "version": "4.2.12",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",