strapi-plugin-magic-sessionmanager 1.0.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.
@@ -0,0 +1,2127 @@
1
+ "use strict";
2
+ const require$$0 = require("crypto");
3
+ const require$$1 = require("os");
4
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
5
+ const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0);
6
+ const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
7
+ function getDefaultExportFromCjs(x) {
8
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
9
+ }
10
+ var register$1 = async ({ strapi: strapi2 }) => {
11
+ strapi2.log.info("[magic-sessionmanager] 🚀 Plugin registration starting...");
12
+ try {
13
+ const userCT = strapi2.contentType("plugin::users-permissions.user");
14
+ if (!userCT) {
15
+ strapi2.log.error("[magic-sessionmanager] User content type not found");
16
+ return;
17
+ }
18
+ if (userCT.attributes && userCT.attributes.sessions) {
19
+ delete userCT.attributes.sessions;
20
+ strapi2.log.info("[magic-sessionmanager] ✅ Removed sessions field from User content type");
21
+ }
22
+ strapi2.log.info("[magic-sessionmanager] ✅ Plugin registered successfully");
23
+ } catch (err) {
24
+ strapi2.log.error("[magic-sessionmanager] ❌ Registration error:", err);
25
+ }
26
+ };
27
+ const getClientIp$1 = (ctx) => {
28
+ try {
29
+ const headers = ctx.request.headers || ctx.request.header || {};
30
+ if (headers["cf-connecting-ip"]) {
31
+ return cleanIp(headers["cf-connecting-ip"]);
32
+ }
33
+ if (headers["true-client-ip"]) {
34
+ return cleanIp(headers["true-client-ip"]);
35
+ }
36
+ if (headers["x-real-ip"]) {
37
+ return cleanIp(headers["x-real-ip"]);
38
+ }
39
+ if (headers["x-forwarded-for"]) {
40
+ const forwardedIps = headers["x-forwarded-for"].split(",");
41
+ const clientIp = forwardedIps[0].trim();
42
+ if (clientIp && !isPrivateIp(clientIp)) {
43
+ return cleanIp(clientIp);
44
+ }
45
+ }
46
+ if (headers["x-client-ip"]) {
47
+ return cleanIp(headers["x-client-ip"]);
48
+ }
49
+ if (headers["x-cluster-client-ip"]) {
50
+ return cleanIp(headers["x-cluster-client-ip"]);
51
+ }
52
+ if (headers["forwarded"]) {
53
+ const match = headers["forwarded"].match(/for=([^;,\s]+)/);
54
+ if (match && match[1]) {
55
+ return cleanIp(match[1].replace(/"/g, ""));
56
+ }
57
+ }
58
+ if (ctx.request.ip) {
59
+ return cleanIp(ctx.request.ip);
60
+ }
61
+ return "unknown";
62
+ } catch (error) {
63
+ console.error("[getClientIp] Error extracting IP:", error);
64
+ return "unknown";
65
+ }
66
+ };
67
+ const cleanIp = (ip) => {
68
+ if (!ip) return "unknown";
69
+ ip = ip.split(":")[0];
70
+ if (ip.startsWith("::ffff:")) {
71
+ ip = ip.substring(7);
72
+ }
73
+ ip = ip.trim();
74
+ return ip || "unknown";
75
+ };
76
+ const isPrivateIp = (ip) => {
77
+ if (!ip) return true;
78
+ if (ip === "127.0.0.1" || ip === "localhost" || ip === "::1") return true;
79
+ if (ip.startsWith("192.168.")) return true;
80
+ if (ip.startsWith("10.")) return true;
81
+ if (ip.startsWith("172.")) {
82
+ const second = parseInt(ip.split(".")[1]);
83
+ if (second >= 16 && second <= 31) return true;
84
+ }
85
+ if (ip.startsWith("fc00:") || ip.startsWith("fd00:")) return true;
86
+ if (ip.startsWith("fe80:")) return true;
87
+ return false;
88
+ };
89
+ var getClientIp_1 = getClientIp$1;
90
+ var lastSeen = ({ strapi: strapi2, sessionService }) => {
91
+ return async (ctx, next) => {
92
+ if (ctx.state.user && ctx.state.user.id) {
93
+ try {
94
+ const userId = ctx.state.user.id;
95
+ const activeSessions = await strapi2.entityService.findMany("api::session.session", {
96
+ filters: {
97
+ user: { id: userId },
98
+ isActive: true
99
+ },
100
+ limit: 1
101
+ });
102
+ if (!activeSessions || activeSessions.length === 0) {
103
+ strapi2.log.info(`[magic-sessionmanager] 🚫 Blocked request - User ${userId} has no active sessions`);
104
+ return ctx.unauthorized("All sessions have been terminated. Please login again.");
105
+ }
106
+ } catch (err) {
107
+ strapi2.log.debug("[magic-sessionmanager] Error checking active sessions:", err.message);
108
+ }
109
+ }
110
+ await next();
111
+ if (ctx.state.user && ctx.state.user.id) {
112
+ try {
113
+ const userId = ctx.state.user.id;
114
+ const sessionId = ctx.state.sessionId;
115
+ await sessionService.touch({
116
+ userId,
117
+ sessionId
118
+ });
119
+ } catch (err) {
120
+ strapi2.log.debug("[magic-sessionmanager] Error updating lastSeen:", err.message);
121
+ }
122
+ }
123
+ };
124
+ };
125
+ const getClientIp = getClientIp_1;
126
+ var bootstrap$1 = async ({ strapi: strapi2 }) => {
127
+ strapi2.log.info("[magic-sessionmanager] 🚀 Bootstrap starting...");
128
+ try {
129
+ const licenseGuardService = strapi2.plugin("magic-sessionmanager").service("license-guard");
130
+ setTimeout(async () => {
131
+ const licenseStatus = await licenseGuardService.initialize();
132
+ if (!licenseStatus.valid) {
133
+ strapi2.log.error("╔════════════════════════════════════════════════════════════════╗");
134
+ strapi2.log.error("║ ❌ SESSION MANAGER - NO VALID LICENSE ║");
135
+ strapi2.log.error("║ ║");
136
+ strapi2.log.error("║ This plugin requires a valid license to operate. ║");
137
+ strapi2.log.error("║ Please activate your license via Admin UI: ║");
138
+ strapi2.log.error("║ Go to Settings → Sessions → License ║");
139
+ strapi2.log.error("║ ║");
140
+ strapi2.log.error("║ The plugin will run with limited functionality until ║");
141
+ strapi2.log.error("║ a valid license is activated. ║");
142
+ strapi2.log.error("╚════════════════════════════════════════════════════════════════╝");
143
+ } else if (licenseStatus.valid) {
144
+ const pluginStore = strapi2.store({
145
+ type: "plugin",
146
+ name: "magic-sessionmanager"
147
+ });
148
+ const storedKey = await pluginStore.get({ key: "licenseKey" });
149
+ strapi2.log.info("╔════════════════════════════════════════════════════════════════╗");
150
+ strapi2.log.info("║ ✅ SESSION MANAGER LICENSE ACTIVE ║");
151
+ strapi2.log.info("║ ║");
152
+ if (licenseStatus.data) {
153
+ strapi2.log.info(`║ License: ${licenseStatus.data.licenseKey}`.padEnd(66) + "║");
154
+ strapi2.log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + "║");
155
+ strapi2.log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + "║");
156
+ } else if (storedKey) {
157
+ strapi2.log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + "║");
158
+ strapi2.log.info(`║ Status: Grace Period Active`.padEnd(66) + "║");
159
+ }
160
+ strapi2.log.info("║ ║");
161
+ strapi2.log.info("║ 🔄 Auto-pinging every 15 minutes ║");
162
+ strapi2.log.info("╚════════════════════════════════════════════════════════════════╝");
163
+ }
164
+ }, 3e3);
165
+ const sessionService = strapi2.plugin("magic-sessionmanager").service("session");
166
+ strapi2.log.info("[magic-sessionmanager] Running initial session cleanup...");
167
+ await sessionService.cleanupInactiveSessions();
168
+ const cleanupInterval = 30 * 60 * 1e3;
169
+ const cleanupIntervalHandle = setInterval(async () => {
170
+ try {
171
+ const service = strapi2.plugin("magic-sessionmanager").service("session");
172
+ await service.cleanupInactiveSessions();
173
+ } catch (err) {
174
+ strapi2.log.error("[magic-sessionmanager] Periodic cleanup error:", err);
175
+ }
176
+ }, cleanupInterval);
177
+ strapi2.log.info("[magic-sessionmanager] ⏰ Periodic cleanup scheduled (every 30 minutes)");
178
+ if (!strapi2.sessionManagerIntervals) {
179
+ strapi2.sessionManagerIntervals = {};
180
+ }
181
+ strapi2.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
182
+ strapi2.server.routes([{
183
+ method: "POST",
184
+ path: "/api/auth/logout",
185
+ handler: async (ctx) => {
186
+ try {
187
+ const token = ctx.request.headers?.authorization?.replace("Bearer ", "");
188
+ if (!token) {
189
+ ctx.status = 200;
190
+ ctx.body = { message: "Logged out successfully" };
191
+ return;
192
+ }
193
+ const sessions = await strapi2.entityService.findMany("api::session.session", {
194
+ filters: {
195
+ token,
196
+ isActive: true
197
+ },
198
+ limit: 1
199
+ });
200
+ if (sessions.length > 0) {
201
+ await sessionService.terminateSession({ sessionId: sessions[0].id });
202
+ strapi2.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
203
+ }
204
+ ctx.status = 200;
205
+ ctx.body = { message: "Logged out successfully" };
206
+ } catch (err) {
207
+ strapi2.log.error("[magic-sessionmanager] Logout error:", err);
208
+ ctx.status = 200;
209
+ ctx.body = { message: "Logged out successfully" };
210
+ }
211
+ },
212
+ config: {
213
+ auth: false
214
+ }
215
+ }]);
216
+ strapi2.log.info("[magic-sessionmanager] ✅ /api/auth/logout route registered");
217
+ strapi2.server.use(async (ctx, next) => {
218
+ await next();
219
+ const isAuthLocal = ctx.path === "/api/auth/local" && ctx.method === "POST";
220
+ const isMagicLink = ctx.path.includes("/magic-link/login") && ctx.method === "POST";
221
+ if ((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.user) {
222
+ try {
223
+ const user = ctx.body.user;
224
+ const ip = getClientIp(ctx);
225
+ const userAgent = ctx.request.headers?.["user-agent"] || ctx.request.header?.["user-agent"] || "unknown";
226
+ strapi2.log.info(`[magic-sessionmanager] 🔍 Login detected! User: ${user.id} (${user.email || user.username}) from IP: ${ip}`);
227
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
228
+ let shouldBlock = false;
229
+ let blockReason = "";
230
+ let geoData = null;
231
+ if (config2.enableGeolocation || config2.enableSecurityScoring) {
232
+ try {
233
+ const geolocationService = strapi2.plugin("magic-sessionmanager").service("geolocation");
234
+ geoData = await geolocationService.getIpInfo(ip);
235
+ if (config2.blockSuspiciousSessions && geoData) {
236
+ if (geoData.isThreat) {
237
+ shouldBlock = true;
238
+ blockReason = "Known threat IP detected";
239
+ } else if (geoData.isVpn && config2.alertOnVpnProxy) {
240
+ shouldBlock = true;
241
+ blockReason = "VPN detected";
242
+ } else if (geoData.isProxy && config2.alertOnVpnProxy) {
243
+ shouldBlock = true;
244
+ blockReason = "Proxy detected";
245
+ } else if (geoData.securityScore < 50) {
246
+ shouldBlock = true;
247
+ blockReason = `Low security score: ${geoData.securityScore}/100`;
248
+ }
249
+ }
250
+ if (config2.enableGeofencing && geoData && geoData.country_code) {
251
+ const countryCode = geoData.country_code;
252
+ if (config2.blockedCountries && config2.blockedCountries.includes(countryCode)) {
253
+ shouldBlock = true;
254
+ blockReason = `Country ${countryCode} is blocked`;
255
+ }
256
+ if (config2.allowedCountries && config2.allowedCountries.length > 0) {
257
+ if (!config2.allowedCountries.includes(countryCode)) {
258
+ shouldBlock = true;
259
+ blockReason = `Country ${countryCode} is not in allowlist`;
260
+ }
261
+ }
262
+ }
263
+ } catch (geoErr) {
264
+ strapi2.log.warn("[magic-sessionmanager] Geolocation check failed:", geoErr.message);
265
+ }
266
+ }
267
+ if (shouldBlock) {
268
+ strapi2.log.warn(`[magic-sessionmanager] 🚫 Blocking login: ${blockReason}`);
269
+ ctx.status = 403;
270
+ ctx.body = {
271
+ error: {
272
+ status: 403,
273
+ message: "Login blocked for security reasons",
274
+ details: { reason: blockReason }
275
+ }
276
+ };
277
+ return;
278
+ }
279
+ const newSession = await sessionService.createSession({
280
+ userId: user.id,
281
+ ip,
282
+ userAgent,
283
+ token: ctx.body.jwt
284
+ // Store JWT token reference
285
+ });
286
+ strapi2.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
287
+ if (geoData && (config2.enableEmailAlerts || config2.enableWebhooks)) {
288
+ try {
289
+ const notificationService = strapi2.plugin("magic-sessionmanager").service("notifications");
290
+ const isSuspicious = geoData.isVpn || geoData.isProxy || geoData.isThreat || geoData.securityScore < 70;
291
+ if (config2.enableEmailAlerts && config2.alertOnSuspiciousLogin && isSuspicious) {
292
+ await notificationService.sendSuspiciousLoginAlert({
293
+ user,
294
+ session: newSession,
295
+ reason: {
296
+ isVpn: geoData.isVpn,
297
+ isProxy: geoData.isProxy,
298
+ isThreat: geoData.isThreat,
299
+ securityScore: geoData.securityScore
300
+ },
301
+ geoData
302
+ });
303
+ }
304
+ if (config2.enableWebhooks) {
305
+ const webhookData = notificationService.formatDiscordWebhook({
306
+ event: isSuspicious ? "login.suspicious" : "login.success",
307
+ session: newSession,
308
+ user,
309
+ geoData
310
+ });
311
+ if (config2.discordWebhookUrl) {
312
+ await notificationService.sendWebhook({
313
+ event: "session.login",
314
+ data: webhookData,
315
+ webhookUrl: config2.discordWebhookUrl
316
+ });
317
+ }
318
+ }
319
+ } catch (notifErr) {
320
+ strapi2.log.warn("[magic-sessionmanager] Notification failed:", notifErr.message);
321
+ }
322
+ }
323
+ } catch (err) {
324
+ strapi2.log.error("[magic-sessionmanager] ❌ Error creating session:", err);
325
+ }
326
+ }
327
+ });
328
+ strapi2.log.info("[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted");
329
+ strapi2.server.use(
330
+ lastSeen({ strapi: strapi2, sessionService })
331
+ );
332
+ strapi2.log.info("[magic-sessionmanager] ✅ LastSeen middleware mounted");
333
+ strapi2.log.info("[magic-sessionmanager] ✅ Bootstrap complete");
334
+ strapi2.log.info("[magic-sessionmanager] 🎉 Session Manager ready! Sessions stored in api::session.session");
335
+ } catch (err) {
336
+ strapi2.log.error("[magic-sessionmanager] ❌ Bootstrap error:", err);
337
+ }
338
+ };
339
+ var destroy$1 = async ({ strapi: strapi2 }) => {
340
+ if (strapi2.licenseGuard && strapi2.licenseGuard.pingInterval) {
341
+ clearInterval(strapi2.licenseGuard.pingInterval);
342
+ strapi2.log.info("[magic-sessionmanager] 🛑 License pinging stopped");
343
+ }
344
+ if (strapi2.sessionManagerIntervals && strapi2.sessionManagerIntervals.cleanup) {
345
+ clearInterval(strapi2.sessionManagerIntervals.cleanup);
346
+ strapi2.log.info("[magic-sessionmanager] 🛑 Session cleanup interval stopped");
347
+ }
348
+ strapi2.log.info("[magic-sessionmanager] ✅ Plugin cleanup completed");
349
+ };
350
+ var config$1 = {
351
+ default: {
352
+ // Rate limit for lastSeen updates (in milliseconds)
353
+ lastSeenRateLimit: 3e4,
354
+ // 30 seconds
355
+ // Session inactivity timeout (in milliseconds)
356
+ // After this time without activity, a session is considered inactive
357
+ inactivityTimeout: 15 * 60 * 1e3
358
+ // 15 minutes
359
+ },
360
+ validator: (config2) => {
361
+ if (config2.lastSeenRateLimit && typeof config2.lastSeenRateLimit !== "number") {
362
+ throw new Error("lastSeenRateLimit must be a number (milliseconds)");
363
+ }
364
+ if (config2.inactivityTimeout && typeof config2.inactivityTimeout !== "number") {
365
+ throw new Error("inactivityTimeout must be a number (milliseconds)");
366
+ }
367
+ }
368
+ };
369
+ var contentApi$1 = {
370
+ type: "content-api",
371
+ routes: [
372
+ // ============================================================
373
+ // LOGOUT ENDPOINTS
374
+ // ============================================================
375
+ {
376
+ method: "POST",
377
+ path: "/logout",
378
+ handler: "session.logout",
379
+ config: {
380
+ auth: { strategies: ["users-permissions"] },
381
+ description: "Logout current session (requires JWT)"
382
+ }
383
+ },
384
+ {
385
+ method: "POST",
386
+ path: "/logout-all",
387
+ handler: "session.logoutAll",
388
+ config: {
389
+ auth: { strategies: ["users-permissions"] },
390
+ description: "Logout from all devices (requires JWT)"
391
+ }
392
+ },
393
+ // ============================================================
394
+ // SESSION QUERIES
395
+ // ============================================================
396
+ {
397
+ method: "GET",
398
+ path: "/user/:userId/sessions",
399
+ handler: "session.getUserSessions",
400
+ config: {
401
+ auth: { strategies: ["users-permissions"] },
402
+ description: "Get own sessions (controller validates user can only see own sessions)"
403
+ }
404
+ }
405
+ ]
406
+ };
407
+ var admin$1 = {
408
+ type: "admin",
409
+ routes: [
410
+ {
411
+ method: "GET",
412
+ path: "/sessions",
413
+ handler: "session.getAllSessionsAdmin",
414
+ config: {
415
+ policies: ["admin::isAuthenticatedAdmin"],
416
+ description: "Get all sessions - active and inactive (admin)"
417
+ }
418
+ },
419
+ {
420
+ method: "GET",
421
+ path: "/sessions/active",
422
+ handler: "session.getActiveSessions",
423
+ config: {
424
+ policies: ["admin::isAuthenticatedAdmin"],
425
+ description: "Get only active sessions (admin)"
426
+ }
427
+ },
428
+ {
429
+ method: "GET",
430
+ path: "/user/:userId/sessions",
431
+ handler: "session.getUserSessions",
432
+ config: {
433
+ policies: ["admin::isAuthenticatedAdmin"],
434
+ description: "Get user sessions (admin)"
435
+ }
436
+ },
437
+ {
438
+ method: "POST",
439
+ path: "/sessions/:sessionId/terminate",
440
+ handler: "session.terminateSingleSession",
441
+ config: {
442
+ policies: ["admin::isAuthenticatedAdmin"],
443
+ description: "Terminate a specific session (admin)"
444
+ }
445
+ },
446
+ {
447
+ method: "DELETE",
448
+ path: "/sessions/:sessionId",
449
+ handler: "session.deleteSession",
450
+ config: {
451
+ policies: ["admin::isAuthenticatedAdmin"],
452
+ description: "Delete a single session permanently (admin)"
453
+ }
454
+ },
455
+ {
456
+ method: "POST",
457
+ path: "/sessions/clean-inactive",
458
+ handler: "session.cleanInactiveSessions",
459
+ config: {
460
+ policies: ["admin::isAuthenticatedAdmin"],
461
+ description: "Delete all inactive sessions from database (admin)"
462
+ }
463
+ },
464
+ {
465
+ method: "POST",
466
+ path: "/user/:userId/terminate-all",
467
+ handler: "session.terminateAllUserSessions",
468
+ config: {
469
+ policies: ["admin::isAuthenticatedAdmin"],
470
+ description: "Terminate all sessions for a user (admin)"
471
+ }
472
+ },
473
+ {
474
+ method: "POST",
475
+ path: "/user/:userId/toggle-block",
476
+ handler: "session.toggleUserBlock",
477
+ config: {
478
+ policies: ["admin::isAuthenticatedAdmin"],
479
+ description: "Toggle user blocked status (admin)"
480
+ }
481
+ },
482
+ // License Management
483
+ {
484
+ method: "GET",
485
+ path: "/license/status",
486
+ handler: "license.getStatus",
487
+ config: {
488
+ policies: []
489
+ }
490
+ },
491
+ {
492
+ method: "POST",
493
+ path: "/license/auto-create",
494
+ handler: "license.autoCreate",
495
+ config: {
496
+ policies: []
497
+ }
498
+ },
499
+ {
500
+ method: "POST",
501
+ path: "/license/create",
502
+ handler: "license.createAndActivate",
503
+ config: {
504
+ policies: []
505
+ }
506
+ },
507
+ {
508
+ method: "POST",
509
+ path: "/license/ping",
510
+ handler: "license.ping",
511
+ config: {
512
+ policies: []
513
+ }
514
+ },
515
+ {
516
+ method: "POST",
517
+ path: "/license/store-key",
518
+ handler: "license.storeKey",
519
+ config: {
520
+ policies: []
521
+ }
522
+ },
523
+ // Geolocation (Premium Feature)
524
+ {
525
+ method: "GET",
526
+ path: "/geolocation/:ipAddress",
527
+ handler: "session.getIpGeolocation",
528
+ config: {
529
+ policies: ["admin::isAuthenticatedAdmin"],
530
+ description: "Get IP geolocation data (Premium feature)"
531
+ }
532
+ },
533
+ // Settings Management
534
+ {
535
+ method: "GET",
536
+ path: "/settings",
537
+ handler: "settings.getSettings",
538
+ config: {
539
+ policies: ["admin::isAuthenticatedAdmin"],
540
+ description: "Get plugin settings"
541
+ }
542
+ },
543
+ {
544
+ method: "PUT",
545
+ path: "/settings",
546
+ handler: "settings.updateSettings",
547
+ config: {
548
+ policies: ["admin::isAuthenticatedAdmin"],
549
+ description: "Update plugin settings"
550
+ }
551
+ }
552
+ ]
553
+ };
554
+ const contentApi = contentApi$1;
555
+ const admin = admin$1;
556
+ var routes$1 = {
557
+ admin,
558
+ "content-api": contentApi
559
+ };
560
+ var session$3 = {
561
+ /**
562
+ * Get ALL sessions (active + inactive) - Admin only
563
+ * GET /magic-sessionmanager/sessions
564
+ */
565
+ async getAllSessionsAdmin(ctx) {
566
+ try {
567
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
568
+ const sessions = await sessionService.getAllSessions();
569
+ ctx.body = {
570
+ data: sessions,
571
+ meta: {
572
+ count: sessions.length,
573
+ active: sessions.filter((s) => s.isTrulyActive).length,
574
+ inactive: sessions.filter((s) => !s.isTrulyActive).length
575
+ }
576
+ };
577
+ } catch (err) {
578
+ ctx.throw(500, "Error fetching sessions");
579
+ }
580
+ },
581
+ /**
582
+ * Get active sessions only
583
+ * GET /magic-sessionmanager/sessions/active
584
+ */
585
+ async getActiveSessions(ctx) {
586
+ try {
587
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
588
+ const sessions = await sessionService.getActiveSessions();
589
+ ctx.body = {
590
+ data: sessions,
591
+ meta: {
592
+ count: sessions.length
593
+ }
594
+ };
595
+ } catch (err) {
596
+ ctx.throw(500, "Error fetching active sessions");
597
+ }
598
+ },
599
+ /**
600
+ * Get user's sessions
601
+ * GET /magic-sessionmanager/user/:userId/sessions
602
+ * SECURITY: User can only access their own sessions
603
+ */
604
+ async getUserSessions(ctx) {
605
+ try {
606
+ const { userId } = ctx.params;
607
+ const requestingUserId = ctx.state.user?.id;
608
+ if (requestingUserId && String(requestingUserId) !== String(userId)) {
609
+ strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
610
+ return ctx.forbidden("You can only access your own sessions");
611
+ }
612
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
613
+ const sessions = await sessionService.getUserSessions(userId);
614
+ ctx.body = {
615
+ data: sessions,
616
+ meta: {
617
+ count: sessions.length
618
+ }
619
+ };
620
+ } catch (err) {
621
+ ctx.throw(500, "Error fetching user sessions");
622
+ }
623
+ },
624
+ /**
625
+ * Logout handler - terminates current session
626
+ * POST /api/magic-sessionmanager/logout
627
+ */
628
+ async logout(ctx) {
629
+ try {
630
+ const userId = ctx.state.user?.id;
631
+ const token = ctx.request.headers.authorization?.replace("Bearer ", "");
632
+ if (!userId) {
633
+ return ctx.throw(401, "Unauthorized");
634
+ }
635
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
636
+ const sessions = await strapi.entityService.findMany("api::session.session", {
637
+ filters: {
638
+ user: { id: userId },
639
+ token,
640
+ isActive: true
641
+ }
642
+ });
643
+ if (sessions.length > 0) {
644
+ await sessionService.terminateSession({ sessionId: sessions[0].id });
645
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
646
+ }
647
+ ctx.body = {
648
+ message: "Logged out successfully"
649
+ };
650
+ } catch (err) {
651
+ strapi.log.error("[magic-sessionmanager] Logout error:", err);
652
+ ctx.throw(500, "Error during logout");
653
+ }
654
+ },
655
+ /**
656
+ * Logout from all devices - terminates all sessions for current user
657
+ * POST /api/magic-sessionmanager/logout-all
658
+ */
659
+ async logoutAll(ctx) {
660
+ try {
661
+ const userId = ctx.state.user?.id;
662
+ if (!userId) {
663
+ return ctx.throw(401, "Unauthorized");
664
+ }
665
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
666
+ await sessionService.terminateSession({ userId });
667
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out from all devices`);
668
+ ctx.body = {
669
+ message: "Logged out from all devices successfully"
670
+ };
671
+ } catch (err) {
672
+ strapi.log.error("[magic-sessionmanager] Logout-all error:", err);
673
+ ctx.throw(500, "Error during logout");
674
+ }
675
+ },
676
+ /**
677
+ * Terminate specific session
678
+ * DELETE /magic-sessionmanager/sessions/:sessionId
679
+ */
680
+ async terminateSession(ctx) {
681
+ try {
682
+ const { sessionId } = ctx.params;
683
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
684
+ await sessionService.terminateSession({ sessionId });
685
+ ctx.body = {
686
+ message: `Session ${sessionId} terminated`
687
+ };
688
+ } catch (err) {
689
+ ctx.throw(500, "Error terminating session");
690
+ }
691
+ },
692
+ /**
693
+ * Terminate a single session (Admin action)
694
+ * POST /magic-sessionmanager/sessions/:sessionId/terminate
695
+ */
696
+ async terminateSingleSession(ctx) {
697
+ try {
698
+ const { sessionId } = ctx.params;
699
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
700
+ await sessionService.terminateSession({ sessionId });
701
+ ctx.body = {
702
+ message: `Session ${sessionId} terminated`,
703
+ success: true
704
+ };
705
+ } catch (err) {
706
+ strapi.log.error("[magic-sessionmanager] Error terminating session:", err);
707
+ ctx.throw(500, "Error terminating session");
708
+ }
709
+ },
710
+ /**
711
+ * Terminate ALL sessions for a specific user (Admin action)
712
+ * POST /magic-sessionmanager/user/:userId/terminate-all
713
+ */
714
+ async terminateAllUserSessions(ctx) {
715
+ try {
716
+ const { userId } = ctx.params;
717
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
718
+ await sessionService.terminateSession({ userId });
719
+ ctx.body = {
720
+ message: `All sessions terminated for user ${userId}`,
721
+ success: true
722
+ };
723
+ } catch (err) {
724
+ strapi.log.error("[magic-sessionmanager] Error terminating all user sessions:", err);
725
+ ctx.throw(500, "Error terminating all user sessions");
726
+ }
727
+ },
728
+ /**
729
+ * Get IP Geolocation data (Premium feature)
730
+ * GET /magic-sessionmanager/geolocation/:ipAddress
731
+ */
732
+ async getIpGeolocation(ctx) {
733
+ try {
734
+ const { ipAddress } = ctx.params;
735
+ if (!ipAddress) {
736
+ return ctx.badRequest("IP address is required");
737
+ }
738
+ const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
739
+ const pluginStore = strapi.store({
740
+ type: "plugin",
741
+ name: "magic-sessionmanager"
742
+ });
743
+ const licenseKey = await pluginStore.get({ key: "licenseKey" });
744
+ if (!licenseKey) {
745
+ return ctx.forbidden("Premium license required for geolocation features");
746
+ }
747
+ const license2 = await licenseGuard2.getLicenseByKey(licenseKey);
748
+ if (!license2 || !license2.featurePremium) {
749
+ return ctx.forbidden("Premium license required for geolocation features");
750
+ }
751
+ const geolocationService = strapi.plugin("magic-sessionmanager").service("geolocation");
752
+ const ipData = await geolocationService.getIpInfo(ipAddress);
753
+ ctx.body = {
754
+ success: true,
755
+ data: ipData
756
+ };
757
+ } catch (err) {
758
+ strapi.log.error("[magic-sessionmanager] Error getting IP geolocation:", err);
759
+ ctx.throw(500, "Error fetching IP geolocation data");
760
+ }
761
+ },
762
+ /**
763
+ * Delete a single session permanently (Admin action)
764
+ * DELETE /magic-sessionmanager/sessions/:sessionId
765
+ */
766
+ async deleteSession(ctx) {
767
+ try {
768
+ const { sessionId } = ctx.params;
769
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
770
+ await sessionService.deleteSession(sessionId);
771
+ ctx.body = {
772
+ message: `Session ${sessionId} permanently deleted`,
773
+ success: true
774
+ };
775
+ } catch (err) {
776
+ strapi.log.error("[magic-sessionmanager] Error deleting session:", err);
777
+ ctx.throw(500, "Error deleting session");
778
+ }
779
+ },
780
+ /**
781
+ * Delete all inactive sessions (Admin action)
782
+ * POST /magic-sessionmanager/sessions/clean-inactive
783
+ */
784
+ async cleanInactiveSessions(ctx) {
785
+ try {
786
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
787
+ const deletedCount = await sessionService.deleteInactiveSessions();
788
+ ctx.body = {
789
+ message: `Successfully deleted ${deletedCount} inactive sessions`,
790
+ success: true,
791
+ deletedCount
792
+ };
793
+ } catch (err) {
794
+ strapi.log.error("[magic-sessionmanager] Error cleaning inactive sessions:", err);
795
+ ctx.throw(500, "Error deleting inactive sessions");
796
+ }
797
+ },
798
+ /**
799
+ * Toggle user blocked status
800
+ * POST /magic-sessionmanager/user/:userId/toggle-block
801
+ */
802
+ async toggleUserBlock(ctx) {
803
+ try {
804
+ const { userId } = ctx.params;
805
+ const user = await strapi.entityService.findOne("plugin::users-permissions.user", userId);
806
+ if (!user) {
807
+ return ctx.throw(404, "User not found");
808
+ }
809
+ const newBlockedStatus = !user.blocked;
810
+ await strapi.entityService.update("plugin::users-permissions.user", userId, {
811
+ data: {
812
+ blocked: newBlockedStatus
813
+ }
814
+ });
815
+ if (newBlockedStatus) {
816
+ const sessionService = strapi.plugin("magic-sessionmanager").service("session");
817
+ await sessionService.terminateSession({ userId });
818
+ }
819
+ ctx.body = {
820
+ message: `User ${newBlockedStatus ? "blocked" : "unblocked"} successfully`,
821
+ blocked: newBlockedStatus,
822
+ success: true
823
+ };
824
+ } catch (err) {
825
+ strapi.log.error("[magic-sessionmanager] Error toggling user block:", err);
826
+ ctx.throw(500, "Error toggling user block status");
827
+ }
828
+ }
829
+ };
830
+ var license$1 = ({ strapi: strapi2 }) => ({
831
+ /**
832
+ * Auto-create license with logged-in admin user data
833
+ */
834
+ async autoCreate(ctx) {
835
+ try {
836
+ const adminUser = ctx.state.user;
837
+ if (!adminUser) {
838
+ return ctx.unauthorized("No admin user logged in");
839
+ }
840
+ const licenseGuard2 = strapi2.plugin("magic-sessionmanager").service("license-guard");
841
+ const license2 = await licenseGuard2.createLicense({
842
+ email: adminUser.email,
843
+ firstName: adminUser.firstname || "Admin",
844
+ lastName: adminUser.lastname || "User"
845
+ });
846
+ if (!license2) {
847
+ return ctx.badRequest("Failed to create license");
848
+ }
849
+ await licenseGuard2.storeLicenseKey(license2.licenseKey);
850
+ const pingInterval = licenseGuard2.startPinging(license2.licenseKey, 15);
851
+ strapi2.licenseGuard = {
852
+ licenseKey: license2.licenseKey,
853
+ pingInterval,
854
+ data: license2
855
+ };
856
+ return ctx.send({
857
+ success: true,
858
+ message: "License automatically created and activated",
859
+ data: license2
860
+ });
861
+ } catch (error) {
862
+ strapi2.log.error("[magic-sessionmanager] Error auto-creating license:", error);
863
+ return ctx.badRequest("Error creating license");
864
+ }
865
+ },
866
+ /**
867
+ * Get current license status
868
+ */
869
+ async getStatus(ctx) {
870
+ try {
871
+ const licenseGuard2 = strapi2.plugin("magic-sessionmanager").service("license-guard");
872
+ const pluginStore = strapi2.store({
873
+ type: "plugin",
874
+ name: "magic-sessionmanager"
875
+ });
876
+ const licenseKey = await pluginStore.get({ key: "licenseKey" });
877
+ if (!licenseKey) {
878
+ strapi2.log.debug("[magic-sessionmanager] No license key in store - demo mode");
879
+ return ctx.send({
880
+ success: false,
881
+ demo: true,
882
+ valid: false,
883
+ message: "No license found. Running in demo mode."
884
+ });
885
+ }
886
+ strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey}`);
887
+ const verification = await licenseGuard2.verifyLicense(licenseKey);
888
+ const license2 = await licenseGuard2.getLicenseByKey(licenseKey);
889
+ strapi2.log.info("[magic-sessionmanager/license-controller] License data from MagicAPI:", {
890
+ licenseKey: license2?.licenseKey,
891
+ email: license2?.email,
892
+ featurePremium: license2?.featurePremium,
893
+ isActive: license2?.isActive,
894
+ pluginName: license2?.pluginName
895
+ });
896
+ return ctx.send({
897
+ success: true,
898
+ valid: verification.valid,
899
+ demo: false,
900
+ data: {
901
+ licenseKey,
902
+ email: license2?.email || null,
903
+ firstName: license2?.firstName || null,
904
+ lastName: license2?.lastName || null,
905
+ isActive: license2?.isActive || false,
906
+ isExpired: license2?.isExpired || false,
907
+ isOnline: license2?.isOnline || false,
908
+ expiresAt: license2?.expiresAt,
909
+ lastPingAt: license2?.lastPingAt,
910
+ deviceName: license2?.deviceName,
911
+ deviceId: license2?.deviceId,
912
+ ipAddress: license2?.ipAddress,
913
+ features: {
914
+ premium: license2?.featurePremium || false,
915
+ advanced: license2?.featureAdvanced || false,
916
+ enterprise: license2?.featureEnterprise || false,
917
+ custom: license2?.featureCustom || false
918
+ },
919
+ maxDevices: license2?.maxDevices || 1,
920
+ currentDevices: license2?.currentDevices || 0
921
+ }
922
+ });
923
+ } catch (error) {
924
+ strapi2.log.error("[magic-sessionmanager] Error getting license status:", error);
925
+ return ctx.badRequest("Error getting license status");
926
+ }
927
+ },
928
+ /**
929
+ * Create and activate a new license
930
+ */
931
+ async createAndActivate(ctx) {
932
+ try {
933
+ const { email, firstName, lastName } = ctx.request.body;
934
+ if (!email || !firstName || !lastName) {
935
+ return ctx.badRequest("Email, firstName, and lastName are required");
936
+ }
937
+ const licenseGuard2 = strapi2.plugin("magic-sessionmanager").service("license-guard");
938
+ const license2 = await licenseGuard2.createLicense({ email, firstName, lastName });
939
+ if (!license2) {
940
+ return ctx.badRequest("Failed to create license");
941
+ }
942
+ await licenseGuard2.storeLicenseKey(license2.licenseKey);
943
+ const pingInterval = licenseGuard2.startPinging(license2.licenseKey, 15);
944
+ strapi2.licenseGuard = {
945
+ licenseKey: license2.licenseKey,
946
+ pingInterval,
947
+ data: license2
948
+ };
949
+ return ctx.send({
950
+ success: true,
951
+ message: "License created and activated successfully",
952
+ data: license2
953
+ });
954
+ } catch (error) {
955
+ strapi2.log.error("[magic-sessionmanager] Error creating license:", error);
956
+ return ctx.badRequest("Error creating license");
957
+ }
958
+ },
959
+ /**
960
+ * Manually ping the current license
961
+ */
962
+ async ping(ctx) {
963
+ try {
964
+ const pluginStore = strapi2.store({
965
+ type: "plugin",
966
+ name: "magic-sessionmanager"
967
+ });
968
+ const licenseKey = await pluginStore.get({ key: "licenseKey" });
969
+ if (!licenseKey) {
970
+ return ctx.badRequest("No license key found");
971
+ }
972
+ const licenseGuard2 = strapi2.plugin("magic-sessionmanager").service("license-guard");
973
+ const pingResult = await licenseGuard2.pingLicense(licenseKey);
974
+ if (!pingResult) {
975
+ return ctx.badRequest("Ping failed");
976
+ }
977
+ return ctx.send({
978
+ success: true,
979
+ message: "License pinged successfully",
980
+ data: pingResult
981
+ });
982
+ } catch (error) {
983
+ strapi2.log.error("[magic-sessionmanager] Error pinging license:", error);
984
+ return ctx.badRequest("Error pinging license");
985
+ }
986
+ },
987
+ /**
988
+ * Store and validate an existing license key
989
+ */
990
+ async storeKey(ctx) {
991
+ try {
992
+ const { licenseKey, email } = ctx.request.body;
993
+ if (!licenseKey || !licenseKey.trim()) {
994
+ return ctx.badRequest("License key is required");
995
+ }
996
+ if (!email || !email.trim()) {
997
+ return ctx.badRequest("Email address is required");
998
+ }
999
+ const trimmedKey = licenseKey.trim();
1000
+ const trimmedEmail = email.trim().toLowerCase();
1001
+ const licenseGuard2 = strapi2.plugin("magic-sessionmanager").service("license-guard");
1002
+ const verification = await licenseGuard2.verifyLicense(trimmedKey);
1003
+ if (!verification.valid) {
1004
+ strapi2.log.warn(`[magic-sessionmanager] ⚠️ Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
1005
+ return ctx.badRequest("Invalid or expired license key");
1006
+ }
1007
+ const license2 = await licenseGuard2.getLicenseByKey(trimmedKey);
1008
+ if (!license2) {
1009
+ strapi2.log.warn(`[magic-sessionmanager] ⚠️ License not found in database: ${trimmedKey.substring(0, 8)}...`);
1010
+ return ctx.badRequest("License not found");
1011
+ }
1012
+ if (license2.email.toLowerCase() !== trimmedEmail) {
1013
+ strapi2.log.warn(`[magic-sessionmanager] ⚠️ Email mismatch for license key: ${trimmedKey.substring(0, 8)}... (Attempted: ${trimmedEmail})`);
1014
+ return ctx.badRequest("Email address does not match this license key");
1015
+ }
1016
+ await licenseGuard2.storeLicenseKey(trimmedKey);
1017
+ const pingInterval = licenseGuard2.startPinging(trimmedKey, 15);
1018
+ strapi2.licenseGuard = {
1019
+ licenseKey: trimmedKey,
1020
+ pingInterval,
1021
+ data: verification.data
1022
+ };
1023
+ strapi2.log.info(`[magic-sessionmanager] ✅ Existing license key validated and stored: ${trimmedKey.substring(0, 8)}... (Email: ${trimmedEmail})`);
1024
+ return ctx.send({
1025
+ success: true,
1026
+ message: "License key validated and activated successfully",
1027
+ data: verification.data
1028
+ });
1029
+ } catch (error) {
1030
+ strapi2.log.error("[magic-sessionmanager] Error storing license key:", error);
1031
+ return ctx.badRequest("Error validating license key");
1032
+ }
1033
+ }
1034
+ });
1035
+ var settings$1 = {
1036
+ /**
1037
+ * Get plugin settings
1038
+ */
1039
+ async getSettings(ctx) {
1040
+ try {
1041
+ const pluginStore = strapi.store({
1042
+ type: "plugin",
1043
+ name: "magic-sessionmanager"
1044
+ });
1045
+ let settings2 = await pluginStore.get({ key: "settings" });
1046
+ if (!settings2) {
1047
+ settings2 = {
1048
+ inactivityTimeout: 15,
1049
+ cleanupInterval: 30,
1050
+ lastSeenRateLimit: 30,
1051
+ retentionDays: 90,
1052
+ enableGeolocation: true,
1053
+ enableSecurityScoring: true,
1054
+ blockSuspiciousSessions: false,
1055
+ maxFailedLogins: 5,
1056
+ enableEmailAlerts: false,
1057
+ alertOnSuspiciousLogin: true,
1058
+ alertOnNewLocation: true,
1059
+ alertOnVpnProxy: true,
1060
+ enableWebhooks: false,
1061
+ discordWebhookUrl: "",
1062
+ slackWebhookUrl: "",
1063
+ enableGeofencing: false,
1064
+ allowedCountries: [],
1065
+ blockedCountries: [],
1066
+ emailTemplates: {
1067
+ suspiciousLogin: { subject: "", html: "", text: "" },
1068
+ newLocation: { subject: "", html: "", text: "" },
1069
+ vpnProxy: { subject: "", html: "", text: "" }
1070
+ }
1071
+ };
1072
+ }
1073
+ ctx.send({
1074
+ settings: settings2,
1075
+ success: true
1076
+ });
1077
+ } catch (error) {
1078
+ strapi.log.error("[magic-sessionmanager/settings] Error getting settings:", error);
1079
+ ctx.badRequest("Error loading settings");
1080
+ }
1081
+ },
1082
+ /**
1083
+ * Update plugin settings
1084
+ */
1085
+ async updateSettings(ctx) {
1086
+ try {
1087
+ const { body } = ctx.request;
1088
+ if (!body) {
1089
+ return ctx.badRequest("Settings data is required");
1090
+ }
1091
+ const pluginStore = strapi.store({
1092
+ type: "plugin",
1093
+ name: "magic-sessionmanager"
1094
+ });
1095
+ const sanitizedSettings = {
1096
+ inactivityTimeout: parseInt(body.inactivityTimeout) || 15,
1097
+ cleanupInterval: parseInt(body.cleanupInterval) || 30,
1098
+ lastSeenRateLimit: parseInt(body.lastSeenRateLimit) || 30,
1099
+ retentionDays: parseInt(body.retentionDays) || 90,
1100
+ enableGeolocation: !!body.enableGeolocation,
1101
+ enableSecurityScoring: !!body.enableSecurityScoring,
1102
+ blockSuspiciousSessions: !!body.blockSuspiciousSessions,
1103
+ maxFailedLogins: parseInt(body.maxFailedLogins) || 5,
1104
+ enableEmailAlerts: !!body.enableEmailAlerts,
1105
+ alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
1106
+ alertOnNewLocation: !!body.alertOnNewLocation,
1107
+ alertOnVpnProxy: !!body.alertOnVpnProxy,
1108
+ enableWebhooks: !!body.enableWebhooks,
1109
+ discordWebhookUrl: String(body.discordWebhookUrl || ""),
1110
+ slackWebhookUrl: String(body.slackWebhookUrl || ""),
1111
+ enableGeofencing: !!body.enableGeofencing,
1112
+ allowedCountries: Array.isArray(body.allowedCountries) ? body.allowedCountries : [],
1113
+ blockedCountries: Array.isArray(body.blockedCountries) ? body.blockedCountries : [],
1114
+ emailTemplates: body.emailTemplates || {
1115
+ suspiciousLogin: { subject: "", html: "", text: "" },
1116
+ newLocation: { subject: "", html: "", text: "" },
1117
+ vpnProxy: { subject: "", html: "", text: "" }
1118
+ }
1119
+ };
1120
+ await pluginStore.set({
1121
+ key: "settings",
1122
+ value: sanitizedSettings
1123
+ });
1124
+ strapi.log.info("[magic-sessionmanager/settings] Settings updated successfully");
1125
+ ctx.send({
1126
+ settings: sanitizedSettings,
1127
+ success: true,
1128
+ message: "Settings saved successfully!"
1129
+ });
1130
+ } catch (error) {
1131
+ strapi.log.error("[magic-sessionmanager/settings] Error updating settings:", error);
1132
+ ctx.badRequest("Error saving settings");
1133
+ }
1134
+ }
1135
+ };
1136
+ const session$2 = session$3;
1137
+ const license = license$1;
1138
+ const settings = settings$1;
1139
+ var controllers$1 = {
1140
+ session: session$2,
1141
+ license,
1142
+ settings
1143
+ };
1144
+ var session$1 = ({ strapi: strapi2 }) => ({
1145
+ /**
1146
+ * Create a new session record
1147
+ * @param {Object} params - { userId, ip, userAgent, token }
1148
+ * @returns {Promise<Object>} Created session
1149
+ */
1150
+ async createSession({ userId, ip = "unknown", userAgent = "unknown", token }) {
1151
+ try {
1152
+ const now = /* @__PURE__ */ new Date();
1153
+ const session2 = await strapi2.entityService.create("api::session.session", {
1154
+ data: {
1155
+ user: userId,
1156
+ ipAddress: ip.substring(0, 45),
1157
+ userAgent: userAgent.substring(0, 500),
1158
+ loginTime: now,
1159
+ lastActive: now,
1160
+ isActive: true,
1161
+ token
1162
+ // Store JWT for logout matching
1163
+ }
1164
+ });
1165
+ strapi2.log.info(`[magic-sessionmanager] ✅ Session ${session2.id} created for user ${userId}`);
1166
+ return session2;
1167
+ } catch (err) {
1168
+ strapi2.log.error("[magic-sessionmanager] Error creating session:", err);
1169
+ throw err;
1170
+ }
1171
+ },
1172
+ /**
1173
+ * Terminate a session or all sessions for a user
1174
+ * @param {Object} params - { sessionId | userId }
1175
+ * @returns {Promise<void>}
1176
+ */
1177
+ async terminateSession({ sessionId, userId }) {
1178
+ try {
1179
+ const now = /* @__PURE__ */ new Date();
1180
+ if (sessionId) {
1181
+ await strapi2.entityService.update("api::session.session", sessionId, {
1182
+ data: {
1183
+ isActive: false,
1184
+ logoutTime: now
1185
+ }
1186
+ });
1187
+ strapi2.log.info(`[magic-sessionmanager] Session ${sessionId} terminated`);
1188
+ } else if (userId) {
1189
+ const activeSessions = await strapi2.entityService.findMany("api::session.session", {
1190
+ filters: {
1191
+ user: { id: userId },
1192
+ isActive: true
1193
+ }
1194
+ });
1195
+ for (const session2 of activeSessions) {
1196
+ await strapi2.entityService.update("api::session.session", session2.id, {
1197
+ data: {
1198
+ isActive: false,
1199
+ logoutTime: now
1200
+ }
1201
+ });
1202
+ }
1203
+ strapi2.log.info(`[magic-sessionmanager] All sessions terminated for user ${userId}`);
1204
+ }
1205
+ } catch (err) {
1206
+ strapi2.log.error("[magic-sessionmanager] Error terminating session:", err);
1207
+ throw err;
1208
+ }
1209
+ },
1210
+ /**
1211
+ * Get ALL sessions (active + inactive) with accurate online status
1212
+ * @returns {Promise<Array>} All sessions with enhanced data
1213
+ */
1214
+ async getAllSessions() {
1215
+ try {
1216
+ const sessions = await strapi2.entityService.findMany("api::session.session", {
1217
+ populate: { user: { fields: ["id", "email", "username"] } },
1218
+ sort: { loginTime: "desc" },
1219
+ limit: 1e3
1220
+ // Reasonable limit
1221
+ });
1222
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
1223
+ const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
1224
+ const now = /* @__PURE__ */ new Date();
1225
+ const enhancedSessions = sessions.map((session2) => {
1226
+ const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1227
+ const timeSinceActive = now - lastActiveTime;
1228
+ const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1229
+ const { token, ...sessionWithoutToken } = session2;
1230
+ return {
1231
+ ...sessionWithoutToken,
1232
+ isTrulyActive,
1233
+ minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1234
+ };
1235
+ });
1236
+ return enhancedSessions;
1237
+ } catch (err) {
1238
+ strapi2.log.error("[magic-sessionmanager] Error getting all sessions:", err);
1239
+ throw err;
1240
+ }
1241
+ },
1242
+ /**
1243
+ * Get all active sessions with accurate online status
1244
+ * @returns {Promise<Array>} Active sessions with user data and online status
1245
+ */
1246
+ async getActiveSessions() {
1247
+ try {
1248
+ const sessions = await strapi2.entityService.findMany("api::session.session", {
1249
+ filters: { isActive: true },
1250
+ populate: { user: { fields: ["id", "email", "username"] } },
1251
+ sort: { loginTime: "desc" }
1252
+ });
1253
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
1254
+ const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
1255
+ const now = /* @__PURE__ */ new Date();
1256
+ const enhancedSessions = sessions.map((session2) => {
1257
+ const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1258
+ const timeSinceActive = now - lastActiveTime;
1259
+ const isTrulyActive = timeSinceActive < inactivityTimeout;
1260
+ const { token, ...sessionWithoutToken } = session2;
1261
+ return {
1262
+ ...sessionWithoutToken,
1263
+ isTrulyActive,
1264
+ minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1265
+ };
1266
+ });
1267
+ return enhancedSessions.filter((s) => s.isTrulyActive);
1268
+ } catch (err) {
1269
+ strapi2.log.error("[magic-sessionmanager] Error getting active sessions:", err);
1270
+ throw err;
1271
+ }
1272
+ },
1273
+ /**
1274
+ * Get all sessions for a specific user
1275
+ * @param {number} userId
1276
+ * @returns {Promise<Array>} User's sessions with accurate online status
1277
+ */
1278
+ async getUserSessions(userId) {
1279
+ try {
1280
+ const sessions = await strapi2.entityService.findMany("api::session.session", {
1281
+ filters: { user: { id: userId } },
1282
+ sort: { loginTime: "desc" }
1283
+ });
1284
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
1285
+ const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
1286
+ const now = /* @__PURE__ */ new Date();
1287
+ const enhancedSessions = sessions.map((session2) => {
1288
+ const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1289
+ const timeSinceActive = now - lastActiveTime;
1290
+ const isTrulyActive = session2.isActive && timeSinceActive < inactivityTimeout;
1291
+ const { token, ...sessionWithoutToken } = session2;
1292
+ return {
1293
+ ...sessionWithoutToken,
1294
+ isTrulyActive,
1295
+ minutesSinceActive: Math.floor(timeSinceActive / 1e3 / 60)
1296
+ };
1297
+ });
1298
+ return enhancedSessions;
1299
+ } catch (err) {
1300
+ strapi2.log.error("[magic-sessionmanager] Error getting user sessions:", err);
1301
+ throw err;
1302
+ }
1303
+ },
1304
+ /**
1305
+ * Update lastActive timestamp on session (rate-limited to avoid DB noise)
1306
+ * @param {Object} params - { userId, sessionId }
1307
+ * @returns {Promise<void>}
1308
+ */
1309
+ async touch({ userId, sessionId }) {
1310
+ try {
1311
+ const now = /* @__PURE__ */ new Date();
1312
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
1313
+ const rateLimit = config2.lastSeenRateLimit || 3e4;
1314
+ if (sessionId) {
1315
+ const session2 = await strapi2.entityService.findOne("api::session.session", sessionId);
1316
+ if (session2 && session2.lastActive) {
1317
+ const lastActiveTime = new Date(session2.lastActive).getTime();
1318
+ const currentTime = now.getTime();
1319
+ if (currentTime - lastActiveTime > rateLimit) {
1320
+ await strapi2.entityService.update("api::session.session", sessionId, {
1321
+ data: { lastActive: now }
1322
+ });
1323
+ }
1324
+ } else if (session2) {
1325
+ await strapi2.entityService.update("api::session.session", sessionId, {
1326
+ data: { lastActive: now }
1327
+ });
1328
+ }
1329
+ }
1330
+ } catch (err) {
1331
+ strapi2.log.debug("[magic-sessionmanager] Error touching session:", err.message);
1332
+ }
1333
+ },
1334
+ /**
1335
+ * Cleanup inactive sessions - set isActive to false for sessions older than inactivityTimeout
1336
+ * Should be called on bootstrap to clean up stale sessions
1337
+ */
1338
+ async cleanupInactiveSessions() {
1339
+ try {
1340
+ const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
1341
+ const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
1342
+ const now = /* @__PURE__ */ new Date();
1343
+ const cutoffTime = new Date(now.getTime() - inactivityTimeout);
1344
+ strapi2.log.info(`[magic-sessionmanager] 🧹 Cleaning up sessions inactive since before ${cutoffTime.toISOString()}`);
1345
+ const activeSessions = await strapi2.entityService.findMany("api::session.session", {
1346
+ filters: { isActive: true },
1347
+ fields: ["id", "lastActive", "loginTime"]
1348
+ });
1349
+ let deactivatedCount = 0;
1350
+ for (const session2 of activeSessions) {
1351
+ const lastActiveTime = session2.lastActive ? new Date(session2.lastActive) : new Date(session2.loginTime);
1352
+ if (lastActiveTime < cutoffTime) {
1353
+ await strapi2.entityService.update("api::session.session", session2.id, {
1354
+ data: { isActive: false }
1355
+ });
1356
+ deactivatedCount++;
1357
+ }
1358
+ }
1359
+ strapi2.log.info(`[magic-sessionmanager] ✅ Cleanup complete: ${deactivatedCount} sessions deactivated`);
1360
+ return deactivatedCount;
1361
+ } catch (err) {
1362
+ strapi2.log.error("[magic-sessionmanager] Error cleaning up inactive sessions:", err);
1363
+ throw err;
1364
+ }
1365
+ },
1366
+ /**
1367
+ * Delete a single session from database
1368
+ * WARNING: This permanently deletes the record!
1369
+ * @param {number} sessionId - Session ID to delete
1370
+ * @returns {Promise<boolean>} Success status
1371
+ */
1372
+ async deleteSession(sessionId) {
1373
+ try {
1374
+ await strapi2.entityService.delete("api::session.session", sessionId);
1375
+ strapi2.log.info(`[magic-sessionmanager] 🗑️ Session ${sessionId} permanently deleted`);
1376
+ return true;
1377
+ } catch (err) {
1378
+ strapi2.log.error("[magic-sessionmanager] Error deleting session:", err);
1379
+ throw err;
1380
+ }
1381
+ },
1382
+ /**
1383
+ * Delete all inactive sessions from database
1384
+ * WARNING: This permanently deletes records!
1385
+ * @returns {Promise<number>} Number of deleted sessions
1386
+ */
1387
+ async deleteInactiveSessions() {
1388
+ try {
1389
+ strapi2.log.info("[magic-sessionmanager] 🗑️ Deleting all inactive sessions...");
1390
+ const inactiveSessions = await strapi2.entityService.findMany("api::session.session", {
1391
+ filters: { isActive: false },
1392
+ fields: ["id"]
1393
+ });
1394
+ let deletedCount = 0;
1395
+ for (const session2 of inactiveSessions) {
1396
+ await strapi2.entityService.delete("api::session.session", session2.id);
1397
+ deletedCount++;
1398
+ }
1399
+ strapi2.log.info(`[magic-sessionmanager] ✅ Deleted ${deletedCount} inactive sessions`);
1400
+ return deletedCount;
1401
+ } catch (err) {
1402
+ strapi2.log.error("[magic-sessionmanager] Error deleting inactive sessions:", err);
1403
+ throw err;
1404
+ }
1405
+ }
1406
+ });
1407
+ const crypto = require$$0__default.default;
1408
+ const os = require$$1__default.default;
1409
+ const LICENSE_SERVER_URL = "https://magicapi.fitlex.me";
1410
+ var licenseGuard$1 = ({ strapi: strapi2 }) => ({
1411
+ /**
1412
+ * Get license server URL
1413
+ */
1414
+ getLicenseServerUrl() {
1415
+ return LICENSE_SERVER_URL;
1416
+ },
1417
+ /**
1418
+ * Generate device ID
1419
+ */
1420
+ generateDeviceId() {
1421
+ try {
1422
+ const networkInterfaces = os.networkInterfaces();
1423
+ const macAddresses = [];
1424
+ Object.values(networkInterfaces).forEach((interfaces) => {
1425
+ interfaces?.forEach((iface) => {
1426
+ if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
1427
+ macAddresses.push(iface.mac);
1428
+ }
1429
+ });
1430
+ });
1431
+ const identifier = `${macAddresses.join("-")}-${os.hostname()}`;
1432
+ return crypto.createHash("sha256").update(identifier).digest("hex").substring(0, 32);
1433
+ } catch (error) {
1434
+ return crypto.randomBytes(16).toString("hex");
1435
+ }
1436
+ },
1437
+ getDeviceName() {
1438
+ try {
1439
+ return os.hostname() || "Unknown Device";
1440
+ } catch (error) {
1441
+ return "Unknown Device";
1442
+ }
1443
+ },
1444
+ getIpAddress() {
1445
+ try {
1446
+ const networkInterfaces = os.networkInterfaces();
1447
+ for (const name of Object.keys(networkInterfaces)) {
1448
+ const interfaces = networkInterfaces[name];
1449
+ if (interfaces) {
1450
+ for (const iface of interfaces) {
1451
+ if (iface.family === "IPv4" && !iface.internal) {
1452
+ return iface.address;
1453
+ }
1454
+ }
1455
+ }
1456
+ }
1457
+ return "127.0.0.1";
1458
+ } catch (error) {
1459
+ return "127.0.0.1";
1460
+ }
1461
+ },
1462
+ getUserAgent() {
1463
+ return `Strapi/${strapi2.config.get("info.strapi") || "5.0.0"} Node/${process.version} ${os.platform()}/${os.release()}`;
1464
+ },
1465
+ async createLicense({ email, firstName, lastName }) {
1466
+ try {
1467
+ const deviceId = this.generateDeviceId();
1468
+ const deviceName = this.getDeviceName();
1469
+ const ipAddress = this.getIpAddress();
1470
+ const userAgent = this.getUserAgent();
1471
+ const licenseServerUrl = this.getLicenseServerUrl();
1472
+ const response = await fetch(`${licenseServerUrl}/api/licenses/create`, {
1473
+ method: "POST",
1474
+ headers: { "Content-Type": "application/json" },
1475
+ body: JSON.stringify({
1476
+ email,
1477
+ firstName,
1478
+ lastName,
1479
+ deviceName,
1480
+ deviceId,
1481
+ ipAddress,
1482
+ userAgent,
1483
+ pluginName: "magic-sessionmanager",
1484
+ productName: "Magic Session Manager - Premium Session Tracking"
1485
+ })
1486
+ });
1487
+ const data = await response.json();
1488
+ if (data.success) {
1489
+ strapi2.log.info("[magic-sessionmanager] ✅ License created:", data.data.licenseKey);
1490
+ return data.data;
1491
+ } else {
1492
+ strapi2.log.error("[magic-sessionmanager] ❌ License creation failed:", data);
1493
+ return null;
1494
+ }
1495
+ } catch (error) {
1496
+ strapi2.log.error("[magic-sessionmanager] ❌ Error creating license:", error);
1497
+ return null;
1498
+ }
1499
+ },
1500
+ async verifyLicense(licenseKey, allowGracePeriod = false) {
1501
+ try {
1502
+ const controller = new AbortController();
1503
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
1504
+ const licenseServerUrl = this.getLicenseServerUrl();
1505
+ const response = await fetch(`${licenseServerUrl}/api/licenses/verify`, {
1506
+ method: "POST",
1507
+ headers: { "Content-Type": "application/json" },
1508
+ body: JSON.stringify({
1509
+ licenseKey,
1510
+ pluginName: "magic-sessionmanager",
1511
+ productName: "Magic Session Manager - Premium Session Tracking"
1512
+ }),
1513
+ signal: controller.signal
1514
+ });
1515
+ clearTimeout(timeoutId);
1516
+ const data = await response.json();
1517
+ if (data.success && data.data) {
1518
+ return { valid: true, data: data.data, gracePeriod: false };
1519
+ } else {
1520
+ return { valid: false, data: null };
1521
+ }
1522
+ } catch (error) {
1523
+ if (allowGracePeriod) {
1524
+ return { valid: true, data: null, gracePeriod: true };
1525
+ }
1526
+ return { valid: false, data: null };
1527
+ }
1528
+ },
1529
+ async getLicenseByKey(licenseKey) {
1530
+ try {
1531
+ const licenseServerUrl = this.getLicenseServerUrl();
1532
+ const url = `${licenseServerUrl}/api/licenses/key/${licenseKey}`;
1533
+ strapi2.log.debug(`[magic-sessionmanager/license-guard] Fetching license from: ${url}`);
1534
+ const response = await fetch(url, {
1535
+ method: "GET",
1536
+ headers: { "Content-Type": "application/json" }
1537
+ });
1538
+ const data = await response.json();
1539
+ if (data.success && data.data) {
1540
+ strapi2.log.debug(`[magic-sessionmanager/license-guard] License fetched: ${data.data.email}, featurePremium: ${data.data.featurePremium}`);
1541
+ return data.data;
1542
+ }
1543
+ strapi2.log.warn(`[magic-sessionmanager/license-guard] License API returned no data`);
1544
+ return null;
1545
+ } catch (error) {
1546
+ strapi2.log.error("[magic-sessionmanager/license-guard] Error fetching license by key:", error);
1547
+ return null;
1548
+ }
1549
+ },
1550
+ async pingLicense(licenseKey) {
1551
+ try {
1552
+ const deviceId = this.generateDeviceId();
1553
+ const deviceName = this.getDeviceName();
1554
+ const ipAddress = this.getIpAddress();
1555
+ const userAgent = this.getUserAgent();
1556
+ const licenseServerUrl = this.getLicenseServerUrl();
1557
+ const response = await fetch(`${licenseServerUrl}/api/licenses/ping`, {
1558
+ method: "POST",
1559
+ headers: { "Content-Type": "application/json" },
1560
+ body: JSON.stringify({
1561
+ licenseKey,
1562
+ deviceId,
1563
+ deviceName,
1564
+ ipAddress,
1565
+ userAgent,
1566
+ pluginName: "magic-sessionmanager"
1567
+ })
1568
+ });
1569
+ const data = await response.json();
1570
+ return data.success ? data.data : null;
1571
+ } catch (error) {
1572
+ return null;
1573
+ }
1574
+ },
1575
+ async storeLicenseKey(licenseKey) {
1576
+ const pluginStore = strapi2.store({
1577
+ type: "plugin",
1578
+ name: "magic-sessionmanager"
1579
+ });
1580
+ await pluginStore.set({ key: "licenseKey", value: licenseKey });
1581
+ strapi2.log.info(`[magic-sessionmanager] ✅ License key stored: ${licenseKey.substring(0, 8)}...`);
1582
+ },
1583
+ startPinging(licenseKey, intervalMinutes = 15) {
1584
+ strapi2.log.info(`[magic-sessionmanager] ⏰ Starting license pings every ${intervalMinutes} minutes`);
1585
+ this.pingLicense(licenseKey);
1586
+ const interval = setInterval(async () => {
1587
+ try {
1588
+ await this.pingLicense(licenseKey);
1589
+ } catch (error) {
1590
+ strapi2.log.error("[magic-sessionmanager] Ping error:", error);
1591
+ }
1592
+ }, intervalMinutes * 60 * 1e3);
1593
+ return interval;
1594
+ },
1595
+ /**
1596
+ * Initialize license guard
1597
+ * Checks for existing license and starts pinging
1598
+ */
1599
+ async initialize() {
1600
+ try {
1601
+ strapi2.log.info("[magic-sessionmanager] 🔐 Initializing License Guard...");
1602
+ const pluginStore = strapi2.store({
1603
+ type: "plugin",
1604
+ name: "magic-sessionmanager"
1605
+ });
1606
+ const licenseKey = await pluginStore.get({ key: "licenseKey" });
1607
+ if (!licenseKey) {
1608
+ strapi2.log.info("[magic-sessionmanager] ℹ️ No license found - Running in demo mode");
1609
+ return {
1610
+ valid: false,
1611
+ demo: true,
1612
+ data: null
1613
+ };
1614
+ }
1615
+ const lastValidated = await pluginStore.get({ key: "lastValidated" });
1616
+ const now = /* @__PURE__ */ new Date();
1617
+ const gracePeriodHours = 24;
1618
+ let withinGracePeriod = false;
1619
+ if (lastValidated) {
1620
+ const lastValidatedDate = new Date(lastValidated);
1621
+ const hoursSinceValidation = (now.getTime() - lastValidatedDate.getTime()) / (1e3 * 60 * 60);
1622
+ withinGracePeriod = hoursSinceValidation < gracePeriodHours;
1623
+ }
1624
+ const verification = await this.verifyLicense(licenseKey, withinGracePeriod);
1625
+ if (verification.valid) {
1626
+ await pluginStore.set({
1627
+ key: "lastValidated",
1628
+ value: now.toISOString()
1629
+ });
1630
+ const pingInterval = this.startPinging(licenseKey, 15);
1631
+ strapi2.licenseGuard = {
1632
+ licenseKey,
1633
+ pingInterval,
1634
+ data: verification.data
1635
+ };
1636
+ return {
1637
+ valid: true,
1638
+ demo: false,
1639
+ data: verification.data,
1640
+ gracePeriod: verification.gracePeriod || false
1641
+ };
1642
+ } else {
1643
+ strapi2.log.error("[magic-sessionmanager] ❌ License validation failed");
1644
+ return {
1645
+ valid: false,
1646
+ demo: true,
1647
+ error: "Invalid or expired license",
1648
+ data: null
1649
+ };
1650
+ }
1651
+ } catch (error) {
1652
+ strapi2.log.error("[magic-sessionmanager] ❌ Error initializing License Guard:", error);
1653
+ return {
1654
+ valid: false,
1655
+ demo: true,
1656
+ error: error.message,
1657
+ data: null
1658
+ };
1659
+ }
1660
+ }
1661
+ });
1662
+ var geolocation$1 = ({ strapi: strapi2 }) => ({
1663
+ /**
1664
+ * Get IP information from ipapi.co
1665
+ * @param {string} ipAddress - IP to lookup
1666
+ * @returns {Promise<Object>} Geolocation data
1667
+ */
1668
+ async getIpInfo(ipAddress) {
1669
+ try {
1670
+ if (!ipAddress || ipAddress === "127.0.0.1" || ipAddress === "::1" || ipAddress.startsWith("192.168.") || ipAddress.startsWith("10.")) {
1671
+ return {
1672
+ ip: ipAddress,
1673
+ country: "Local Network",
1674
+ country_code: "XX",
1675
+ country_flag: "🏠",
1676
+ city: "Localhost",
1677
+ region: "Private Network",
1678
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1679
+ latitude: null,
1680
+ longitude: null,
1681
+ isVpn: false,
1682
+ isProxy: false,
1683
+ isThreat: false,
1684
+ securityScore: 100,
1685
+ riskLevel: "None"
1686
+ };
1687
+ }
1688
+ const response = await fetch(`https://ipapi.co/${ipAddress}/json/`, {
1689
+ method: "GET",
1690
+ headers: {
1691
+ "User-Agent": "Strapi-Magic-SessionManager/1.0"
1692
+ }
1693
+ });
1694
+ if (!response.ok) {
1695
+ throw new Error(`API returned ${response.status}`);
1696
+ }
1697
+ const data = await response.json();
1698
+ if (data.error) {
1699
+ strapi2.log.warn(`[magic-sessionmanager/geolocation] API Error: ${data.reason}`);
1700
+ return this.getFallbackData(ipAddress);
1701
+ }
1702
+ const result = {
1703
+ ip: data.ip,
1704
+ country: data.country_name,
1705
+ country_code: data.country_code,
1706
+ country_flag: this.getCountryFlag(data.country_code),
1707
+ city: data.city,
1708
+ region: data.region,
1709
+ timezone: data.timezone,
1710
+ latitude: data.latitude,
1711
+ longitude: data.longitude,
1712
+ postal: data.postal,
1713
+ org: data.org,
1714
+ asn: data.asn,
1715
+ // Security features (available in ipapi.co response)
1716
+ isVpn: data.threat?.is_vpn || false,
1717
+ isProxy: data.threat?.is_proxy || false,
1718
+ isThreat: data.threat?.is_threat || false,
1719
+ isAnonymous: data.threat?.is_anonymous || false,
1720
+ isTor: data.threat?.is_tor || false,
1721
+ // Calculate security score (0-100, higher is safer)
1722
+ securityScore: this.calculateSecurityScore({
1723
+ isVpn: data.threat?.is_vpn,
1724
+ isProxy: data.threat?.is_proxy,
1725
+ isThreat: data.threat?.is_threat,
1726
+ isAnonymous: data.threat?.is_anonymous,
1727
+ isTor: data.threat?.is_tor
1728
+ }),
1729
+ riskLevel: this.getRiskLevel({
1730
+ isVpn: data.threat?.is_vpn,
1731
+ isProxy: data.threat?.is_proxy,
1732
+ isThreat: data.threat?.is_threat
1733
+ })
1734
+ };
1735
+ strapi2.log.debug(`[magic-sessionmanager/geolocation] IP ${ipAddress}: ${result.city}, ${result.country} (Score: ${result.securityScore})`);
1736
+ return result;
1737
+ } catch (error) {
1738
+ strapi2.log.error(`[magic-sessionmanager/geolocation] Error fetching IP info for ${ipAddress}:`, error.message);
1739
+ return this.getFallbackData(ipAddress);
1740
+ }
1741
+ },
1742
+ /**
1743
+ * Calculate security score based on threat indicators
1744
+ */
1745
+ calculateSecurityScore({ isVpn, isProxy, isThreat, isAnonymous, isTor }) {
1746
+ let score = 100;
1747
+ if (isTor) score -= 50;
1748
+ if (isThreat) score -= 40;
1749
+ if (isVpn) score -= 20;
1750
+ if (isProxy) score -= 15;
1751
+ if (isAnonymous) score -= 10;
1752
+ return Math.max(0, score);
1753
+ },
1754
+ /**
1755
+ * Get risk level based on indicators
1756
+ */
1757
+ getRiskLevel({ isVpn, isProxy, isThreat }) {
1758
+ if (isThreat) return "High";
1759
+ if (isVpn && isProxy) return "Medium-High";
1760
+ if (isVpn || isProxy) return "Medium";
1761
+ return "Low";
1762
+ },
1763
+ /**
1764
+ * Get country flag emoji
1765
+ */
1766
+ getCountryFlag(countryCode) {
1767
+ if (!countryCode) return "🏳️";
1768
+ const codePoints = countryCode.toUpperCase().split("").map((char) => 127397 + char.charCodeAt());
1769
+ return String.fromCodePoint(...codePoints);
1770
+ },
1771
+ /**
1772
+ * Fallback data when API fails
1773
+ */
1774
+ getFallbackData(ipAddress) {
1775
+ return {
1776
+ ip: ipAddress,
1777
+ country: "Unknown",
1778
+ country_code: "XX",
1779
+ country_flag: "🌍",
1780
+ city: "Unknown",
1781
+ region: "Unknown",
1782
+ timezone: "Unknown",
1783
+ latitude: null,
1784
+ longitude: null,
1785
+ isVpn: false,
1786
+ isProxy: false,
1787
+ isThreat: false,
1788
+ securityScore: 50,
1789
+ riskLevel: "Unknown"
1790
+ };
1791
+ },
1792
+ /**
1793
+ * Batch lookup multiple IPs (for efficiency)
1794
+ */
1795
+ async batchGetIpInfo(ipAddresses) {
1796
+ const results = await Promise.all(
1797
+ ipAddresses.map((ip) => this.getIpInfo(ip))
1798
+ );
1799
+ return results;
1800
+ }
1801
+ });
1802
+ var notifications$1 = ({ strapi: strapi2 }) => ({
1803
+ /**
1804
+ * Get email templates from database settings
1805
+ * Falls back to default hardcoded templates if not found
1806
+ */
1807
+ async getEmailTemplates() {
1808
+ try {
1809
+ const pluginStore = strapi2.store({
1810
+ type: "plugin",
1811
+ name: "magic-sessionmanager"
1812
+ });
1813
+ const settings2 = await pluginStore.get({ key: "settings" });
1814
+ if (settings2?.emailTemplates && Object.keys(settings2.emailTemplates).length > 0) {
1815
+ const hasContent = Object.values(settings2.emailTemplates).some(
1816
+ (template) => template.html || template.text
1817
+ );
1818
+ if (hasContent) {
1819
+ strapi2.log.debug("[magic-sessionmanager/notifications] Using templates from database");
1820
+ return settings2.emailTemplates;
1821
+ }
1822
+ }
1823
+ } catch (err) {
1824
+ strapi2.log.warn("[magic-sessionmanager/notifications] Could not load templates from DB, using defaults:", err.message);
1825
+ }
1826
+ strapi2.log.debug("[magic-sessionmanager/notifications] Using default fallback templates");
1827
+ return {
1828
+ suspiciousLogin: {
1829
+ subject: "🚨 Suspicious Login Alert - Session Manager",
1830
+ html: `
1831
+ <html>
1832
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
1833
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb; border-radius: 10px;">
1834
+ <h2 style="color: #dc2626;">🚨 Suspicious Login Detected</h2>
1835
+ <p>A potentially suspicious login was detected for your account.</p>
1836
+
1837
+ <div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
1838
+ <h3 style="margin-top: 0;">Account Information:</h3>
1839
+ <ul>
1840
+ <li><strong>Email:</strong> {{user.email}}</li>
1841
+ <li><strong>Username:</strong> {{user.username}}</li>
1842
+ </ul>
1843
+
1844
+ <h3>Login Details:</h3>
1845
+ <ul>
1846
+ <li><strong>Time:</strong> {{session.loginTime}}</li>
1847
+ <li><strong>IP Address:</strong> {{session.ipAddress}}</li>
1848
+ <li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
1849
+ <li><strong>Timezone:</strong> {{geo.timezone}}</li>
1850
+ <li><strong>Device:</strong> {{session.userAgent}}</li>
1851
+ </ul>
1852
+
1853
+ <h3 style="color: #dc2626;">Security Alert:</h3>
1854
+ <ul>
1855
+ <li>VPN Detected: {{reason.isVpn}}</li>
1856
+ <li>Proxy Detected: {{reason.isProxy}}</li>
1857
+ <li>Threat Detected: {{reason.isThreat}}</li>
1858
+ <li>Security Score: {{reason.securityScore}}/100</li>
1859
+ </ul>
1860
+ </div>
1861
+
1862
+ <p>If this was you, you can safely ignore this email. If you don't recognize this activity, please secure your account immediately.</p>
1863
+
1864
+ <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
1865
+ <p style="color: #666; font-size: 12px;">This is an automated security notification from Magic Session Manager.</p>
1866
+ </div>
1867
+ </body>
1868
+ </html>`,
1869
+ text: `🚨 Suspicious Login Detected
1870
+
1871
+ A potentially suspicious login was detected for your account.
1872
+
1873
+ Account: {{user.email}}
1874
+ Username: {{user.username}}
1875
+
1876
+ Login Details:
1877
+ - Time: {{session.loginTime}}
1878
+ - IP: {{session.ipAddress}}
1879
+ - Location: {{geo.city}}, {{geo.country}}
1880
+
1881
+ Security: VPN={{reason.isVpn}}, Proxy={{reason.isProxy}}, Threat={{reason.isThreat}}, Score={{reason.securityScore}}/100`
1882
+ },
1883
+ newLocation: {
1884
+ subject: "📍 New Location Login Detected",
1885
+ html: `<h2>📍 New Location Login</h2><p>Account: {{user.email}}</p><p>Time: {{session.loginTime}}</p><p>Location: {{geo.city}}, {{geo.country}}</p><p>IP: {{session.ipAddress}}</p>`,
1886
+ text: `📍 New Location Login
1887
+
1888
+ Account: {{user.email}}
1889
+ Time: {{session.loginTime}}
1890
+ Location: {{geo.city}}, {{geo.country}}
1891
+ IP: {{session.ipAddress}}`
1892
+ },
1893
+ vpnProxy: {
1894
+ subject: "⚠️ VPN/Proxy Login Detected",
1895
+ html: `<h2>⚠️ VPN/Proxy Detected</h2><p>Account: {{user.email}}</p><p>Time: {{session.loginTime}}</p><p>IP: {{session.ipAddress}}</p><p>VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}</p>`,
1896
+ text: `⚠️ VPN/Proxy Detected
1897
+
1898
+ Account: {{user.email}}
1899
+ Time: {{session.loginTime}}
1900
+ IP: {{session.ipAddress}}
1901
+ VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
1902
+ }
1903
+ };
1904
+ },
1905
+ /**
1906
+ * Replace template variables with actual values
1907
+ */
1908
+ replaceVariables(template, data) {
1909
+ let result = template;
1910
+ result = result.replace(/\{\{user\.email\}\}/g, data.user?.email || "N/A");
1911
+ result = result.replace(/\{\{user\.username\}\}/g, data.user?.username || "N/A");
1912
+ result = result.replace(
1913
+ /\{\{session\.loginTime\}\}/g,
1914
+ data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A"
1915
+ );
1916
+ result = result.replace(/\{\{session\.ipAddress\}\}/g, data.session?.ipAddress || "N/A");
1917
+ result = result.replace(/\{\{session\.userAgent\}\}/g, data.session?.userAgent || "N/A");
1918
+ result = result.replace(/\{\{geo\.city\}\}/g, data.geoData?.city || "Unknown");
1919
+ result = result.replace(/\{\{geo\.country\}\}/g, data.geoData?.country || "Unknown");
1920
+ result = result.replace(/\{\{geo\.timezone\}\}/g, data.geoData?.timezone || "Unknown");
1921
+ result = result.replace(/\{\{reason\.isVpn\}\}/g, data.reason?.isVpn ? "Yes" : "No");
1922
+ result = result.replace(/\{\{reason\.isProxy\}\}/g, data.reason?.isProxy ? "Yes" : "No");
1923
+ result = result.replace(/\{\{reason\.isThreat\}\}/g, data.reason?.isThreat ? "Yes" : "No");
1924
+ result = result.replace(/\{\{reason\.securityScore\}\}/g, data.reason?.securityScore || "0");
1925
+ return result;
1926
+ },
1927
+ /**
1928
+ * Send suspicious login alert
1929
+ * @param {Object} params - { user, session, reason, geoData }
1930
+ */
1931
+ async sendSuspiciousLoginAlert({ user, session: session2, reason, geoData }) {
1932
+ try {
1933
+ const templates = await this.getEmailTemplates();
1934
+ const template = templates.suspiciousLogin;
1935
+ const data = { user, session: session2, reason, geoData };
1936
+ const htmlContent = this.replaceVariables(template.html, data);
1937
+ const textContent = this.replaceVariables(template.text, data);
1938
+ await strapi2.plugins["email"].services.email.send({
1939
+ to: user.email,
1940
+ subject: template.subject,
1941
+ html: htmlContent,
1942
+ text: textContent
1943
+ });
1944
+ strapi2.log.info(`[magic-sessionmanager/notifications] Suspicious login alert sent to ${user.email}`);
1945
+ return true;
1946
+ } catch (err) {
1947
+ strapi2.log.error("[magic-sessionmanager/notifications] Error sending email:", err);
1948
+ return false;
1949
+ }
1950
+ },
1951
+ /**
1952
+ * Send new location login alert
1953
+ * @param {Object} params - { user, session, geoData }
1954
+ */
1955
+ async sendNewLocationAlert({ user, session: session2, geoData }) {
1956
+ try {
1957
+ const templates = await this.getEmailTemplates();
1958
+ const template = templates.newLocation;
1959
+ const data = { user, session: session2, geoData, reason: {} };
1960
+ const htmlContent = this.replaceVariables(template.html, data);
1961
+ const textContent = this.replaceVariables(template.text, data);
1962
+ await strapi2.plugins["email"].services.email.send({
1963
+ to: user.email,
1964
+ subject: template.subject,
1965
+ html: htmlContent,
1966
+ text: textContent
1967
+ });
1968
+ strapi2.log.info(`[magic-sessionmanager/notifications] New location alert sent to ${user.email}`);
1969
+ return true;
1970
+ } catch (err) {
1971
+ strapi2.log.error("[magic-sessionmanager/notifications] Error sending new location email:", err);
1972
+ return false;
1973
+ }
1974
+ },
1975
+ /**
1976
+ * Send VPN/Proxy login alert
1977
+ * @param {Object} params - { user, session, reason, geoData }
1978
+ */
1979
+ async sendVpnProxyAlert({ user, session: session2, reason, geoData }) {
1980
+ try {
1981
+ const templates = await this.getEmailTemplates();
1982
+ const template = templates.vpnProxy;
1983
+ const data = { user, session: session2, reason, geoData };
1984
+ const htmlContent = this.replaceVariables(template.html, data);
1985
+ const textContent = this.replaceVariables(template.text, data);
1986
+ await strapi2.plugins["email"].services.email.send({
1987
+ to: user.email,
1988
+ subject: template.subject,
1989
+ html: htmlContent,
1990
+ text: textContent
1991
+ });
1992
+ strapi2.log.info(`[magic-sessionmanager/notifications] VPN/Proxy alert sent to ${user.email}`);
1993
+ return true;
1994
+ } catch (err) {
1995
+ strapi2.log.error("[magic-sessionmanager/notifications] Error sending VPN/Proxy email:", err);
1996
+ return false;
1997
+ }
1998
+ },
1999
+ /**
2000
+ * Send webhook notification
2001
+ * @param {Object} params - { event, data, webhookUrl }
2002
+ */
2003
+ async sendWebhook({ event, data, webhookUrl }) {
2004
+ try {
2005
+ const payload = {
2006
+ event,
2007
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2008
+ data,
2009
+ source: "magic-sessionmanager"
2010
+ };
2011
+ const response = await fetch(webhookUrl, {
2012
+ method: "POST",
2013
+ headers: {
2014
+ "Content-Type": "application/json",
2015
+ "User-Agent": "Strapi-Magic-SessionManager-Webhook/1.0"
2016
+ },
2017
+ body: JSON.stringify(payload)
2018
+ });
2019
+ if (response.ok) {
2020
+ strapi2.log.info(`[magic-sessionmanager/notifications] Webhook sent: ${event}`);
2021
+ return true;
2022
+ } else {
2023
+ strapi2.log.warn(`[magic-sessionmanager/notifications] Webhook failed: ${response.status}`);
2024
+ return false;
2025
+ }
2026
+ } catch (err) {
2027
+ strapi2.log.error("[magic-sessionmanager/notifications] Webhook error:", err);
2028
+ return false;
2029
+ }
2030
+ },
2031
+ /**
2032
+ * Format webhook for Discord
2033
+ * @param {Object} params - { event, session, user, geoData }
2034
+ */
2035
+ formatDiscordWebhook({ event, session: session2, user, geoData }) {
2036
+ const embed = {
2037
+ title: this.getEventTitle(event),
2038
+ color: this.getEventColor(event),
2039
+ fields: [
2040
+ { name: "👤 User", value: `${user.email}
2041
+ ${user.username || "N/A"}`, inline: true },
2042
+ { name: "🌐 IP", value: session2.ipAddress, inline: true },
2043
+ { name: "📅 Time", value: new Date(session2.loginTime).toLocaleString(), inline: false }
2044
+ ],
2045
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2046
+ footer: { text: "Magic Session Manager" }
2047
+ };
2048
+ if (geoData) {
2049
+ embed.fields.push({
2050
+ name: "📍 Location",
2051
+ value: `${geoData.country_flag} ${geoData.city}, ${geoData.country}`,
2052
+ inline: true
2053
+ });
2054
+ if (geoData.isVpn || geoData.isProxy || geoData.isThreat) {
2055
+ const warnings = [];
2056
+ if (geoData.isVpn) warnings.push("VPN");
2057
+ if (geoData.isProxy) warnings.push("Proxy");
2058
+ if (geoData.isThreat) warnings.push("Threat");
2059
+ embed.fields.push({
2060
+ name: "⚠️ Security",
2061
+ value: `${warnings.join(", ")} detected
2062
+ Score: ${geoData.securityScore}/100`,
2063
+ inline: true
2064
+ });
2065
+ }
2066
+ }
2067
+ return { embeds: [embed] };
2068
+ },
2069
+ getEventTitle(event) {
2070
+ const titles = {
2071
+ "login.suspicious": "🚨 Suspicious Login",
2072
+ "login.new_location": "📍 New Location Login",
2073
+ "login.vpn": "🔴 VPN Login Detected",
2074
+ "login.threat": "⛔ Threat IP Login",
2075
+ "session.terminated": "🔴 Session Terminated"
2076
+ };
2077
+ return titles[event] || "📊 Session Event";
2078
+ },
2079
+ getEventColor(event) {
2080
+ const colors = {
2081
+ "login.suspicious": 16711680,
2082
+ // Red
2083
+ "login.new_location": 16753920,
2084
+ // Orange
2085
+ "login.vpn": 16739179,
2086
+ // Light Red
2087
+ "login.threat": 9109504,
2088
+ // Dark Red
2089
+ "session.terminated": 8421504
2090
+ // Gray
2091
+ };
2092
+ return colors[event] || 5793266;
2093
+ }
2094
+ });
2095
+ const session = session$1;
2096
+ const licenseGuard = licenseGuard$1;
2097
+ const geolocation = geolocation$1;
2098
+ const notifications = notifications$1;
2099
+ var services$1 = {
2100
+ session,
2101
+ "license-guard": licenseGuard,
2102
+ geolocation,
2103
+ notifications
2104
+ };
2105
+ var middlewares$1 = {
2106
+ "last-seen": lastSeen
2107
+ };
2108
+ const register = register$1;
2109
+ const bootstrap = bootstrap$1;
2110
+ const destroy = destroy$1;
2111
+ const config = config$1;
2112
+ const routes = routes$1;
2113
+ const controllers = controllers$1;
2114
+ const services = services$1;
2115
+ const middlewares = middlewares$1;
2116
+ var src = {
2117
+ register,
2118
+ bootstrap,
2119
+ destroy,
2120
+ config,
2121
+ routes,
2122
+ controllers,
2123
+ services,
2124
+ middlewares
2125
+ };
2126
+ const index = /* @__PURE__ */ getDefaultExportFromCjs(src);
2127
+ module.exports = index;