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 +183 -9
- package/dist/server/index.js +231 -53
- package/dist/server/index.mjs +231 -53
- package/package.json +1 -1
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
|
-
## 📋
|
|
393
|
+
## 📋 Content-API Endpoints (For Frontend/Apps)
|
|
394
394
|
|
|
395
|
-
|
|
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
|
-
###
|
|
562
|
+
### Get Active Sessions Only
|
|
403
563
|
|
|
404
564
|
```bash
|
|
405
|
-
|
|
406
|
-
POST /api/auth/logout
|
|
565
|
+
GET /magic-sessionmanager/sessions/active
|
|
407
566
|
```
|
|
408
567
|
|
|
409
|
-
### Force
|
|
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
|
-
|
|
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
|
|
package/dist/server/index.js
CHANGED
|
@@ -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$
|
|
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$
|
|
217
|
+
hashToken: hashToken$4
|
|
218
218
|
};
|
|
219
219
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
220
|
-
const { hashToken: hashToken$
|
|
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$
|
|
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$
|
|
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$
|
|
566
|
-
const newRefreshTokenHash = newRefreshToken ? hashToken$
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1267
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2249
|
+
const version = "4.2.10";
|
|
2072
2250
|
const require$$2 = {
|
|
2073
2251
|
version
|
|
2074
2252
|
};
|
package/dist/server/index.mjs
CHANGED
|
@@ -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$
|
|
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$
|
|
213
|
+
hashToken: hashToken$4
|
|
214
214
|
};
|
|
215
215
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
216
|
-
const { hashToken: hashToken$
|
|
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$
|
|
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$
|
|
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$
|
|
562
|
-
const newRefreshTokenHash = newRefreshToken ? hashToken$
|
|
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
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1263
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2245
|
+
const version = "4.2.10";
|
|
2068
2246
|
const require$$2 = {
|
|
2069
2247
|
version
|
|
2070
2248
|
};
|
package/package.json
CHANGED