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.
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer.jsx +11 -0
- package/admin/src/components/LicenseGuard.jsx +591 -0
- package/admin/src/components/OnlineUsersWidget.jsx +208 -0
- package/admin/src/components/PluginIcon.jsx +8 -0
- package/admin/src/components/SessionDetailModal.jsx +445 -0
- package/admin/src/components/SessionInfoCard.jsx +151 -0
- package/admin/src/components/SessionInfoPanel.jsx +375 -0
- package/admin/src/components/index.jsx +5 -0
- package/admin/src/hooks/useLicense.js +103 -0
- package/admin/src/index.js +137 -0
- package/admin/src/pages/ActiveSessions.jsx +12 -0
- package/admin/src/pages/Analytics.jsx +735 -0
- package/admin/src/pages/App.jsx +12 -0
- package/admin/src/pages/HomePage.jsx +1248 -0
- package/admin/src/pages/License.jsx +603 -0
- package/admin/src/pages/Settings.jsx +1497 -0
- package/admin/src/pages/SettingsNew.jsx +1204 -0
- package/admin/src/pages/index.jsx +3 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +20 -0
- package/admin/src/translations/en.json +20 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/parseUserAgent.js +79 -0
- package/dist/server/index.js +91 -2
- package/dist/server/index.mjs +91 -2
- package/package.json +3 -1
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +297 -0
- package/server/src/config/index.js +20 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/session/schema.json +76 -0
- package/server/src/controllers/controller.js +11 -0
- package/server/src/controllers/index.js +11 -0
- package/server/src/controllers/license.js +266 -0
- package/server/src/controllers/session.js +362 -0
- package/server/src/controllers/settings.js +122 -0
- package/server/src/destroy.js +18 -0
- package/server/src/index.js +23 -0
- package/server/src/middlewares/index.js +5 -0
- package/server/src/middlewares/last-seen.js +56 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +32 -0
- package/server/src/routes/admin.js +149 -0
- package/server/src/routes/content-api.js +51 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/geolocation.js +180 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/license-guard.js +308 -0
- package/server/src/services/notifications.js +319 -0
- package/server/src/services/service.js +7 -0
- package/server/src/services/session.js +345 -0
- 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,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,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
|
+
|