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.mjs
CHANGED
|
@@ -159,11 +159,19 @@ const getClientIp$1 = (ctx) => {
|
|
|
159
159
|
};
|
|
160
160
|
const cleanIp = (ip) => {
|
|
161
161
|
if (!ip) return "unknown";
|
|
162
|
-
ip = ip.
|
|
162
|
+
ip = ip.trim();
|
|
163
|
+
if (ip.startsWith("[")) {
|
|
164
|
+
const bracketEnd = ip.indexOf("]");
|
|
165
|
+
if (bracketEnd !== -1) {
|
|
166
|
+
ip = ip.substring(1, bracketEnd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
163
169
|
if (ip.startsWith("::ffff:")) {
|
|
164
170
|
ip = ip.substring(7);
|
|
165
171
|
}
|
|
166
|
-
ip
|
|
172
|
+
if (ip.includes(".") && ip.includes(":") && ip.indexOf(":") === ip.lastIndexOf(":")) {
|
|
173
|
+
ip = ip.split(":")[0];
|
|
174
|
+
}
|
|
167
175
|
return ip || "unknown";
|
|
168
176
|
};
|
|
169
177
|
const isPrivateIp = (ip) => {
|
|
@@ -186,14 +194,15 @@ const IV_LENGTH = 16;
|
|
|
186
194
|
function getEncryptionKey() {
|
|
187
195
|
const envKey = process.env.SESSION_ENCRYPTION_KEY;
|
|
188
196
|
if (envKey) {
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
return crypto$1.createHash("sha256").update(envKey).digest();
|
|
198
|
+
}
|
|
199
|
+
const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT;
|
|
200
|
+
if (!strapiKeys) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"[magic-sessionmanager] No encryption key available. Set SESSION_ENCRYPTION_KEY in your .env file, or ensure APP_KEYS is configured."
|
|
203
|
+
);
|
|
191
204
|
}
|
|
192
|
-
|
|
193
|
-
const key = crypto$1.createHash("sha256").update(strapiKeys).digest();
|
|
194
|
-
console.warn("[magic-sessionmanager/encryption] [WARNING] No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).");
|
|
195
|
-
console.warn("[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.");
|
|
196
|
-
return key;
|
|
205
|
+
return crypto$1.createHash("sha256").update(strapiKeys).digest();
|
|
197
206
|
}
|
|
198
207
|
function encryptToken$2(token) {
|
|
199
208
|
if (!token) return null;
|
|
@@ -206,11 +215,12 @@ function encryptToken$2(token) {
|
|
|
206
215
|
const authTag = cipher.getAuthTag();
|
|
207
216
|
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
208
217
|
} catch (err) {
|
|
209
|
-
|
|
218
|
+
const errMsg = err instanceof Error ? err.message : "Unknown encryption error";
|
|
219
|
+
console.error("[magic-sessionmanager/encryption] Encryption failed:", errMsg);
|
|
210
220
|
throw new Error("Failed to encrypt token");
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
|
-
function decryptToken$
|
|
223
|
+
function decryptToken$2(encryptedToken) {
|
|
214
224
|
if (!encryptedToken) return null;
|
|
215
225
|
try {
|
|
216
226
|
const key = getEncryptionKey();
|
|
@@ -227,7 +237,8 @@ function decryptToken$3(encryptedToken) {
|
|
|
227
237
|
decrypted += decipher.final("utf8");
|
|
228
238
|
return decrypted;
|
|
229
239
|
} catch (err) {
|
|
230
|
-
|
|
240
|
+
const errMsg = err instanceof Error ? err.message : "Unknown decryption error";
|
|
241
|
+
console.error("[magic-sessionmanager/encryption] Decryption failed:", errMsg);
|
|
231
242
|
return null;
|
|
232
243
|
}
|
|
233
244
|
}
|
|
@@ -243,7 +254,7 @@ function hashToken$3(token) {
|
|
|
243
254
|
}
|
|
244
255
|
var encryption = {
|
|
245
256
|
encryptToken: encryptToken$2,
|
|
246
|
-
decryptToken: decryptToken$
|
|
257
|
+
decryptToken: decryptToken$2,
|
|
247
258
|
generateSessionId: generateSessionId$1,
|
|
248
259
|
hashToken: hashToken$3
|
|
249
260
|
};
|
|
@@ -274,12 +285,25 @@ function isAuthEndpoint(path2) {
|
|
|
274
285
|
}
|
|
275
286
|
const userIdCache = /* @__PURE__ */ new Map();
|
|
276
287
|
const CACHE_TTL = 5 * 60 * 1e3;
|
|
288
|
+
const CACHE_MAX_SIZE = 1e3;
|
|
277
289
|
async function getDocumentIdFromNumericId(strapi2, numericId) {
|
|
278
290
|
const cacheKey = `user_${numericId}`;
|
|
279
291
|
const cached2 = userIdCache.get(cacheKey);
|
|
280
292
|
if (cached2 && Date.now() - cached2.timestamp < CACHE_TTL) {
|
|
281
293
|
return cached2.documentId;
|
|
282
294
|
}
|
|
295
|
+
if (userIdCache.size >= CACHE_MAX_SIZE) {
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
for (const [key, value] of userIdCache) {
|
|
298
|
+
if (now - value.timestamp >= CACHE_TTL) {
|
|
299
|
+
userIdCache.delete(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (userIdCache.size >= CACHE_MAX_SIZE) {
|
|
303
|
+
const keysToDelete = [...userIdCache.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
|
|
304
|
+
keysToDelete.forEach((key) => userIdCache.delete(key));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
283
307
|
try {
|
|
284
308
|
const user = await strapi2.entityService.findOne(USER_UID$3, numericId, {
|
|
285
309
|
fields: ["documentId"]
|
|
@@ -322,7 +346,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
322
346
|
isActive: false
|
|
323
347
|
},
|
|
324
348
|
limit: 5,
|
|
325
|
-
fields: ["documentId", "terminatedManually", "lastActive"],
|
|
349
|
+
fields: ["documentId", "terminatedManually", "lastActive", "loginTime"],
|
|
326
350
|
sort: [{ lastActive: "desc" }]
|
|
327
351
|
});
|
|
328
352
|
if (inactiveSessions && inactiveSessions.length > 0) {
|
|
@@ -332,6 +356,14 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
332
356
|
return ctx.unauthorized("Session has been terminated. Please login again.");
|
|
333
357
|
}
|
|
334
358
|
const sessionToReactivate = inactiveSessions[0];
|
|
359
|
+
const maxAgeDays = config2.maxSessionAgeDays || 30;
|
|
360
|
+
const loginTime = sessionToReactivate.loginTime ? new Date(sessionToReactivate.loginTime).getTime() : sessionToReactivate.lastActive ? new Date(sessionToReactivate.lastActive).getTime() : 0;
|
|
361
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
362
|
+
const isExpired = loginTime > 0 && Date.now() - loginTime > maxAgeMs;
|
|
363
|
+
if (isExpired) {
|
|
364
|
+
strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session exceeded max age of ${maxAgeDays} days (user: ${userDocId2.substring(0, 8)}...)`);
|
|
365
|
+
return ctx.unauthorized("Session expired. Please login again.");
|
|
366
|
+
}
|
|
335
367
|
await strapi2.documents(SESSION_UID$4).update({
|
|
336
368
|
documentId: sessionToReactivate.documentId,
|
|
337
369
|
data: {
|
|
@@ -372,7 +404,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
|
|
|
372
404
|
};
|
|
373
405
|
};
|
|
374
406
|
const getClientIp = getClientIp_1;
|
|
375
|
-
const { encryptToken: encryptToken$1, decryptToken: decryptToken$
|
|
407
|
+
const { encryptToken: encryptToken$1, decryptToken: decryptToken$1, hashToken: hashToken$2 } = encryption;
|
|
376
408
|
const { createLogger: createLogger$3 } = logger;
|
|
377
409
|
const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
|
|
378
410
|
const USER_UID$2 = "plugin::users-permissions.user";
|
|
@@ -406,11 +438,11 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
406
438
|
log.info("║ [SUCCESS] SESSION MANAGER LICENSE ACTIVE ║");
|
|
407
439
|
log.info("║ ║");
|
|
408
440
|
if (licenseStatus.data) {
|
|
409
|
-
|
|
441
|
+
const maskedKey = licenseStatus.data.licenseKey ? `${licenseStatus.data.licenseKey.substring(0, 8)}...` : "N/A";
|
|
442
|
+
log.info(`║ License: ${maskedKey}`.padEnd(66) + "║");
|
|
410
443
|
log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + "║");
|
|
411
|
-
log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + "║");
|
|
412
444
|
} else if (storedKey) {
|
|
413
|
-
log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + "║");
|
|
445
|
+
log.info(`║ License: ${storedKey.substring(0, 8)}... (Offline Mode)`.padEnd(66) + "║");
|
|
414
446
|
log.info(`║ Status: Grace Period Active`.padEnd(66) + "║");
|
|
415
447
|
}
|
|
416
448
|
log.info("║ ║");
|
|
@@ -442,10 +474,21 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
442
474
|
try {
|
|
443
475
|
const token = ctx.request.headers?.authorization?.replace("Bearer ", "");
|
|
444
476
|
if (!token) {
|
|
445
|
-
ctx.status =
|
|
446
|
-
ctx.body = { message: "
|
|
477
|
+
ctx.status = 401;
|
|
478
|
+
ctx.body = { error: { status: 401, message: "Authorization token required" } };
|
|
447
479
|
return;
|
|
448
480
|
}
|
|
481
|
+
try {
|
|
482
|
+
const jwtService = strapi2.plugin("users-permissions").service("jwt");
|
|
483
|
+
const decoded = await jwtService.verify(token);
|
|
484
|
+
if (!decoded || !decoded.id) {
|
|
485
|
+
ctx.status = 401;
|
|
486
|
+
ctx.body = { error: { status: 401, message: "Invalid token" } };
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
} catch (jwtErr) {
|
|
490
|
+
log.debug("JWT verify failed during logout (cleaning up anyway):", jwtErr.message);
|
|
491
|
+
}
|
|
449
492
|
const tokenHashValue = hashToken$2(token);
|
|
450
493
|
const matchingSession = await strapi2.documents(SESSION_UID$3).findFirst({
|
|
451
494
|
filters: {
|
|
@@ -467,6 +510,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
467
510
|
},
|
|
468
511
|
config: {
|
|
469
512
|
auth: false
|
|
513
|
+
// We handle auth manually above to support expired-but-valid tokens
|
|
470
514
|
}
|
|
471
515
|
}]);
|
|
472
516
|
log.info("[SUCCESS] /api/auth/logout route registered");
|
|
@@ -526,8 +570,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
526
570
|
ctx.body = {
|
|
527
571
|
error: {
|
|
528
572
|
status: 403,
|
|
529
|
-
message: "Login blocked for security reasons"
|
|
530
|
-
details: { reason: blockReason }
|
|
573
|
+
message: "Login blocked for security reasons. Please contact support."
|
|
531
574
|
}
|
|
532
575
|
};
|
|
533
576
|
return;
|
|
@@ -589,11 +632,22 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
|
589
632
|
geoData
|
|
590
633
|
});
|
|
591
634
|
if (config2.discordWebhookUrl) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
635
|
+
const webhookUrl = config2.discordWebhookUrl;
|
|
636
|
+
try {
|
|
637
|
+
const parsed = new URL(webhookUrl);
|
|
638
|
+
const isValidDomain = parsed.protocol === "https:" && (parsed.hostname === "discord.com" || parsed.hostname === "discordapp.com" || parsed.hostname.endsWith(".discord.com") || parsed.hostname === "hooks.slack.com");
|
|
639
|
+
if (isValidDomain) {
|
|
640
|
+
await notificationService.sendWebhook({
|
|
641
|
+
event: "session.login",
|
|
642
|
+
data: webhookData,
|
|
643
|
+
webhookUrl
|
|
644
|
+
});
|
|
645
|
+
} else {
|
|
646
|
+
log.warn(`[SECURITY] Blocked webhook to untrusted domain: ${parsed.hostname}`);
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
log.warn("[SECURITY] Invalid webhook URL in plugin config");
|
|
650
|
+
}
|
|
597
651
|
}
|
|
598
652
|
}
|
|
599
653
|
} catch (notifErr) {
|
|
@@ -774,6 +828,30 @@ async function ensureTokenHashIndex(strapi2, log) {
|
|
|
774
828
|
log.debug("[INDEX] Could not create tokenHash index (will retry on next startup):", err.message);
|
|
775
829
|
}
|
|
776
830
|
}
|
|
831
|
+
const sessionCheckErrors = { count: 0, lastReset: Date.now() };
|
|
832
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
833
|
+
const ERROR_RESET_INTERVAL = 60 * 1e3;
|
|
834
|
+
function shouldFailOpen() {
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
if (now - sessionCheckErrors.lastReset > ERROR_RESET_INTERVAL) {
|
|
837
|
+
sessionCheckErrors.count = 0;
|
|
838
|
+
sessionCheckErrors.lastReset = now;
|
|
839
|
+
}
|
|
840
|
+
sessionCheckErrors.count++;
|
|
841
|
+
if (sessionCheckErrors.count > MAX_CONSECUTIVE_ERRORS) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
function resetErrorCounter() {
|
|
847
|
+
sessionCheckErrors.count = 0;
|
|
848
|
+
}
|
|
849
|
+
function isSessionExpired(session2, maxAgeDays = 30) {
|
|
850
|
+
if (!session2.loginTime) return false;
|
|
851
|
+
const loginTime = new Date(session2.loginTime).getTime();
|
|
852
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
853
|
+
return Date.now() - loginTime > maxAgeMs;
|
|
854
|
+
}
|
|
777
855
|
async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
778
856
|
try {
|
|
779
857
|
const usersPermissionsPlugin = strapi2.plugin("users-permissions");
|
|
@@ -795,15 +873,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
795
873
|
}
|
|
796
874
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
797
875
|
const strictMode = config2.strictSessionEnforcement === true;
|
|
876
|
+
const maxSessionAgeDays = config2.maxSessionAgeDays || 30;
|
|
798
877
|
try {
|
|
799
878
|
const tokenHashValue = hashToken$2(token);
|
|
800
|
-
let userDocId = null;
|
|
801
879
|
const user = await strapi2.entityService.findOne(
|
|
802
880
|
"plugin::users-permissions.user",
|
|
803
881
|
decoded.id,
|
|
804
882
|
{ fields: ["documentId"] }
|
|
805
883
|
);
|
|
806
|
-
userDocId = user?.documentId;
|
|
884
|
+
const userDocId = user?.documentId;
|
|
807
885
|
if (!userDocId) {
|
|
808
886
|
strapi2.log.debug("[magic-sessionmanager] [JWT] No documentId found, allowing through");
|
|
809
887
|
return decoded;
|
|
@@ -813,9 +891,19 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
813
891
|
user: { documentId: userDocId },
|
|
814
892
|
tokenHash: tokenHashValue
|
|
815
893
|
},
|
|
816
|
-
fields: ["documentId", "isActive", "terminatedManually", "lastActive"]
|
|
894
|
+
fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
|
|
817
895
|
});
|
|
818
896
|
if (thisSession) {
|
|
897
|
+
if (isSessionExpired(thisSession, maxSessionAgeDays)) {
|
|
898
|
+
strapi2.log.info(
|
|
899
|
+
`[magic-sessionmanager] [JWT-EXPIRED] Session exceeded max age of ${maxSessionAgeDays} days (user: ${userDocId.substring(0, 8)}...)`
|
|
900
|
+
);
|
|
901
|
+
await strapi2.documents(SESSION_UID$3).update({
|
|
902
|
+
documentId: thisSession.documentId,
|
|
903
|
+
data: { isActive: false, terminatedManually: true, logoutTime: /* @__PURE__ */ new Date() }
|
|
904
|
+
});
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
819
907
|
if (thisSession.terminatedManually === true) {
|
|
820
908
|
strapi2.log.info(
|
|
821
909
|
`[magic-sessionmanager] [JWT-BLOCKED] Session was manually terminated (user: ${userDocId.substring(0, 8)}...)`
|
|
@@ -823,38 +911,32 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
823
911
|
return null;
|
|
824
912
|
}
|
|
825
913
|
if (thisSession.isActive) {
|
|
914
|
+
resetErrorCounter();
|
|
826
915
|
return decoded;
|
|
827
916
|
}
|
|
828
917
|
await strapi2.documents(SESSION_UID$3).update({
|
|
829
918
|
documentId: thisSession.documentId,
|
|
830
|
-
data: {
|
|
831
|
-
isActive: true,
|
|
832
|
-
lastActive: /* @__PURE__ */ new Date()
|
|
833
|
-
}
|
|
919
|
+
data: { isActive: true, lastActive: /* @__PURE__ */ new Date() }
|
|
834
920
|
});
|
|
835
921
|
strapi2.log.info(
|
|
836
922
|
`[magic-sessionmanager] [JWT-REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`
|
|
837
923
|
);
|
|
924
|
+
resetErrorCounter();
|
|
838
925
|
return decoded;
|
|
839
926
|
}
|
|
840
927
|
const anyActiveSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
841
|
-
filters: {
|
|
842
|
-
user: { documentId: userDocId },
|
|
843
|
-
isActive: true
|
|
844
|
-
},
|
|
928
|
+
filters: { user: { documentId: userDocId }, isActive: true },
|
|
845
929
|
limit: 1
|
|
846
930
|
});
|
|
847
931
|
if (anyActiveSessions && anyActiveSessions.length > 0) {
|
|
848
932
|
strapi2.log.debug(
|
|
849
933
|
`[magic-sessionmanager] [JWT] No session for token but user has other active sessions (allowing)`
|
|
850
934
|
);
|
|
935
|
+
resetErrorCounter();
|
|
851
936
|
return decoded;
|
|
852
937
|
}
|
|
853
938
|
const terminatedSessions = await strapi2.documents(SESSION_UID$3).findMany({
|
|
854
|
-
filters: {
|
|
855
|
-
user: { documentId: userDocId },
|
|
856
|
-
terminatedManually: true
|
|
857
|
-
},
|
|
939
|
+
filters: { user: { documentId: userDocId }, terminatedManually: true },
|
|
858
940
|
limit: 1
|
|
859
941
|
});
|
|
860
942
|
if (terminatedSessions && terminatedSessions.length > 0) {
|
|
@@ -872,10 +954,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
|
|
|
872
954
|
strapi2.log.warn(
|
|
873
955
|
`[magic-sessionmanager] [JWT-WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`
|
|
874
956
|
);
|
|
957
|
+
resetErrorCounter();
|
|
875
958
|
return decoded;
|
|
876
959
|
} catch (err) {
|
|
877
|
-
|
|
878
|
-
|
|
960
|
+
if (shouldFailOpen()) {
|
|
961
|
+
strapi2.log.warn("[magic-sessionmanager] [JWT] Session check error (allowing):", err.message);
|
|
962
|
+
return decoded;
|
|
963
|
+
}
|
|
964
|
+
strapi2.log.error("[magic-sessionmanager] [JWT] Too many consecutive errors, blocking request:", err.message);
|
|
965
|
+
return null;
|
|
879
966
|
}
|
|
880
967
|
};
|
|
881
968
|
strapi2.log.info("[magic-sessionmanager] [AUTH] [SUCCESS] JWT verify wrapped with session validation");
|
|
@@ -1185,7 +1272,7 @@ var admin$1 = {
|
|
|
1185
1272
|
path: "/license/status",
|
|
1186
1273
|
handler: "license.getStatus",
|
|
1187
1274
|
config: {
|
|
1188
|
-
policies: []
|
|
1275
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1189
1276
|
}
|
|
1190
1277
|
},
|
|
1191
1278
|
{
|
|
@@ -1193,7 +1280,7 @@ var admin$1 = {
|
|
|
1193
1280
|
path: "/license/auto-create",
|
|
1194
1281
|
handler: "license.autoCreate",
|
|
1195
1282
|
config: {
|
|
1196
|
-
policies: []
|
|
1283
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1197
1284
|
}
|
|
1198
1285
|
},
|
|
1199
1286
|
{
|
|
@@ -1201,7 +1288,7 @@ var admin$1 = {
|
|
|
1201
1288
|
path: "/license/create",
|
|
1202
1289
|
handler: "license.createAndActivate",
|
|
1203
1290
|
config: {
|
|
1204
|
-
policies: []
|
|
1291
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1205
1292
|
}
|
|
1206
1293
|
},
|
|
1207
1294
|
{
|
|
@@ -1209,7 +1296,7 @@ var admin$1 = {
|
|
|
1209
1296
|
path: "/license/ping",
|
|
1210
1297
|
handler: "license.ping",
|
|
1211
1298
|
config: {
|
|
1212
|
-
policies: []
|
|
1299
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1213
1300
|
}
|
|
1214
1301
|
},
|
|
1215
1302
|
{
|
|
@@ -1217,7 +1304,7 @@ var admin$1 = {
|
|
|
1217
1304
|
path: "/license/store-key",
|
|
1218
1305
|
handler: "license.storeKey",
|
|
1219
1306
|
config: {
|
|
1220
|
-
policies: []
|
|
1307
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1221
1308
|
}
|
|
1222
1309
|
},
|
|
1223
1310
|
// Geolocation (Premium Feature)
|
|
@@ -1340,7 +1427,7 @@ function extractVersion(userAgent, regex) {
|
|
|
1340
1427
|
var userAgentParser = {
|
|
1341
1428
|
parseUserAgent: parseUserAgent$2
|
|
1342
1429
|
};
|
|
1343
|
-
const {
|
|
1430
|
+
const { hashToken: hashToken$1 } = encryption;
|
|
1344
1431
|
const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
|
|
1345
1432
|
const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
|
|
1346
1433
|
const USER_UID$1 = "plugin::users-permissions.user";
|
|
@@ -1400,7 +1487,8 @@ var session$3 = {
|
|
|
1400
1487
|
}
|
|
1401
1488
|
const allSessions = await strapi.documents(SESSION_UID$2).findMany({
|
|
1402
1489
|
filters: { user: { documentId: userId } },
|
|
1403
|
-
sort: { loginTime: "desc" }
|
|
1490
|
+
sort: { loginTime: "desc" },
|
|
1491
|
+
limit: 200
|
|
1404
1492
|
});
|
|
1405
1493
|
const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
|
|
1406
1494
|
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
@@ -1438,7 +1526,7 @@ var session$3 = {
|
|
|
1438
1526
|
strapi.documents(SESSION_UID$2).update({
|
|
1439
1527
|
documentId: session2.documentId,
|
|
1440
1528
|
data: {
|
|
1441
|
-
geoLocation
|
|
1529
|
+
geoLocation,
|
|
1442
1530
|
securityScore: geoData.securityScore || null
|
|
1443
1531
|
}
|
|
1444
1532
|
}).catch(() => {
|
|
@@ -1500,9 +1588,15 @@ var session$3 = {
|
|
|
1500
1588
|
const { userId } = ctx.params;
|
|
1501
1589
|
const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
|
|
1502
1590
|
const requestingUserDocId = ctx.state.user?.documentId;
|
|
1503
|
-
if (!isAdminRequest
|
|
1504
|
-
|
|
1505
|
-
|
|
1591
|
+
if (!isAdminRequest) {
|
|
1592
|
+
if (!requestingUserDocId) {
|
|
1593
|
+
strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
|
|
1594
|
+
return ctx.forbidden("Cannot verify user identity");
|
|
1595
|
+
}
|
|
1596
|
+
if (String(requestingUserDocId) !== String(userId)) {
|
|
1597
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
|
|
1598
|
+
return ctx.forbidden("You can only access your own sessions");
|
|
1599
|
+
}
|
|
1506
1600
|
}
|
|
1507
1601
|
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1508
1602
|
const sessions = await sessionService.getUserSessions(userId);
|
|
@@ -1626,7 +1720,7 @@ var session$3 = {
|
|
|
1626
1720
|
strapi.documents(SESSION_UID$2).update({
|
|
1627
1721
|
documentId: currentSession.documentId,
|
|
1628
1722
|
data: {
|
|
1629
|
-
geoLocation
|
|
1723
|
+
geoLocation,
|
|
1630
1724
|
securityScore: geoData.securityScore || null
|
|
1631
1725
|
}
|
|
1632
1726
|
}).catch(() => {
|
|
@@ -1708,22 +1802,6 @@ var session$3 = {
|
|
|
1708
1802
|
ctx.throw(500, "Error terminating session");
|
|
1709
1803
|
}
|
|
1710
1804
|
},
|
|
1711
|
-
/**
|
|
1712
|
-
* Terminate specific session
|
|
1713
|
-
* DELETE /magic-sessionmanager/sessions/:sessionId
|
|
1714
|
-
*/
|
|
1715
|
-
async terminateSession(ctx) {
|
|
1716
|
-
try {
|
|
1717
|
-
const { sessionId } = ctx.params;
|
|
1718
|
-
const sessionService = strapi.plugin("magic-sessionmanager").service("session");
|
|
1719
|
-
await sessionService.terminateSession({ sessionId });
|
|
1720
|
-
ctx.body = {
|
|
1721
|
-
message: `Session ${sessionId} terminated`
|
|
1722
|
-
};
|
|
1723
|
-
} catch (err) {
|
|
1724
|
-
ctx.throw(500, "Error terminating session");
|
|
1725
|
-
}
|
|
1726
|
-
},
|
|
1727
1805
|
/**
|
|
1728
1806
|
* Simulate session timeout for testing (Admin action)
|
|
1729
1807
|
* POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
|
|
@@ -1732,6 +1810,10 @@ var session$3 = {
|
|
|
1732
1810
|
*/
|
|
1733
1811
|
async simulateTimeout(ctx) {
|
|
1734
1812
|
try {
|
|
1813
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
1814
|
+
if (nodeEnv === "production") {
|
|
1815
|
+
return ctx.forbidden("simulate-timeout is disabled in production");
|
|
1816
|
+
}
|
|
1735
1817
|
const { sessionId } = ctx.params;
|
|
1736
1818
|
const session2 = await strapi.documents(SESSION_UID$2).findOne({
|
|
1737
1819
|
documentId: sessionId
|
|
@@ -1804,6 +1886,17 @@ var session$3 = {
|
|
|
1804
1886
|
if (!ipAddress) {
|
|
1805
1887
|
return ctx.badRequest("IP address is required");
|
|
1806
1888
|
}
|
|
1889
|
+
const IPV4_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1890
|
+
const ipv4Match = ipAddress.match(IPV4_REGEX);
|
|
1891
|
+
const isValidIpv4 = ipv4Match && ipv4Match.slice(1).every((octet) => {
|
|
1892
|
+
const n = parseInt(octet, 10);
|
|
1893
|
+
return n >= 0 && n <= 255;
|
|
1894
|
+
});
|
|
1895
|
+
const IPV6_REGEX = /^[0-9a-fA-F:]{3,45}$/;
|
|
1896
|
+
const isValidIpv6 = !isValidIpv4 && IPV6_REGEX.test(ipAddress) && (ipAddress.match(/:/g) || []).length >= 2;
|
|
1897
|
+
if (!isValidIpv4 && !isValidIpv6) {
|
|
1898
|
+
return ctx.badRequest("Invalid IP address format");
|
|
1899
|
+
}
|
|
1807
1900
|
const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
|
|
1808
1901
|
const pluginStore = strapi.store({
|
|
1809
1902
|
type: "plugin",
|
|
@@ -1963,12 +2056,11 @@ var license$1 = ({ strapi: strapi2 }) => ({
|
|
|
1963
2056
|
message: "No license found. Running in demo mode."
|
|
1964
2057
|
});
|
|
1965
2058
|
}
|
|
1966
|
-
strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey}
|
|
2059
|
+
strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey.substring(0, 8)}...`);
|
|
1967
2060
|
const verification = await licenseGuard2.verifyLicense(licenseKey);
|
|
1968
2061
|
const license2 = await licenseGuard2.getLicenseByKey(licenseKey);
|
|
1969
2062
|
strapi2.log.info("[magic-sessionmanager/license-controller] License data from MagicAPI:", {
|
|
1970
|
-
licenseKey: license2?.licenseKey,
|
|
1971
|
-
email: license2?.email,
|
|
2063
|
+
licenseKey: license2?.licenseKey ? `${license2.licenseKey.substring(0, 8)}...` : "N/A",
|
|
1972
2064
|
featurePremium: license2?.featurePremium,
|
|
1973
2065
|
isActive: license2?.isActive,
|
|
1974
2066
|
pluginName: license2?.pluginName
|
|
@@ -2112,6 +2204,55 @@ var license$1 = ({ strapi: strapi2 }) => ({
|
|
|
2112
2204
|
}
|
|
2113
2205
|
}
|
|
2114
2206
|
});
|
|
2207
|
+
const ALLOWED_WEBHOOK_DOMAINS = {
|
|
2208
|
+
discord: ["discord.com", "discordapp.com"],
|
|
2209
|
+
slack: ["hooks.slack.com"]
|
|
2210
|
+
};
|
|
2211
|
+
function sanitizeWebhookUrl(url, type2) {
|
|
2212
|
+
if (!url || typeof url !== "string") return "";
|
|
2213
|
+
const trimmed = url.trim();
|
|
2214
|
+
if (!trimmed) return "";
|
|
2215
|
+
try {
|
|
2216
|
+
const parsed = new URL(trimmed);
|
|
2217
|
+
if (parsed.protocol !== "https:") return "";
|
|
2218
|
+
const allowedDomains = ALLOWED_WEBHOOK_DOMAINS[type2] || [];
|
|
2219
|
+
const isAllowed = allowedDomains.some(
|
|
2220
|
+
(domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)
|
|
2221
|
+
);
|
|
2222
|
+
if (!isAllowed) return "";
|
|
2223
|
+
return trimmed;
|
|
2224
|
+
} catch {
|
|
2225
|
+
return "";
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
function sanitizeCountryList(list) {
|
|
2229
|
+
if (!Array.isArray(list)) return [];
|
|
2230
|
+
return list.filter((code) => typeof code === "string" && /^[A-Z]{2}$/.test(code.trim().toUpperCase())).map((code) => code.trim().toUpperCase());
|
|
2231
|
+
}
|
|
2232
|
+
function sanitizeEmailTemplates(templates) {
|
|
2233
|
+
const defaults = {
|
|
2234
|
+
suspiciousLogin: { subject: "", html: "", text: "" },
|
|
2235
|
+
newLocation: { subject: "", html: "", text: "" },
|
|
2236
|
+
vpnProxy: { subject: "", html: "", text: "" }
|
|
2237
|
+
};
|
|
2238
|
+
if (!templates || typeof templates !== "object") return defaults;
|
|
2239
|
+
const dangerousTags = /<\s*\/?\s*(script|iframe|object|embed|form|input|button|link|meta|base)\b[^>]*>/gi;
|
|
2240
|
+
const dangerousAttrs = /\s(on\w+|javascript\s*:)[^=]*=/gi;
|
|
2241
|
+
const result = {};
|
|
2242
|
+
for (const [key, defaultVal] of Object.entries(defaults)) {
|
|
2243
|
+
const tpl = templates[key];
|
|
2244
|
+
if (!tpl || typeof tpl !== "object") {
|
|
2245
|
+
result[key] = defaultVal;
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
result[key] = {
|
|
2249
|
+
subject: typeof tpl.subject === "string" ? tpl.subject.substring(0, 200) : "",
|
|
2250
|
+
html: typeof tpl.html === "string" ? tpl.html.replace(dangerousTags, "").replace(dangerousAttrs, " ").substring(0, 1e4) : "",
|
|
2251
|
+
text: typeof tpl.text === "string" ? tpl.text.substring(0, 5e3) : ""
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
return result;
|
|
2255
|
+
}
|
|
2115
2256
|
var settings$1 = {
|
|
2116
2257
|
/**
|
|
2117
2258
|
* Get plugin settings
|
|
@@ -2173,29 +2314,26 @@ var settings$1 = {
|
|
|
2173
2314
|
name: "magic-sessionmanager"
|
|
2174
2315
|
});
|
|
2175
2316
|
const sanitizedSettings = {
|
|
2176
|
-
inactivityTimeout: parseInt(body.inactivityTimeout) || 15,
|
|
2177
|
-
cleanupInterval: parseInt(body.cleanupInterval) || 30,
|
|
2178
|
-
lastSeenRateLimit: parseInt(body.lastSeenRateLimit) || 30,
|
|
2179
|
-
retentionDays: parseInt(body.retentionDays) || 90,
|
|
2317
|
+
inactivityTimeout: Math.max(1, Math.min(parseInt(body.inactivityTimeout) || 15, 1440)),
|
|
2318
|
+
cleanupInterval: Math.max(5, Math.min(parseInt(body.cleanupInterval) || 30, 1440)),
|
|
2319
|
+
lastSeenRateLimit: Math.max(5, Math.min(parseInt(body.lastSeenRateLimit) || 30, 300)),
|
|
2320
|
+
retentionDays: Math.max(1, Math.min(parseInt(body.retentionDays) || 90, 365)),
|
|
2321
|
+
maxSessionAgeDays: Math.max(1, Math.min(parseInt(body.maxSessionAgeDays) || 30, 365)),
|
|
2180
2322
|
enableGeolocation: !!body.enableGeolocation,
|
|
2181
2323
|
enableSecurityScoring: !!body.enableSecurityScoring,
|
|
2182
2324
|
blockSuspiciousSessions: !!body.blockSuspiciousSessions,
|
|
2183
|
-
maxFailedLogins: parseInt(body.maxFailedLogins) || 5,
|
|
2325
|
+
maxFailedLogins: Math.max(1, Math.min(parseInt(body.maxFailedLogins) || 5, 100)),
|
|
2184
2326
|
enableEmailAlerts: !!body.enableEmailAlerts,
|
|
2185
2327
|
alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
|
|
2186
2328
|
alertOnNewLocation: !!body.alertOnNewLocation,
|
|
2187
2329
|
alertOnVpnProxy: !!body.alertOnVpnProxy,
|
|
2188
2330
|
enableWebhooks: !!body.enableWebhooks,
|
|
2189
|
-
discordWebhookUrl:
|
|
2190
|
-
slackWebhookUrl:
|
|
2331
|
+
discordWebhookUrl: sanitizeWebhookUrl(body.discordWebhookUrl, "discord"),
|
|
2332
|
+
slackWebhookUrl: sanitizeWebhookUrl(body.slackWebhookUrl, "slack"),
|
|
2191
2333
|
enableGeofencing: !!body.enableGeofencing,
|
|
2192
|
-
allowedCountries:
|
|
2193
|
-
blockedCountries:
|
|
2194
|
-
emailTemplates: body.emailTemplates
|
|
2195
|
-
suspiciousLogin: { subject: "", html: "", text: "" },
|
|
2196
|
-
newLocation: { subject: "", html: "", text: "" },
|
|
2197
|
-
vpnProxy: { subject: "", html: "", text: "" }
|
|
2198
|
-
}
|
|
2334
|
+
allowedCountries: sanitizeCountryList(body.allowedCountries),
|
|
2335
|
+
blockedCountries: sanitizeCountryList(body.blockedCountries),
|
|
2336
|
+
emailTemplates: sanitizeEmailTemplates(body.emailTemplates)
|
|
2199
2337
|
};
|
|
2200
2338
|
await pluginStore.set({
|
|
2201
2339
|
key: "settings",
|
|
@@ -2266,15 +2404,15 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2266
2404
|
deviceType: parsedUA.deviceType,
|
|
2267
2405
|
browserName: parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName,
|
|
2268
2406
|
osName: parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName,
|
|
2269
|
-
// Geolocation data (
|
|
2270
|
-
geoLocation: geoData ?
|
|
2407
|
+
// Geolocation data (stored as JSON object, schema type: json)
|
|
2408
|
+
geoLocation: geoData ? {
|
|
2271
2409
|
country: geoData.country,
|
|
2272
2410
|
country_code: geoData.country_code,
|
|
2273
2411
|
country_flag: geoData.country_flag,
|
|
2274
2412
|
city: geoData.city,
|
|
2275
2413
|
region: geoData.region,
|
|
2276
2414
|
timezone: geoData.timezone
|
|
2277
|
-
}
|
|
2415
|
+
} : null,
|
|
2278
2416
|
securityScore: geoData?.securityScore || null
|
|
2279
2417
|
}
|
|
2280
2418
|
});
|
|
@@ -2295,6 +2433,14 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2295
2433
|
try {
|
|
2296
2434
|
const now = /* @__PURE__ */ new Date();
|
|
2297
2435
|
if (sessionId) {
|
|
2436
|
+
const existing = await strapi2.documents(SESSION_UID$1).findOne({
|
|
2437
|
+
documentId: sessionId,
|
|
2438
|
+
fields: ["documentId"]
|
|
2439
|
+
});
|
|
2440
|
+
if (!existing) {
|
|
2441
|
+
log.warn(`Session ${sessionId} not found for termination`);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2298
2444
|
await strapi2.documents(SESSION_UID$1).update({
|
|
2299
2445
|
documentId: sessionId,
|
|
2300
2446
|
data: {
|
|
@@ -2386,7 +2532,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2386
2532
|
strapi2.documents(SESSION_UID$1).update({
|
|
2387
2533
|
documentId: session2.documentId,
|
|
2388
2534
|
data: {
|
|
2389
|
-
geoLocation
|
|
2535
|
+
geoLocation,
|
|
2390
2536
|
securityScore: geoData.securityScore || null
|
|
2391
2537
|
}
|
|
2392
2538
|
}).catch(() => {
|
|
@@ -2436,7 +2582,8 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2436
2582
|
const sessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2437
2583
|
filters: { isActive: true },
|
|
2438
2584
|
populate: { user: { fields: ["documentId", "email", "username"] } },
|
|
2439
|
-
sort: { loginTime: "desc" }
|
|
2585
|
+
sort: { loginTime: "desc" },
|
|
2586
|
+
limit: 1e3
|
|
2440
2587
|
});
|
|
2441
2588
|
const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
2442
2589
|
const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
|
|
@@ -2473,7 +2620,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2473
2620
|
strapi2.documents(SESSION_UID$1).update({
|
|
2474
2621
|
documentId: session2.documentId,
|
|
2475
2622
|
data: {
|
|
2476
|
-
geoLocation
|
|
2623
|
+
geoLocation,
|
|
2477
2624
|
securityScore: geoData.securityScore || null
|
|
2478
2625
|
}
|
|
2479
2626
|
}).catch(() => {
|
|
@@ -2566,7 +2713,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2566
2713
|
strapi2.documents(SESSION_UID$1).update({
|
|
2567
2714
|
documentId: session2.documentId,
|
|
2568
2715
|
data: {
|
|
2569
|
-
geoLocation
|
|
2716
|
+
geoLocation,
|
|
2570
2717
|
securityScore: geoData.securityScore || null
|
|
2571
2718
|
}
|
|
2572
2719
|
}).catch(() => {
|
|
@@ -2703,20 +2850,36 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2703
2850
|
}
|
|
2704
2851
|
},
|
|
2705
2852
|
/**
|
|
2706
|
-
* Delete all inactive sessions from database
|
|
2853
|
+
* Delete all inactive sessions from database in batches
|
|
2707
2854
|
* WARNING: This permanently deletes records!
|
|
2708
2855
|
* @returns {Promise<number>} Number of deleted sessions
|
|
2709
2856
|
*/
|
|
2710
2857
|
async deleteInactiveSessions() {
|
|
2711
2858
|
try {
|
|
2712
2859
|
log.info("[DELETE] Deleting all inactive sessions...");
|
|
2713
|
-
const inactiveSessions = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2714
|
-
filters: { isActive: false }
|
|
2715
|
-
});
|
|
2716
2860
|
let deletedCount = 0;
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2861
|
+
const BATCH_SIZE = 100;
|
|
2862
|
+
let hasMore = true;
|
|
2863
|
+
while (hasMore) {
|
|
2864
|
+
const batch = await strapi2.documents(SESSION_UID$1).findMany({
|
|
2865
|
+
filters: { isActive: false },
|
|
2866
|
+
fields: ["documentId"],
|
|
2867
|
+
limit: BATCH_SIZE
|
|
2868
|
+
});
|
|
2869
|
+
if (!batch || batch.length === 0) {
|
|
2870
|
+
hasMore = false;
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
const deleteResults = await Promise.allSettled(
|
|
2874
|
+
batch.map(
|
|
2875
|
+
(session2) => strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId })
|
|
2876
|
+
)
|
|
2877
|
+
);
|
|
2878
|
+
const batchDeleted = deleteResults.filter((r) => r.status === "fulfilled").length;
|
|
2879
|
+
deletedCount += batchDeleted;
|
|
2880
|
+
if (batch.length < BATCH_SIZE) {
|
|
2881
|
+
hasMore = false;
|
|
2882
|
+
}
|
|
2720
2883
|
}
|
|
2721
2884
|
log.info(`[SUCCESS] Deleted ${deletedCount} inactive sessions`);
|
|
2722
2885
|
return deletedCount;
|
|
@@ -2727,7 +2890,7 @@ var session$1 = ({ strapi: strapi2 }) => {
|
|
|
2727
2890
|
}
|
|
2728
2891
|
};
|
|
2729
2892
|
};
|
|
2730
|
-
const version$1 = "4.4.
|
|
2893
|
+
const version$1 = "4.4.5";
|
|
2731
2894
|
const require$$2 = {
|
|
2732
2895
|
version: version$1
|
|
2733
2896
|
};
|
|
@@ -3001,7 +3164,7 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
|
|
|
3001
3164
|
*/
|
|
3002
3165
|
async getIpInfo(ipAddress) {
|
|
3003
3166
|
try {
|
|
3004
|
-
if (!ipAddress ||
|
|
3167
|
+
if (!ipAddress || this.isPrivateIp(ipAddress)) {
|
|
3005
3168
|
return {
|
|
3006
3169
|
ip: ipAddress,
|
|
3007
3170
|
country: "Local Network",
|
|
@@ -3104,6 +3267,26 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
|
|
|
3104
3267
|
const codePoints = countryCode.toUpperCase().split("").map((char) => 127397 + char.charCodeAt());
|
|
3105
3268
|
return String.fromCodePoint(...codePoints);
|
|
3106
3269
|
},
|
|
3270
|
+
/**
|
|
3271
|
+
* Checks if an IP address is private/local (RFC 1918, RFC 4193, loopback, link-local)
|
|
3272
|
+
* @param {string} ip - IP address to check
|
|
3273
|
+
* @returns {boolean} True if IP is private/local
|
|
3274
|
+
*/
|
|
3275
|
+
isPrivateIp(ip) {
|
|
3276
|
+
if (!ip || ip === "unknown") return true;
|
|
3277
|
+
if (ip === "127.0.0.1" || ip === "localhost" || ip === "::1") return true;
|
|
3278
|
+
if (ip.startsWith("192.168.")) return true;
|
|
3279
|
+
if (ip.startsWith("10.")) return true;
|
|
3280
|
+
if (ip.startsWith("172.")) {
|
|
3281
|
+
const second = parseInt(ip.split(".")[1], 10);
|
|
3282
|
+
if (second >= 16 && second <= 31) return true;
|
|
3283
|
+
}
|
|
3284
|
+
if (ip.startsWith("169.254.")) return true;
|
|
3285
|
+
if (ip.startsWith("fc00:") || ip.startsWith("fd00:")) return true;
|
|
3286
|
+
if (ip.startsWith("fe80:")) return true;
|
|
3287
|
+
if (ip === "::1") return true;
|
|
3288
|
+
return false;
|
|
3289
|
+
},
|
|
3107
3290
|
/**
|
|
3108
3291
|
* Fallback data when API fails
|
|
3109
3292
|
*/
|
|
@@ -3239,25 +3422,36 @@ VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
|
|
|
3239
3422
|
};
|
|
3240
3423
|
},
|
|
3241
3424
|
/**
|
|
3242
|
-
*
|
|
3425
|
+
* Escapes HTML special characters to prevent XSS in email templates
|
|
3426
|
+
* @param {string} str - String to escape
|
|
3427
|
+
* @returns {string} HTML-safe string
|
|
3428
|
+
*/
|
|
3429
|
+
escapeHtml(str2) {
|
|
3430
|
+
if (!str2 || typeof str2 !== "string") return str2 || "";
|
|
3431
|
+
return str2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3432
|
+
},
|
|
3433
|
+
/**
|
|
3434
|
+
* Replace template variables with HTML-escaped actual values
|
|
3435
|
+
* SECURITY: All dynamic values are HTML-escaped to prevent XSS via email
|
|
3243
3436
|
*/
|
|
3244
3437
|
replaceVariables(template2, data) {
|
|
3245
3438
|
let result = template2;
|
|
3246
|
-
|
|
3247
|
-
result = result.replace(/\{\{user\.
|
|
3439
|
+
const esc2 = this.escapeHtml.bind(this);
|
|
3440
|
+
result = result.replace(/\{\{user\.email\}\}/g, esc2(data.user?.email || "N/A"));
|
|
3441
|
+
result = result.replace(/\{\{user\.username\}\}/g, esc2(data.user?.username || "N/A"));
|
|
3248
3442
|
result = result.replace(
|
|
3249
3443
|
/\{\{session\.loginTime\}\}/g,
|
|
3250
|
-
data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A"
|
|
3444
|
+
esc2(data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A")
|
|
3251
3445
|
);
|
|
3252
|
-
result = result.replace(/\{\{session\.ipAddress\}\}/g, data.session?.ipAddress || "N/A");
|
|
3253
|
-
result = result.replace(/\{\{session\.userAgent\}\}/g, data.session?.userAgent || "N/A");
|
|
3254
|
-
result = result.replace(/\{\{geo\.city\}\}/g, data.geoData?.city || "Unknown");
|
|
3255
|
-
result = result.replace(/\{\{geo\.country\}\}/g, data.geoData?.country || "Unknown");
|
|
3256
|
-
result = result.replace(/\{\{geo\.timezone\}\}/g, data.geoData?.timezone || "Unknown");
|
|
3446
|
+
result = result.replace(/\{\{session\.ipAddress\}\}/g, esc2(data.session?.ipAddress || "N/A"));
|
|
3447
|
+
result = result.replace(/\{\{session\.userAgent\}\}/g, esc2(data.session?.userAgent || "N/A"));
|
|
3448
|
+
result = result.replace(/\{\{geo\.city\}\}/g, esc2(data.geoData?.city || "Unknown"));
|
|
3449
|
+
result = result.replace(/\{\{geo\.country\}\}/g, esc2(data.geoData?.country || "Unknown"));
|
|
3450
|
+
result = result.replace(/\{\{geo\.timezone\}\}/g, esc2(data.geoData?.timezone || "Unknown"));
|
|
3257
3451
|
result = result.replace(/\{\{reason\.isVpn\}\}/g, data.reason?.isVpn ? "Yes" : "No");
|
|
3258
3452
|
result = result.replace(/\{\{reason\.isProxy\}\}/g, data.reason?.isProxy ? "Yes" : "No");
|
|
3259
3453
|
result = result.replace(/\{\{reason\.isThreat\}\}/g, data.reason?.isThreat ? "Yes" : "No");
|
|
3260
|
-
result = result.replace(/\{\{reason\.securityScore\}\}/g, data.reason?.securityScore || "0");
|
|
3454
|
+
result = result.replace(/\{\{reason\.securityScore\}\}/g, esc2(String(data.reason?.securityScore || "0")));
|
|
3261
3455
|
return result;
|
|
3262
3456
|
},
|
|
3263
3457
|
/**
|
|
@@ -39033,7 +39227,7 @@ const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
|
|
|
39033
39227
|
const require$$0 = /* @__PURE__ */ getAugmentedNamespace(dist);
|
|
39034
39228
|
const SESSION_UID = "plugin::magic-sessionmanager.session";
|
|
39035
39229
|
const { errors } = require$$0;
|
|
39036
|
-
var sessionRequired$1 = async (policyContext,
|
|
39230
|
+
var sessionRequired$1 = async (policyContext, _policyConfig, { strapi: strapi2 }) => {
|
|
39037
39231
|
if (!policyContext.state.user) {
|
|
39038
39232
|
return true;
|
|
39039
39233
|
}
|
|
@@ -39051,8 +39245,8 @@ var sessionRequired$1 = async (policyContext, config2, { strapi: strapi2 }) => {
|
|
|
39051
39245
|
if (!userDocId) {
|
|
39052
39246
|
return true;
|
|
39053
39247
|
}
|
|
39054
|
-
const
|
|
39055
|
-
const strictMode =
|
|
39248
|
+
const pluginConfig = strapi2.config.get("plugin::magic-sessionmanager") || {};
|
|
39249
|
+
const strictMode = pluginConfig.strictSessionEnforcement === true;
|
|
39056
39250
|
const activeSessions = await strapi2.documents(SESSION_UID).findMany({
|
|
39057
39251
|
filters: {
|
|
39058
39252
|
user: { documentId: userDocId },
|