strapi-plugin-magic-sessionmanager 4.0.0 → 4.0.2
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 +2 -2
- package/admin/src/components/LicenseGuard.jsx +6 -6
- package/admin/src/components/OnlineUsersWidget.jsx +11 -7
- package/admin/src/components/SessionDetailModal.jsx +45 -41
- package/admin/src/components/SessionInfoCard.jsx +3 -3
- package/admin/src/components/SessionInfoPanel.jsx +31 -21
- package/admin/src/index.js +9 -0
- package/admin/src/pages/Analytics.jsx +2 -2
- package/admin/src/pages/HomePage.jsx +129 -165
- package/admin/src/pages/License.jsx +5 -5
- package/admin/src/pages/Settings.jsx +148 -144
- package/admin/src/pages/SettingsNew.jsx +21 -21
- package/admin/src/pages/UpgradePage.jsx +448 -0
- package/admin/src/pluginId.js +1 -0
- package/admin/src/translations/de.json +294 -15
- package/admin/src/translations/en.json +293 -14
- package/admin/src/translations/es.json +284 -18
- package/admin/src/translations/fr.json +284 -18
- package/admin/src/translations/pt.json +284 -18
- package/admin/src/utils/parseUserAgent.js +6 -6
- package/admin/src/utils/theme.js +85 -0
- package/dist/_chunks/{Analytics-mYu_uGwU.mjs → Analytics-DTE_zmRV.mjs} +4 -4
- package/dist/_chunks/{Analytics-ioaeEh-E.js → Analytics-lw_JaOVy.js} +4 -4
- package/dist/_chunks/{App-DdnUYWbC.js → App-DDKYCjKw.js} +221 -216
- package/dist/_chunks/{App-BXpIS12l.mjs → App-DJW1ZNl5.mjs} +221 -216
- package/dist/_chunks/{License-C03C2j9P.mjs → License-DaOFuImm.mjs} +6 -10
- package/dist/_chunks/{License-DZYrOgcx.js → License-Tk-6UfPl.js} +6 -10
- package/dist/_chunks/{OnlineUsersWidget-B8JS1xZu.js → OnlineUsersWidget-C1qTpsws.js} +11 -7
- package/dist/_chunks/{OnlineUsersWidget-ArMl0nen.mjs → OnlineUsersWidget-CADphbXG.mjs} +11 -7
- package/dist/_chunks/{Settings-0ocB3qHk.mjs → Settings-C9xvckgq.mjs} +200 -188
- package/dist/_chunks/{Settings-C6_CqpCC.js → Settings-DyEAuTNQ.js} +200 -188
- package/dist/_chunks/UpgradePage-Dssk8A0Z.js +354 -0
- package/dist/_chunks/UpgradePage-cINvE9zY.mjs +352 -0
- package/dist/_chunks/de-CDA1V0rF.mjs +292 -0
- package/dist/_chunks/de-I-Q-pWqu.js +292 -0
- package/dist/_chunks/en-Bd7_h-4e.js +292 -0
- package/dist/_chunks/en-DzmOCyzQ.mjs +292 -0
- package/dist/_chunks/es-BcAx18XG.js +277 -0
- package/dist/_chunks/es-Cx-SN6qV.mjs +277 -0
- package/dist/_chunks/fr-DCzYMuJ-.js +277 -0
- package/dist/_chunks/fr-DXlXE5Eo.mjs +277 -0
- package/dist/_chunks/{index-DC8Y0qxx.js → index-CWcvrfXc.js} +52 -49
- package/dist/_chunks/{index-DBRS3kt5.mjs → index-DQO9bNP7.mjs} +52 -49
- package/dist/_chunks/pt-21-MAb72.js +277 -0
- package/dist/_chunks/pt-zsdTSjba.mjs +277 -0
- package/dist/_chunks/{useLicense-qgGfMvse.js → useLicense-DtvJOszr.js} +1 -1
- package/dist/_chunks/{useLicense-DSLL9n3Y.mjs → useLicense-DxbD4Wf8.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +142 -33
- package/dist/server/index.mjs +142 -33
- package/package.json +1 -1
- package/server/src/bootstrap.js +76 -4
- package/server/src/controllers/session.js +59 -9
- package/server/src/middlewares/last-seen.js +5 -4
- package/server/src/routes/content-api.js +11 -2
- package/server/src/services/notifications.js +10 -10
- package/server/src/services/session.js +24 -4
- package/dist/_chunks/de-BxFx1pwE.js +0 -23
- package/dist/_chunks/de-CdO3s01z.mjs +0 -23
- package/dist/_chunks/en-CsPpPJL3.mjs +0 -23
- package/dist/_chunks/en-RqmpDHdS.js +0 -23
- package/dist/_chunks/es-CuLHazN1.js +0 -23
- package/dist/_chunks/es-Dkmjhy9c.mjs +0 -23
- package/dist/_chunks/fr-BAJp2yhI.js +0 -23
- package/dist/_chunks/fr-Bssg_3UF.mjs +0 -23
- package/dist/_chunks/pt-BAP9cKs3.js +0 -23
- package/dist/_chunks/pt-BVNoNcuY.mjs +0 -23
package/dist/server/index.mjs
CHANGED
|
@@ -148,9 +148,9 @@ var encryption = {
|
|
|
148
148
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
149
149
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
150
150
|
return async (ctx, next) => {
|
|
151
|
-
if (ctx.state.user && ctx.state.user.
|
|
151
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
152
152
|
try {
|
|
153
|
-
const userId = ctx.state.user.
|
|
153
|
+
const userId = ctx.state.user.documentId;
|
|
154
154
|
const activeSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
155
155
|
filters: {
|
|
156
156
|
user: { documentId: userId },
|
|
@@ -167,9 +167,9 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
await next();
|
|
170
|
-
if (ctx.state.user && ctx.state.user.
|
|
170
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
171
171
|
try {
|
|
172
|
-
const userId = ctx.state.user.
|
|
172
|
+
const userId = ctx.state.user.documentId;
|
|
173
173
|
const sessionId = ctx.state.sessionId;
|
|
174
174
|
await sessionService.touch({
|
|
175
175
|
userId,
|
|
@@ -184,6 +184,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
184
184
|
const getClientIp = getClientIp_1;
|
|
185
185
|
const { encryptToken: encryptToken$1, decryptToken: decryptToken$2 } = encryption;
|
|
186
186
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
187
|
+
const USER_UID$2 = "plugin::users-permissions.user";
|
|
187
188
|
var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
188
189
|
strapi2.log.info("[magic-sessionmanager] [START] Bootstrap starting...");
|
|
189
190
|
try {
|
|
@@ -291,7 +292,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
291
292
|
const user = ctx.body.user;
|
|
292
293
|
const ip = getClientIp(ctx);
|
|
293
294
|
const userAgent = ctx.request.headers?.["user-agent"] || ctx.request.header?.["user-agent"] || "unknown";
|
|
294
|
-
strapi2.log.info(`[magic-sessionmanager] [CHECK] Login detected! User: ${user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
295
|
+
strapi2.log.info(`[magic-sessionmanager] [CHECK] Login detected! User: ${user.documentId || user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
295
296
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
296
297
|
let shouldBlock = false;
|
|
297
298
|
let blockReason = "";
|
|
@@ -344,8 +345,13 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
344
345
|
};
|
|
345
346
|
return;
|
|
346
347
|
}
|
|
348
|
+
let userDocId = user.documentId;
|
|
349
|
+
if (!userDocId && user.id) {
|
|
350
|
+
const fullUser = await strapi2.entityService.findOne(USER_UID$2, user.id);
|
|
351
|
+
userDocId = fullUser?.documentId || user.id;
|
|
352
|
+
}
|
|
347
353
|
const newSession = await sessionService.createSession({
|
|
348
|
-
userId:
|
|
354
|
+
userId: userDocId,
|
|
349
355
|
ip,
|
|
350
356
|
userAgent,
|
|
351
357
|
token: ctx.body.jwt,
|
|
@@ -353,7 +359,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
353
359
|
refreshToken: ctx.body.refreshToken
|
|
354
360
|
// Store Refresh Token (encrypted) if exists
|
|
355
361
|
});
|
|
356
|
-
strapi2.log.info(`[magic-sessionmanager] [SUCCESS] Session created for user ${
|
|
362
|
+
strapi2.log.info(`[magic-sessionmanager] [SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
|
|
357
363
|
if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
|
|
358
364
|
try {
|
|
359
365
|
const notificationService = strapi2.plugin("magic-sessionmanager").service("notifications");
|
|
@@ -479,12 +485,55 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
479
485
|
lastSeen({ strapi: strapi2, sessionService })
|
|
480
486
|
);
|
|
481
487
|
strapi2.log.info("[magic-sessionmanager] [SUCCESS] LastSeen middleware mounted");
|
|
488
|
+
await ensureContentApiPermissions(strapi2);
|
|
482
489
|
strapi2.log.info("[magic-sessionmanager] [SUCCESS] Bootstrap complete");
|
|
483
490
|
strapi2.log.info("[magic-sessionmanager] [READY] Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session");
|
|
484
491
|
} catch (err) {
|
|
485
492
|
strapi2.log.error("[magic-sessionmanager] [ERROR] Bootstrap error:", err);
|
|
486
493
|
}
|
|
487
494
|
};
|
|
495
|
+
async function ensureContentApiPermissions(strapi2) {
|
|
496
|
+
try {
|
|
497
|
+
const authenticatedRole = await strapi2.query("plugin::users-permissions.role").findOne({
|
|
498
|
+
where: { type: "authenticated" }
|
|
499
|
+
});
|
|
500
|
+
if (!authenticatedRole) {
|
|
501
|
+
strapi2.log.warn("[magic-sessionmanager] Authenticated role not found - skipping permission setup");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const requiredActions = [
|
|
505
|
+
"plugin::magic-sessionmanager.session.logout",
|
|
506
|
+
"plugin::magic-sessionmanager.session.logoutAll",
|
|
507
|
+
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
508
|
+
"plugin::magic-sessionmanager.session.getUserSessions"
|
|
509
|
+
];
|
|
510
|
+
const existingPermissions = await strapi2.query("plugin::users-permissions.permission").findMany({
|
|
511
|
+
where: {
|
|
512
|
+
role: authenticatedRole.id,
|
|
513
|
+
action: { $in: requiredActions }
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
const existingActions = existingPermissions.map((p) => p.action);
|
|
517
|
+
const missingActions = requiredActions.filter((action) => !existingActions.includes(action));
|
|
518
|
+
if (missingActions.length === 0) {
|
|
519
|
+
strapi2.log.debug("[magic-sessionmanager] Content-API permissions already configured");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
for (const action of missingActions) {
|
|
523
|
+
await strapi2.query("plugin::users-permissions.permission").create({
|
|
524
|
+
data: {
|
|
525
|
+
action,
|
|
526
|
+
role: authenticatedRole.id
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
strapi2.log.info(`[magic-sessionmanager] [PERMISSION] Enabled ${action} for authenticated users`);
|
|
530
|
+
}
|
|
531
|
+
strapi2.log.info("[magic-sessionmanager] [SUCCESS] Content-API permissions configured for authenticated users");
|
|
532
|
+
} catch (err) {
|
|
533
|
+
strapi2.log.warn("[magic-sessionmanager] Could not auto-configure permissions:", err.message);
|
|
534
|
+
strapi2.log.warn("[magic-sessionmanager] Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
488
537
|
var destroy$1 = async ({ strapi: strapi2 }) => {
|
|
489
538
|
if (strapi2.licenseGuard && strapi2.licenseGuard.pingInterval) {
|
|
490
539
|
clearInterval(strapi2.licenseGuard.pingInterval);
|
|
@@ -637,13 +686,22 @@ var contentApi$1 = {
|
|
|
637
686
|
// ============================================================
|
|
638
687
|
// SESSION QUERIES
|
|
639
688
|
// ============================================================
|
|
689
|
+
{
|
|
690
|
+
method: "GET",
|
|
691
|
+
path: "/my-sessions",
|
|
692
|
+
handler: "session.getOwnSessions",
|
|
693
|
+
config: {
|
|
694
|
+
auth: { strategies: ["users-permissions"] },
|
|
695
|
+
description: "Get own sessions (automatically uses authenticated user)"
|
|
696
|
+
}
|
|
697
|
+
},
|
|
640
698
|
{
|
|
641
699
|
method: "GET",
|
|
642
700
|
path: "/user/:userId/sessions",
|
|
643
701
|
handler: "session.getUserSessions",
|
|
644
702
|
config: {
|
|
645
703
|
auth: { strategies: ["users-permissions"] },
|
|
646
|
-
description: "Get
|
|
704
|
+
description: "Get sessions by userId (validates user can only see own sessions)"
|
|
647
705
|
}
|
|
648
706
|
}
|
|
649
707
|
]
|
|
@@ -803,7 +861,7 @@ var routes$1 = {
|
|
|
803
861
|
};
|
|
804
862
|
const { decryptToken: decryptToken$1 } = encryption;
|
|
805
863
|
const SESSION_UID$1 = "plugin::magic-sessionmanager.session";
|
|
806
|
-
const USER_UID = "plugin::users-permissions.user";
|
|
864
|
+
const USER_UID$1 = "plugin::users-permissions.user";
|
|
807
865
|
var session$3 = {
|
|
808
866
|
/**
|
|
809
867
|
* Get ALL sessions (active + inactive) - Admin only
|
|
@@ -843,6 +901,30 @@ var session$3 = {
|
|
|
843
901
|
ctx.throw(500, "Error fetching active sessions");
|
|
844
902
|
}
|
|
845
903
|
},
|
|
904
|
+
/**
|
|
905
|
+
* Get own sessions (authenticated user)
|
|
906
|
+
* GET /api/magic-sessionmanager/my-sessions
|
|
907
|
+
* Automatically uses the authenticated user's documentId
|
|
908
|
+
*/
|
|
909
|
+
async getOwnSessions(ctx) {
|
|
910
|
+
try {
|
|
911
|
+
const userId = ctx.state.user?.documentId;
|
|
912
|
+
if (!userId) {
|
|
913
|
+
return ctx.throw(401, "Unauthorized");
|
|
914
|
+
}
|
|
915
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
916
|
+
const sessions = await sessionService.getUserSessions(userId);
|
|
917
|
+
ctx.body = {
|
|
918
|
+
data: sessions,
|
|
919
|
+
meta: {
|
|
920
|
+
count: sessions.length
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
} catch (err) {
|
|
924
|
+
strapi.log.error("[magic-sessionmanager] Error fetching own sessions:", err);
|
|
925
|
+
ctx.throw(500, "Error fetching sessions");
|
|
926
|
+
}
|
|
927
|
+
},
|
|
846
928
|
/**
|
|
847
929
|
* Get user's sessions
|
|
848
930
|
* GET /magic-sessionmanager/user/:userId/sessions (Admin API)
|
|
@@ -853,9 +935,9 @@ var session$3 = {
|
|
|
853
935
|
try {
|
|
854
936
|
const { userId } = ctx.params;
|
|
855
937
|
const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
|
|
856
|
-
const
|
|
857
|
-
if (!isAdminRequest &&
|
|
858
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
938
|
+
const requestingUserDocId = ctx.state.user?.documentId;
|
|
939
|
+
if (!isAdminRequest && requestingUserDocId && String(requestingUserDocId) !== String(userId)) {
|
|
940
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
|
|
859
941
|
return ctx.forbidden("You can only access your own sessions");
|
|
860
942
|
}
|
|
861
943
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
@@ -876,7 +958,7 @@ var session$3 = {
|
|
|
876
958
|
*/
|
|
877
959
|
async logout(ctx) {
|
|
878
960
|
try {
|
|
879
|
-
const userId = ctx.state.user?.
|
|
961
|
+
const userId = ctx.state.user?.documentId;
|
|
880
962
|
const token = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
881
963
|
if (!userId) {
|
|
882
964
|
return ctx.throw(401, "Unauthorized");
|
|
@@ -915,7 +997,7 @@ var session$3 = {
|
|
|
915
997
|
*/
|
|
916
998
|
async logoutAll(ctx) {
|
|
917
999
|
try {
|
|
918
|
-
const userId = ctx.state.user?.
|
|
1000
|
+
const userId = ctx.state.user?.documentId;
|
|
919
1001
|
if (!userId) {
|
|
920
1002
|
return ctx.throw(401, "Unauthorized");
|
|
921
1003
|
}
|
|
@@ -1055,24 +1137,34 @@ var session$3 = {
|
|
|
1055
1137
|
/**
|
|
1056
1138
|
* Toggle user blocked status
|
|
1057
1139
|
* POST /magic-sessionmanager/user/:userId/toggle-block
|
|
1140
|
+
* Supports both numeric id (from Content Manager) and documentId
|
|
1058
1141
|
*/
|
|
1059
1142
|
async toggleUserBlock(ctx) {
|
|
1060
1143
|
try {
|
|
1061
1144
|
const { userId } = ctx.params;
|
|
1062
|
-
|
|
1145
|
+
let userDocumentId = userId;
|
|
1146
|
+
let user = null;
|
|
1147
|
+
user = await strapi.documents(USER_UID$1).findOne({ documentId: userId });
|
|
1148
|
+
if (!user && !isNaN(userId)) {
|
|
1149
|
+
const numericUser = await strapi.entityService.findOne(USER_UID$1, parseInt(userId, 10));
|
|
1150
|
+
if (numericUser) {
|
|
1151
|
+
userDocumentId = numericUser.documentId;
|
|
1152
|
+
user = numericUser;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1063
1155
|
if (!user) {
|
|
1064
1156
|
return ctx.throw(404, "User not found");
|
|
1065
1157
|
}
|
|
1066
1158
|
const newBlockedStatus = !user.blocked;
|
|
1067
|
-
await strapi.documents(USER_UID).update({
|
|
1068
|
-
documentId:
|
|
1159
|
+
await strapi.documents(USER_UID$1).update({
|
|
1160
|
+
documentId: userDocumentId,
|
|
1069
1161
|
data: {
|
|
1070
1162
|
blocked: newBlockedStatus
|
|
1071
1163
|
}
|
|
1072
1164
|
});
|
|
1073
1165
|
if (newBlockedStatus) {
|
|
1074
1166
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1075
|
-
await sessionService.terminateSession({ userId });
|
|
1167
|
+
await sessionService.terminateSession({ userId: userDocumentId });
|
|
1076
1168
|
}
|
|
1077
1169
|
ctx.body = {
|
|
1078
1170
|
message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
|
|
@@ -1401,6 +1493,7 @@ var controllers$1 = {
|
|
|
1401
1493
|
};
|
|
1402
1494
|
const { encryptToken, decryptToken, generateSessionId } = encryption;
|
|
1403
1495
|
const SESSION_UID = "plugin::magic-sessionmanager.session";
|
|
1496
|
+
const USER_UID = "plugin::users-permissions.user";
|
|
1404
1497
|
var session$1 = ({ strapi: strapi2 }) => ({
|
|
1405
1498
|
/**
|
|
1406
1499
|
* Create a new session record
|
|
@@ -1439,6 +1532,7 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1439
1532
|
},
|
|
1440
1533
|
/**
|
|
1441
1534
|
* Terminate a session or all sessions for a user
|
|
1535
|
+
* Supports both numeric id (legacy) and documentId (Strapi v5)
|
|
1442
1536
|
* @param {Object} params - { sessionId | userId }
|
|
1443
1537
|
* @returns {Promise<void>}
|
|
1444
1538
|
*/
|
|
@@ -1455,9 +1549,16 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1455
1549
|
});
|
|
1456
1550
|
strapi2.log.info(`[magic-sessionmanager] Session ${sessionId} terminated`);
|
|
1457
1551
|
} else if (userId) {
|
|
1552
|
+
let userDocumentId = userId;
|
|
1553
|
+
if (!isNaN(userId)) {
|
|
1554
|
+
const user = await strapi2.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
1555
|
+
if (user) {
|
|
1556
|
+
userDocumentId = user.documentId;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1458
1559
|
const activeSessions = await strapi2.documents(SESSION_UID).findMany({
|
|
1459
1560
|
filters: {
|
|
1460
|
-
user: { documentId:
|
|
1561
|
+
user: { documentId: userDocumentId },
|
|
1461
1562
|
// Deep filtering syntax
|
|
1462
1563
|
isActive: true
|
|
1463
1564
|
}
|
|
@@ -1471,7 +1572,7 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1471
1572
|
}
|
|
1472
1573
|
});
|
|
1473
1574
|
}
|
|
1474
|
-
strapi2.log.info(`[magic-sessionmanager] All sessions terminated for user ${
|
|
1575
|
+
strapi2.log.info(`[magic-sessionmanager] All sessions terminated for user ${userDocumentId}`);
|
|
1475
1576
|
}
|
|
1476
1577
|
} catch (err) {
|
|
1477
1578
|
strapi2.log.error("[magic-sessionmanager] Error terminating session:", err);
|
|
@@ -1543,13 +1644,21 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1543
1644
|
},
|
|
1544
1645
|
/**
|
|
1545
1646
|
* Get all sessions for a specific user
|
|
1546
|
-
*
|
|
1647
|
+
* Supports both numeric id (legacy) and documentId (Strapi v5)
|
|
1648
|
+
* @param {string|number} userId - User documentId or numeric id
|
|
1547
1649
|
* @returns {Promise<Array>} User's sessions with accurate online status
|
|
1548
1650
|
*/
|
|
1549
1651
|
async getUserSessions(userId) {
|
|
1550
1652
|
try {
|
|
1653
|
+
let userDocumentId = userId;
|
|
1654
|
+
if (!isNaN(userId)) {
|
|
1655
|
+
const user = await strapi2.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
1656
|
+
if (user) {
|
|
1657
|
+
userDocumentId = user.documentId;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1551
1660
|
const sessions = await strapi2.documents(SESSION_UID).findMany({
|
|
1552
|
-
filters: { user: { documentId:
|
|
1661
|
+
filters: { user: { documentId: userDocumentId } },
|
|
1553
1662
|
sort: { loginTime: "desc" }
|
|
1554
1663
|
});
|
|
1555
1664
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
@@ -1677,7 +1786,7 @@ var session$1 = ({ strapi: strapi2 }) => ({
|
|
|
1677
1786
|
}
|
|
1678
1787
|
}
|
|
1679
1788
|
});
|
|
1680
|
-
const version = "
|
|
1789
|
+
const version = "4.0.1";
|
|
1681
1790
|
const require$$2 = {
|
|
1682
1791
|
version
|
|
1683
1792
|
};
|
|
@@ -2108,12 +2217,12 @@ var notifications$1 = ({ strapi: strapi2 }) => ({
|
|
|
2108
2217
|
strapi2.log.debug("[magic-sessionmanager/notifications] Using default fallback templates");
|
|
2109
2218
|
return {
|
|
2110
2219
|
suspiciousLogin: {
|
|
2111
|
-
subject: "
|
|
2220
|
+
subject: "[ALERT] Suspicious Login Alert - Session Manager",
|
|
2112
2221
|
html: `
|
|
2113
2222
|
<html>
|
|
2114
2223
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
2115
2224
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb; border-radius: 10px;">
|
|
2116
|
-
<h2 style="color: #dc2626;"
|
|
2225
|
+
<h2 style="color: #dc2626;">[ALERT] Suspicious Login Detected</h2>
|
|
2117
2226
|
<p>A potentially suspicious login was detected for your account.</p>
|
|
2118
2227
|
|
|
2119
2228
|
<div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
@@ -2148,7 +2257,7 @@ var notifications$1 = ({ strapi: strapi2 }) => ({
|
|
|
2148
2257
|
</div>
|
|
2149
2258
|
</body>
|
|
2150
2259
|
</html>`,
|
|
2151
|
-
text:
|
|
2260
|
+
text: `[ALERT] Suspicious Login Detected
|
|
2152
2261
|
|
|
2153
2262
|
A potentially suspicious login was detected for your account.
|
|
2154
2263
|
|
|
@@ -2319,10 +2428,10 @@ VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
|
|
|
2319
2428
|
title: this.getEventTitle(event),
|
|
2320
2429
|
color: this.getEventColor(event),
|
|
2321
2430
|
fields: [
|
|
2322
|
-
{ name: "
|
|
2431
|
+
{ name: "User", value: `${user.email}
|
|
2323
2432
|
${user.username || "N/A"}`, inline: true },
|
|
2324
|
-
{ name: "
|
|
2325
|
-
{ name: "
|
|
2433
|
+
{ name: "IP", value: session2.ipAddress, inline: true },
|
|
2434
|
+
{ name: "Time", value: new Date(session2.loginTime).toLocaleString(), inline: false }
|
|
2326
2435
|
],
|
|
2327
2436
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2328
2437
|
footer: { text: "Magic Session Manager" }
|
|
@@ -2350,11 +2459,11 @@ Score: ${geoData.securityScore}/100`,
|
|
|
2350
2459
|
},
|
|
2351
2460
|
getEventTitle(event) {
|
|
2352
2461
|
const titles = {
|
|
2353
|
-
"login.suspicious": "
|
|
2462
|
+
"login.suspicious": "[ALERT] Suspicious Login",
|
|
2354
2463
|
"login.new_location": "[LOCATION] New Location Login",
|
|
2355
|
-
"login.vpn": "
|
|
2356
|
-
"login.threat": "
|
|
2357
|
-
"session.terminated": "
|
|
2464
|
+
"login.vpn": "[WARNING] VPN Login Detected",
|
|
2465
|
+
"login.threat": "[THREAT] Threat IP Login",
|
|
2466
|
+
"session.terminated": "[INFO] Session Terminated"
|
|
2358
2467
|
};
|
|
2359
2468
|
return titles[event] || "[STATS] Session Event";
|
|
2360
2469
|
},
|
package/package.json
CHANGED
package/server/src/bootstrap.js
CHANGED
|
@@ -163,7 +163,8 @@ module.exports = async ({ strapi }) => {
|
|
|
163
163
|
const ip = getClientIp(ctx);
|
|
164
164
|
const userAgent = ctx.request.headers?.['user-agent'] || ctx.request.header?.['user-agent'] || 'unknown';
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// Strapi v5: Use documentId for session creation
|
|
167
|
+
strapi.log.info(`[magic-sessionmanager] [CHECK] Login detected! User: ${user.documentId || user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
167
168
|
|
|
168
169
|
// Get config
|
|
169
170
|
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
@@ -235,16 +236,23 @@ module.exports = async ({ strapi }) => {
|
|
|
235
236
|
return; // Stop here
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
// Create a new session
|
|
239
|
+
// Create a new session (Strapi v5: Use documentId instead of numeric id)
|
|
240
|
+
// If login response doesn't include documentId, fetch it from DB
|
|
241
|
+
let userDocId = user.documentId;
|
|
242
|
+
if (!userDocId && user.id) {
|
|
243
|
+
const fullUser = await strapi.entityService.findOne(USER_UID, user.id);
|
|
244
|
+
userDocId = fullUser?.documentId || user.id;
|
|
245
|
+
}
|
|
246
|
+
|
|
239
247
|
const newSession = await sessionService.createSession({
|
|
240
|
-
userId:
|
|
248
|
+
userId: userDocId,
|
|
241
249
|
ip,
|
|
242
250
|
userAgent,
|
|
243
251
|
token: ctx.body.jwt, // Store Access Token (encrypted)
|
|
244
252
|
refreshToken: ctx.body.refreshToken, // Store Refresh Token (encrypted) if exists
|
|
245
253
|
});
|
|
246
254
|
|
|
247
|
-
strapi.log.info(`[magic-sessionmanager] [SUCCESS] Session created for user ${
|
|
255
|
+
strapi.log.info(`[magic-sessionmanager] [SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
|
|
248
256
|
|
|
249
257
|
// Advanced: Send notifications
|
|
250
258
|
if (geoData && (config.enableEmailAlerts || config.enableWebhooks)) {
|
|
@@ -407,6 +415,10 @@ module.exports = async ({ strapi }) => {
|
|
|
407
415
|
);
|
|
408
416
|
|
|
409
417
|
strapi.log.info('[magic-sessionmanager] [SUCCESS] LastSeen middleware mounted');
|
|
418
|
+
|
|
419
|
+
// Auto-enable Content-API permissions for authenticated users
|
|
420
|
+
await ensureContentApiPermissions(strapi);
|
|
421
|
+
|
|
410
422
|
strapi.log.info('[magic-sessionmanager] [SUCCESS] Bootstrap complete');
|
|
411
423
|
strapi.log.info('[magic-sessionmanager] [READY] Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
|
|
412
424
|
|
|
@@ -414,3 +426,63 @@ module.exports = async ({ strapi }) => {
|
|
|
414
426
|
strapi.log.error('[magic-sessionmanager] [ERROR] Bootstrap error:', err);
|
|
415
427
|
}
|
|
416
428
|
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Auto-enable Content-API permissions for authenticated users
|
|
432
|
+
* This ensures plugin endpoints are accessible after installation
|
|
433
|
+
* @param {object} strapi - Strapi instance
|
|
434
|
+
*/
|
|
435
|
+
async function ensureContentApiPermissions(strapi) {
|
|
436
|
+
try {
|
|
437
|
+
// Get the authenticated role
|
|
438
|
+
const authenticatedRole = await strapi.query('plugin::users-permissions.role').findOne({
|
|
439
|
+
where: { type: 'authenticated' },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!authenticatedRole) {
|
|
443
|
+
strapi.log.warn('[magic-sessionmanager] Authenticated role not found - skipping permission setup');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Content-API actions that should be enabled for authenticated users
|
|
448
|
+
const requiredActions = [
|
|
449
|
+
'plugin::magic-sessionmanager.session.logout',
|
|
450
|
+
'plugin::magic-sessionmanager.session.logoutAll',
|
|
451
|
+
'plugin::magic-sessionmanager.session.getOwnSessions',
|
|
452
|
+
'plugin::magic-sessionmanager.session.getUserSessions',
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
// Get existing permissions for this role
|
|
456
|
+
const existingPermissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
|
457
|
+
where: {
|
|
458
|
+
role: authenticatedRole.id,
|
|
459
|
+
action: { $in: requiredActions },
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Find which actions are missing
|
|
464
|
+
const existingActions = existingPermissions.map(p => p.action);
|
|
465
|
+
const missingActions = requiredActions.filter(action => !existingActions.includes(action));
|
|
466
|
+
|
|
467
|
+
if (missingActions.length === 0) {
|
|
468
|
+
strapi.log.debug('[magic-sessionmanager] Content-API permissions already configured');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Create missing permissions
|
|
473
|
+
for (const action of missingActions) {
|
|
474
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
475
|
+
data: {
|
|
476
|
+
action,
|
|
477
|
+
role: authenticatedRole.id,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
strapi.log.info(`[magic-sessionmanager] [PERMISSION] Enabled ${action} for authenticated users`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Content-API permissions configured for authenticated users');
|
|
484
|
+
} catch (err) {
|
|
485
|
+
strapi.log.warn('[magic-sessionmanager] Could not auto-configure permissions:', err.message);
|
|
486
|
+
strapi.log.warn('[magic-sessionmanager] Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -58,6 +58,38 @@ module.exports = {
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Get own sessions (authenticated user)
|
|
63
|
+
* GET /api/magic-sessionmanager/my-sessions
|
|
64
|
+
* Automatically uses the authenticated user's documentId
|
|
65
|
+
*/
|
|
66
|
+
async getOwnSessions(ctx) {
|
|
67
|
+
try {
|
|
68
|
+
// Strapi v5: Use documentId from authenticated user
|
|
69
|
+
const userId = ctx.state.user?.documentId;
|
|
70
|
+
|
|
71
|
+
if (!userId) {
|
|
72
|
+
return ctx.throw(401, 'Unauthorized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sessionService = strapi
|
|
76
|
+
.plugin('magic-sessionmanager')
|
|
77
|
+
.service('session');
|
|
78
|
+
|
|
79
|
+
const sessions = await sessionService.getUserSessions(userId);
|
|
80
|
+
|
|
81
|
+
ctx.body = {
|
|
82
|
+
data: sessions,
|
|
83
|
+
meta: {
|
|
84
|
+
count: sessions.length,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
strapi.log.error('[magic-sessionmanager] Error fetching own sessions:', err);
|
|
89
|
+
ctx.throw(500, 'Error fetching sessions');
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
61
93
|
/**
|
|
62
94
|
* Get user's sessions
|
|
63
95
|
* GET /magic-sessionmanager/user/:userId/sessions (Admin API)
|
|
@@ -70,12 +102,13 @@ module.exports = {
|
|
|
70
102
|
|
|
71
103
|
// Check if this is an admin request
|
|
72
104
|
const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
|
|
73
|
-
|
|
105
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
106
|
+
const requestingUserDocId = ctx.state.user?.documentId;
|
|
74
107
|
|
|
75
108
|
// SECURITY CHECK: Content API users can only see their own sessions
|
|
76
109
|
// Admins can see any user's sessions
|
|
77
|
-
if (!isAdminRequest &&
|
|
78
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
110
|
+
if (!isAdminRequest && requestingUserDocId && String(requestingUserDocId) !== String(userId)) {
|
|
111
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
|
|
79
112
|
return ctx.forbidden('You can only access your own sessions');
|
|
80
113
|
}
|
|
81
114
|
|
|
@@ -102,7 +135,8 @@ module.exports = {
|
|
|
102
135
|
*/
|
|
103
136
|
async logout(ctx) {
|
|
104
137
|
try {
|
|
105
|
-
|
|
138
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
139
|
+
const userId = ctx.state.user?.documentId;
|
|
106
140
|
const token = ctx.request.headers.authorization?.replace('Bearer ', '');
|
|
107
141
|
|
|
108
142
|
if (!userId) {
|
|
@@ -153,7 +187,8 @@ module.exports = {
|
|
|
153
187
|
*/
|
|
154
188
|
async logoutAll(ctx) {
|
|
155
189
|
try {
|
|
156
|
-
|
|
190
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
191
|
+
const userId = ctx.state.user?.documentId;
|
|
157
192
|
|
|
158
193
|
if (!userId) {
|
|
159
194
|
return ctx.throw(401, 'Unauthorized');
|
|
@@ -340,13 +375,28 @@ module.exports = {
|
|
|
340
375
|
/**
|
|
341
376
|
* Toggle user blocked status
|
|
342
377
|
* POST /magic-sessionmanager/user/:userId/toggle-block
|
|
378
|
+
* Supports both numeric id (from Content Manager) and documentId
|
|
343
379
|
*/
|
|
344
380
|
async toggleUserBlock(ctx) {
|
|
345
381
|
try {
|
|
346
382
|
const { userId } = ctx.params;
|
|
347
383
|
|
|
348
|
-
//
|
|
349
|
-
|
|
384
|
+
// Strapi v5: userId from params could be numeric id or documentId
|
|
385
|
+
// If numeric, look up the documentId first using entityService (fallback)
|
|
386
|
+
let userDocumentId = userId;
|
|
387
|
+
let user = null;
|
|
388
|
+
|
|
389
|
+
// Try to find by documentId first (preferred)
|
|
390
|
+
user = await strapi.documents(USER_UID).findOne({ documentId: userId });
|
|
391
|
+
|
|
392
|
+
// If not found, try numeric id lookup via entityService (fallback for Content Manager)
|
|
393
|
+
if (!user && !isNaN(userId)) {
|
|
394
|
+
const numericUser = await strapi.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
395
|
+
if (numericUser) {
|
|
396
|
+
userDocumentId = numericUser.documentId;
|
|
397
|
+
user = numericUser;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
350
400
|
|
|
351
401
|
if (!user) {
|
|
352
402
|
return ctx.throw(404, 'User not found');
|
|
@@ -356,7 +406,7 @@ module.exports = {
|
|
|
356
406
|
const newBlockedStatus = !user.blocked;
|
|
357
407
|
|
|
358
408
|
await strapi.documents(USER_UID).update({
|
|
359
|
-
documentId:
|
|
409
|
+
documentId: userDocumentId,
|
|
360
410
|
data: {
|
|
361
411
|
blocked: newBlockedStatus,
|
|
362
412
|
},
|
|
@@ -367,7 +417,7 @@ module.exports = {
|
|
|
367
417
|
const sessionService = strapi
|
|
368
418
|
.plugin('magic-sessionmanager')
|
|
369
419
|
.service('session');
|
|
370
|
-
await sessionService.terminateSession({ userId });
|
|
420
|
+
await sessionService.terminateSession({ userId: userDocumentId });
|
|
371
421
|
}
|
|
372
422
|
|
|
373
423
|
ctx.body = {
|
|
@@ -13,9 +13,10 @@ const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
|
13
13
|
module.exports = ({ strapi, sessionService }) => {
|
|
14
14
|
return async (ctx, next) => {
|
|
15
15
|
// BEFORE processing request: Check if user's sessions are active
|
|
16
|
-
|
|
16
|
+
// Strapi v5: Use documentId instead of numeric id for Document Service API
|
|
17
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
17
18
|
try {
|
|
18
|
-
const userId = ctx.state.user.
|
|
19
|
+
const userId = ctx.state.user.documentId;
|
|
19
20
|
|
|
20
21
|
// Check if user has ANY active sessions
|
|
21
22
|
const activeSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
@@ -41,9 +42,9 @@ module.exports = ({ strapi, sessionService }) => {
|
|
|
41
42
|
await next();
|
|
42
43
|
|
|
43
44
|
// AFTER response: Update activity timestamps if user is authenticated
|
|
44
|
-
if (ctx.state.user && ctx.state.user.
|
|
45
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
45
46
|
try {
|
|
46
|
-
const userId = ctx.state.user.
|
|
47
|
+
const userId = ctx.state.user.documentId;
|
|
47
48
|
|
|
48
49
|
// Try to find or extract sessionId from context
|
|
49
50
|
const sessionId = ctx.state.sessionId;
|
|
@@ -38,14 +38,23 @@ module.exports = {
|
|
|
38
38
|
// SESSION QUERIES
|
|
39
39
|
// ============================================================
|
|
40
40
|
|
|
41
|
+
{
|
|
42
|
+
method: 'GET',
|
|
43
|
+
path: '/my-sessions',
|
|
44
|
+
handler: 'session.getOwnSessions',
|
|
45
|
+
config: {
|
|
46
|
+
auth: { strategies: ['users-permissions'] },
|
|
47
|
+
description: 'Get own sessions (automatically uses authenticated user)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
41
50
|
{
|
|
42
51
|
method: 'GET',
|
|
43
52
|
path: '/user/:userId/sessions',
|
|
44
53
|
handler: 'session.getUserSessions',
|
|
45
54
|
config: {
|
|
46
55
|
auth: { strategies: ['users-permissions'] },
|
|
47
|
-
description: 'Get
|
|
56
|
+
description: 'Get sessions by userId (validates user can only see own sessions)',
|
|
57
|
+
},
|
|
48
58
|
},
|
|
49
|
-
},
|
|
50
59
|
],
|
|
51
60
|
};
|