strapi-plugin-magic-sessionmanager 2.0.1 → 2.0.3

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.
Files changed (54) hide show
  1. package/admin/jsconfig.json +10 -0
  2. package/admin/src/components/Initializer.jsx +11 -0
  3. package/admin/src/components/LicenseGuard.jsx +591 -0
  4. package/admin/src/components/OnlineUsersWidget.jsx +208 -0
  5. package/admin/src/components/PluginIcon.jsx +8 -0
  6. package/admin/src/components/SessionDetailModal.jsx +445 -0
  7. package/admin/src/components/SessionInfoCard.jsx +151 -0
  8. package/admin/src/components/SessionInfoPanel.jsx +375 -0
  9. package/admin/src/components/index.jsx +5 -0
  10. package/admin/src/hooks/useLicense.js +103 -0
  11. package/admin/src/index.js +137 -0
  12. package/admin/src/pages/ActiveSessions.jsx +12 -0
  13. package/admin/src/pages/Analytics.jsx +735 -0
  14. package/admin/src/pages/App.jsx +12 -0
  15. package/admin/src/pages/HomePage.jsx +1248 -0
  16. package/admin/src/pages/License.jsx +603 -0
  17. package/admin/src/pages/Settings.jsx +1497 -0
  18. package/admin/src/pages/SettingsNew.jsx +1204 -0
  19. package/admin/src/pages/index.jsx +3 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +20 -0
  22. package/admin/src/translations/en.json +20 -0
  23. package/admin/src/utils/getTranslation.js +5 -0
  24. package/admin/src/utils/index.js +2 -0
  25. package/admin/src/utils/parseUserAgent.js +79 -0
  26. package/dist/server/index.js +91 -2
  27. package/dist/server/index.mjs +91 -2
  28. package/package.json +3 -1
  29. package/server/jsconfig.json +10 -0
  30. package/server/src/bootstrap.js +297 -0
  31. package/server/src/config/index.js +20 -0
  32. package/server/src/content-types/index.js +9 -0
  33. package/server/src/content-types/session/schema.json +76 -0
  34. package/server/src/controllers/controller.js +11 -0
  35. package/server/src/controllers/index.js +11 -0
  36. package/server/src/controllers/license.js +266 -0
  37. package/server/src/controllers/session.js +362 -0
  38. package/server/src/controllers/settings.js +122 -0
  39. package/server/src/destroy.js +18 -0
  40. package/server/src/index.js +23 -0
  41. package/server/src/middlewares/index.js +5 -0
  42. package/server/src/middlewares/last-seen.js +56 -0
  43. package/server/src/policies/index.js +3 -0
  44. package/server/src/register.js +32 -0
  45. package/server/src/routes/admin.js +149 -0
  46. package/server/src/routes/content-api.js +51 -0
  47. package/server/src/routes/index.js +9 -0
  48. package/server/src/services/geolocation.js +180 -0
  49. package/server/src/services/index.js +13 -0
  50. package/server/src/services/license-guard.js +308 -0
  51. package/server/src/services/notifications.js +319 -0
  52. package/server/src/services/service.js +7 -0
  53. package/server/src/services/session.js +345 -0
  54. package/server/src/utils/getClientIp.js +118 -0
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Bootstrap: Mount middleware for session tracking
5
+ * Sessions are managed via plugin::magic-sessionmanager.session content type
6
+ *
7
+ * NOTE: For multi-instance deployments, consider Redis locks or session store
8
+ */
9
+
10
+ const getClientIp = require('./utils/getClientIp');
11
+
12
+ module.exports = async ({ strapi }) => {
13
+ strapi.log.info('[magic-sessionmanager] 🚀 Bootstrap starting...');
14
+
15
+ try {
16
+ // Initialize License Guard
17
+ const licenseGuardService = strapi.plugin('magic-sessionmanager').service('license-guard');
18
+
19
+ // Wait a bit for all services to be ready
20
+ setTimeout(async () => {
21
+ const licenseStatus = await licenseGuardService.initialize();
22
+
23
+ if (!licenseStatus.valid) {
24
+ strapi.log.error('╔════════════════════════════════════════════════════════════════╗');
25
+ strapi.log.error('║ ❌ SESSION MANAGER - NO VALID LICENSE ║');
26
+ strapi.log.error('║ ║');
27
+ strapi.log.error('║ This plugin requires a valid license to operate. ║');
28
+ strapi.log.error('║ Please activate your license via Admin UI: ║');
29
+ strapi.log.error('║ Go to Settings → Sessions → License ║');
30
+ strapi.log.error('║ ║');
31
+ strapi.log.error('║ The plugin will run with limited functionality until ║');
32
+ strapi.log.error('║ a valid license is activated. ║');
33
+ strapi.log.error('╚════════════════════════════════════════════════════════════════╝');
34
+ } else if (licenseStatus.valid) {
35
+ const pluginStore = strapi.store({
36
+ type: 'plugin',
37
+ name: 'magic-sessionmanager',
38
+ });
39
+ const storedKey = await pluginStore.get({ key: 'licenseKey' });
40
+
41
+ strapi.log.info('╔════════════════════════════════════════════════════════════════╗');
42
+ strapi.log.info('║ ✅ SESSION MANAGER LICENSE ACTIVE ║');
43
+ strapi.log.info('║ ║');
44
+
45
+ if (licenseStatus.data) {
46
+ strapi.log.info(`║ License: ${licenseStatus.data.licenseKey}`.padEnd(66) + '║');
47
+ strapi.log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + '║');
48
+ strapi.log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + '║');
49
+ } else if (storedKey) {
50
+ strapi.log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + '║');
51
+ strapi.log.info(`║ Status: Grace Period Active`.padEnd(66) + '║');
52
+ }
53
+
54
+ strapi.log.info('║ ║');
55
+ strapi.log.info('║ 🔄 Auto-pinging every 15 minutes ║');
56
+ strapi.log.info('╚════════════════════════════════════════════════════════════════╝');
57
+ }
58
+ }, 3000); // Wait 3 seconds for API to be ready
59
+
60
+ // Get session service
61
+ const sessionService = strapi
62
+ .plugin('magic-sessionmanager')
63
+ .service('session');
64
+
65
+ // Cleanup inactive sessions on startup
66
+ strapi.log.info('[magic-sessionmanager] Running initial session cleanup...');
67
+ await sessionService.cleanupInactiveSessions();
68
+
69
+ // Schedule periodic cleanup every 30 minutes
70
+ const cleanupInterval = 30 * 60 * 1000; // 30 minutes
71
+
72
+ const cleanupIntervalHandle = setInterval(async () => {
73
+ try {
74
+ // Get fresh reference to service to avoid scope issues
75
+ const service = strapi.plugin('magic-sessionmanager').service('session');
76
+ await service.cleanupInactiveSessions();
77
+ } catch (err) {
78
+ strapi.log.error('[magic-sessionmanager] Periodic cleanup error:', err);
79
+ }
80
+ }, cleanupInterval);
81
+
82
+ strapi.log.info('[magic-sessionmanager] ⏰ Periodic cleanup scheduled (every 30 minutes)');
83
+
84
+ // Store interval handle for cleanup on shutdown
85
+ if (!strapi.sessionManagerIntervals) {
86
+ strapi.sessionManagerIntervals = {};
87
+ }
88
+ strapi.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
89
+
90
+ // HIGH PRIORITY: Register /api/auth/logout route BEFORE other plugins
91
+ strapi.server.routes([{
92
+ method: 'POST',
93
+ path: '/api/auth/logout',
94
+ handler: async (ctx) => {
95
+ try {
96
+ const token = ctx.request.headers?.authorization?.replace('Bearer ', '');
97
+
98
+ if (!token) {
99
+ ctx.status = 200;
100
+ ctx.body = { message: 'Logged out successfully' };
101
+ return;
102
+ }
103
+
104
+ // Find and terminate session by token
105
+ const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
106
+ filters: {
107
+ token: token,
108
+ isActive: true,
109
+ },
110
+ limit: 1,
111
+ });
112
+
113
+ if (sessions.length > 0) {
114
+ await sessionService.terminateSession({ sessionId: sessions[0].id });
115
+ strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
116
+ }
117
+
118
+ ctx.status = 200;
119
+ ctx.body = { message: 'Logged out successfully' };
120
+ } catch (err) {
121
+ strapi.log.error('[magic-sessionmanager] Logout error:', err);
122
+ ctx.status = 200;
123
+ ctx.body = { message: 'Logged out successfully' };
124
+ }
125
+ },
126
+ config: {
127
+ auth: false,
128
+ },
129
+ }]);
130
+
131
+ strapi.log.info('[magic-sessionmanager] ✅ /api/auth/logout route registered');
132
+
133
+ // Middleware to intercept logins
134
+ strapi.server.use(async (ctx, next) => {
135
+ // Execute the actual request
136
+ await next();
137
+
138
+ // Check if this was a successful login request
139
+ const isAuthLocal = ctx.path === '/api/auth/local' && ctx.method === 'POST';
140
+ const isMagicLink = ctx.path.includes('/magic-link/login') && ctx.method === 'POST';
141
+
142
+ if ((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.user) {
143
+ try {
144
+ const user = ctx.body.user;
145
+
146
+ // Extract REAL client IP (handles proxies, load balancers, Cloudflare, etc.)
147
+ const ip = getClientIp(ctx);
148
+ const userAgent = ctx.request.headers?.['user-agent'] || ctx.request.header?.['user-agent'] || 'unknown';
149
+
150
+ strapi.log.info(`[magic-sessionmanager] 🔍 Login detected! User: ${user.id} (${user.email || user.username}) from IP: ${ip}`);
151
+
152
+ // Get config
153
+ const config = strapi.config.get('plugin::magic-sessionmanager') || {};
154
+
155
+ // Check if we should analyze this session (Premium/Advanced features)
156
+ let shouldBlock = false;
157
+ let blockReason = '';
158
+ let geoData = null;
159
+
160
+ // Premium: Get geolocation data
161
+ if (config.enableGeolocation || config.enableSecurityScoring) {
162
+ try {
163
+ const geolocationService = strapi.plugin('magic-sessionmanager').service('geolocation');
164
+ geoData = await geolocationService.getIpInfo(ip);
165
+
166
+ // Advanced: Auto-blocking
167
+ if (config.blockSuspiciousSessions && geoData) {
168
+ if (geoData.isThreat) {
169
+ shouldBlock = true;
170
+ blockReason = 'Known threat IP detected';
171
+ } else if (geoData.isVpn && config.alertOnVpnProxy) {
172
+ shouldBlock = true;
173
+ blockReason = 'VPN detected';
174
+ } else if (geoData.isProxy && config.alertOnVpnProxy) {
175
+ shouldBlock = true;
176
+ blockReason = 'Proxy detected';
177
+ } else if (geoData.securityScore < 50) {
178
+ shouldBlock = true;
179
+ blockReason = `Low security score: ${geoData.securityScore}/100`;
180
+ }
181
+ }
182
+
183
+ // Advanced: Geo-fencing
184
+ if (config.enableGeofencing && geoData && geoData.country_code) {
185
+ const countryCode = geoData.country_code;
186
+
187
+ // Check blocked countries
188
+ if (config.blockedCountries && config.blockedCountries.includes(countryCode)) {
189
+ shouldBlock = true;
190
+ blockReason = `Country ${countryCode} is blocked`;
191
+ }
192
+
193
+ // Check allowed countries (whitelist)
194
+ if (config.allowedCountries && config.allowedCountries.length > 0) {
195
+ if (!config.allowedCountries.includes(countryCode)) {
196
+ shouldBlock = true;
197
+ blockReason = `Country ${countryCode} is not in allowlist`;
198
+ }
199
+ }
200
+ }
201
+ } catch (geoErr) {
202
+ strapi.log.warn('[magic-sessionmanager] Geolocation check failed:', geoErr.message);
203
+ }
204
+ }
205
+
206
+ // Block if needed
207
+ if (shouldBlock) {
208
+ strapi.log.warn(`[magic-sessionmanager] 🚫 Blocking login: ${blockReason}`);
209
+
210
+ // Don't create session, return error
211
+ ctx.status = 403;
212
+ ctx.body = {
213
+ error: {
214
+ status: 403,
215
+ message: 'Login blocked for security reasons',
216
+ details: { reason: blockReason }
217
+ }
218
+ };
219
+ return; // Stop here
220
+ }
221
+
222
+ // Create a new session
223
+ const newSession = await sessionService.createSession({
224
+ userId: user.id,
225
+ ip,
226
+ userAgent,
227
+ token: ctx.body.jwt, // Store JWT token reference
228
+ });
229
+
230
+ strapi.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
231
+
232
+ // Advanced: Send notifications
233
+ if (geoData && (config.enableEmailAlerts || config.enableWebhooks)) {
234
+ try {
235
+ const notificationService = strapi.plugin('magic-sessionmanager').service('notifications');
236
+
237
+ // Determine if suspicious
238
+ const isSuspicious = geoData.isVpn || geoData.isProxy || geoData.isThreat || geoData.securityScore < 70;
239
+
240
+ // Email alerts
241
+ if (config.enableEmailAlerts && config.alertOnSuspiciousLogin && isSuspicious) {
242
+ await notificationService.sendSuspiciousLoginAlert({
243
+ user,
244
+ session: newSession,
245
+ reason: {
246
+ isVpn: geoData.isVpn,
247
+ isProxy: geoData.isProxy,
248
+ isThreat: geoData.isThreat,
249
+ securityScore: geoData.securityScore,
250
+ },
251
+ geoData,
252
+ });
253
+ }
254
+
255
+ // Webhook notifications (Discord/Slack)
256
+ if (config.enableWebhooks) {
257
+ const webhookData = notificationService.formatDiscordWebhook({
258
+ event: isSuspicious ? 'login.suspicious' : 'login.success',
259
+ session: newSession,
260
+ user,
261
+ geoData,
262
+ });
263
+
264
+ if (config.discordWebhookUrl) {
265
+ await notificationService.sendWebhook({
266
+ event: 'session.login',
267
+ data: webhookData,
268
+ webhookUrl: config.discordWebhookUrl,
269
+ });
270
+ }
271
+ }
272
+ } catch (notifErr) {
273
+ strapi.log.warn('[magic-sessionmanager] Notification failed:', notifErr.message);
274
+ }
275
+ }
276
+ } catch (err) {
277
+ strapi.log.error('[magic-sessionmanager] ❌ Error creating session:', err);
278
+ // Don't throw - login should still succeed even if session creation fails
279
+ }
280
+ }
281
+ });
282
+
283
+ strapi.log.info('[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted');
284
+
285
+ // Mount lastSeen update middleware
286
+ strapi.server.use(
287
+ require('./middlewares/last-seen')({ strapi, sessionService })
288
+ );
289
+
290
+ strapi.log.info('[magic-sessionmanager] ✅ LastSeen middleware mounted');
291
+ strapi.log.info('[magic-sessionmanager] ✅ Bootstrap complete');
292
+ strapi.log.info('[magic-sessionmanager] 🎉 Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
293
+
294
+ } catch (err) {
295
+ strapi.log.error('[magic-sessionmanager] ❌ Bootstrap error:', err);
296
+ }
297
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ default: {
5
+ // Rate limit for lastSeen updates (in milliseconds)
6
+ lastSeenRateLimit: 30000, // 30 seconds
7
+
8
+ // Session inactivity timeout (in milliseconds)
9
+ // After this time without activity, a session is considered inactive
10
+ inactivityTimeout: 15 * 60 * 1000, // 15 minutes
11
+ },
12
+ validator: (config) => {
13
+ if (config.lastSeenRateLimit && typeof config.lastSeenRateLimit !== 'number') {
14
+ throw new Error('lastSeenRateLimit must be a number (milliseconds)');
15
+ }
16
+ if (config.inactivityTimeout && typeof config.inactivityTimeout !== 'number') {
17
+ throw new Error('inactivityTimeout must be a number (milliseconds)');
18
+ }
19
+ },
20
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const session = require('./session/schema.json');
4
+
5
+ module.exports = {
6
+ 'plugin::magic-sessionmanager.session': {
7
+ schema: session,
8
+ },
9
+ };
@@ -0,0 +1,76 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "sessions",
4
+ "info": {
5
+ "singularName": "session",
6
+ "pluralName": "sessions",
7
+ "displayName": "Session",
8
+ "description": "User session tracking with IP, device, and activity information"
9
+ },
10
+ "options": {
11
+ "draftAndPublish": false,
12
+ "comment": ""
13
+ },
14
+ "pluginOptions": {
15
+ "content-manager": {
16
+ "visible": true
17
+ },
18
+ "content-type-builder": {
19
+ "visible": false
20
+ }
21
+ },
22
+ "attributes": {
23
+ "user": {
24
+ "type": "relation",
25
+ "relation": "manyToOne",
26
+ "target": "plugin::users-permissions.user",
27
+ "inversedBy": "sessions"
28
+ },
29
+ "ipAddress": {
30
+ "type": "string",
31
+ "maxLength": 45,
32
+ "required": true
33
+ },
34
+ "userAgent": {
35
+ "type": "text",
36
+ "maxLength": 500
37
+ },
38
+ "token": {
39
+ "type": "text",
40
+ "private": true
41
+ },
42
+ "loginTime": {
43
+ "type": "datetime",
44
+ "required": true
45
+ },
46
+ "logoutTime": {
47
+ "type": "datetime"
48
+ },
49
+ "lastActive": {
50
+ "type": "datetime"
51
+ },
52
+ "isActive": {
53
+ "type": "boolean",
54
+ "default": true,
55
+ "required": true
56
+ },
57
+ "geoLocation": {
58
+ "type": "json"
59
+ },
60
+ "securityScore": {
61
+ "type": "integer",
62
+ "min": 0,
63
+ "max": 100
64
+ },
65
+ "deviceType": {
66
+ "type": "string"
67
+ },
68
+ "browserName": {
69
+ "type": "string"
70
+ },
71
+ "osName": {
72
+ "type": "string"
73
+ }
74
+ }
75
+ }
76
+
@@ -0,0 +1,11 @@
1
+ const controller = ({ strapi }) => ({
2
+ index(ctx) {
3
+ ctx.body = strapi
4
+ .plugin('magic-sessionmanager')
5
+ // the name of the service file & the method.
6
+ .service('service')
7
+ .getWelcomeMessage();
8
+ },
9
+ });
10
+
11
+ export default controller;
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const session = require('./session');
4
+ const license = require('./license');
5
+ const settings = require('./settings');
6
+
7
+ module.exports = {
8
+ session,
9
+ license,
10
+ settings,
11
+ };
@@ -0,0 +1,266 @@
1
+ /**
2
+ * License Controller for Magic Session Manager Plugin
3
+ * Manages licenses directly from the Admin Panel
4
+ */
5
+
6
+ module.exports = ({ strapi }) => ({
7
+ /**
8
+ * Auto-create license with logged-in admin user data
9
+ */
10
+ async autoCreate(ctx) {
11
+ try {
12
+ // Get the logged-in admin user
13
+ const adminUser = ctx.state.user;
14
+
15
+ if (!adminUser) {
16
+ return ctx.unauthorized('No admin user logged in');
17
+ }
18
+
19
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
20
+
21
+ // Use admin user data for license creation
22
+ const license = await licenseGuard.createLicense({
23
+ email: adminUser.email,
24
+ firstName: adminUser.firstname || 'Admin',
25
+ lastName: adminUser.lastname || 'User',
26
+ });
27
+
28
+ if (!license) {
29
+ return ctx.badRequest('Failed to create license');
30
+ }
31
+
32
+ // Store the license key
33
+ await licenseGuard.storeLicenseKey(license.licenseKey);
34
+
35
+ // Start pinging
36
+ const pingInterval = licenseGuard.startPinging(license.licenseKey, 15);
37
+
38
+ // Update global license guard
39
+ strapi.licenseGuard = {
40
+ licenseKey: license.licenseKey,
41
+ pingInterval,
42
+ data: license,
43
+ };
44
+
45
+ return ctx.send({
46
+ success: true,
47
+ message: 'License automatically created and activated',
48
+ data: license,
49
+ });
50
+ } catch (error) {
51
+ strapi.log.error('[magic-sessionmanager] Error auto-creating license:', error);
52
+ return ctx.badRequest('Error creating license');
53
+ }
54
+ },
55
+
56
+ /**
57
+ * Get current license status
58
+ */
59
+ async getStatus(ctx) {
60
+ try {
61
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
62
+ const pluginStore = strapi.store({
63
+ type: 'plugin',
64
+ name: 'magic-sessionmanager'
65
+ });
66
+ const licenseKey = await pluginStore.get({ key: 'licenseKey' });
67
+
68
+ if (!licenseKey) {
69
+ strapi.log.debug('[magic-sessionmanager] No license key in store - demo mode');
70
+ return ctx.send({
71
+ success: false,
72
+ demo: true,
73
+ valid: false,
74
+ message: 'No license found. Running in demo mode.',
75
+ });
76
+ }
77
+
78
+ strapi.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey}`);
79
+
80
+ const verification = await licenseGuard.verifyLicense(licenseKey);
81
+ const license = await licenseGuard.getLicenseByKey(licenseKey);
82
+
83
+ strapi.log.info('[magic-sessionmanager/license-controller] License data from MagicAPI:', {
84
+ licenseKey: license?.licenseKey,
85
+ email: license?.email,
86
+ featurePremium: license?.featurePremium,
87
+ isActive: license?.isActive,
88
+ pluginName: license?.pluginName,
89
+ });
90
+
91
+ return ctx.send({
92
+ success: true,
93
+ valid: verification.valid,
94
+ demo: false,
95
+ data: {
96
+ licenseKey,
97
+ email: license?.email || null,
98
+ firstName: license?.firstName || null,
99
+ lastName: license?.lastName || null,
100
+ isActive: license?.isActive || false,
101
+ isExpired: license?.isExpired || false,
102
+ isOnline: license?.isOnline || false,
103
+ expiresAt: license?.expiresAt,
104
+ lastPingAt: license?.lastPingAt,
105
+ deviceName: license?.deviceName,
106
+ deviceId: license?.deviceId,
107
+ ipAddress: license?.ipAddress,
108
+ features: {
109
+ premium: license?.featurePremium || false,
110
+ advanced: license?.featureAdvanced || false,
111
+ enterprise: license?.featureEnterprise || false,
112
+ custom: license?.featureCustom || false,
113
+ },
114
+ maxDevices: license?.maxDevices || 1,
115
+ currentDevices: license?.currentDevices || 0,
116
+ },
117
+ });
118
+ } catch (error) {
119
+ strapi.log.error('[magic-sessionmanager] Error getting license status:', error);
120
+ return ctx.badRequest('Error getting license status');
121
+ }
122
+ },
123
+
124
+ /**
125
+ * Create and activate a new license
126
+ */
127
+ async createAndActivate(ctx) {
128
+ try {
129
+ const { email, firstName, lastName } = ctx.request.body;
130
+
131
+ if (!email || !firstName || !lastName) {
132
+ return ctx.badRequest('Email, firstName, and lastName are required');
133
+ }
134
+
135
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
136
+ const license = await licenseGuard.createLicense({ email, firstName, lastName });
137
+
138
+ if (!license) {
139
+ return ctx.badRequest('Failed to create license');
140
+ }
141
+
142
+ // Store the license key
143
+ await licenseGuard.storeLicenseKey(license.licenseKey);
144
+
145
+ // Start pinging
146
+ const pingInterval = licenseGuard.startPinging(license.licenseKey, 15);
147
+
148
+ // Update global license guard
149
+ strapi.licenseGuard = {
150
+ licenseKey: license.licenseKey,
151
+ pingInterval,
152
+ data: license,
153
+ };
154
+
155
+ return ctx.send({
156
+ success: true,
157
+ message: 'License created and activated successfully',
158
+ data: license,
159
+ });
160
+ } catch (error) {
161
+ strapi.log.error('[magic-sessionmanager] Error creating license:', error);
162
+ return ctx.badRequest('Error creating license');
163
+ }
164
+ },
165
+
166
+ /**
167
+ * Manually ping the current license
168
+ */
169
+ async ping(ctx) {
170
+ try {
171
+ const pluginStore = strapi.store({
172
+ type: 'plugin',
173
+ name: 'magic-sessionmanager'
174
+ });
175
+ const licenseKey = await pluginStore.get({ key: 'licenseKey' });
176
+
177
+ if (!licenseKey) {
178
+ return ctx.badRequest('No license key found');
179
+ }
180
+
181
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
182
+ const pingResult = await licenseGuard.pingLicense(licenseKey);
183
+
184
+ if (!pingResult) {
185
+ return ctx.badRequest('Ping failed');
186
+ }
187
+
188
+ return ctx.send({
189
+ success: true,
190
+ message: 'License pinged successfully',
191
+ data: pingResult,
192
+ });
193
+ } catch (error) {
194
+ strapi.log.error('[magic-sessionmanager] Error pinging license:', error);
195
+ return ctx.badRequest('Error pinging license');
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Store and validate an existing license key
201
+ */
202
+ async storeKey(ctx) {
203
+ try {
204
+ const { licenseKey, email } = ctx.request.body;
205
+
206
+ if (!licenseKey || !licenseKey.trim()) {
207
+ return ctx.badRequest('License key is required');
208
+ }
209
+
210
+ if (!email || !email.trim()) {
211
+ return ctx.badRequest('Email address is required');
212
+ }
213
+
214
+ const trimmedKey = licenseKey.trim();
215
+ const trimmedEmail = email.trim().toLowerCase();
216
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
217
+
218
+ // Verify the license key first
219
+ const verification = await licenseGuard.verifyLicense(trimmedKey);
220
+
221
+ if (!verification.valid) {
222
+ strapi.log.warn(`[magic-sessionmanager] ⚠️ Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
223
+ return ctx.badRequest('Invalid or expired license key');
224
+ }
225
+
226
+ // Get license details to verify email
227
+ const license = await licenseGuard.getLicenseByKey(trimmedKey);
228
+
229
+ if (!license) {
230
+ strapi.log.warn(`[magic-sessionmanager] ⚠️ License not found in database: ${trimmedKey.substring(0, 8)}...`);
231
+ return ctx.badRequest('License not found');
232
+ }
233
+
234
+ // Verify email matches
235
+ if (license.email.toLowerCase() !== trimmedEmail) {
236
+ strapi.log.warn(`[magic-sessionmanager] ⚠️ Email mismatch for license key: ${trimmedKey.substring(0, 8)}... (Attempted: ${trimmedEmail})`);
237
+ return ctx.badRequest('Email address does not match this license key');
238
+ }
239
+
240
+ // Store the license key
241
+ await licenseGuard.storeLicenseKey(trimmedKey);
242
+
243
+ // Start pinging
244
+ const pingInterval = licenseGuard.startPinging(trimmedKey, 15);
245
+
246
+ // Update global license guard
247
+ strapi.licenseGuard = {
248
+ licenseKey: trimmedKey,
249
+ pingInterval,
250
+ data: verification.data,
251
+ };
252
+
253
+ strapi.log.info(`[magic-sessionmanager] ✅ Existing license key validated and stored: ${trimmedKey.substring(0, 8)}... (Email: ${trimmedEmail})`);
254
+
255
+ return ctx.send({
256
+ success: true,
257
+ message: 'License key validated and activated successfully',
258
+ data: verification.data,
259
+ });
260
+ } catch (error) {
261
+ strapi.log.error('[magic-sessionmanager] Error storing license key:', error);
262
+ return ctx.badRequest('Error validating license key');
263
+ }
264
+ },
265
+ });
266
+