strapi-plugin-magic-sessionmanager 4.2.6 → 4.2.8
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/dist/server/index.js +243 -28
- package/dist/server/index.mjs +243 -28
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -179,7 +179,7 @@ function encryptToken$2(token) {
|
|
|
179
179
|
throw new Error("Failed to encrypt token");
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
function decryptToken$
|
|
182
|
+
function decryptToken$4(encryptedToken) {
|
|
183
183
|
if (!encryptedToken) return null;
|
|
184
184
|
try {
|
|
185
185
|
const key = getEncryptionKey();
|
|
@@ -208,39 +208,96 @@ function generateSessionId$1(userId) {
|
|
|
208
208
|
}
|
|
209
209
|
var encryption = {
|
|
210
210
|
encryptToken: encryptToken$2,
|
|
211
|
-
decryptToken: decryptToken$
|
|
211
|
+
decryptToken: decryptToken$4,
|
|
212
212
|
generateSessionId: generateSessionId$1
|
|
213
213
|
};
|
|
214
214
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
215
|
+
const { decryptToken: decryptToken$3 } = encryption;
|
|
216
|
+
const lastTouchCache = /* @__PURE__ */ new Map();
|
|
215
217
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
216
218
|
return async (ctx, next) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
220
|
+
if (!currentToken) {
|
|
221
|
+
await next();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const skipPaths = ["/admin", "/_health", "/favicon.ico"];
|
|
225
|
+
if (skipPaths.some((p) => ctx.path.startsWith(p))) {
|
|
226
|
+
await next();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let matchingSession = null;
|
|
230
|
+
let userId = null;
|
|
231
|
+
try {
|
|
232
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
233
|
+
userId = ctx.state.user.documentId;
|
|
220
234
|
const activeSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
221
235
|
filters: {
|
|
222
236
|
user: { documentId: userId },
|
|
223
237
|
isActive: true
|
|
224
|
-
}
|
|
225
|
-
limit: 1
|
|
238
|
+
}
|
|
226
239
|
});
|
|
227
240
|
if (!activeSessions || activeSessions.length === 0) {
|
|
228
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED]
|
|
241
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] User ${userId} has no active sessions`);
|
|
229
242
|
return ctx.unauthorized("All sessions have been terminated. Please login again.");
|
|
230
243
|
}
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
for (const session2 of activeSessions) {
|
|
245
|
+
if (!session2.token) continue;
|
|
246
|
+
try {
|
|
247
|
+
const decrypted = decryptToken$3(session2.token);
|
|
248
|
+
if (decrypted === currentToken) {
|
|
249
|
+
matchingSession = session2;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!matchingSession) {
|
|
256
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session for user ${userId} has been terminated`);
|
|
257
|
+
return ctx.unauthorized("This session has been terminated. Please login again.");
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const allActiveSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
261
|
+
filters: { isActive: true },
|
|
262
|
+
populate: { user: { fields: ["documentId"] } },
|
|
263
|
+
limit: 500
|
|
264
|
+
// Reasonable limit for performance
|
|
265
|
+
});
|
|
266
|
+
for (const session2 of allActiveSessions) {
|
|
267
|
+
if (!session2.token) continue;
|
|
268
|
+
try {
|
|
269
|
+
const decrypted = decryptToken$3(session2.token);
|
|
270
|
+
if (decrypted === currentToken) {
|
|
271
|
+
matchingSession = session2;
|
|
272
|
+
userId = session2.user?.documentId;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (matchingSession) {
|
|
280
|
+
ctx.state.sessionId = matchingSession.documentId;
|
|
281
|
+
ctx.state.currentSession = matchingSession;
|
|
233
282
|
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
strapi2.log.debug("[magic-sessionmanager] Error checking session:", err.message);
|
|
234
285
|
}
|
|
235
286
|
await next();
|
|
236
|
-
if (
|
|
287
|
+
if (matchingSession) {
|
|
237
288
|
try {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
289
|
+
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
290
|
+
const rateLimit = config2.lastSeenRateLimit || 3e4;
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const lastTouch = lastTouchCache.get(matchingSession.documentId) || 0;
|
|
293
|
+
if (now - lastTouch > rateLimit) {
|
|
294
|
+
lastTouchCache.set(matchingSession.documentId, now);
|
|
295
|
+
await strapi2.documents(SESSION_UID$3).update({
|
|
296
|
+
documentId: matchingSession.documentId,
|
|
297
|
+
data: { lastActive: /* @__PURE__ */ new Date() }
|
|
298
|
+
});
|
|
299
|
+
strapi2.log.debug(`[magic-sessionmanager] [TOUCH] Session ${matchingSession.documentId} activity updated`);
|
|
300
|
+
}
|
|
244
301
|
} catch (err) {
|
|
245
302
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
246
303
|
}
|
|
@@ -562,9 +619,13 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
562
619
|
};
|
|
563
620
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
564
621
|
try {
|
|
565
|
-
const
|
|
566
|
-
|
|
622
|
+
const ROLE_UID = "plugin::users-permissions.role";
|
|
623
|
+
const PERMISSION_UID = "plugin::users-permissions.permission";
|
|
624
|
+
const roles = await strapi2.entityService.findMany(ROLE_UID, {
|
|
625
|
+
filters: { type: "authenticated" },
|
|
626
|
+
limit: 1
|
|
567
627
|
});
|
|
628
|
+
const authenticatedRole = roles?.[0];
|
|
568
629
|
if (!authenticatedRole) {
|
|
569
630
|
log.warn("Authenticated role not found - skipping permission setup");
|
|
570
631
|
return;
|
|
@@ -573,10 +634,12 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
573
634
|
"plugin::magic-sessionmanager.session.logout",
|
|
574
635
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
575
636
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
576
|
-
"plugin::magic-sessionmanager.session.getUserSessions"
|
|
637
|
+
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
638
|
+
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
639
|
+
"plugin::magic-sessionmanager.session.terminateOwnSession"
|
|
577
640
|
];
|
|
578
|
-
const existingPermissions = await strapi2.
|
|
579
|
-
|
|
641
|
+
const existingPermissions = await strapi2.entityService.findMany(PERMISSION_UID, {
|
|
642
|
+
filters: {
|
|
580
643
|
role: authenticatedRole.id,
|
|
581
644
|
action: { $in: requiredActions }
|
|
582
645
|
}
|
|
@@ -588,7 +651,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
588
651
|
return;
|
|
589
652
|
}
|
|
590
653
|
for (const action of missingActions) {
|
|
591
|
-
await strapi2.
|
|
654
|
+
await strapi2.entityService.create(PERMISSION_UID, {
|
|
592
655
|
data: {
|
|
593
656
|
action,
|
|
594
657
|
role: authenticatedRole.id
|
|
@@ -767,6 +830,15 @@ var contentApi$1 = {
|
|
|
767
830
|
description: "Get own sessions (automatically uses authenticated user)"
|
|
768
831
|
}
|
|
769
832
|
},
|
|
833
|
+
{
|
|
834
|
+
method: "GET",
|
|
835
|
+
path: "/current-session",
|
|
836
|
+
handler: "session.getCurrentSession",
|
|
837
|
+
config: {
|
|
838
|
+
auth: { strategies: ["users-permissions"] },
|
|
839
|
+
description: "Get current session info based on JWT token"
|
|
840
|
+
}
|
|
841
|
+
},
|
|
770
842
|
{
|
|
771
843
|
method: "GET",
|
|
772
844
|
path: "/user/:userId/sessions",
|
|
@@ -775,6 +847,18 @@ var contentApi$1 = {
|
|
|
775
847
|
auth: { strategies: ["users-permissions"] },
|
|
776
848
|
description: "Get sessions by userId (validates user can only see own sessions)"
|
|
777
849
|
}
|
|
850
|
+
},
|
|
851
|
+
// ============================================================
|
|
852
|
+
// SESSION MANAGEMENT (for own sessions only)
|
|
853
|
+
// ============================================================
|
|
854
|
+
{
|
|
855
|
+
method: "DELETE",
|
|
856
|
+
path: "/my-sessions/:sessionId",
|
|
857
|
+
handler: "session.terminateOwnSession",
|
|
858
|
+
config: {
|
|
859
|
+
auth: { strategies: ["users-permissions"] },
|
|
860
|
+
description: "Terminate a specific own session (not current)"
|
|
861
|
+
}
|
|
778
862
|
}
|
|
779
863
|
]
|
|
780
864
|
};
|
|
@@ -977,19 +1061,52 @@ var session$3 = {
|
|
|
977
1061
|
* Get own sessions (authenticated user)
|
|
978
1062
|
* GET /api/magic-sessionmanager/my-sessions
|
|
979
1063
|
* Automatically uses the authenticated user's documentId
|
|
1064
|
+
* Marks which session is the current one (based on JWT token)
|
|
980
1065
|
*/
|
|
981
1066
|
async getOwnSessions(ctx) {
|
|
982
1067
|
try {
|
|
983
1068
|
const userId = ctx.state.user?.documentId;
|
|
1069
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
984
1070
|
if (!userId) {
|
|
985
1071
|
return ctx.throw(401, "Unauthorized");
|
|
986
1072
|
}
|
|
987
|
-
const
|
|
988
|
-
|
|
1073
|
+
const allSessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1074
|
+
filters: { user: { documentId: userId } },
|
|
1075
|
+
sort: { loginTime: "desc" }
|
|
1076
|
+
});
|
|
1077
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1078
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1079
|
+
const now = /* @__PURE__ */ new Date();
|
|
1080
|
+
const sessionsWithCurrent = allSessions.map((session2) => {
|
|
1081
|
+
const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
|
|
1082
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1083
|
+
const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
|
|
1084
|
+
let isCurrentSession = false;
|
|
1085
|
+
if (session2.token && currentToken) {
|
|
1086
|
+
try {
|
|
1087
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1088
|
+
isCurrentSession = decrypted === currentToken;
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const { token, refreshToken, ...sessionWithoutTokens } = session2;
|
|
1093
|
+
return {
|
|
1094
|
+
...sessionWithoutTokens,
|
|
1095
|
+
isCurrentSession,
|
|
1096
|
+
isTrulyActive,
|
|
1097
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1098
|
+
};
|
|
1099
|
+
});
|
|
1100
|
+
sessionsWithCurrent.sort((a, b) => {
|
|
1101
|
+
if (a.isCurrentSession) return -1;
|
|
1102
|
+
if (b.isCurrentSession) return 1;
|
|
1103
|
+
return new Date(b.loginTime) - new Date(a.loginTime);
|
|
1104
|
+
});
|
|
989
1105
|
ctx.body = {
|
|
990
|
-
data:
|
|
1106
|
+
data: sessionsWithCurrent,
|
|
991
1107
|
meta: {
|
|
992
|
-
count:
|
|
1108
|
+
count: sessionsWithCurrent.length,
|
|
1109
|
+
active: sessionsWithCurrent.filter((s) => s.isTrulyActive).length
|
|
993
1110
|
}
|
|
994
1111
|
};
|
|
995
1112
|
} catch (err) {
|
|
@@ -1084,6 +1201,104 @@ var session$3 = {
|
|
|
1084
1201
|
ctx.throw(500, "Error during logout");
|
|
1085
1202
|
}
|
|
1086
1203
|
},
|
|
1204
|
+
/**
|
|
1205
|
+
* Get current session info based on JWT token
|
|
1206
|
+
* GET /api/magic-sessionmanager/current-session
|
|
1207
|
+
* Returns the session associated with the current JWT token
|
|
1208
|
+
*/
|
|
1209
|
+
async getCurrentSession(ctx) {
|
|
1210
|
+
try {
|
|
1211
|
+
const userId = ctx.state.user?.documentId;
|
|
1212
|
+
const token = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1213
|
+
if (!userId || !token) {
|
|
1214
|
+
return ctx.throw(401, "Unauthorized");
|
|
1215
|
+
}
|
|
1216
|
+
const sessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1217
|
+
filters: {
|
|
1218
|
+
user: { documentId: userId },
|
|
1219
|
+
isActive: true
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
const currentSession = sessions.find((session2) => {
|
|
1223
|
+
if (!session2.token) return false;
|
|
1224
|
+
try {
|
|
1225
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1226
|
+
return decrypted === token;
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
if (!currentSession) {
|
|
1232
|
+
return ctx.notFound("Current session not found");
|
|
1233
|
+
}
|
|
1234
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1235
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1236
|
+
const now = /* @__PURE__ */ new Date();
|
|
1237
|
+
const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
|
|
1238
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1239
|
+
const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
|
|
1240
|
+
ctx.body = {
|
|
1241
|
+
data: {
|
|
1242
|
+
...sessionWithoutTokens,
|
|
1243
|
+
isCurrentSession: true,
|
|
1244
|
+
isTrulyActive: timeSinceActive < inactivityTimeout,
|
|
1245
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
strapi.log.error("[magic-sessionmanager] Error getting current session:", err);
|
|
1250
|
+
ctx.throw(500, "Error fetching current session");
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
/**
|
|
1254
|
+
* Terminate a specific own session (not the current one)
|
|
1255
|
+
* DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
1256
|
+
* SECURITY: User can only terminate their OWN sessions
|
|
1257
|
+
*/
|
|
1258
|
+
async terminateOwnSession(ctx) {
|
|
1259
|
+
try {
|
|
1260
|
+
const userId = ctx.state.user?.documentId;
|
|
1261
|
+
const { sessionId } = ctx.params;
|
|
1262
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1263
|
+
if (!userId) {
|
|
1264
|
+
return ctx.throw(401, "Unauthorized");
|
|
1265
|
+
}
|
|
1266
|
+
if (!sessionId) {
|
|
1267
|
+
return ctx.badRequest("Session ID is required");
|
|
1268
|
+
}
|
|
1269
|
+
const sessionToTerminate = await strapi.documents(SESSION_UID$1).findOne({
|
|
1270
|
+
documentId: sessionId,
|
|
1271
|
+
populate: { user: { fields: ["documentId"] } }
|
|
1272
|
+
});
|
|
1273
|
+
if (!sessionToTerminate) {
|
|
1274
|
+
return ctx.notFound("Session not found");
|
|
1275
|
+
}
|
|
1276
|
+
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
1277
|
+
if (sessionUserId !== userId) {
|
|
1278
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
1279
|
+
return ctx.forbidden("You can only terminate your own sessions");
|
|
1280
|
+
}
|
|
1281
|
+
if (sessionToTerminate.token && currentToken) {
|
|
1282
|
+
try {
|
|
1283
|
+
const decrypted = decryptToken$1(sessionToTerminate.token);
|
|
1284
|
+
if (decrypted === currentToken) {
|
|
1285
|
+
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
1286
|
+
}
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1291
|
+
await sessionService.terminateSession({ sessionId });
|
|
1292
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
1293
|
+
ctx.body = {
|
|
1294
|
+
message: `Session ${sessionId} terminated successfully`,
|
|
1295
|
+
success: true
|
|
1296
|
+
};
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
1299
|
+
ctx.throw(500, "Error terminating session");
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1087
1302
|
/**
|
|
1088
1303
|
* Terminate specific session
|
|
1089
1304
|
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
@@ -1862,7 +2077,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
1862
2077
|
}
|
|
1863
2078
|
};
|
|
1864
2079
|
};
|
|
1865
|
-
const version = "4.2.
|
|
2080
|
+
const version = "4.2.7";
|
|
1866
2081
|
const require$$2 = {
|
|
1867
2082
|
version
|
|
1868
2083
|
};
|
package/dist/server/index.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function encryptToken$2(token) {
|
|
|
175
175
|
throw new Error("Failed to encrypt token");
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
function decryptToken$
|
|
178
|
+
function decryptToken$4(encryptedToken) {
|
|
179
179
|
if (!encryptedToken) return null;
|
|
180
180
|
try {
|
|
181
181
|
const key = getEncryptionKey();
|
|
@@ -204,39 +204,96 @@ function generateSessionId$1(userId) {
|
|
|
204
204
|
}
|
|
205
205
|
var encryption = {
|
|
206
206
|
encryptToken: encryptToken$2,
|
|
207
|
-
decryptToken: decryptToken$
|
|
207
|
+
decryptToken: decryptToken$4,
|
|
208
208
|
generateSessionId: generateSessionId$1
|
|
209
209
|
};
|
|
210
210
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
211
|
+
const { decryptToken: decryptToken$3 } = encryption;
|
|
212
|
+
const lastTouchCache = /* @__PURE__ */ new Map();
|
|
211
213
|
var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
212
214
|
return async (ctx, next) => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
216
|
+
if (!currentToken) {
|
|
217
|
+
await next();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const skipPaths = ["/admin", "/_health", "/favicon.ico"];
|
|
221
|
+
if (skipPaths.some((p) => ctx.path.startsWith(p))) {
|
|
222
|
+
await next();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let matchingSession = null;
|
|
226
|
+
let userId = null;
|
|
227
|
+
try {
|
|
228
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
229
|
+
userId = ctx.state.user.documentId;
|
|
216
230
|
const activeSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
217
231
|
filters: {
|
|
218
232
|
user: { documentId: userId },
|
|
219
233
|
isActive: true
|
|
220
|
-
}
|
|
221
|
-
limit: 1
|
|
234
|
+
}
|
|
222
235
|
});
|
|
223
236
|
if (!activeSessions || activeSessions.length === 0) {
|
|
224
|
-
strapi2.log.info(`[magic-sessionmanager] [BLOCKED]
|
|
237
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] User ${userId} has no active sessions`);
|
|
225
238
|
return ctx.unauthorized("All sessions have been terminated. Please login again.");
|
|
226
239
|
}
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
for (const session2 of activeSessions) {
|
|
241
|
+
if (!session2.token) continue;
|
|
242
|
+
try {
|
|
243
|
+
const decrypted = decryptToken$3(session2.token);
|
|
244
|
+
if (decrypted === currentToken) {
|
|
245
|
+
matchingSession = session2;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!matchingSession) {
|
|
252
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session for user ${userId} has been terminated`);
|
|
253
|
+
return ctx.unauthorized("This session has been terminated. Please login again.");
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
const allActiveSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
257
|
+
filters: { isActive: true },
|
|
258
|
+
populate: { user: { fields: ["documentId"] } },
|
|
259
|
+
limit: 500
|
|
260
|
+
// Reasonable limit for performance
|
|
261
|
+
});
|
|
262
|
+
for (const session2 of allActiveSessions) {
|
|
263
|
+
if (!session2.token) continue;
|
|
264
|
+
try {
|
|
265
|
+
const decrypted = decryptToken$3(session2.token);
|
|
266
|
+
if (decrypted === currentToken) {
|
|
267
|
+
matchingSession = session2;
|
|
268
|
+
userId = session2.user?.documentId;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (matchingSession) {
|
|
276
|
+
ctx.state.sessionId = matchingSession.documentId;
|
|
277
|
+
ctx.state.currentSession = matchingSession;
|
|
229
278
|
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
strapi2.log.debug("[magic-sessionmanager] Error checking session:", err.message);
|
|
230
281
|
}
|
|
231
282
|
await next();
|
|
232
|
-
if (
|
|
283
|
+
if (matchingSession) {
|
|
233
284
|
try {
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
285
|
+
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
286
|
+
const rateLimit = config2.lastSeenRateLimit || 3e4;
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const lastTouch = lastTouchCache.get(matchingSession.documentId) || 0;
|
|
289
|
+
if (now - lastTouch > rateLimit) {
|
|
290
|
+
lastTouchCache.set(matchingSession.documentId, now);
|
|
291
|
+
await strapi2.documents(SESSION_UID$3).update({
|
|
292
|
+
documentId: matchingSession.documentId,
|
|
293
|
+
data: { lastActive: /* @__PURE__ */ new Date() }
|
|
294
|
+
});
|
|
295
|
+
strapi2.log.debug(`[magic-sessionmanager] [TOUCH] Session ${matchingSession.documentId} activity updated`);
|
|
296
|
+
}
|
|
240
297
|
} catch (err) {
|
|
241
298
|
strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
|
|
242
299
|
}
|
|
@@ -558,9 +615,13 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
558
615
|
};
|
|
559
616
|
async function ensureContentApiPermissions(strapi2, log) {
|
|
560
617
|
try {
|
|
561
|
-
const
|
|
562
|
-
|
|
618
|
+
const ROLE_UID = "plugin::users-permissions.role";
|
|
619
|
+
const PERMISSION_UID = "plugin::users-permissions.permission";
|
|
620
|
+
const roles = await strapi2.entityService.findMany(ROLE_UID, {
|
|
621
|
+
filters: { type: "authenticated" },
|
|
622
|
+
limit: 1
|
|
563
623
|
});
|
|
624
|
+
const authenticatedRole = roles?.[0];
|
|
564
625
|
if (!authenticatedRole) {
|
|
565
626
|
log.warn("Authenticated role not found - skipping permission setup");
|
|
566
627
|
return;
|
|
@@ -569,10 +630,12 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
569
630
|
"plugin::magic-sessionmanager.session.logout",
|
|
570
631
|
"plugin::magic-sessionmanager.session.logoutAll",
|
|
571
632
|
"plugin::magic-sessionmanager.session.getOwnSessions",
|
|
572
|
-
"plugin::magic-sessionmanager.session.getUserSessions"
|
|
633
|
+
"plugin::magic-sessionmanager.session.getUserSessions",
|
|
634
|
+
"plugin::magic-sessionmanager.session.getCurrentSession",
|
|
635
|
+
"plugin::magic-sessionmanager.session.terminateOwnSession"
|
|
573
636
|
];
|
|
574
|
-
const existingPermissions = await strapi2.
|
|
575
|
-
|
|
637
|
+
const existingPermissions = await strapi2.entityService.findMany(PERMISSION_UID, {
|
|
638
|
+
filters: {
|
|
576
639
|
role: authenticatedRole.id,
|
|
577
640
|
action: { $in: requiredActions }
|
|
578
641
|
}
|
|
@@ -584,7 +647,7 @@ async function ensureContentApiPermissions(strapi2, log) {
|
|
|
584
647
|
return;
|
|
585
648
|
}
|
|
586
649
|
for (const action of missingActions) {
|
|
587
|
-
await strapi2.
|
|
650
|
+
await strapi2.entityService.create(PERMISSION_UID, {
|
|
588
651
|
data: {
|
|
589
652
|
action,
|
|
590
653
|
role: authenticatedRole.id
|
|
@@ -763,6 +826,15 @@ var contentApi$1 = {
|
|
|
763
826
|
description: "Get own sessions (automatically uses authenticated user)"
|
|
764
827
|
}
|
|
765
828
|
},
|
|
829
|
+
{
|
|
830
|
+
method: "GET",
|
|
831
|
+
path: "/current-session",
|
|
832
|
+
handler: "session.getCurrentSession",
|
|
833
|
+
config: {
|
|
834
|
+
auth: { strategies: ["users-permissions"] },
|
|
835
|
+
description: "Get current session info based on JWT token"
|
|
836
|
+
}
|
|
837
|
+
},
|
|
766
838
|
{
|
|
767
839
|
method: "GET",
|
|
768
840
|
path: "/user/:userId/sessions",
|
|
@@ -771,6 +843,18 @@ var contentApi$1 = {
|
|
|
771
843
|
auth: { strategies: ["users-permissions"] },
|
|
772
844
|
description: "Get sessions by userId (validates user can only see own sessions)"
|
|
773
845
|
}
|
|
846
|
+
},
|
|
847
|
+
// ============================================================
|
|
848
|
+
// SESSION MANAGEMENT (for own sessions only)
|
|
849
|
+
// ============================================================
|
|
850
|
+
{
|
|
851
|
+
method: "DELETE",
|
|
852
|
+
path: "/my-sessions/:sessionId",
|
|
853
|
+
handler: "session.terminateOwnSession",
|
|
854
|
+
config: {
|
|
855
|
+
auth: { strategies: ["users-permissions"] },
|
|
856
|
+
description: "Terminate a specific own session (not current)"
|
|
857
|
+
}
|
|
774
858
|
}
|
|
775
859
|
]
|
|
776
860
|
};
|
|
@@ -973,19 +1057,52 @@ var session$3 = {
|
|
|
973
1057
|
* Get own sessions (authenticated user)
|
|
974
1058
|
* GET /api/magic-sessionmanager/my-sessions
|
|
975
1059
|
* Automatically uses the authenticated user's documentId
|
|
1060
|
+
* Marks which session is the current one (based on JWT token)
|
|
976
1061
|
*/
|
|
977
1062
|
async getOwnSessions(ctx) {
|
|
978
1063
|
try {
|
|
979
1064
|
const userId = ctx.state.user?.documentId;
|
|
1065
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
980
1066
|
if (!userId) {
|
|
981
1067
|
return ctx.throw(401, "Unauthorized");
|
|
982
1068
|
}
|
|
983
|
-
const
|
|
984
|
-
|
|
1069
|
+
const allSessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1070
|
+
filters: { user: { documentId: userId } },
|
|
1071
|
+
sort: { loginTime: "desc" }
|
|
1072
|
+
});
|
|
1073
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1074
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1075
|
+
const now = /* @__PURE__ */ new Date();
|
|
1076
|
+
const sessionsWithCurrent = allSessions.map((session2) => {
|
|
1077
|
+
const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
|
|
1078
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1079
|
+
const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
|
|
1080
|
+
let isCurrentSession = false;
|
|
1081
|
+
if (session2.token && currentToken) {
|
|
1082
|
+
try {
|
|
1083
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1084
|
+
isCurrentSession = decrypted === currentToken;
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const { token, refreshToken, ...sessionWithoutTokens } = session2;
|
|
1089
|
+
return {
|
|
1090
|
+
...sessionWithoutTokens,
|
|
1091
|
+
isCurrentSession,
|
|
1092
|
+
isTrulyActive,
|
|
1093
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
sessionsWithCurrent.sort((a, b) => {
|
|
1097
|
+
if (a.isCurrentSession) return -1;
|
|
1098
|
+
if (b.isCurrentSession) return 1;
|
|
1099
|
+
return new Date(b.loginTime) - new Date(a.loginTime);
|
|
1100
|
+
});
|
|
985
1101
|
ctx.body = {
|
|
986
|
-
data:
|
|
1102
|
+
data: sessionsWithCurrent,
|
|
987
1103
|
meta: {
|
|
988
|
-
count:
|
|
1104
|
+
count: sessionsWithCurrent.length,
|
|
1105
|
+
active: sessionsWithCurrent.filter((s) => s.isTrulyActive).length
|
|
989
1106
|
}
|
|
990
1107
|
};
|
|
991
1108
|
} catch (err) {
|
|
@@ -1080,6 +1197,104 @@ var session$3 = {
|
|
|
1080
1197
|
ctx.throw(500, "Error during logout");
|
|
1081
1198
|
}
|
|
1082
1199
|
},
|
|
1200
|
+
/**
|
|
1201
|
+
* Get current session info based on JWT token
|
|
1202
|
+
* GET /api/magic-sessionmanager/current-session
|
|
1203
|
+
* Returns the session associated with the current JWT token
|
|
1204
|
+
*/
|
|
1205
|
+
async getCurrentSession(ctx) {
|
|
1206
|
+
try {
|
|
1207
|
+
const userId = ctx.state.user?.documentId;
|
|
1208
|
+
const token = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1209
|
+
if (!userId || !token) {
|
|
1210
|
+
return ctx.throw(401, "Unauthorized");
|
|
1211
|
+
}
|
|
1212
|
+
const sessions = await strapi.documents(SESSION_UID$1).findMany({
|
|
1213
|
+
filters: {
|
|
1214
|
+
user: { documentId: userId },
|
|
1215
|
+
isActive: true
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
const currentSession = sessions.find((session2) => {
|
|
1219
|
+
if (!session2.token) return false;
|
|
1220
|
+
try {
|
|
1221
|
+
const decrypted = decryptToken$1(session2.token);
|
|
1222
|
+
return decrypted === token;
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
if (!currentSession) {
|
|
1228
|
+
return ctx.notFound("Current session not found");
|
|
1229
|
+
}
|
|
1230
|
+
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1231
|
+
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
1232
|
+
const now = /* @__PURE__ */ new Date();
|
|
1233
|
+
const lastActiveTime = currentSession.lastActive ? new Date(currentSession.lastActive) : new Date(currentSession.loginTime);
|
|
1234
|
+
const timeSinceActive = now - lastActiveTime;
|
|
1235
|
+
const { token: _, refreshToken: __, ...sessionWithoutTokens } = currentSession;
|
|
1236
|
+
ctx.body = {
|
|
1237
|
+
data: {
|
|
1238
|
+
...sessionWithoutTokens,
|
|
1239
|
+
isCurrentSession: true,
|
|
1240
|
+
isTrulyActive: timeSinceActive < inactivityTimeout,
|
|
1241
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
strapi.log.error("[magic-sessionmanager] Error getting current session:", err);
|
|
1246
|
+
ctx.throw(500, "Error fetching current session");
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
/**
|
|
1250
|
+
* Terminate a specific own session (not the current one)
|
|
1251
|
+
* DELETE /api/magic-sessionmanager/my-sessions/:sessionId
|
|
1252
|
+
* SECURITY: User can only terminate their OWN sessions
|
|
1253
|
+
*/
|
|
1254
|
+
async terminateOwnSession(ctx) {
|
|
1255
|
+
try {
|
|
1256
|
+
const userId = ctx.state.user?.documentId;
|
|
1257
|
+
const { sessionId } = ctx.params;
|
|
1258
|
+
const currentToken = ctx.request.headers.authorization?.replace("Bearer ", "");
|
|
1259
|
+
if (!userId) {
|
|
1260
|
+
return ctx.throw(401, "Unauthorized");
|
|
1261
|
+
}
|
|
1262
|
+
if (!sessionId) {
|
|
1263
|
+
return ctx.badRequest("Session ID is required");
|
|
1264
|
+
}
|
|
1265
|
+
const sessionToTerminate = await strapi.documents(SESSION_UID$1).findOne({
|
|
1266
|
+
documentId: sessionId,
|
|
1267
|
+
populate: { user: { fields: ["documentId"] } }
|
|
1268
|
+
});
|
|
1269
|
+
if (!sessionToTerminate) {
|
|
1270
|
+
return ctx.notFound("Session not found");
|
|
1271
|
+
}
|
|
1272
|
+
const sessionUserId = sessionToTerminate.user?.documentId;
|
|
1273
|
+
if (sessionUserId !== userId) {
|
|
1274
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${userId} tried to terminate session ${sessionId} of user ${sessionUserId}`);
|
|
1275
|
+
return ctx.forbidden("You can only terminate your own sessions");
|
|
1276
|
+
}
|
|
1277
|
+
if (sessionToTerminate.token && currentToken) {
|
|
1278
|
+
try {
|
|
1279
|
+
const decrypted = decryptToken$1(sessionToTerminate.token);
|
|
1280
|
+
if (decrypted === currentToken) {
|
|
1281
|
+
return ctx.badRequest("Cannot terminate current session. Use /logout instead.");
|
|
1282
|
+
}
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1287
|
+
await sessionService.terminateSession({ sessionId });
|
|
1288
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} terminated own session ${sessionId}`);
|
|
1289
|
+
ctx.body = {
|
|
1290
|
+
message: `Session ${sessionId} terminated successfully`,
|
|
1291
|
+
success: true
|
|
1292
|
+
};
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
strapi.log.error("[magic-sessionmanager] Error terminating own session:", err);
|
|
1295
|
+
ctx.throw(500, "Error terminating session");
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1083
1298
|
/**
|
|
1084
1299
|
* Terminate specific session
|
|
1085
1300
|
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
@@ -1858,7 +2073,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
1858
2073
|
}
|
|
1859
2074
|
};
|
|
1860
2075
|
};
|
|
1861
|
-
const version = "4.2.
|
|
2076
|
+
const version = "4.2.7";
|
|
1862
2077
|
const require$$2 = {
|
|
1863
2078
|
version
|
|
1864
2079
|
};
|
package/package.json
CHANGED