strapi-plugin-magic-sessionmanager 4.4.4 → 4.4.6
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/dist/server/index.js +312 -118
- package/dist/server/index.mjs +312 -118
- package/package.json +3 -3
package/dist/server/index.js
CHANGED
|
@@ -172,11 +172,19 @@ const getClientIp$1 = (ctx) => {
|
|
|
172
172
|
};
|
|
173
173
|
const cleanIp = (ip) => {
|
|
174
174
|
if (!ip) return "unknown";
|
|
175
|
-
ip = ip.
|
|
175
|
+
ip = ip.trim();
|
|
176
|
+
if (ip.startsWith("[")) {
|
|
177
|
+
const bracketEnd = ip.indexOf("]");
|
|
178
|
+
if (bracketEnd !== -1) {
|
|
179
|
+
ip = ip.substring(1, bracketEnd);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
176
182
|
if (ip.startsWith("::ffff:")) {
|
|
177
183
|
ip = ip.substring(7);
|
|
178
184
|
}
|
|
179
|
-
ip
|
|
185
|
+
if (ip.includes(".") && ip.includes(":") && ip.indexOf(":") === ip.lastIndexOf(":")) {
|
|
186
|
+
ip = ip.split(":")[0];
|
|
187
|
+
}
|
|
180
188
|
return ip || "unknown";
|
|
181
189
|
};
|
|
182
190
|
const isPrivateIp = (ip) => {
|
|
@@ -199,14 +207,15 @@ const IV_LENGTH = 16;
|
|
|
199
207
|
function getEncryptionKey() {
|
|
200
208
|
const envKey = process.env.SESSION_ENCRYPTION_KEY;
|
|
201
209
|
if (envKey) {
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
return crypto$1.createHash("sha256").update(envKey).digest();
|
|
211
|
+
}
|
|
212
|
+
const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT;
|
|
213
|
+
if (!strapiKeys) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"[magic-sessionmanager] No encryption key available. Set SESSION_ENCRYPTION_KEY in your .env file, or ensure APP_KEYS is configured."
|
|
216
|
+
);
|
|
204
217
|
}
|
|
205
|
-
|
|
206
|
-
const key = crypto$1.createHash("sha256").update(strapiKeys).digest();
|
|
207
|
-
console.warn("[magic-sessionmanager/encryption] [WARNING] No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).");
|
|
208
|
-
console.warn("[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.");
|
|
209
|
-
return key;
|
|
218
|
+
return crypto$1.createHash("sha256").update(strapiKeys).digest();
|
|
210
219
|
}
|
|
211
220
|
function encryptToken$2(token) {
|
|
212
221
|
if (!token) return null;
|
|
@@ -219,11 +228,12 @@ function encryptToken$2(token) {
|
|
|
219
228
|
const authTag = cipher.getAuthTag();
|
|
220
229
|
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
221
230
|
} catch (err) {
|
|
222
|
-
|
|
231
|
+
const errMsg = err instanceof Error ? err.message : "Unknown encryption error";
|
|
232
|
+
console.error("[magic-sessionmanager/encryption] Encryption failed:", errMsg);
|
|
223
233
|
throw new Error("Failed to encrypt token");
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
|
-
function decryptToken$
|
|
236
|
+
function decryptToken$2(encryptedToken) {
|
|
227
237
|
if (!encryptedToken) return null;
|
|
228
238
|
try {
|
|
229
239
|
const key = getEncryptionKey();
|
|
@@ -240,7 +250,8 @@ function decryptToken$3(encryptedToken) {
|
|
|
240
250
|
decrypted += decipher.final("utf8");
|
|
241
251
|
return decrypted;
|
|
242
252
|
} catch (err) {
|
|
243
|
-
|
|
253
|
+
const errMsg = err instanceof Error ? err.message : "Unknown decryption error";
|
|
254
|
+
console.error("[magic-sessionmanager/encryption] Decryption failed:", errMsg);
|
|
244
255
|
return null;
|
|
245
256
|
}
|
|
246
257
|
}
|
|
@@ -256,7 +267,7 @@ function hashToken$3(token) {
|
|
|
256
267
|
}
|
|
257
268
|
var encryption = {
|
|
258
269
|
encryptToken: encryptToken$2,
|
|
259
|
-
decryptToken: decryptToken$
|
|
270
|
+
decryptToken: decryptToken$2,
|
|
260
271
|
generateSessionId: generateSessionId$1,
|
|
261
272
|
hashToken: hashToken$3
|
|
262
273
|
};
|
|
@@ -287,12 +298,25 @@ function isAuthEndpoint(path2) {
|
|
|
287
298
|
}
|
|
288
299
|
const userIdCache = /* @__PURE__ */ new Map();
|
|
289
300
|
const CACHE_TTL = 5 * 60 * 1e3;
|
|
301
|
+
const CACHE_MAX_SIZE = 1e3;
|
|
290
302
|
async function getDocumentIdFromNumericId(strapi2, numericId) {
|
|
291
303
|
const cacheKey = `user_${numericId}`;
|
|
292
304
|
const cached2 = userIdCache.get(cacheKey);
|
|
293
305
|
if (cached2 && Date.now() - cached2.timestamp < CACHE_TTL) {
|
|
294
306
|
return cached2.documentId;
|
|
295
307
|
}
|
|
308
|
+
if (userIdCache.size >= CACHE_MAX_SIZE) {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
for (const [key, value] of userIdCache) {
|
|
311
|
+
if (now - value.timestamp >= CACHE_TTL) {
|
|
312
|
+
userIdCache.delete(key);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (userIdCache.size >= CACHE_MAX_SIZE) {
|
|
316
|
+
const keysToDelete = [...userIdCache.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
317
|
+
keysToDelete.forEach((key) => userIdCache.delete(key));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
296
320
|
try {
|
|
297
321
|
const user = await strapi2.entityService.findOne(USER_UID$3, numericId, {
|
|
298
322
|
fields: ["documentId"]
|
|
@@ -335,7 +359,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
335
359
|
isActive: false
|
|
336
360
|
},
|
|
337
361
|
limit: 5,
|
|
338
|
-
fields: ["documentId", "terminatedManually", "lastActive"],
|
|
362
|
+
fields: ["documentId", "terminatedManually", "lastActive", "loginTime"],
|
|
339
363
|
sort: [{ lastActive: "desc" }]
|
|
340
364
|
});
|
|
341
365
|
if (inactiveSessions && inactiveSessions.length > 0) {
|
|
@@ -345,6 +369,14 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
345
369
|
return ctx.unauthorized("Session has been terminated. Please login again.");
|
|
346
370
|
}
|
|
347
371
|
const sessionToReactivate = inactiveSessions[0];
|
|
372
|
+
const maxAgeDays = config2.maxSessionAgeDays || 30;
|
|
373
|
+
const loginTime = sessionToReactivate.loginTime ? new Date(sessionToReactivate.loginTime).getTime() : sessionToReactivate.lastActive ? new Date(sessionToReactivate.lastActive).getTime() : 0;
|
|
374
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
375
|
+
const isExpired = loginTime > 0 && Date.now() - loginTime > maxAgeMs;
|
|
376
|
+
if (isExpired) {
|
|
377
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session exceeded max age of ${maxAgeDays} days (user: ${userDocId2.substring(0, 8)}...)`);
|
|
378
|
+
return ctx.unauthorized("Session expired. Please login again.");
|
|
379
|
+
}
|
|
348
380
|
await strapi2.documents(SESSION_UID$4).update({
|
|
349
381
|
documentId: sessionToReactivate.documentId,
|
|
350
382
|
data: {
|
|
@@ -385,7 +417,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
385
417
|
};
|
|
386
418
|
};
|
|
387
419
|
const getClientIp = getClientIp_1;
|
|
388
|
-
const { encryptToken: encryptToken$1, decryptToken: decryptToken$
|
|
420
|
+
const { encryptToken: encryptToken$1, decryptToken: decryptToken$1, hashToken: hashToken$2 } = encryption;
|
|
389
421
|
const { createLogger: createLogger$3 } = logger;
|
|
390
422
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
391
423
|
const USER_UID$2 = "plugin::users-permissions.user";
|
|
@@ -419,11 +451,11 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
419
451
|
log.info("║ [SUCCESS] SESSION MANAGER LICENSE ACTIVE ║");
|
|
420
452
|
log.info("║ ║");
|
|
421
453
|
if (licenseStatus.data) {
|
|
422
|
-
|
|
454
|
+
const maskedKey = licenseStatus.data.licenseKey ? `${licenseStatus.data.licenseKey.substring(0, 8)}...` : "N/A";
|
|
455
|
+
log.info(`║ License: ${maskedKey}`.padEnd(66) + "║");
|
|
423
456
|
log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + "║");
|
|
424
|
-
log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + "║");
|
|
425
457
|
} else if (storedKey) {
|
|
426
|
-
log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + "║");
|
|
458
|
+
log.info(`║ License: ${storedKey.substring(0, 8)}... (Offline Mode)`.padEnd(66) + "║");
|
|
427
459
|
log.info(`║ Status: Grace Period Active`.padEnd(66) + "║");
|
|
428
460
|
}
|
|
429
461
|
log.info("║ ║");
|
|
@@ -455,10 +487,21 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
455
487
|
try {
|
|
456
488
|
const token = ctx.request.headers?.authorization?.replace("Bearer ", "");
|
|
457
489
|
if (!token) {
|
|
458
|
-
ctx.status =
|
|
459
|
-
ctx.body = { message: "
|
|
490
|
+
ctx.status = 401;
|
|
491
|
+
ctx.body = { error: { status: 401, message: "Authorization token required" } };
|
|
460
492
|
return;
|
|
461
493
|
}
|
|
494
|
+
try {
|
|
495
|
+
const jwtService = strapi2.plugin("users-permissions").service("jwt");
|
|
496
|
+
const decoded = await jwtService.verify(token);
|
|
497
|
+
if (!decoded || !decoded.id) {
|
|
498
|
+
ctx.status = 401;
|
|
499
|
+
ctx.body = { error: { status: 401, message: "Invalid token" } };
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
} catch (jwtErr) {
|
|
503
|
+
log.debug("JWT verify failed during logout (cleaning up anyway):", jwtErr.message);
|
|
504
|
+
}
|
|
462
505
|
const tokenHashValue = hashToken$2(token);
|
|
463
506
|
const matchingSession = await strapi2.documents(SESSION_UID$3).findFirst({
|
|
464
507
|
filters: {
|
|
@@ -480,6 +523,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
480
523
|
},
|
|
481
524
|
config: {
|
|
482
525
|
auth: false
|
|
526
|
+
// We handle auth manually above to support expired-but-valid tokens
|
|
483
527
|
}
|
|
484
528
|
}]);
|
|
485
529
|
log.info("[SUCCESS] /api/auth/logout route registered");
|
|
@@ -539,8 +583,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
539
583
|
ctx.body = {
|
|
540
584
|
error: {
|
|
541
585
|
status: 403,
|
|
542
|
-
message: "Login blocked for security reasons"
|
|
543
|
-
details: { reason: blockReason }
|
|
586
|
+
message: "Login blocked for security reasons. Please contact support."
|
|
544
587
|
}
|
|
545
588
|
};
|
|
546
589
|
return;
|
|
@@ -602,11 +645,22 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
602
645
|
geoData
|
|
603
646
|
});
|
|
604
647
|
if (config2.discordWebhookUrl) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
648
|
+
const webhookUrl = config2.discordWebhookUrl;
|
|
649
|
+
try {
|
|
650
|
+
const parsed = new URL(webhookUrl);
|
|
651
|
+
const isValidDomain = parsed.protocol === "https:" && (parsed.hostname === "discord.com" || parsed.hostname === "discordapp.com" || parsed.hostname.endsWith(".discord.com") || parsed.hostname === "hooks.slack.com");
|
|
652
|
+
if (isValidDomain) {
|
|
653
|
+
await notificationService.sendWebhook({
|
|
654
|
+
event: "session.login",
|
|
655
|
+
data: webhookData,
|
|
656
|
+
webhookUrl
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
log.warn(`[SECURITY] Blocked webhook to untrusted domain: ${parsed.hostname}`);
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
log.warn("[SECURITY] Invalid webhook URL in plugin config");
|
|
663
|
+
}
|
|
610
664
|
}
|
|
611
665
|
}
|
|
612
666
|
} catch (notifErr) {
|
|
@@ -787,6 +841,30 @@ async function ensureTokenHashIndex(strapi2, log) {
|
|
|
787
841
|
log.debug("[INDEX] Could not create tokenHash index (will retry on next startup):", err.message);
|
|
788
842
|
}
|
|
789
843
|
}
|
|
844
|
+
const sessionCheckErrors = { count: 0, lastReset: Date.now() };
|
|
845
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
846
|
+
const ERROR_RESET_INTERVAL = 60 * 1e3;
|
|
847
|
+
function shouldFailOpen() {
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
if (now - sessionCheckErrors.lastReset > ERROR_RESET_INTERVAL) {
|
|
850
|
+
sessionCheckErrors.count = 0;
|
|
851
|
+
sessionCheckErrors.lastReset = now;
|
|
852
|
+
}
|
|
853
|
+
sessionCheckErrors.count++;
|
|
854
|
+
if (sessionCheckErrors.count > MAX_CONSECUTIVE_ERRORS) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
function resetErrorCounter() {
|
|
860
|
+
sessionCheckErrors.count = 0;
|
|
861
|
+
}
|
|
862
|
+
function isSessionExpired(session2, maxAgeDays = 30) {
|
|
863
|
+
if (!session2.loginTime) return false;
|
|
864
|
+
const loginTime = new Date(session2.loginTime).getTime();
|
|
865
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
866
|
+
return Date.now() - loginTime > maxAgeMs;
|
|
867
|
+
}
|
|
790
868
|
async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
791
869
|
try {
|
|
792
870
|
const usersPermissionsPlugin = strapi2.plugin("users-permissions");
|
|
@@ -808,15 +886,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
808
886
|
}
|
|
809
887
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
810
888
|
const strictMode = config2.strictSessionEnforcement === true;
|
|
889
|
+
const maxSessionAgeDays = config2.maxSessionAgeDays || 30;
|
|
811
890
|
try {
|
|
812
891
|
const tokenHashValue = hashToken$2(token);
|
|
813
|
-
let userDocId = null;
|
|
814
892
|
const user = await strapi2.entityService.findOne(
|
|
815
893
|
"plugin::users-permissions.user",
|
|
816
894
|
decoded.id,
|
|
817
895
|
{ fields: ["documentId"] }
|
|
818
896
|
);
|
|
819
|
-
userDocId = user?.documentId;
|
|
897
|
+
const userDocId = user?.documentId;
|
|
820
898
|
if (!userDocId) {
|
|
821
899
|
strapi2.log.debug("[magic-sessionmanager] [JWT] No documentId found, allowing through");
|
|
822
900
|
return decoded;
|
|
@@ -826,9 +904,19 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
826
904
|
user: { documentId: userDocId },
|
|
827
905
|
tokenHash: tokenHashValue
|
|
828
906
|
},
|
|
829
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive"]
|
|
907
|
+
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
830
908
|
});
|
|
831
909
|
if (thisSession) {
|
|
910
|
+
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
911
|
+
strapi2.log.info(
|
|
912
|
+
`[magic-sessionmanager] [JWT-EXPIRED] Session exceeded max age of ${maxSessionAgeDays} days (user: ${userDocId.substring(0, 8)}...)`
|
|
913
|
+
);
|
|
914
|
+
await strapi2.documents(SESSION_UID$3).update({
|
|
915
|
+
documentId: thisSession.documentId,
|
|
916
|
+
data: { isActive: false, terminatedManually: true, logoutTime: /* @__PURE__ */ new Date() }
|
|
917
|
+
});
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
832
920
|
if (thisSession.terminatedManually === true) {
|
|
833
921
|
strapi2.log.info(
|
|
834
922
|
`[magic-sessionmanager] [JWT-BLOCKED] Session was manually terminated (user: ${userDocId.substring(0, 8)}...)`
|
|
@@ -836,38 +924,32 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
836
924
|
return null;
|
|
837
925
|
}
|
|
838
926
|
if (thisSession.isActive) {
|
|
927
|
+
resetErrorCounter();
|
|
839
928
|
return decoded;
|
|
840
929
|
}
|
|
841
930
|
await strapi2.documents(SESSION_UID$3).update({
|
|
842
931
|
documentId: thisSession.documentId,
|
|
843
|
-
data: {
|
|
844
|
-
isActive: true,
|
|
845
|
-
lastActive: /* @__PURE__ */ new Date()
|
|
846
|
-
}
|
|
932
|
+
data: { isActive: true, lastActive: /* @__PURE__ */ new Date() }
|
|
847
933
|
});
|
|
848
934
|
strapi2.log.info(
|
|
849
935
|
`[magic-sessionmanager] [JWT-REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`
|
|
850
936
|
);
|
|
937
|
+
resetErrorCounter();
|
|
851
938
|
return decoded;
|
|
852
939
|
}
|
|
853
940
|
const anyActiveSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
854
|
-
filters: {
|
|
855
|
-
user: { documentId: userDocId },
|
|
856
|
-
isActive: true
|
|
857
|
-
},
|
|
941
|
+
filters: { user: { documentId: userDocId }, isActive: true },
|
|
858
942
|
limit: 1
|
|
859
943
|
});
|
|
860
944
|
if (anyActiveSessions && anyActiveSessions.length > 0) {
|
|
861
945
|
strapi2.log.debug(
|
|
862
946
|
`[magic-sessionmanager] [JWT] No session for token but user has other active sessions (allowing)`
|
|
863
947
|
);
|
|
948
|
+
resetErrorCounter();
|
|
864
949
|
return decoded;
|
|
865
950
|
}
|
|
866
951
|
const terminatedSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
867
|
-
filters: {
|
|
868
|
-
user: { documentId: userDocId },
|
|
869
|
-
terminatedManually: true
|
|
870
|
-
},
|
|
952
|
+
filters: { user: { documentId: userDocId }, terminatedManually: true },
|
|
871
953
|
limit: 1
|
|
872
954
|
});
|
|
873
955
|
if (terminatedSessions && terminatedSessions.length > 0) {
|
|
@@ -885,10 +967,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
885
967
|
strapi2.log.warn(
|
|
886
968
|
`[magic-sessionmanager] [JWT-WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`
|
|
887
969
|
);
|
|
970
|
+
resetErrorCounter();
|
|
888
971
|
return decoded;
|
|
889
972
|
} catch (err) {
|
|
890
|
-
|
|
891
|
-
|
|
973
|
+
if (shouldFailOpen()) {
|
|
974
|
+
strapi2.log.warn("[magic-sessionmanager] [JWT] Session check error (allowing):", err.message);
|
|
975
|
+
return decoded;
|
|
976
|
+
}
|
|
977
|
+
strapi2.log.error("[magic-sessionmanager] [JWT] Too many consecutive errors, blocking request:", err.message);
|
|
978
|
+
return null;
|
|
892
979
|
}
|
|
893
980
|
};
|
|
894
981
|
strapi2.log.info("[magic-sessionmanager] [AUTH] [SUCCESS] JWT verify wrapped with session validation");
|
|
@@ -1198,7 +1285,7 @@ var admin$1 = {
|
|
|
1198
1285
|
path: "/license/status",
|
|
1199
1286
|
handler: "license.getStatus",
|
|
1200
1287
|
config: {
|
|
1201
|
-
policies: []
|
|
1288
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1202
1289
|
}
|
|
1203
1290
|
},
|
|
1204
1291
|
{
|
|
@@ -1206,7 +1293,7 @@ var admin$1 = {
|
|
|
1206
1293
|
path: "/license/auto-create",
|
|
1207
1294
|
handler: "license.autoCreate",
|
|
1208
1295
|
config: {
|
|
1209
|
-
policies: []
|
|
1296
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1210
1297
|
}
|
|
1211
1298
|
},
|
|
1212
1299
|
{
|
|
@@ -1214,7 +1301,7 @@ var admin$1 = {
|
|
|
1214
1301
|
path: "/license/create",
|
|
1215
1302
|
handler: "license.createAndActivate",
|
|
1216
1303
|
config: {
|
|
1217
|
-
policies: []
|
|
1304
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1218
1305
|
}
|
|
1219
1306
|
},
|
|
1220
1307
|
{
|
|
@@ -1222,7 +1309,7 @@ var admin$1 = {
|
|
|
1222
1309
|
path: "/license/ping",
|
|
1223
1310
|
handler: "license.ping",
|
|
1224
1311
|
config: {
|
|
1225
|
-
policies: []
|
|
1312
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1226
1313
|
}
|
|
1227
1314
|
},
|
|
1228
1315
|
{
|
|
@@ -1230,7 +1317,7 @@ var admin$1 = {
|
|
|
1230
1317
|
path: "/license/store-key",
|
|
1231
1318
|
handler: "license.storeKey",
|
|
1232
1319
|
config: {
|
|
1233
|
-
policies: []
|
|
1320
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1234
1321
|
}
|
|
1235
1322
|
},
|
|
1236
1323
|
// Geolocation (Premium Feature)
|
|
@@ -1353,7 +1440,7 @@ function extractVersion(userAgent, regex) {
|
|
|
1353
1440
|
var userAgentParser = {
|
|
1354
1441
|
parseUserAgent: parseUserAgent$2
|
|
1355
1442
|
};
|
|
1356
|
-
const {
|
|
1443
|
+
const { hashToken: hashToken$1 } = encryption;
|
|
1357
1444
|
const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
|
|
1358
1445
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
1359
1446
|
const USER_UID$1 = "plugin::users-permissions.user";
|
|
@@ -1413,7 +1500,8 @@ var session$3 = {
|
|
|
1413
1500
|
}
|
|
1414
1501
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
1415
1502
|
filters: { user: { documentId: userId } },
|
|
1416
|
-
sort: { loginTime: "desc" }
|
|
1503
|
+
sort: { loginTime: "desc" },
|
|
1504
|
+
limit: 200
|
|
1417
1505
|
});
|
|
1418
1506
|
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1419
1507
|
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
@@ -1451,7 +1539,7 @@ var session$3 = {
|
|
|
1451
1539
|
strapi.documents(SESSION_UID$2).update({
|
|
1452
1540
|
documentId: session2.documentId,
|
|
1453
1541
|
data: {
|
|
1454
|
-
geoLocation
|
|
1542
|
+
geoLocation,
|
|
1455
1543
|
securityScore: geoData.securityScore || null
|
|
1456
1544
|
}
|
|
1457
1545
|
}).catch(() => {
|
|
@@ -1513,9 +1601,15 @@ var session$3 = {
|
|
|
1513
1601
|
const { userId } = ctx.params;
|
|
1514
1602
|
const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
|
|
1515
1603
|
const requestingUserDocId = ctx.state.user?.documentId;
|
|
1516
|
-
if (!isAdminRequest
|
|
1517
|
-
|
|
1518
|
-
|
|
1604
|
+
if (!isAdminRequest) {
|
|
1605
|
+
if (!requestingUserDocId) {
|
|
1606
|
+
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
1607
|
+
return ctx.forbidden("Cannot verify user identity");
|
|
1608
|
+
}
|
|
1609
|
+
if (String(requestingUserDocId) !== String(userId)) {
|
|
1610
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
|
|
1611
|
+
return ctx.forbidden("You can only access your own sessions");
|
|
1612
|
+
}
|
|
1519
1613
|
}
|
|
1520
1614
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1521
1615
|
const sessions = await sessionService.getUserSessions(userId);
|
|
@@ -1639,7 +1733,7 @@ var session$3 = {
|
|
|
1639
1733
|
strapi.documents(SESSION_UID$2).update({
|
|
1640
1734
|
documentId: currentSession.documentId,
|
|
1641
1735
|
data: {
|
|
1642
|
-
geoLocation
|
|
1736
|
+
geoLocation,
|
|
1643
1737
|
securityScore: geoData.securityScore || null
|
|
1644
1738
|
}
|
|
1645
1739
|
}).catch(() => {
|
|
@@ -1721,22 +1815,6 @@ var session$3 = {
|
|
|
1721
1815
|
ctx.throw(500, "Error terminating session");
|
|
1722
1816
|
}
|
|
1723
1817
|
},
|
|
1724
|
-
/**
|
|
1725
|
-
* Terminate specific session
|
|
1726
|
-
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
1727
|
-
*/
|
|
1728
|
-
async terminateSession(ctx) {
|
|
1729
|
-
try {
|
|
1730
|
-
const { sessionId } = ctx.params;
|
|
1731
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1732
|
-
await sessionService.terminateSession({ sessionId });
|
|
1733
|
-
ctx.body = {
|
|
1734
|
-
message: `Session ${sessionId} terminated`
|
|
1735
|
-
};
|
|
1736
|
-
} catch (err) {
|
|
1737
|
-
ctx.throw(500, "Error terminating session");
|
|
1738
|
-
}
|
|
1739
|
-
},
|
|
1740
1818
|
/**
|
|
1741
1819
|
* Simulate session timeout for testing (Admin action)
|
|
1742
1820
|
* POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
@@ -1745,6 +1823,10 @@ var session$3 = {
|
|
|
1745
1823
|
*/
|
|
1746
1824
|
async simulateTimeout(ctx) {
|
|
1747
1825
|
try {
|
|
1826
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
1827
|
+
if (nodeEnv === "production") {
|
|
1828
|
+
return ctx.forbidden("simulate-timeout is disabled in production");
|
|
1829
|
+
}
|
|
1748
1830
|
const { sessionId } = ctx.params;
|
|
1749
1831
|
const session2 = await strapi.documents(SESSION_UID$2).findOne({
|
|
1750
1832
|
documentId: sessionId
|
|
@@ -1817,6 +1899,17 @@ var session$3 = {
|
|
|
1817
1899
|
if (!ipAddress) {
|
|
1818
1900
|
return ctx.badRequest("IP address is required");
|
|
1819
1901
|
}
|
|
1902
|
+
const IPV4_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1903
|
+
const ipv4Match = ipAddress.match(IPV4_REGEX);
|
|
1904
|
+
const isValidIpv4 = ipv4Match && ipv4Match.slice(1).every((octet) => {
|
|
1905
|
+
const n = parseInt(octet, 10);
|
|
1906
|
+
return n >= 0 && n <= 255;
|
|
1907
|
+
});
|
|
1908
|
+
const IPV6_REGEX = /^[0-9a-fA-F:]{3,45}$/;
|
|
1909
|
+
const isValidIpv6 = !isValidIpv4 && IPV6_REGEX.test(ipAddress) && (ipAddress.match(/:/g) || []).length >= 2;
|
|
1910
|
+
if (!isValidIpv4 && !isValidIpv6) {
|
|
1911
|
+
return ctx.badRequest("Invalid IP address format");
|
|
1912
|
+
}
|
|
1820
1913
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
1821
1914
|
const pluginStore = strapi.store({
|
|
1822
1915
|
type: "plugin",
|
|
@@ -1976,12 +2069,11 @@ var license$1 = ({ strapi: strapi2 }) => ({
|
|
|
1976
2069
|
message: "No license found. Running in demo mode."
|
|
1977
2070
|
});
|
|
1978
2071
|
}
|
|
1979
|
-
strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey}
|
|
2072
|
+
strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey.substring(0, 8)}...`);
|
|
1980
2073
|
const verification = await licenseGuard2.verifyLicense(licenseKey);
|
|
1981
2074
|
const license2 = await licenseGuard2.getLicenseByKey(licenseKey);
|
|
1982
2075
|
strapi2.log.info("[magic-sessionmanager/license-controller] License data from MagicAPI:", {
|
|
1983
|
-
licenseKey: license2?.licenseKey,
|
|
1984
|
-
email: license2?.email,
|
|
2076
|
+
licenseKey: license2?.licenseKey ? `${license2.licenseKey.substring(0, 8)}...` : "N/A",
|
|
1985
2077
|
featurePremium: license2?.featurePremium,
|
|
1986
2078
|
isActive: license2?.isActive,
|
|
1987
2079
|
pluginName: license2?.pluginName
|
|
@@ -2125,6 +2217,55 @@ var license$1 = ({ strapi: strapi2 }) => ({
|
|
|
2125
2217
|
}
|
|
2126
2218
|
}
|
|
2127
2219
|
});
|
|
2220
|
+
const ALLOWED_WEBHOOK_DOMAINS = {
|
|
2221
|
+
discord: ["discord.com", "discordapp.com"],
|
|
2222
|
+
slack: ["hooks.slack.com"]
|
|
2223
|
+
};
|
|
2224
|
+
function sanitizeWebhookUrl(url, type2) {
|
|
2225
|
+
if (!url || typeof url !== "string") return "";
|
|
2226
|
+
const trimmed = url.trim();
|
|
2227
|
+
if (!trimmed) return "";
|
|
2228
|
+
try {
|
|
2229
|
+
const parsed = new URL(trimmed);
|
|
2230
|
+
if (parsed.protocol !== "https:") return "";
|
|
2231
|
+
const allowedDomains = ALLOWED_WEBHOOK_DOMAINS[type2] || [];
|
|
2232
|
+
const isAllowed = allowedDomains.some(
|
|
2233
|
+
(domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)
|
|
2234
|
+
);
|
|
2235
|
+
if (!isAllowed) return "";
|
|
2236
|
+
return trimmed;
|
|
2237
|
+
} catch {
|
|
2238
|
+
return "";
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function sanitizeCountryList(list) {
|
|
2242
|
+
if (!Array.isArray(list)) return [];
|
|
2243
|
+
return list.filter((code) => typeof code === "string" && /^[A-Z]{2}$/.test(code.trim().toUpperCase())).map((code) => code.trim().toUpperCase());
|
|
2244
|
+
}
|
|
2245
|
+
function sanitizeEmailTemplates(templates) {
|
|
2246
|
+
const defaults = {
|
|
2247
|
+
suspiciousLogin: { subject: "", html: "", text: "" },
|
|
2248
|
+
newLocation: { subject: "", html: "", text: "" },
|
|
2249
|
+
vpnProxy: { subject: "", html: "", text: "" }
|
|
2250
|
+
};
|
|
2251
|
+
if (!templates || typeof templates !== "object") return defaults;
|
|
2252
|
+
const dangerousTags = /<\s*\/?\s*(script|iframe|object|embed|form|input|button|link|meta|base)\b[^>]*>/gi;
|
|
2253
|
+
const dangerousAttrs = /\s(on\w+|javascript\s*:)[^=]*=/gi;
|
|
2254
|
+
const result = {};
|
|
2255
|
+
for (const [key, defaultVal] of Object.entries(defaults)) {
|
|
2256
|
+
const tpl = templates[key];
|
|
2257
|
+
if (!tpl || typeof tpl !== "object") {
|
|
2258
|
+
result[key] = defaultVal;
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
result[key] = {
|
|
2262
|
+
subject: typeof tpl.subject === "string" ? tpl.subject.substring(0, 200) : "",
|
|
2263
|
+
html: typeof tpl.html === "string" ? tpl.html.replace(dangerousTags, "").replace(dangerousAttrs, " ").substring(0, 1e4) : "",
|
|
2264
|
+
text: typeof tpl.text === "string" ? tpl.text.substring(0, 5e3) : ""
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
return result;
|
|
2268
|
+
}
|
|
2128
2269
|
var settings$1 = {
|
|
2129
2270
|
/**
|
|
2130
2271
|
* Get plugin settings
|
|
@@ -2186,29 +2327,26 @@ var settings$1 = {
|
|
|
2186
2327
|
name: "magic-sessionmanager"
|
|
2187
2328
|
});
|
|
2188
2329
|
const sanitizedSettings = {
|
|
2189
|
-
inactivityTimeout: parseInt(body.inactivityTimeout) || 15,
|
|
2190
|
-
cleanupInterval: parseInt(body.cleanupInterval) || 30,
|
|
2191
|
-
lastSeenRateLimit: parseInt(body.lastSeenRateLimit) || 30,
|
|
2192
|
-
retentionDays: parseInt(body.retentionDays) || 90,
|
|
2330
|
+
inactivityTimeout: Math.max(1, Math.min(parseInt(body.inactivityTimeout) || 15, 1440)),
|
|
2331
|
+
cleanupInterval: Math.max(5, Math.min(parseInt(body.cleanupInterval) || 30, 1440)),
|
|
2332
|
+
lastSeenRateLimit: Math.max(5, Math.min(parseInt(body.lastSeenRateLimit) || 30, 300)),
|
|
2333
|
+
retentionDays: Math.max(1, Math.min(parseInt(body.retentionDays) || 90, 365)),
|
|
2334
|
+
maxSessionAgeDays: Math.max(1, Math.min(parseInt(body.maxSessionAgeDays) || 30, 365)),
|
|
2193
2335
|
enableGeolocation: !!body.enableGeolocation,
|
|
2194
2336
|
enableSecurityScoring: !!body.enableSecurityScoring,
|
|
2195
2337
|
blockSuspiciousSessions: !!body.blockSuspiciousSessions,
|
|
2196
|
-
maxFailedLogins: parseInt(body.maxFailedLogins) || 5,
|
|
2338
|
+
maxFailedLogins: Math.max(1, Math.min(parseInt(body.maxFailedLogins) || 5, 100)),
|
|
2197
2339
|
enableEmailAlerts: !!body.enableEmailAlerts,
|
|
2198
2340
|
alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
|
|
2199
2341
|
alertOnNewLocation: !!body.alertOnNewLocation,
|
|
2200
2342
|
alertOnVpnProxy: !!body.alertOnVpnProxy,
|
|
2201
2343
|
enableWebhooks: !!body.enableWebhooks,
|
|
2202
|
-
discordWebhookUrl:
|
|
2203
|
-
slackWebhookUrl:
|
|
2344
|
+
discordWebhookUrl: sanitizeWebhookUrl(body.discordWebhookUrl, "discord"),
|
|
2345
|
+
slackWebhookUrl: sanitizeWebhookUrl(body.slackWebhookUrl, "slack"),
|
|
2204
2346
|
enableGeofencing: !!body.enableGeofencing,
|
|
2205
|
-
allowedCountries:
|
|
2206
|
-
blockedCountries:
|
|
2207
|
-
emailTemplates: body.emailTemplates
|
|
2208
|
-
suspiciousLogin: { subject: "", html: "", text: "" },
|
|
2209
|
-
newLocation: { subject: "", html: "", text: "" },
|
|
2210
|
-
vpnProxy: { subject: "", html: "", text: "" }
|
|
2211
|
-
}
|
|
2347
|
+
allowedCountries: sanitizeCountryList(body.allowedCountries),
|
|
2348
|
+
blockedCountries: sanitizeCountryList(body.blockedCountries),
|
|
2349
|
+
emailTemplates: sanitizeEmailTemplates(body.emailTemplates)
|
|
2212
2350
|
};
|
|
2213
2351
|
await pluginStore.set({
|
|
2214
2352
|
key: "settings",
|
|
@@ -2279,15 +2417,15 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2279
2417
|
deviceType: parsedUA.deviceType,
|
|
2280
2418
|
browserName: parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName,
|
|
2281
2419
|
osName: parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName,
|
|
2282
|
-
// Geolocation data (
|
|
2283
|
-
geoLocation: geoData ?
|
|
2420
|
+
// Geolocation data (stored as JSON object, schema type: json)
|
|
2421
|
+
geoLocation: geoData ? {
|
|
2284
2422
|
country: geoData.country,
|
|
2285
2423
|
country_code: geoData.country_code,
|
|
2286
2424
|
country_flag: geoData.country_flag,
|
|
2287
2425
|
city: geoData.city,
|
|
2288
2426
|
region: geoData.region,
|
|
2289
2427
|
timezone: geoData.timezone
|
|
2290
|
-
}
|
|
2428
|
+
} : null,
|
|
2291
2429
|
securityScore: geoData?.securityScore || null
|
|
2292
2430
|
}
|
|
2293
2431
|
});
|
|
@@ -2308,6 +2446,14 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2308
2446
|
try {
|
|
2309
2447
|
const now = /* @__PURE__ */ new Date();
|
|
2310
2448
|
if (sessionId) {
|
|
2449
|
+
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
2450
|
+
documentId: sessionId,
|
|
2451
|
+
fields: ["documentId"]
|
|
2452
|
+
});
|
|
2453
|
+
if (!existing) {
|
|
2454
|
+
log.warn(`Session ${sessionId} not found for termination`);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2311
2457
|
await strapi2.documents(SESSION_UID$1).update({
|
|
2312
2458
|
documentId: sessionId,
|
|
2313
2459
|
data: {
|
|
@@ -2399,7 +2545,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2399
2545
|
strapi2.documents(SESSION_UID$1).update({
|
|
2400
2546
|
documentId: session2.documentId,
|
|
2401
2547
|
data: {
|
|
2402
|
-
geoLocation
|
|
2548
|
+
geoLocation,
|
|
2403
2549
|
securityScore: geoData.securityScore || null
|
|
2404
2550
|
}
|
|
2405
2551
|
}).catch(() => {
|
|
@@ -2449,7 +2595,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2449
2595
|
const sessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2450
2596
|
filters: { isActive: true },
|
|
2451
2597
|
populate: { user: { fields: ["documentId", "email", "username"] } },
|
|
2452
|
-
sort: { loginTime: "desc" }
|
|
2598
|
+
sort: { loginTime: "desc" },
|
|
2599
|
+
limit: 1e3
|
|
2453
2600
|
});
|
|
2454
2601
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
2455
2602
|
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
@@ -2486,7 +2633,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2486
2633
|
strapi2.documents(SESSION_UID$1).update({
|
|
2487
2634
|
documentId: session2.documentId,
|
|
2488
2635
|
data: {
|
|
2489
|
-
geoLocation
|
|
2636
|
+
geoLocation,
|
|
2490
2637
|
securityScore: geoData.securityScore || null
|
|
2491
2638
|
}
|
|
2492
2639
|
}).catch(() => {
|
|
@@ -2579,7 +2726,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2579
2726
|
strapi2.documents(SESSION_UID$1).update({
|
|
2580
2727
|
documentId: session2.documentId,
|
|
2581
2728
|
data: {
|
|
2582
|
-
geoLocation
|
|
2729
|
+
geoLocation,
|
|
2583
2730
|
securityScore: geoData.securityScore || null
|
|
2584
2731
|
}
|
|
2585
2732
|
}).catch(() => {
|
|
@@ -2716,20 +2863,36 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2716
2863
|
}
|
|
2717
2864
|
},
|
|
2718
2865
|
/**
|
|
2719
|
-
* Delete all inactive sessions from database
|
|
2866
|
+
* Delete all inactive sessions from database in batches
|
|
2720
2867
|
* WARNING: This permanently deletes records!
|
|
2721
2868
|
* @returns {Promise<number>} Number of deleted sessions
|
|
2722
2869
|
*/
|
|
2723
2870
|
async deleteInactiveSessions() {
|
|
2724
2871
|
try {
|
|
2725
2872
|
log.info("[DELETE] Deleting all inactive sessions...");
|
|
2726
|
-
const inactiveSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2727
|
-
filters: { isActive: false }
|
|
2728
|
-
});
|
|
2729
2873
|
let deletedCount = 0;
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2874
|
+
const BATCH_SIZE = 100;
|
|
2875
|
+
let hasMore = true;
|
|
2876
|
+
while (hasMore) {
|
|
2877
|
+
const batch = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2878
|
+
filters: { isActive: false },
|
|
2879
|
+
fields: ["documentId"],
|
|
2880
|
+
limit: BATCH_SIZE
|
|
2881
|
+
});
|
|
2882
|
+
if (!batch || batch.length === 0) {
|
|
2883
|
+
hasMore = false;
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
const deleteResults = await Promise.allSettled(
|
|
2887
|
+
batch.map(
|
|
2888
|
+
(session2) => strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId })
|
|
2889
|
+
)
|
|
2890
|
+
);
|
|
2891
|
+
const batchDeleted = deleteResults.filter((r) => r.status === "fulfilled").length;
|
|
2892
|
+
deletedCount += batchDeleted;
|
|
2893
|
+
if (batch.length < BATCH_SIZE) {
|
|
2894
|
+
hasMore = false;
|
|
2895
|
+
}
|
|
2733
2896
|
}
|
|
2734
2897
|
log.info(`[SUCCESS] Deleted ${deletedCount} inactive sessions`);
|
|
2735
2898
|
return deletedCount;
|
|
@@ -2740,7 +2903,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2740
2903
|
}
|
|
2741
2904
|
};
|
|
2742
2905
|
};
|
|
2743
|
-
const version$1 = "4.4.
|
|
2906
|
+
const version$1 = "4.4.5";
|
|
2744
2907
|
const require$$2 = {
|
|
2745
2908
|
version: version$1
|
|
2746
2909
|
};
|
|
@@ -3014,7 +3177,7 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
|
|
|
3014
3177
|
*/
|
|
3015
3178
|
async getIpInfo(ipAddress) {
|
|
3016
3179
|
try {
|
|
3017
|
-
if (!ipAddress ||
|
|
3180
|
+
if (!ipAddress || this.isPrivateIp(ipAddress)) {
|
|
3018
3181
|
return {
|
|
3019
3182
|
ip: ipAddress,
|
|
3020
3183
|
country: "Local Network",
|
|
@@ -3117,6 +3280,26 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
|
|
|
3117
3280
|
const codePoints = countryCode.toUpperCase().split("").map((char) => 127397 + char.charCodeAt());
|
|
3118
3281
|
return String.fromCodePoint(...codePoints);
|
|
3119
3282
|
},
|
|
3283
|
+
/**
|
|
3284
|
+
* Checks if an IP address is private/local (RFC 1918, RFC 4193, loopback, link-local)
|
|
3285
|
+
* @param {string} ip - IP address to check
|
|
3286
|
+
* @returns {boolean} True if IP is private/local
|
|
3287
|
+
*/
|
|
3288
|
+
isPrivateIp(ip) {
|
|
3289
|
+
if (!ip || ip === "unknown") return true;
|
|
3290
|
+
if (ip === "127.0.0.1" || ip === "localhost" || ip === "::1") return true;
|
|
3291
|
+
if (ip.startsWith("192.168.")) return true;
|
|
3292
|
+
if (ip.startsWith("10.")) return true;
|
|
3293
|
+
if (ip.startsWith("172.")) {
|
|
3294
|
+
const second = parseInt(ip.split(".")[1], 10);
|
|
3295
|
+
if (second >= 16 && second <= 31) return true;
|
|
3296
|
+
}
|
|
3297
|
+
if (ip.startsWith("169.254.")) return true;
|
|
3298
|
+
if (ip.startsWith("fc00:") || ip.startsWith("fd00:")) return true;
|
|
3299
|
+
if (ip.startsWith("fe80:")) return true;
|
|
3300
|
+
if (ip === "::1") return true;
|
|
3301
|
+
return false;
|
|
3302
|
+
},
|
|
3120
3303
|
/**
|
|
3121
3304
|
* Fallback data when API fails
|
|
3122
3305
|
*/
|
|
@@ -3252,25 +3435,36 @@ VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
|
|
|
3252
3435
|
};
|
|
3253
3436
|
},
|
|
3254
3437
|
/**
|
|
3255
|
-
*
|
|
3438
|
+
* Escapes HTML special characters to prevent XSS in email templates
|
|
3439
|
+
* @param {string} str - String to escape
|
|
3440
|
+
* @returns {string} HTML-safe string
|
|
3441
|
+
*/
|
|
3442
|
+
escapeHtml(str2) {
|
|
3443
|
+
if (!str2 || typeof str2 !== "string") return str2 || "";
|
|
3444
|
+
return str2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3445
|
+
},
|
|
3446
|
+
/**
|
|
3447
|
+
* Replace template variables with HTML-escaped actual values
|
|
3448
|
+
* SECURITY: All dynamic values are HTML-escaped to prevent XSS via email
|
|
3256
3449
|
*/
|
|
3257
3450
|
replaceVariables(template2, data) {
|
|
3258
3451
|
let result = template2;
|
|
3259
|
-
|
|
3260
|
-
result = result.replace(/\{\{user\.
|
|
3452
|
+
const esc2 = this.escapeHtml.bind(this);
|
|
3453
|
+
result = result.replace(/\{\{user\.email\}\}/g, esc2(data.user?.email || "N/A"));
|
|
3454
|
+
result = result.replace(/\{\{user\.username\}\}/g, esc2(data.user?.username || "N/A"));
|
|
3261
3455
|
result = result.replace(
|
|
3262
3456
|
/\{\{session\.loginTime\}\}/g,
|
|
3263
|
-
data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A"
|
|
3457
|
+
esc2(data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A")
|
|
3264
3458
|
);
|
|
3265
|
-
result = result.replace(/\{\{session\.ipAddress\}\}/g, data.session?.ipAddress || "N/A");
|
|
3266
|
-
result = result.replace(/\{\{session\.userAgent\}\}/g, data.session?.userAgent || "N/A");
|
|
3267
|
-
result = result.replace(/\{\{geo\.city\}\}/g, data.geoData?.city || "Unknown");
|
|
3268
|
-
result = result.replace(/\{\{geo\.country\}\}/g, data.geoData?.country || "Unknown");
|
|
3269
|
-
result = result.replace(/\{\{geo\.timezone\}\}/g, data.geoData?.timezone || "Unknown");
|
|
3459
|
+
result = result.replace(/\{\{session\.ipAddress\}\}/g, esc2(data.session?.ipAddress || "N/A"));
|
|
3460
|
+
result = result.replace(/\{\{session\.userAgent\}\}/g, esc2(data.session?.userAgent || "N/A"));
|
|
3461
|
+
result = result.replace(/\{\{geo\.city\}\}/g, esc2(data.geoData?.city || "Unknown"));
|
|
3462
|
+
result = result.replace(/\{\{geo\.country\}\}/g, esc2(data.geoData?.country || "Unknown"));
|
|
3463
|
+
result = result.replace(/\{\{geo\.timezone\}\}/g, esc2(data.geoData?.timezone || "Unknown"));
|
|
3270
3464
|
result = result.replace(/\{\{reason\.isVpn\}\}/g, data.reason?.isVpn ? "Yes" : "No");
|
|
3271
3465
|
result = result.replace(/\{\{reason\.isProxy\}\}/g, data.reason?.isProxy ? "Yes" : "No");
|
|
3272
3466
|
result = result.replace(/\{\{reason\.isThreat\}\}/g, data.reason?.isThreat ? "Yes" : "No");
|
|
3273
|
-
result = result.replace(/\{\{reason\.securityScore\}\}/g, data.reason?.securityScore || "0");
|
|
3467
|
+
result = result.replace(/\{\{reason\.securityScore\}\}/g, esc2(String(data.reason?.securityScore || "0")));
|
|
3274
3468
|
return result;
|
|
3275
3469
|
},
|
|
3276
3470
|
/**
|
|
@@ -39046,7 +39240,7 @@ const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
|
|
|
39046
39240
|
const require$$0 = /* @__PURE__ */ getAugmentedNamespace(dist);
|
|
39047
39241
|
const SESSION_UID = "plugin::magic-sessionmanager.session";
|
|
39048
39242
|
const { errors } = require$$0;
|
|
39049
|
-
var sessionRequired$1 = async (policyContext,
|
|
39243
|
+
var sessionRequired$1 = async (policyContext, _policyConfig, { strapi: strapi2 }) => {
|
|
39050
39244
|
if (!policyContext.state.user) {
|
|
39051
39245
|
return true;
|
|
39052
39246
|
}
|
|
@@ -39064,8 +39258,8 @@ var sessionRequired$1 = async (policyContext, config2, { strapi: strapi2 }) => {
|
|
|
39064
39258
|
if (!userDocId) {
|
|
39065
39259
|
return true;
|
|
39066
39260
|
}
|
|
39067
|
-
const
|
|
39068
|
-
const strictMode =
|
|
39261
|
+
const pluginConfig = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
39262
|
+
const strictMode = pluginConfig.strictSessionEnforcement === true;
|
|
39069
39263
|
const activeSessions = await strapi2.documents(SESSION_UID).findMany({
|
|
39070
39264
|
filters: {
|
|
39071
39265
|
user: { documentId: userDocId },
|