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.
@@ -179,7 +179,7 @@ function encryptToken$2(token) {
179
179
  throw new Error("Failed to encrypt token");
180
180
  }
181
181
  }
182
- function decryptToken$3(encryptedToken) {
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$3,
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
- if (ctx.state.user && ctx.state.user.documentId) {
218
- try {
219
- const userId = ctx.state.user.documentId;
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] Blocked request - User ${userId} has no active sessions`);
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
- } catch (err) {
232
- strapi2.log.debug("[magic-sessionmanager] Error checking active sessions:", err.message);
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 (ctx.state.user && ctx.state.user.documentId) {
287
+ if (matchingSession) {
237
288
  try {
238
- const userId = ctx.state.user.documentId;
239
- const sessionId = ctx.state.sessionId;
240
- await sessionService.touch({
241
- userId,
242
- sessionId
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 authenticatedRole = await strapi2.query("plugin::users-permissions.role").findOne({
566
- where: { type: "authenticated" }
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.query("plugin::users-permissions.permission").findMany({
579
- where: {
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.query("plugin::users-permissions.permission").create({
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 sessionService = strapi.plugin("magic-sessionmanager").service("session");
988
- const sessions = await sessionService.getUserSessions(userId);
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: sessions,
1106
+ data: sessionsWithCurrent,
991
1107
  meta: {
992
- count: sessions.length
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.5";
2080
+ const version = "4.2.7";
1866
2081
  const require$$2 = {
1867
2082
  version
1868
2083
  };
@@ -175,7 +175,7 @@ function encryptToken$2(token) {
175
175
  throw new Error("Failed to encrypt token");
176
176
  }
177
177
  }
178
- function decryptToken$3(encryptedToken) {
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$3,
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
- if (ctx.state.user && ctx.state.user.documentId) {
214
- try {
215
- const userId = ctx.state.user.documentId;
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] Blocked request - User ${userId} has no active sessions`);
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
- } catch (err) {
228
- strapi2.log.debug("[magic-sessionmanager] Error checking active sessions:", err.message);
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 (ctx.state.user && ctx.state.user.documentId) {
283
+ if (matchingSession) {
233
284
  try {
234
- const userId = ctx.state.user.documentId;
235
- const sessionId = ctx.state.sessionId;
236
- await sessionService.touch({
237
- userId,
238
- sessionId
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 authenticatedRole = await strapi2.query("plugin::users-permissions.role").findOne({
562
- where: { type: "authenticated" }
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.query("plugin::users-permissions.permission").findMany({
575
- where: {
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.query("plugin::users-permissions.permission").create({
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 sessionService = strapi.plugin("magic-sessionmanager").service("session");
984
- const sessions = await sessionService.getUserSessions(userId);
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: sessions,
1102
+ data: sessionsWithCurrent,
987
1103
  meta: {
988
- count: sessions.length
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.5";
2076
+ const version = "4.2.7";
1862
2077
  const require$$2 = {
1863
2078
  version
1864
2079
  };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.2.6",
2
+ "version": "4.2.8",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",