strapi-plugin-magic-sessionmanager 3.0.2 → 3.2.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.
@@ -6,16 +6,16 @@ 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-DqtQaEBL.js");
10
- const useLicense = require("./useLicense-DA-averf.js");
9
+ const index = require("./index-W_QbTAYU.js");
10
+ const useLicense = require("./useLicense-C_Rneohy.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const theme = {
14
14
  colors: {
15
- primary: { 600: "#0284C7", 700: "#075985" },
15
+ primary: { 600: "#0284C7", 700: "#075985", 100: "#E0F2FE", 50: "#F0F9FF" },
16
16
  success: { 600: "#16A34A", 700: "#15803D" },
17
17
  danger: { 600: "#DC2626" },
18
- neutral: { 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
18
+ neutral: { 0: "#FFFFFF", 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
19
19
  },
20
20
  shadows: { sm: "0 1px 3px rgba(0,0,0,0.1)" },
21
21
  borderRadius: { md: "8px", lg: "12px" }
@@ -326,6 +326,16 @@ Login Details:
326
326
  - VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
327
327
  }
328
328
  });
329
+ const generateSecureKey = () => {
330
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
331
+ let key = "";
332
+ const array = new Uint8Array(32);
333
+ crypto.getRandomValues(array);
334
+ for (let i = 0; i < 32; i++) {
335
+ key += chars[array[i] % chars.length];
336
+ }
337
+ return key;
338
+ };
329
339
  const SettingsPage = () => {
330
340
  const { get, post, put } = admin.useFetchClient();
331
341
  const { toggleNotification } = admin.useNotification();
@@ -335,6 +345,8 @@ const SettingsPage = () => {
335
345
  const [hasChanges, setHasChanges] = react.useState(false);
336
346
  const [cleaning, setCleaning] = react.useState(false);
337
347
  const [activeTemplateTab, setActiveTemplateTab] = react.useState("suspiciousLogin");
348
+ const [encryptionKey, setEncryptionKey] = react.useState("");
349
+ const [showEncryptionKey, setShowEncryptionKey] = react.useState(false);
338
350
  const [settings, setSettings] = react.useState({
339
351
  inactivityTimeout: 15,
340
352
  cleanupInterval: 30,
@@ -662,6 +674,142 @@ const SettingsPage = () => {
662
674
  ) }),
663
675
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Accordion.Content, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 6, children: [
664
676
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", fontWeight: "bold", style: { marginBottom: "16px", display: "block", color: theme.colors.neutral[700] }, children: "🔒 SECURITY OPTIONS" }),
677
+ /* @__PURE__ */ jsxRuntime.jsx(
678
+ designSystem.Box,
679
+ {
680
+ background: "neutral0",
681
+ padding: 6,
682
+ style: {
683
+ borderRadius: theme.borderRadius.lg,
684
+ marginBottom: "32px",
685
+ border: `2px solid ${theme.colors.primary[100]}`,
686
+ background: `linear-gradient(135deg, ${theme.colors.neutral[0]} 0%, ${theme.colors.primary[50]} 100%)`
687
+ },
688
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
689
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 3, children: [
690
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Shield, { style: { width: 24, height: 24, color: theme.colors.primary[600] } }),
691
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "JWT Encryption Key Generator" })
692
+ ] }),
693
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", style: { lineHeight: 1.6 }, children: "Generate a secure 32-character encryption key for JWT token storage. This key is used to encrypt tokens before saving them to the database." }),
694
+ /* @__PURE__ */ jsxRuntime.jsxs(
695
+ designSystem.Alert,
696
+ {
697
+ variant: "default",
698
+ title: "Important",
699
+ style: { marginTop: 8 },
700
+ children: [
701
+ "Add this key to your ",
702
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: ".env" }),
703
+ " file as ",
704
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "SESSION_ENCRYPTION_KEY" }),
705
+ " for production."
706
+ ]
707
+ }
708
+ ),
709
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "flex-end", children: [
710
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
711
+ designSystem.TextInput,
712
+ {
713
+ label: "Generated Encryption Key",
714
+ value: encryptionKey,
715
+ onChange: (e) => setEncryptionKey(e.target.value),
716
+ placeholder: "Click 'Generate Key' to create a secure key",
717
+ type: showEncryptionKey ? "text" : "password"
718
+ }
719
+ ) }),
720
+ /* @__PURE__ */ jsxRuntime.jsx(
721
+ designSystem.Button,
722
+ {
723
+ variant: "secondary",
724
+ onClick: () => setShowEncryptionKey(!showEncryptionKey),
725
+ size: "L",
726
+ children: showEncryptionKey ? "Hide" : "Show"
727
+ }
728
+ )
729
+ ] }),
730
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, children: [
731
+ /* @__PURE__ */ jsxRuntime.jsx(
732
+ designSystem.Button,
733
+ {
734
+ variant: "default",
735
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Code, {}),
736
+ onClick: () => {
737
+ const key = generateSecureKey();
738
+ setEncryptionKey(key);
739
+ setShowEncryptionKey(true);
740
+ toggleNotification({
741
+ type: "success",
742
+ message: "32-character encryption key generated!"
743
+ });
744
+ },
745
+ size: "L",
746
+ children: "Generate Key"
747
+ }
748
+ ),
749
+ /* @__PURE__ */ jsxRuntime.jsx(
750
+ designSystem.Button,
751
+ {
752
+ variant: "tertiary",
753
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Duplicate, {}),
754
+ onClick: () => {
755
+ if (encryptionKey) {
756
+ navigator.clipboard.writeText(encryptionKey);
757
+ toggleNotification({
758
+ type: "success",
759
+ message: "Encryption key copied to clipboard!"
760
+ });
761
+ }
762
+ },
763
+ disabled: !encryptionKey,
764
+ size: "L",
765
+ children: "Copy to Clipboard"
766
+ }
767
+ ),
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ designSystem.Button,
770
+ {
771
+ variant: "tertiary",
772
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Duplicate, {}),
773
+ onClick: () => {
774
+ if (encryptionKey) {
775
+ const envLine = `SESSION_ENCRYPTION_KEY=${encryptionKey}`;
776
+ navigator.clipboard.writeText(envLine);
777
+ toggleNotification({
778
+ type: "success",
779
+ message: "Copied as .env format!"
780
+ });
781
+ }
782
+ },
783
+ disabled: !encryptionKey,
784
+ size: "L",
785
+ children: "Copy for .env"
786
+ }
787
+ )
788
+ ] }),
789
+ encryptionKey && /* @__PURE__ */ jsxRuntime.jsxs(
790
+ designSystem.Box,
791
+ {
792
+ padding: 4,
793
+ background: "neutral100",
794
+ style: {
795
+ borderRadius: theme.borderRadius.md,
796
+ border: "1px solid " + theme.colors.neutral[200],
797
+ fontFamily: "monospace",
798
+ fontSize: "12px",
799
+ wordBreak: "break-all"
800
+ },
801
+ children: [
802
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", style: { marginBottom: 8, display: "block" }, children: "Add to .env file:" }),
803
+ /* @__PURE__ */ jsxRuntime.jsxs("code", { style: { color: theme.colors.primary[700] }, children: [
804
+ "SESSION_ENCRYPTION_KEY=",
805
+ encryptionKey
806
+ ] })
807
+ ]
808
+ }
809
+ )
810
+ ] })
811
+ }
812
+ ),
665
813
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral100", padding: 5, style: { borderRadius: theme.borderRadius.md, marginBottom: "32px" }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
666
814
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
667
815
  ToggleCard,
@@ -403,7 +403,7 @@ const index = {
403
403
  id: `${pluginId}.plugin.name`,
404
404
  defaultMessage: pluginPkg.strapi.displayName
405
405
  },
406
- Component: () => import("./App-DONYhluL.mjs")
406
+ Component: () => import("./App-Zhs_vt59.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-JaiqQ_7r.mjs")
422
+ Component: () => import("./Settings-CL2im8M3.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-Bp1cPJx1.mjs")
431
+ Component: () => import("./Analytics-CwyLwdOZ.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-lcVK7rtT.mjs")
440
+ Component: () => import("./License-CPI0p_W8.mjs")
441
441
  }
442
442
  ]
443
443
  );
@@ -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-DtfZMVae.js"))
407
+ Component: () => Promise.resolve().then(() => require("./App-nGu2Eb87.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-JixgQiB_.js"))
423
+ Component: () => Promise.resolve().then(() => require("./Settings-Lkmxisuv.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-CjqdGXnQ.js"))
432
+ Component: () => Promise.resolve().then(() => require("./Analytics-DRzCKaDF.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-IWH6ClOx.js"))
441
+ Component: () => Promise.resolve().then(() => require("./License-k5vvhgKr.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-DqtQaEBL.js");
4
+ const index = require("./index-W_QbTAYU.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--JzOiQNw.mjs";
3
+ import { a as pluginId } from "./index-B-0VPfeF.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-DqtQaEBL.js");
2
+ const index = require("../_chunks/index-W_QbTAYU.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index--JzOiQNw.mjs";
1
+ import { i } from "../_chunks/index-B-0VPfeF.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.2",
2
+ "version": "3.2.0",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",