strapi-plugin-magic-sessionmanager 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,11 +101,33 @@ npm run build
101
101
  npm run develop
102
102
  ```
103
103
 
104
- ### 4. Access Admin Dashboard
104
+ ### 4. Configure Encryption (Important!) 🔐
105
+
106
+ Generate a secure encryption key for JWT token storage:
107
+
108
+ ```bash
109
+ # Option 1: Use Admin Panel
110
+ # Go to Admin → Sessions → Settings → Security Settings
111
+ # Click "Generate Key" and copy to .env
112
+
113
+ # Option 2: Generate manually
114
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
115
+
116
+ # Add to .env file:
117
+ SESSION_ENCRYPTION_KEY=your-generated-32-char-key-here
118
+ ```
119
+
120
+ **Why this is important:**
121
+ - JWT tokens are encrypted before storing in database
122
+ - Prevents token exposure if database is compromised
123
+ - Uses AES-256-GCM encryption standard
124
+
125
+ ### 5. Access Admin Dashboard
105
126
 
106
127
  - Navigate to Strapi Admin: `http://localhost:1337/admin`
107
128
  - Find **Sessions** in the left sidebar under plugins
108
129
  - Start with the **License** tab to activate your license
130
+ - Go to **Settings → Security** to generate your encryption key
109
131
 
110
132
  ---
111
133
 
@@ -699,6 +721,104 @@ Available through Admin UI **Settings → Sessions → Settings**:
699
721
 
700
722
  ---
701
723
 
724
+ ## 🔐 JWT Token Security
725
+
726
+ ### Encryption
727
+
728
+ All JWT tokens are **encrypted before storing** in the database using **AES-256-GCM** encryption.
729
+
730
+ #### Why Encrypt Tokens?
731
+
732
+ ```
733
+ ❌ Without Encryption:
734
+ Database compromised → Attacker sees JWTs → Can impersonate users!
735
+
736
+ ✅ With Encryption:
737
+ Database compromised → Attacker sees encrypted data → Useless without key!
738
+ ```
739
+
740
+ #### How It Works
741
+
742
+ ```
743
+ Login: User gets JWT
744
+
745
+ JWT: "eyJhbGciOiJIUzI1NiIs..."
746
+
747
+ [Encrypt with AES-256-GCM]
748
+
749
+ Encrypted: "a3f7b2c1:8c4d9e2a:f2a5b8c3d4e5f6a7..."
750
+
751
+ Stored in Database (secure!)
752
+
753
+ Logout: User sends JWT
754
+
755
+ [Fetch all active sessions from DB]
756
+
757
+ [Decrypt each token]
758
+
759
+ [Compare with user's JWT]
760
+
761
+ Match found → Terminate session ✅
762
+ ```
763
+
764
+ #### Configuration
765
+
766
+ **Generate Encryption Key (Admin Panel):**
767
+
768
+ 1. Go to **Admin → Sessions → Settings**
769
+ 2. Open **Security Settings** accordion
770
+ 3. Find **JWT Encryption Key Generator**
771
+ 4. Click **"Generate Key"**
772
+ 5. Copy key with **"Copy for .env"** button
773
+ 6. Add to your `.env` file
774
+
775
+ **Or generate manually:**
776
+
777
+ ```bash
778
+ # Generate secure 32-byte key
779
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
780
+
781
+ # Add to .env
782
+ SESSION_ENCRYPTION_KEY=aBc123XyZ...your-32-char-key
783
+ ```
784
+
785
+ **Fallback Behavior:**
786
+
787
+ If `SESSION_ENCRYPTION_KEY` is not set:
788
+ - Plugin uses `APP_KEYS` or `API_TOKEN_SALT` as fallback
789
+ - ⚠️ Warning logged on startup
790
+ - Still encrypted, but key is derived from Strapi's keys
791
+
792
+ **Production Recommendation:**
793
+ Always use a dedicated `SESSION_ENCRYPTION_KEY` for better security isolation.
794
+
795
+ #### Security Details
796
+
797
+ | Feature | Value |
798
+ |---------|-------|
799
+ | Algorithm | AES-256-GCM |
800
+ | Key Size | 256 bits (32 bytes) |
801
+ | IV Length | 128 bits (16 bytes) |
802
+ | Auth Tag | 128 bits (16 bytes) |
803
+ | Format | `iv:authTag:encryptedData` (hex) |
804
+
805
+ ### Unique Session IDs
806
+
807
+ Each session gets a cryptographically unique identifier:
808
+
809
+ ```javascript
810
+ sessionId: "sess_lx3k7_4f2a8b3c_a1b2c3d4e5f6"
811
+ // prefix^ ^timestamp ^user-hash ^random-bytes
812
+ ```
813
+
814
+ **Benefits:**
815
+ - ✅ No collisions across sessions
816
+ - ✅ Traceable session identifiers
817
+ - ✅ Independent from database IDs
818
+ - ✅ URL-safe for future features
819
+
820
+ ---
821
+
702
822
  ## 🔒 Premium Features
703
823
 
704
824
  ### IP Geolocation & Threat Detection
@@ -366,6 +366,20 @@ Login Details:
366
366
  },
367
367
  });
368
368
 
369
+ // ================ HELPER FUNCTIONS ================
370
+ const generateSecureKey = () => {
371
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
372
+ let key = '';
373
+ const array = new Uint8Array(32);
374
+ crypto.getRandomValues(array);
375
+
376
+ for (let i = 0; i < 32; i++) {
377
+ key += chars[array[i] % chars.length];
378
+ }
379
+
380
+ return key;
381
+ };
382
+
369
383
  const SettingsPage = () => {
370
384
  const { get, post, put } = useFetchClient();
371
385
  const { toggleNotification } = useNotification();
@@ -375,6 +389,8 @@ const SettingsPage = () => {
375
389
  const [hasChanges, setHasChanges] = useState(false);
376
390
  const [cleaning, setCleaning] = useState(false);
377
391
  const [activeTemplateTab, setActiveTemplateTab] = useState('suspiciousLogin');
392
+ const [encryptionKey, setEncryptionKey] = useState('');
393
+ const [showEncryptionKey, setShowEncryptionKey] = useState(false);
378
394
 
379
395
  const [settings, setSettings] = useState({
380
396
  inactivityTimeout: 15,
@@ -772,6 +788,136 @@ const SettingsPage = () => {
772
788
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
773
789
  🔒 SECURITY OPTIONS
774
790
  </Typography>
791
+
792
+ {/* Encryption Key Generator */}
793
+ <Box
794
+ background="neutral0"
795
+ padding={6}
796
+ style={{
797
+ borderRadius: theme.borderRadius.lg,
798
+ marginBottom: '32px',
799
+ border: `2px solid ${theme.colors.primary[100]}`,
800
+ background: `linear-gradient(135deg, ${theme.colors.neutral[0]} 0%, ${theme.colors.primary[50]} 100%)`
801
+ }}
802
+ >
803
+ <Flex direction="column" gap={4}>
804
+ <Flex alignItems="center" gap={3}>
805
+ <Shield style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
806
+ <Typography variant="delta" fontWeight="bold">
807
+ JWT Encryption Key Generator
808
+ </Typography>
809
+ </Flex>
810
+
811
+ <Typography variant="omega" textColor="neutral600" style={{ lineHeight: 1.6 }}>
812
+ Generate a secure 32-character encryption key for JWT token storage.
813
+ This key is used to encrypt tokens before saving them to the database.
814
+ </Typography>
815
+
816
+ <Alert
817
+ variant="default"
818
+ title="Important"
819
+ style={{ marginTop: 8 }}
820
+ >
821
+ Add this key to your <code>.env</code> file as <strong>SESSION_ENCRYPTION_KEY</strong> for production.
822
+ </Alert>
823
+
824
+ <Flex gap={3} alignItems="flex-end">
825
+ <Box style={{ flex: 1 }}>
826
+ <TextInput
827
+ label="Generated Encryption Key"
828
+ value={encryptionKey}
829
+ onChange={(e) => setEncryptionKey(e.target.value)}
830
+ placeholder="Click 'Generate Key' to create a secure key"
831
+ type={showEncryptionKey ? 'text' : 'password'}
832
+ />
833
+ </Box>
834
+ <Button
835
+ variant="secondary"
836
+ onClick={() => setShowEncryptionKey(!showEncryptionKey)}
837
+ size="L"
838
+ >
839
+ {showEncryptionKey ? 'Hide' : 'Show'}
840
+ </Button>
841
+ </Flex>
842
+
843
+ <Flex gap={3}>
844
+ <Button
845
+ variant="default"
846
+ startIcon={<Code />}
847
+ onClick={() => {
848
+ const key = generateSecureKey();
849
+ setEncryptionKey(key);
850
+ setShowEncryptionKey(true);
851
+ toggleNotification({
852
+ type: 'success',
853
+ message: '32-character encryption key generated!'
854
+ });
855
+ }}
856
+ size="L"
857
+ >
858
+ Generate Key
859
+ </Button>
860
+
861
+ <Button
862
+ variant="tertiary"
863
+ startIcon={<Duplicate />}
864
+ onClick={() => {
865
+ if (encryptionKey) {
866
+ navigator.clipboard.writeText(encryptionKey);
867
+ toggleNotification({
868
+ type: 'success',
869
+ message: 'Encryption key copied to clipboard!'
870
+ });
871
+ }
872
+ }}
873
+ disabled={!encryptionKey}
874
+ size="L"
875
+ >
876
+ Copy to Clipboard
877
+ </Button>
878
+
879
+ <Button
880
+ variant="tertiary"
881
+ startIcon={<Duplicate />}
882
+ onClick={() => {
883
+ if (encryptionKey) {
884
+ const envLine = `SESSION_ENCRYPTION_KEY=${encryptionKey}`;
885
+ navigator.clipboard.writeText(envLine);
886
+ toggleNotification({
887
+ type: 'success',
888
+ message: 'Copied as .env format!'
889
+ });
890
+ }
891
+ }}
892
+ disabled={!encryptionKey}
893
+ size="L"
894
+ >
895
+ Copy for .env
896
+ </Button>
897
+ </Flex>
898
+
899
+ {encryptionKey && (
900
+ <Box
901
+ padding={4}
902
+ background="neutral100"
903
+ style={{
904
+ borderRadius: theme.borderRadius.md,
905
+ border: '1px solid ' + theme.colors.neutral[200],
906
+ fontFamily: 'monospace',
907
+ fontSize: '12px',
908
+ wordBreak: 'break-all'
909
+ }}
910
+ >
911
+ <Typography variant="omega" fontWeight="bold" style={{ marginBottom: 8, display: 'block' }}>
912
+ Add to .env file:
913
+ </Typography>
914
+ <code style={{ color: theme.colors.primary[700] }}>
915
+ SESSION_ENCRYPTION_KEY={encryptionKey}
916
+ </code>
917
+ </Box>
918
+ )}
919
+ </Flex>
920
+ </Box>
775
921
 
776
922
  {/* Feature Toggles */}
777
923
  <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
@@ -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--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.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-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 = {
@@ -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--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { p as parseUserAgent, a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.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-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 reactRouterDom = require("react-router-dom");
12
12
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
13
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
@@ -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--JzOiQNw.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
8
  const theme = {
9
9
  colors: {
10
10
  neutral: { 200: "#E5E7EB" }
@@ -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-DqtQaEBL.js");
9
+ const index = require("./index-W_QbTAYU.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const theme = {
@@ -1,17 +1,17 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
- import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSelect, SingleSelectOption, Divider, Toggle, NumberInput, Checkbox, Tabs, TextInput } from "@strapi/design-system";
3
+ import { Flex, Loader, Typography, Button, Box, Badge, Accordion, Grid, SingleSelect, SingleSelectOption, Divider, Alert, TextInput, Toggle, NumberInput, Checkbox, Tabs } from "@strapi/design-system";
4
4
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
- import { Check, Information, Cog, Trash, Shield, Mail, Code } from "@strapi/icons";
5
+ import { Check, Information, Cog, Trash, Shield, Code, Duplicate, Mail } from "@strapi/icons";
6
6
  import styled, { css, keyframes } from "styled-components";
7
- import { a as pluginId } from "./index--JzOiQNw.mjs";
8
- import { u as useLicense } from "./useLicense-W1cxUaca.mjs";
7
+ import { a as pluginId } from "./index-B-0VPfeF.mjs";
8
+ import { u as useLicense } from "./useLicense-DUGjNbQ9.mjs";
9
9
  const theme = {
10
10
  colors: {
11
- primary: { 600: "#0284C7", 700: "#075985" },
11
+ primary: { 600: "#0284C7", 700: "#075985", 100: "#E0F2FE", 50: "#F0F9FF" },
12
12
  success: { 600: "#16A34A", 700: "#15803D" },
13
13
  danger: { 600: "#DC2626" },
14
- neutral: { 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
14
+ neutral: { 0: "#FFFFFF", 200: "#E5E7EB", 400: "#9CA3AF", 700: "#374151" }
15
15
  },
16
16
  shadows: { sm: "0 1px 3px rgba(0,0,0,0.1)" },
17
17
  borderRadius: { md: "8px", lg: "12px" }
@@ -322,6 +322,16 @@ Login Details:
322
322
  - VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
323
323
  }
324
324
  });
325
+ const generateSecureKey = () => {
326
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
327
+ let key = "";
328
+ const array = new Uint8Array(32);
329
+ crypto.getRandomValues(array);
330
+ for (let i = 0; i < 32; i++) {
331
+ key += chars[array[i] % chars.length];
332
+ }
333
+ return key;
334
+ };
325
335
  const SettingsPage = () => {
326
336
  const { get, post, put } = useFetchClient();
327
337
  const { toggleNotification } = useNotification();
@@ -331,6 +341,8 @@ const SettingsPage = () => {
331
341
  const [hasChanges, setHasChanges] = useState(false);
332
342
  const [cleaning, setCleaning] = useState(false);
333
343
  const [activeTemplateTab, setActiveTemplateTab] = useState("suspiciousLogin");
344
+ const [encryptionKey, setEncryptionKey] = useState("");
345
+ const [showEncryptionKey, setShowEncryptionKey] = useState(false);
334
346
  const [settings, setSettings] = useState({
335
347
  inactivityTimeout: 15,
336
348
  cleanupInterval: 30,
@@ -658,6 +670,142 @@ const SettingsPage = () => {
658
670
  ) }),
659
671
  /* @__PURE__ */ jsx(Accordion.Content, { children: /* @__PURE__ */ jsxs(Box, { padding: 6, children: [
660
672
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", fontWeight: "bold", style: { marginBottom: "16px", display: "block", color: theme.colors.neutral[700] }, children: "🔒 SECURITY OPTIONS" }),
673
+ /* @__PURE__ */ jsx(
674
+ Box,
675
+ {
676
+ background: "neutral0",
677
+ padding: 6,
678
+ style: {
679
+ borderRadius: theme.borderRadius.lg,
680
+ marginBottom: "32px",
681
+ border: `2px solid ${theme.colors.primary[100]}`,
682
+ background: `linear-gradient(135deg, ${theme.colors.neutral[0]} 0%, ${theme.colors.primary[50]} 100%)`
683
+ },
684
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
685
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [
686
+ /* @__PURE__ */ jsx(Shield, { style: { width: 24, height: 24, color: theme.colors.primary[600] } }),
687
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "JWT Encryption Key Generator" })
688
+ ] }),
689
+ /* @__PURE__ */ jsx(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." }),
690
+ /* @__PURE__ */ jsxs(
691
+ Alert,
692
+ {
693
+ variant: "default",
694
+ title: "Important",
695
+ style: { marginTop: 8 },
696
+ children: [
697
+ "Add this key to your ",
698
+ /* @__PURE__ */ jsx("code", { children: ".env" }),
699
+ " file as ",
700
+ /* @__PURE__ */ jsx("strong", { children: "SESSION_ENCRYPTION_KEY" }),
701
+ " for production."
702
+ ]
703
+ }
704
+ ),
705
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-end", children: [
706
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
707
+ TextInput,
708
+ {
709
+ label: "Generated Encryption Key",
710
+ value: encryptionKey,
711
+ onChange: (e) => setEncryptionKey(e.target.value),
712
+ placeholder: "Click 'Generate Key' to create a secure key",
713
+ type: showEncryptionKey ? "text" : "password"
714
+ }
715
+ ) }),
716
+ /* @__PURE__ */ jsx(
717
+ Button,
718
+ {
719
+ variant: "secondary",
720
+ onClick: () => setShowEncryptionKey(!showEncryptionKey),
721
+ size: "L",
722
+ children: showEncryptionKey ? "Hide" : "Show"
723
+ }
724
+ )
725
+ ] }),
726
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
727
+ /* @__PURE__ */ jsx(
728
+ Button,
729
+ {
730
+ variant: "default",
731
+ startIcon: /* @__PURE__ */ jsx(Code, {}),
732
+ onClick: () => {
733
+ const key = generateSecureKey();
734
+ setEncryptionKey(key);
735
+ setShowEncryptionKey(true);
736
+ toggleNotification({
737
+ type: "success",
738
+ message: "32-character encryption key generated!"
739
+ });
740
+ },
741
+ size: "L",
742
+ children: "Generate Key"
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsx(
746
+ Button,
747
+ {
748
+ variant: "tertiary",
749
+ startIcon: /* @__PURE__ */ jsx(Duplicate, {}),
750
+ onClick: () => {
751
+ if (encryptionKey) {
752
+ navigator.clipboard.writeText(encryptionKey);
753
+ toggleNotification({
754
+ type: "success",
755
+ message: "Encryption key copied to clipboard!"
756
+ });
757
+ }
758
+ },
759
+ disabled: !encryptionKey,
760
+ size: "L",
761
+ children: "Copy to Clipboard"
762
+ }
763
+ ),
764
+ /* @__PURE__ */ jsx(
765
+ Button,
766
+ {
767
+ variant: "tertiary",
768
+ startIcon: /* @__PURE__ */ jsx(Duplicate, {}),
769
+ onClick: () => {
770
+ if (encryptionKey) {
771
+ const envLine = `SESSION_ENCRYPTION_KEY=${encryptionKey}`;
772
+ navigator.clipboard.writeText(envLine);
773
+ toggleNotification({
774
+ type: "success",
775
+ message: "Copied as .env format!"
776
+ });
777
+ }
778
+ },
779
+ disabled: !encryptionKey,
780
+ size: "L",
781
+ children: "Copy for .env"
782
+ }
783
+ )
784
+ ] }),
785
+ encryptionKey && /* @__PURE__ */ jsxs(
786
+ Box,
787
+ {
788
+ padding: 4,
789
+ background: "neutral100",
790
+ style: {
791
+ borderRadius: theme.borderRadius.md,
792
+ border: "1px solid " + theme.colors.neutral[200],
793
+ fontFamily: "monospace",
794
+ fontSize: "12px",
795
+ wordBreak: "break-all"
796
+ },
797
+ children: [
798
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", style: { marginBottom: 8, display: "block" }, children: "Add to .env file:" }),
799
+ /* @__PURE__ */ jsxs("code", { style: { color: theme.colors.primary[700] }, children: [
800
+ "SESSION_ENCRYPTION_KEY=",
801
+ encryptionKey
802
+ ] })
803
+ ]
804
+ }
805
+ )
806
+ ] })
807
+ }
808
+ ),
661
809
  /* @__PURE__ */ jsx(Box, { background: "neutral100", padding: 5, style: { borderRadius: theme.borderRadius.md, marginBottom: "32px" }, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
662
810
  /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsx(
663
811
  ToggleCard,
@@ -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
  };
@@ -760,14 +760,16 @@ var session$3 = {
760
760
  },
761
761
  /**
762
762
  * Get user's sessions
763
- * GET /magic-sessionmanager/user/:userId/sessions
764
- * SECURITY: User can only access their own sessions
763
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
764
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
765
+ * SECURITY: Admins can view any user, Content API users only their own
765
766
  */
766
767
  async getUserSessions(ctx) {
767
768
  try {
768
769
  const { userId } = ctx.params;
770
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
769
771
  const requestingUserId = ctx.state.user?.id;
770
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
772
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
771
773
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
772
774
  return ctx.forbidden("You can only access your own sessions");
773
775
  }
@@ -756,14 +756,16 @@ var session$3 = {
756
756
  },
757
757
  /**
758
758
  * Get user's sessions
759
- * GET /magic-sessionmanager/user/:userId/sessions
760
- * SECURITY: User can only access their own sessions
759
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
760
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
761
+ * SECURITY: Admins can view any user, Content API users only their own
761
762
  */
762
763
  async getUserSessions(ctx) {
763
764
  try {
764
765
  const { userId } = ctx.params;
766
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
765
767
  const requestingUserId = ctx.state.user?.id;
766
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
768
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
767
769
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
768
770
  return ctx.forbidden("You can only access your own sessions");
769
771
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.1.0",
2
+ "version": "3.2.1",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",
@@ -57,16 +57,21 @@ module.exports = {
57
57
 
58
58
  /**
59
59
  * Get user's sessions
60
- * GET /magic-sessionmanager/user/:userId/sessions
61
- * SECURITY: User can only access their own sessions
60
+ * GET /magic-sessionmanager/user/:userId/sessions (Admin API)
61
+ * GET /api/magic-sessionmanager/user/:userId/sessions (Content API)
62
+ * SECURITY: Admins can view any user, Content API users only their own
62
63
  */
63
64
  async getUserSessions(ctx) {
64
65
  try {
65
66
  const { userId } = ctx.params;
67
+
68
+ // Check if this is an admin request
69
+ const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
66
70
  const requestingUserId = ctx.state.user?.id;
67
71
 
68
- // SECURITY CHECK: User can only see their own sessions
69
- if (requestingUserId && String(requestingUserId) !== String(userId)) {
72
+ // SECURITY CHECK: Content API users can only see their own sessions
73
+ // Admins can see any user's sessions
74
+ if (!isAdminRequest && requestingUserId && String(requestingUserId) !== String(userId)) {
70
75
  strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
71
76
  return ctx.forbidden('You can only access your own sessions');
72
77
  }