strapi-plugin-magic-sessionmanager 3.0.1 → 3.1.0

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.
@@ -30,7 +30,7 @@ const SessionInfoCard = ({ id, model }) => {
30
30
  }
31
31
 
32
32
  try {
33
- const { data } = await get(`/magic-sessionmanager/admin/user/${id}/sessions`);
33
+ const { data } = await get(`/magic-sessionmanager/user/${id}/sessions`);
34
34
  setSessions(data.data || []);
35
35
  } catch (err) {
36
36
  console.error('[SessionInfoCard] Error fetching sessions:', err);
@@ -26,7 +26,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
26
26
 
27
27
  const fetchData = async () => {
28
28
  try {
29
- const { data } = await get(`/magic-sessionmanager/admin/user/${userId}/sessions`);
29
+ const { data } = await get(`/magic-sessionmanager/user/${userId}/sessions`);
30
30
  // Filter by truly active (not just isActive, but also within timeout)
31
31
  const activeSessions = (data.data || []).filter(s => s.isTrulyActive);
32
32
  setSessions(activeSessions);
@@ -4,8 +4,8 @@ import { useFetchClient } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Loader, Typography, Box, Flex, Badge } from "@strapi/design-system";
6
6
  import { ChartBubble, Crown, User, Clock, Monitor } from "@strapi/icons";
7
- import { a as pluginId } from "./index-BLEZRtuD.mjs";
8
- import { u as useLicense } from "./useLicense-D92JSFYj.mjs";
7
+ import { a as pluginId } from "./index--JzOiQNw.mjs";
8
+ import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
9
9
  const theme = {
10
10
  colors: {
11
11
  primary: { 100: "#E0F2FE", 500: "#0EA5E9", 600: "#0284C7" },
@@ -6,8 +6,8 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-CD2Jls2n.js");
10
- const useLicense = require("./useLicense-C3OdhHzJ.js");
9
+ const index = require("./index-DqtQaEBL.js");
10
+ const useLicense = require("./useLicense-DA-averf.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const theme = {
@@ -4,8 +4,8 @@ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Modal, Flex, Box, Typography, Badge, Divider, Button, Loader, SingleSelect, SingleSelectOption, Thead, Tr, Th, Tbody, Td, Table, TextInput } from "@strapi/design-system";
6
6
  import { Check, Information, Monitor, Server, Clock, Cross, Earth, Shield, Crown, Phone, Download, User, Eye, Trash, Search, Key } from "@strapi/icons";
7
- import { p as parseUserAgent, a as pluginId } from "./index-BLEZRtuD.mjs";
8
- import { u as useLicense } from "./useLicense-D92JSFYj.mjs";
7
+ import { p as parseUserAgent, a as pluginId } from "./index--JzOiQNw.mjs";
8
+ import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
9
9
  import { useNavigate } from "react-router-dom";
10
10
  const TwoColumnGrid = styled.div`
11
11
  display: grid;
@@ -6,8 +6,8 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
- const index = require("./index-CD2Jls2n.js");
10
- const useLicense = require("./useLicense-C3OdhHzJ.js");
9
+ const index = require("./index-DqtQaEBL.js");
10
+ const useLicense = require("./useLicense-DA-averf.js");
11
11
  const reactRouterDom = require("react-router-dom");
12
12
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
13
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
@@ -6,7 +6,7 @@ const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
7
  const icons = require("@strapi/icons");
8
8
  const styled = require("styled-components");
9
- const index = require("./index-CD2Jls2n.js");
9
+ const index = require("./index-DqtQaEBL.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const theme = {
@@ -4,7 +4,7 @@ import { Loader, Box, Alert, Flex, Typography, Button, Badge, Accordion } from "
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
5
  import { ArrowClockwise, Duplicate, Download, User, Shield, Sparkle, ChartBubble } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index-BLEZRtuD.mjs";
7
+ import { a as pluginId } from "./index--JzOiQNw.mjs";
8
8
  const theme = {
9
9
  colors: {
10
10
  neutral: { 200: "#E5E7EB" }
@@ -4,8 +4,8 @@ import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSe
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
5
  import { Check, Information, Cog, Trash, Shield, Mail, Code } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index-BLEZRtuD.mjs";
8
- import { u as useLicense } from "./useLicense-D92JSFYj.mjs";
7
+ import { a as pluginId } from "./index--JzOiQNw.mjs";
8
+ import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
9
9
  const theme = {
10
10
  colors: {
11
11
  primary: { 600: "#0284C7", 700: "#075985" },
@@ -6,8 +6,8 @@ const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
7
  const icons = require("@strapi/icons");
8
8
  const styled = require("styled-components");
9
- const index = require("./index-CD2Jls2n.js");
10
- const useLicense = require("./useLicense-C3OdhHzJ.js");
9
+ const index = require("./index-DqtQaEBL.js");
10
+ const useLicense = require("./useLicense-DA-averf.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const theme = {
@@ -95,7 +95,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
95
95
  }
96
96
  const fetchData = async () => {
97
97
  try {
98
- const { data } = await get(`/magic-sessionmanager/admin/user/${userId}/sessions`);
98
+ const { data } = await get(`/magic-sessionmanager/user/${userId}/sessions`);
99
99
  const activeSessions = (data.data || []).filter((s) => s.isTrulyActive);
100
100
  setSessions(activeSessions);
101
101
  setIsBlocked(document?.blocked || false);
@@ -403,7 +403,7 @@ const index = {
403
403
  id: `${pluginId}.plugin.name`,
404
404
  defaultMessage: pluginPkg.strapi.displayName
405
405
  },
406
- Component: () => import("./App-Dy-mAUPv.mjs")
406
+ Component: () => import("./App-DONYhluL.mjs")
407
407
  });
408
408
  app.createSettingSection(
409
409
  {
@@ -419,7 +419,7 @@ const index = {
419
419
  },
420
420
  id: "general",
421
421
  to: `/settings/${pluginId}/general`,
422
- Component: () => import("./Settings-oXa82_Fo.mjs")
422
+ Component: () => import("./Settings-JaiqQ_7r.mjs")
423
423
  },
424
424
  {
425
425
  intlLabel: {
@@ -428,7 +428,7 @@ const index = {
428
428
  },
429
429
  id: "analytics",
430
430
  to: `/settings/${pluginId}/analytics`,
431
- Component: () => import("./Analytics-ByC7HfOE.mjs")
431
+ Component: () => import("./Analytics-Bp1cPJx1.mjs")
432
432
  },
433
433
  {
434
434
  intlLabel: {
@@ -437,7 +437,7 @@ const index = {
437
437
  },
438
438
  id: "license",
439
439
  to: `/settings/${pluginId}/license`,
440
- Component: () => import("./License-DA8KK2GQ.mjs")
440
+ Component: () => import("./License-lcVK7rtT.mjs")
441
441
  }
442
442
  ]
443
443
  );
@@ -96,7 +96,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
96
96
  }
97
97
  const fetchData = async () => {
98
98
  try {
99
- const { data } = await get(`/magic-sessionmanager/admin/user/${userId}/sessions`);
99
+ const { data } = await get(`/magic-sessionmanager/user/${userId}/sessions`);
100
100
  const activeSessions = (data.data || []).filter((s) => s.isTrulyActive);
101
101
  setSessions(activeSessions);
102
102
  setIsBlocked(document?.blocked || false);
@@ -404,7 +404,7 @@ const index = {
404
404
  id: `${pluginId}.plugin.name`,
405
405
  defaultMessage: pluginPkg.strapi.displayName
406
406
  },
407
- Component: () => Promise.resolve().then(() => require("./App-CsWl6uAZ.js"))
407
+ Component: () => Promise.resolve().then(() => require("./App-DtfZMVae.js"))
408
408
  });
409
409
  app.createSettingSection(
410
410
  {
@@ -420,7 +420,7 @@ const index = {
420
420
  },
421
421
  id: "general",
422
422
  to: `/settings/${pluginId}/general`,
423
- Component: () => Promise.resolve().then(() => require("./Settings-BpB9rz0q.js"))
423
+ Component: () => Promise.resolve().then(() => require("./Settings-JixgQiB_.js"))
424
424
  },
425
425
  {
426
426
  intlLabel: {
@@ -429,7 +429,7 @@ const index = {
429
429
  },
430
430
  id: "analytics",
431
431
  to: `/settings/${pluginId}/analytics`,
432
- Component: () => Promise.resolve().then(() => require("./Analytics-CgaMF_rH.js"))
432
+ Component: () => Promise.resolve().then(() => require("./Analytics-CjqdGXnQ.js"))
433
433
  },
434
434
  {
435
435
  intlLabel: {
@@ -438,7 +438,7 @@ const index = {
438
438
  },
439
439
  id: "license",
440
440
  to: `/settings/${pluginId}/license`,
441
- Component: () => Promise.resolve().then(() => require("./License-V6Y3xHzi.js"))
441
+ Component: () => Promise.resolve().then(() => require("./License-IWH6ClOx.js"))
442
442
  }
443
443
  ]
444
444
  );
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const react = require("react");
3
3
  const admin = require("@strapi/strapi/admin");
4
- const index = require("./index-CD2Jls2n.js");
4
+ const index = require("./index-DqtQaEBL.js");
5
5
  const useLicense = () => {
6
6
  const { get } = admin.useFetchClient();
7
7
  const [isPremium, setIsPremium] = react.useState(false);
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import { useFetchClient } from "@strapi/strapi/admin";
3
- import { a as pluginId } from "./index-BLEZRtuD.mjs";
3
+ import { a as pluginId } from "./index--JzOiQNw.mjs";
4
4
  const useLicense = () => {
5
5
  const { get } = useFetchClient();
6
6
  const [isPremium, setIsPremium] = useState(false);
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-CD2Jls2n.js");
2
+ const index = require("../_chunks/index-DqtQaEBL.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-BLEZRtuD.mjs";
1
+ import { i } from "../_chunks/index--JzOiQNw.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -87,6 +87,68 @@ const isPrivateIp = (ip) => {
87
87
  return false;
88
88
  };
89
89
  var getClientIp_1 = getClientIp$1;
90
+ const crypto$1 = require$$0__default.default;
91
+ const ALGORITHM = "aes-256-gcm";
92
+ const IV_LENGTH = 16;
93
+ function getEncryptionKey() {
94
+ const envKey = process.env.SESSION_ENCRYPTION_KEY;
95
+ if (envKey) {
96
+ const key2 = crypto$1.createHash("sha256").update(envKey).digest();
97
+ return key2;
98
+ }
99
+ const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT || "default-insecure-key";
100
+ const key = crypto$1.createHash("sha256").update(strapiKeys).digest();
101
+ console.warn("[magic-sessionmanager/encryption] ⚠️ No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).");
102
+ console.warn("[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.");
103
+ return key;
104
+ }
105
+ function encryptToken$2(token) {
106
+ if (!token) return null;
107
+ try {
108
+ const key = getEncryptionKey();
109
+ const iv = crypto$1.randomBytes(IV_LENGTH);
110
+ const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
111
+ let encrypted = cipher.update(token, "utf8", "hex");
112
+ encrypted += cipher.final("hex");
113
+ const authTag = cipher.getAuthTag();
114
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
115
+ } catch (err) {
116
+ console.error("[magic-sessionmanager/encryption] Encryption failed:", err);
117
+ throw new Error("Failed to encrypt token");
118
+ }
119
+ }
120
+ function decryptToken$3(encryptedToken) {
121
+ if (!encryptedToken) return null;
122
+ try {
123
+ const key = getEncryptionKey();
124
+ const parts = encryptedToken.split(":");
125
+ if (parts.length !== 3) {
126
+ throw new Error("Invalid encrypted token format");
127
+ }
128
+ const iv = Buffer.from(parts[0], "hex");
129
+ const authTag = Buffer.from(parts[1], "hex");
130
+ const encrypted = parts[2];
131
+ const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
132
+ decipher.setAuthTag(authTag);
133
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
134
+ decrypted += decipher.final("utf8");
135
+ return decrypted;
136
+ } catch (err) {
137
+ console.error("[magic-sessionmanager/encryption] Decryption failed:", err);
138
+ return null;
139
+ }
140
+ }
141
+ function generateSessionId$1(userId) {
142
+ const timestamp = Date.now().toString(36);
143
+ const randomBytes = crypto$1.randomBytes(8).toString("hex");
144
+ const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
145
+ return `sess_${timestamp}_${userHash}_${randomBytes}`;
146
+ }
147
+ var encryption = {
148
+ encryptToken: encryptToken$2,
149
+ decryptToken: decryptToken$3,
150
+ generateSessionId: generateSessionId$1
151
+ };
90
152
  var lastSeen = ({ strapi: strapi2, sessionService }) => {
91
153
  return async (ctx, next) => {
92
154
  if (ctx.state.user && ctx.state.user.id) {
@@ -123,6 +185,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
123
185
  };
124
186
  };
125
187
  const getClientIp = getClientIp_1;
188
+ const { encryptToken: encryptToken$1, decryptToken: decryptToken$2 } = encryption;
126
189
  var bootstrap$1 = async ({ strapi: strapi2 }) => {
127
190
  strapi2.log.info("[magic-sessionmanager] 🚀 Bootstrap starting...");
128
191
  try {
@@ -190,16 +253,23 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
190
253
  ctx.body = { message: "Logged out successfully" };
191
254
  return;
192
255
  }
193
- const sessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
256
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
194
257
  filters: {
195
- token,
196
258
  isActive: true
197
- },
198
- limit: 1
259
+ }
260
+ });
261
+ const matchingSession = allSessions.find((session2) => {
262
+ if (!session2.token) return false;
263
+ try {
264
+ const decrypted = decryptToken$2(session2.token);
265
+ return decrypted === token;
266
+ } catch (err) {
267
+ return false;
268
+ }
199
269
  });
200
- if (sessions.length > 0) {
201
- await sessionService.terminateSession({ sessionId: sessions[0].id });
202
- strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
270
+ if (matchingSession) {
271
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
272
+ strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${matchingSession.id} terminated`);
203
273
  }
204
274
  ctx.status = 200;
205
275
  ctx.body = { message: "Logged out successfully" };
@@ -387,6 +457,11 @@ const pluginOptions = {
387
457
  }
388
458
  };
389
459
  const attributes = {
460
+ sessionId: {
461
+ type: "string",
462
+ unique: true,
463
+ required: true
464
+ },
390
465
  user: {
391
466
  type: "relation",
392
467
  relation: "manyToOne",
@@ -643,6 +718,7 @@ var routes$1 = {
643
718
  admin,
644
719
  "content-api": contentApi
645
720
  };
721
+ const { decryptToken: decryptToken$1 } = encryption;
646
722
  var session$3 = {
647
723
  /**
648
724
  * Get ALL sessions (active + inactive) - Admin only
@@ -722,13 +798,21 @@ var session$3 = {
722
798
  const sessions = await strapi.entityService.findMany("plugin::magic-sessionmanager.session", {
723
799
  filters: {
724
800
  user: { id: userId },
725
- token,
726
801
  isActive: true
727
802
  }
728
803
  });
729
- if (sessions.length > 0) {
730
- await sessionService.terminateSession({ sessionId: sessions[0].id });
731
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
804
+ const matchingSession = sessions.find((session2) => {
805
+ if (!session2.token) return false;
806
+ try {
807
+ const decrypted = decryptToken$1(session2.token);
808
+ return decrypted === token;
809
+ } catch (err) {
810
+ return false;
811
+ }
812
+ });
813
+ if (matchingSession) {
814
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
815
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.id})`);
732
816
  }
733
817
  ctx.body = {
734
818
  message: "Logged out successfully"
@@ -1227,6 +1311,7 @@ var controllers$1 = {
1227
1311
  license,
1228
1312
  settings
1229
1313
  };
1314
+ const { encryptToken, decryptToken, generateSessionId } = encryption;
1230
1315
  var session$1 = ({ strapi: strapi2 }) => ({
1231
1316
  /**
1232
1317
  * Create a new session record
@@ -1236,6 +1321,8 @@ var session$1 = ({ strapi: strapi2 }) => ({
1236
1321
  async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
1237
1322
  try {
1238
1323
  const now = /* @__PURE__ */ new Date();
1324
+ const sessionId = generateSessionId(userId);
1325
+ const encryptedToken = token ? encryptToken(token) : null;
1239
1326
  const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
1240
1327
  data: {
1241
1328
  user: userId,
@@ -1244,11 +1331,13 @@ var session$1 = ({ strapi: strapi2 }) => ({
1244
1331
  loginTime: now,
1245
1332
  lastActive: now,
1246
1333
  isActive: true,
1247
- token
1248
- // Store JWT for logout matching
1334
+ token: encryptedToken,
1335
+ // Encrypted JWT for security
1336
+ sessionId
1337
+ // ✅ Unique identifier
1249
1338
  }
1250
1339
  });
1251
- strapi2.log.info(`[magic-sessionmanager] ✅ Session ${session2.id} created for user ${userId}`);
1340
+ strapi2.log.info(`[magic-sessionmanager] ✅ Session ${session2.id} (${sessionId}) created for user ${userId}`);
1252
1341
  return session2;
1253
1342
  } catch (err) {
1254
1343
  strapi2.log.error("[magic-sessionmanager] Error creating session:", err);
@@ -83,6 +83,68 @@ const isPrivateIp = (ip) => {
83
83
  return false;
84
84
  };
85
85
  var getClientIp_1 = getClientIp$1;
86
+ const crypto$1 = require$$0$1;
87
+ const ALGORITHM = "aes-256-gcm";
88
+ const IV_LENGTH = 16;
89
+ function getEncryptionKey() {
90
+ const envKey = process.env.SESSION_ENCRYPTION_KEY;
91
+ if (envKey) {
92
+ const key2 = crypto$1.createHash("sha256").update(envKey).digest();
93
+ return key2;
94
+ }
95
+ const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT || "default-insecure-key";
96
+ const key = crypto$1.createHash("sha256").update(strapiKeys).digest();
97
+ console.warn("[magic-sessionmanager/encryption] ⚠️ No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).");
98
+ console.warn("[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.");
99
+ return key;
100
+ }
101
+ function encryptToken$2(token) {
102
+ if (!token) return null;
103
+ try {
104
+ const key = getEncryptionKey();
105
+ const iv = crypto$1.randomBytes(IV_LENGTH);
106
+ const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
107
+ let encrypted = cipher.update(token, "utf8", "hex");
108
+ encrypted += cipher.final("hex");
109
+ const authTag = cipher.getAuthTag();
110
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
111
+ } catch (err) {
112
+ console.error("[magic-sessionmanager/encryption] Encryption failed:", err);
113
+ throw new Error("Failed to encrypt token");
114
+ }
115
+ }
116
+ function decryptToken$3(encryptedToken) {
117
+ if (!encryptedToken) return null;
118
+ try {
119
+ const key = getEncryptionKey();
120
+ const parts = encryptedToken.split(":");
121
+ if (parts.length !== 3) {
122
+ throw new Error("Invalid encrypted token format");
123
+ }
124
+ const iv = Buffer.from(parts[0], "hex");
125
+ const authTag = Buffer.from(parts[1], "hex");
126
+ const encrypted = parts[2];
127
+ const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
128
+ decipher.setAuthTag(authTag);
129
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
130
+ decrypted += decipher.final("utf8");
131
+ return decrypted;
132
+ } catch (err) {
133
+ console.error("[magic-sessionmanager/encryption] Decryption failed:", err);
134
+ return null;
135
+ }
136
+ }
137
+ function generateSessionId$1(userId) {
138
+ const timestamp = Date.now().toString(36);
139
+ const randomBytes = crypto$1.randomBytes(8).toString("hex");
140
+ const userHash = crypto$1.createHash("sha256").update(userId.toString()).digest("hex").substring(0, 8);
141
+ return `sess_${timestamp}_${userHash}_${randomBytes}`;
142
+ }
143
+ var encryption = {
144
+ encryptToken: encryptToken$2,
145
+ decryptToken: decryptToken$3,
146
+ generateSessionId: generateSessionId$1
147
+ };
86
148
  var lastSeen = ({ strapi: strapi2, sessionService }) => {
87
149
  return async (ctx, next) => {
88
150
  if (ctx.state.user && ctx.state.user.id) {
@@ -119,6 +181,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
119
181
  };
120
182
  };
121
183
  const getClientIp = getClientIp_1;
184
+ const { encryptToken: encryptToken$1, decryptToken: decryptToken$2 } = encryption;
122
185
  var bootstrap$1 = async ({ strapi: strapi2 }) => {
123
186
  strapi2.log.info("[magic-sessionmanager] 🚀 Bootstrap starting...");
124
187
  try {
@@ -186,16 +249,23 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
186
249
  ctx.body = { message: "Logged out successfully" };
187
250
  return;
188
251
  }
189
- const sessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
252
+ const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
190
253
  filters: {
191
- token,
192
254
  isActive: true
193
- },
194
- limit: 1
255
+ }
256
+ });
257
+ const matchingSession = allSessions.find((session2) => {
258
+ if (!session2.token) return false;
259
+ try {
260
+ const decrypted = decryptToken$2(session2.token);
261
+ return decrypted === token;
262
+ } catch (err) {
263
+ return false;
264
+ }
195
265
  });
196
- if (sessions.length > 0) {
197
- await sessionService.terminateSession({ sessionId: sessions[0].id });
198
- strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
266
+ if (matchingSession) {
267
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
268
+ strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${matchingSession.id} terminated`);
199
269
  }
200
270
  ctx.status = 200;
201
271
  ctx.body = { message: "Logged out successfully" };
@@ -383,6 +453,11 @@ const pluginOptions = {
383
453
  }
384
454
  };
385
455
  const attributes = {
456
+ sessionId: {
457
+ type: "string",
458
+ unique: true,
459
+ required: true
460
+ },
386
461
  user: {
387
462
  type: "relation",
388
463
  relation: "manyToOne",
@@ -639,6 +714,7 @@ var routes$1 = {
639
714
  admin,
640
715
  "content-api": contentApi
641
716
  };
717
+ const { decryptToken: decryptToken$1 } = encryption;
642
718
  var session$3 = {
643
719
  /**
644
720
  * Get ALL sessions (active + inactive) - Admin only
@@ -718,13 +794,21 @@ var session$3 = {
718
794
  const sessions = await strapi.entityService.findMany("plugin::magic-sessionmanager.session", {
719
795
  filters: {
720
796
  user: { id: userId },
721
- token,
722
797
  isActive: true
723
798
  }
724
799
  });
725
- if (sessions.length > 0) {
726
- await sessionService.terminateSession({ sessionId: sessions[0].id });
727
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
800
+ const matchingSession = sessions.find((session2) => {
801
+ if (!session2.token) return false;
802
+ try {
803
+ const decrypted = decryptToken$1(session2.token);
804
+ return decrypted === token;
805
+ } catch (err) {
806
+ return false;
807
+ }
808
+ });
809
+ if (matchingSession) {
810
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
811
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.id})`);
728
812
  }
729
813
  ctx.body = {
730
814
  message: "Logged out successfully"
@@ -1223,6 +1307,7 @@ var controllers$1 = {
1223
1307
  license,
1224
1308
  settings
1225
1309
  };
1310
+ const { encryptToken, decryptToken, generateSessionId } = encryption;
1226
1311
  var session$1 = ({ strapi: strapi2 }) => ({
1227
1312
  /**
1228
1313
  * Create a new session record
@@ -1232,6 +1317,8 @@ var session$1 = ({ strapi: strapi2 }) => ({
1232
1317
  async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
1233
1318
  try {
1234
1319
  const now = /* @__PURE__ */ new Date();
1320
+ const sessionId = generateSessionId(userId);
1321
+ const encryptedToken = token ? encryptToken(token) : null;
1235
1322
  const session2 = await strapi2.entityService.create("plugin::magic-sessionmanager.session", {
1236
1323
  data: {
1237
1324
  user: userId,
@@ -1240,11 +1327,13 @@ var session$1 = ({ strapi: strapi2 }) => ({
1240
1327
  loginTime: now,
1241
1328
  lastActive: now,
1242
1329
  isActive: true,
1243
- token
1244
- // Store JWT for logout matching
1330
+ token: encryptedToken,
1331
+ // Encrypted JWT for security
1332
+ sessionId
1333
+ // ✅ Unique identifier
1245
1334
  }
1246
1335
  });
1247
- strapi2.log.info(`[magic-sessionmanager] ✅ Session ${session2.id} created for user ${userId}`);
1336
+ strapi2.log.info(`[magic-sessionmanager] ✅ Session ${session2.id} (${sessionId}) created for user ${userId}`);
1248
1337
  return session2;
1249
1338
  } catch (err) {
1250
1339
  strapi2.log.error("[magic-sessionmanager] Error creating session:", err);
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.0.1",
2
+ "version": "3.1.0",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  const getClientIp = require('./utils/getClientIp');
11
+ const { encryptToken, decryptToken } = require('./utils/encryption');
11
12
 
12
13
  module.exports = async ({ strapi }) => {
13
14
  strapi.log.info('[magic-sessionmanager] 🚀 Bootstrap starting...');
@@ -101,18 +102,28 @@ module.exports = async ({ strapi }) => {
101
102
  return;
102
103
  }
103
104
 
104
- // Find and terminate session by token
105
- const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
105
+ // Find session by decrypting tokens and matching
106
+ // Since tokens are encrypted, we need to get all active sessions and check each one
107
+ const allSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
106
108
  filters: {
107
- token: token,
108
109
  isActive: true,
109
110
  },
110
- limit: 1,
111
111
  });
112
112
 
113
- if (sessions.length > 0) {
114
- await sessionService.terminateSession({ sessionId: sessions[0].id });
115
- strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
113
+ // Find matching session by decrypting and comparing tokens
114
+ const matchingSession = allSessions.find(session => {
115
+ if (!session.token) return false;
116
+ try {
117
+ const decrypted = decryptToken(session.token);
118
+ return decrypted === token;
119
+ } catch (err) {
120
+ return false;
121
+ }
122
+ });
123
+
124
+ if (matchingSession) {
125
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
126
+ strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${matchingSession.id} terminated`);
116
127
  }
117
128
 
118
129
  ctx.status = 200;
@@ -20,6 +20,11 @@
20
20
  }
21
21
  },
22
22
  "attributes": {
23
+ "sessionId": {
24
+ "type": "string",
25
+ "unique": true,
26
+ "required": true
27
+ },
23
28
  "user": {
24
29
  "type": "relation",
25
30
  "relation": "manyToOne",
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { decryptToken } = require('../utils/encryption');
4
+
3
5
  /**
4
6
  * Session Controller
5
7
  * Handles HTTP requests for session management
@@ -103,19 +105,29 @@ module.exports = {
103
105
  .plugin('magic-sessionmanager')
104
106
  .service('session');
105
107
 
106
- // Find current session by token
108
+ // Find current session by decrypting and comparing tokens
107
109
  const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
108
110
  filters: {
109
111
  user: { id: userId },
110
- token: token,
111
112
  isActive: true,
112
113
  },
113
114
  });
114
115
 
115
- if (sessions.length > 0) {
116
+ // Find matching session by decrypting tokens
117
+ const matchingSession = sessions.find(session => {
118
+ if (!session.token) return false;
119
+ try {
120
+ const decrypted = decryptToken(session.token);
121
+ return decrypted === token;
122
+ } catch (err) {
123
+ return false;
124
+ }
125
+ });
126
+
127
+ if (matchingSession) {
116
128
  // Terminate only the current session
117
- await sessionService.terminateSession({ sessionId: sessions[0].id });
118
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
129
+ await sessionService.terminateSession({ sessionId: matchingSession.id });
130
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.id})`);
119
131
  }
120
132
 
121
133
  ctx.body = {
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ const { encryptToken, decryptToken, generateSessionId } = require('../utils/encryption');
4
+
3
5
  /**
4
6
  * Session Service
5
7
  * Uses plugin::magic-sessionmanager.session content type with relation to users
6
8
  * All session tracking happens in the Session collection
7
9
  *
10
+ * SECURITY: JWT tokens are encrypted before storing in database using AES-256-GCM
11
+ *
8
12
  * TODO: For production multi-instance deployments, use Redis for:
9
13
  * - Session store instead of DB
10
14
  * - Rate limiting locks
@@ -20,6 +24,12 @@ module.exports = ({ strapi }) => ({
20
24
  try {
21
25
  const now = new Date();
22
26
 
27
+ // Generate unique session ID
28
+ const sessionId = generateSessionId(userId);
29
+
30
+ // Encrypt JWT token before storing
31
+ const encryptedToken = token ? encryptToken(token) : null;
32
+
23
33
  const session = await strapi.entityService.create('plugin::magic-sessionmanager.session', {
24
34
  data: {
25
35
  user: userId,
@@ -28,11 +38,12 @@ module.exports = ({ strapi }) => ({
28
38
  loginTime: now,
29
39
  lastActive: now,
30
40
  isActive: true,
31
- token: token, // Store JWT for logout matching
41
+ token: encryptedToken, // Encrypted JWT for security
42
+ sessionId: sessionId, // ✅ Unique identifier
32
43
  },
33
44
  });
34
45
 
35
- strapi.log.info(`[magic-sessionmanager] ✅ Session ${session.id} created for user ${userId}`);
46
+ strapi.log.info(`[magic-sessionmanager] ✅ Session ${session.id} (${sessionId}) created for user ${userId}`);
36
47
 
37
48
  return session;
38
49
  } catch (err) {
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * JWT Encryption Utility
7
+ * Uses AES-256-GCM for secure token storage
8
+ *
9
+ * SECURITY: Tokens are encrypted before storing in database
10
+ * This prevents exposure if database is compromised
11
+ */
12
+
13
+ const ALGORITHM = 'aes-256-gcm';
14
+ const IV_LENGTH = 16;
15
+ const AUTH_TAG_LENGTH = 16;
16
+
17
+ /**
18
+ * Get encryption key from environment or generate one
19
+ * IMPORTANT: Set SESSION_ENCRYPTION_KEY in .env for production!
20
+ */
21
+ function getEncryptionKey() {
22
+ const envKey = process.env.SESSION_ENCRYPTION_KEY;
23
+
24
+ if (envKey) {
25
+ // Use provided key (must be 32 bytes for AES-256)
26
+ const key = crypto.createHash('sha256').update(envKey).digest();
27
+ return key;
28
+ }
29
+
30
+ // Fallback: Use Strapi's app keys (not recommended for production)
31
+ const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT || 'default-insecure-key';
32
+ const key = crypto.createHash('sha256').update(strapiKeys).digest();
33
+
34
+ console.warn('[magic-sessionmanager/encryption] ⚠️ No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).');
35
+ console.warn('[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.');
36
+
37
+ return key;
38
+ }
39
+
40
+ /**
41
+ * Encrypt JWT token before storing in database
42
+ * @param {string} token - JWT token to encrypt
43
+ * @returns {string} Encrypted token with IV and auth tag
44
+ */
45
+ function encryptToken(token) {
46
+ if (!token) return null;
47
+
48
+ try {
49
+ const key = getEncryptionKey();
50
+ const iv = crypto.randomBytes(IV_LENGTH);
51
+
52
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
53
+
54
+ let encrypted = cipher.update(token, 'utf8', 'hex');
55
+ encrypted += cipher.final('hex');
56
+
57
+ const authTag = cipher.getAuthTag();
58
+
59
+ // Format: iv:authTag:encryptedData
60
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
61
+ } catch (err) {
62
+ console.error('[magic-sessionmanager/encryption] Encryption failed:', err);
63
+ throw new Error('Failed to encrypt token');
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Decrypt JWT token from database
69
+ * @param {string} encryptedToken - Encrypted token from database
70
+ * @returns {string} Decrypted JWT token
71
+ */
72
+ function decryptToken(encryptedToken) {
73
+ if (!encryptedToken) return null;
74
+
75
+ try {
76
+ const key = getEncryptionKey();
77
+
78
+ // Parse: iv:authTag:encryptedData
79
+ const parts = encryptedToken.split(':');
80
+
81
+ if (parts.length !== 3) {
82
+ throw new Error('Invalid encrypted token format');
83
+ }
84
+
85
+ const iv = Buffer.from(parts[0], 'hex');
86
+ const authTag = Buffer.from(parts[1], 'hex');
87
+ const encrypted = parts[2];
88
+
89
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
90
+ decipher.setAuthTag(authTag);
91
+
92
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
93
+ decrypted += decipher.final('utf8');
94
+
95
+ return decrypted;
96
+ } catch (err) {
97
+ console.error('[magic-sessionmanager/encryption] Decryption failed:', err);
98
+ return null; // Return null if decryption fails (invalid/tampered token)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Generate unique session ID
104
+ * Combines timestamp + random bytes + user ID for uniqueness
105
+ * @param {number} userId - User ID
106
+ * @returns {string} Unique session identifier
107
+ */
108
+ function generateSessionId(userId) {
109
+ const timestamp = Date.now().toString(36);
110
+ const randomBytes = crypto.randomBytes(8).toString('hex');
111
+ const userHash = crypto.createHash('sha256').update(userId.toString()).digest('hex').substring(0, 8);
112
+
113
+ return `sess_${timestamp}_${userHash}_${randomBytes}`;
114
+ }
115
+
116
+ module.exports = {
117
+ encryptToken,
118
+ decryptToken,
119
+ generateSessionId,
120
+ };
121
+