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.
- package/admin/src/components/SessionInfoCard.jsx +1 -1
- package/admin/src/components/SessionInfoPanel.jsx +1 -1
- package/dist/_chunks/{Analytics-ByC7HfOE.mjs → Analytics-Bp1cPJx1.mjs} +2 -2
- package/dist/_chunks/{Analytics-CgaMF_rH.js → Analytics-CjqdGXnQ.js} +2 -2
- package/dist/_chunks/{App-Dy-mAUPv.mjs → App-DONYhluL.mjs} +2 -2
- package/dist/_chunks/{App-CsWl6uAZ.js → App-DtfZMVae.js} +2 -2
- package/dist/_chunks/{License-V6Y3xHzi.js → License-IWH6ClOx.js} +1 -1
- package/dist/_chunks/{License-DA8KK2GQ.mjs → License-lcVK7rtT.mjs} +1 -1
- package/dist/_chunks/{Settings-oXa82_Fo.mjs → Settings-JaiqQ_7r.mjs} +2 -2
- package/dist/_chunks/{Settings-BpB9rz0q.js → Settings-JixgQiB_.js} +2 -2
- package/dist/_chunks/{index-BLEZRtuD.mjs → index--JzOiQNw.mjs} +5 -5
- package/dist/_chunks/{index-CD2Jls2n.js → index-DqtQaEBL.js} +5 -5
- package/dist/_chunks/{useLicense-C3OdhHzJ.js → useLicense-DA-averf.js} +1 -1
- package/dist/_chunks/{useLicense-D92JSFYj.mjs → useLicense-W1cxUaca.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +103 -14
- package/dist/server/index.mjs +103 -14
- package/package.json +1 -1
- package/server/src/bootstrap.js +18 -7
- package/server/src/content-types/session/schema.json +5 -0
- package/server/src/controllers/session.js +17 -5
- package/server/src/services/session.js +13 -2
- package/server/src/utils/encryption.js +121 -0
|
@@ -30,7 +30,7 @@ const SessionInfoCard = ({ id, model }) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
|
-
const { data } = await get(`/magic-sessionmanager/
|
|
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/
|
|
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
|
|
8
|
-
import { u as useLicense } from "./useLicense-
|
|
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-
|
|
10
|
-
const useLicense = require("./useLicense-
|
|
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
|
|
8
|
-
import { u as useLicense } from "./useLicense-
|
|
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-
|
|
10
|
-
const useLicense = require("./useLicense-
|
|
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-
|
|
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
|
|
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
|
|
8
|
-
import { u as useLicense } from "./useLicense-
|
|
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-
|
|
10
|
-
const useLicense = require("./useLicense-
|
|
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/
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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/
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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);
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
256
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
194
257
|
filters: {
|
|
195
|
-
token,
|
|
196
258
|
isActive: true
|
|
197
|
-
}
|
|
198
|
-
|
|
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 (
|
|
201
|
-
await sessionService.terminateSession({ sessionId:
|
|
202
|
-
strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
//
|
|
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);
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
252
|
+
const allSessions = await strapi2.entityService.findMany("plugin::magic-sessionmanager.session", {
|
|
190
253
|
filters: {
|
|
191
|
-
token,
|
|
192
254
|
isActive: true
|
|
193
|
-
}
|
|
194
|
-
|
|
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 (
|
|
197
|
-
await sessionService.terminateSession({ sessionId:
|
|
198
|
-
strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
//
|
|
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
package/server/src/bootstrap.js
CHANGED
|
@@ -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
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
118
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${
|
|
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:
|
|
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
|
+
|