strapi-plugin-magic-sessionmanager 3.7.0 → 4.0.1
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/README.md +90 -0
- package/admin/src/components/LicenseGuard.jsx +6 -6
- package/admin/src/components/SessionDetailModal.jsx +12 -12
- package/admin/src/components/SessionInfoCard.jsx +3 -3
- package/admin/src/components/SessionInfoPanel.jsx +3 -2
- package/admin/src/hooks/useLicense.js +1 -1
- package/admin/src/index.js +2 -2
- package/admin/src/pages/Analytics.jsx +2 -2
- package/admin/src/pages/HomePage.jsx +11 -14
- package/admin/src/pages/License.jsx +2 -2
- package/admin/src/pages/Settings.jsx +24 -25
- package/admin/src/pages/SettingsNew.jsx +21 -21
- package/admin/src/utils/parseUserAgent.js +7 -7
- package/dist/_chunks/{Analytics-Bi-vcT63.js → Analytics-BBdv1I5y.js} +4 -4
- package/dist/_chunks/{Analytics-BM9i88xu.mjs → Analytics-Dv9f_0eZ.mjs} +4 -4
- package/dist/_chunks/{App-DcnJOCL9.mjs → App-CIQ-7sa7.mjs} +26 -31
- package/dist/_chunks/{App-BbiNy_cT.js → App-CJaZPNjt.js} +26 -31
- package/dist/_chunks/{License-kYo8j2yl.js → License-D24rgaZQ.js} +3 -3
- package/dist/_chunks/{License-DsxP-MAL.mjs → License-nrmFxoBm.mjs} +3 -3
- package/dist/_chunks/{Settings-jW0TOE_d.js → Settings-CqxgjU0y.js} +26 -26
- package/dist/_chunks/{Settings-C3sW9eBD.mjs → Settings-D5dLEGc_.mjs} +26 -26
- package/dist/_chunks/{index-DG9XeVSg.mjs → index-Duk1_Wrz.mjs} +15 -15
- package/dist/_chunks/{index-Dr2HT-Dd.js → index-WH04CS1c.js} +15 -15
- package/dist/_chunks/{useLicense-BL_3bX9O.js → useLicense-BwOlCyhc.js} +2 -2
- package/dist/_chunks/{useLicense-DOkJX-tk.mjs → useLicense-Ce8GaxB0.mjs} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +250 -119
- package/dist/server/index.mjs +250 -119
- package/package.json +1 -1
- package/server/src/bootstrap.js +106 -28
- package/server/src/controllers/license.js +4 -4
- package/server/src/controllers/session.js +67 -13
- package/server/src/destroy.js +1 -1
- package/server/src/middlewares/last-seen.js +13 -7
- package/server/src/register.js +4 -4
- package/server/src/routes/content-api.js +11 -2
- package/server/src/services/geolocation.js +4 -2
- package/server/src/services/license-guard.js +13 -10
- package/server/src/services/notifications.js +20 -20
- package/server/src/services/service.js +1 -1
- package/server/src/services/session.js +63 -33
- package/server/src/utils/encryption.js +1 -1
package/package.json
CHANGED
package/server/src/bootstrap.js
CHANGED
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
* Bootstrap: Mount middleware for session tracking
|
|
5
5
|
* Sessions are managed via plugin::magic-sessionmanager.session content type
|
|
6
6
|
*
|
|
7
|
+
* [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
|
|
8
|
+
*
|
|
7
9
|
* NOTE: For multi-instance deployments, consider Redis locks or session store
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const getClientIp = require('./utils/getClientIp');
|
|
11
13
|
const { encryptToken, decryptToken } = require('./utils/encryption');
|
|
12
14
|
|
|
15
|
+
const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
16
|
+
const USER_UID = 'plugin::users-permissions.user';
|
|
17
|
+
|
|
13
18
|
module.exports = async ({ strapi }) => {
|
|
14
|
-
strapi.log.info('[magic-sessionmanager]
|
|
19
|
+
strapi.log.info('[magic-sessionmanager] [START] Bootstrap starting...');
|
|
15
20
|
|
|
16
21
|
try {
|
|
17
22
|
// Initialize License Guard
|
|
@@ -23,7 +28,7 @@ module.exports = async ({ strapi }) => {
|
|
|
23
28
|
|
|
24
29
|
if (!licenseStatus.valid) {
|
|
25
30
|
strapi.log.error('╔════════════════════════════════════════════════════════════════╗');
|
|
26
|
-
strapi.log.error('║
|
|
31
|
+
strapi.log.error('║ [ERROR] SESSION MANAGER - NO VALID LICENSE ║');
|
|
27
32
|
strapi.log.error('║ ║');
|
|
28
33
|
strapi.log.error('║ This plugin requires a valid license to operate. ║');
|
|
29
34
|
strapi.log.error('║ Please activate your license via Admin UI: ║');
|
|
@@ -40,7 +45,7 @@ module.exports = async ({ strapi }) => {
|
|
|
40
45
|
const storedKey = await pluginStore.get({ key: 'licenseKey' });
|
|
41
46
|
|
|
42
47
|
strapi.log.info('╔════════════════════════════════════════════════════════════════╗');
|
|
43
|
-
strapi.log.info('║
|
|
48
|
+
strapi.log.info('║ [SUCCESS] SESSION MANAGER LICENSE ACTIVE ║');
|
|
44
49
|
strapi.log.info('║ ║');
|
|
45
50
|
|
|
46
51
|
if (licenseStatus.data) {
|
|
@@ -53,7 +58,7 @@ module.exports = async ({ strapi }) => {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
strapi.log.info('║ ║');
|
|
56
|
-
strapi.log.info('║
|
|
61
|
+
strapi.log.info('║ [RELOAD] Auto-pinging every 15 minutes ║');
|
|
57
62
|
strapi.log.info('╚════════════════════════════════════════════════════════════════╝');
|
|
58
63
|
}
|
|
59
64
|
}, 3000); // Wait 3 seconds for API to be ready
|
|
@@ -80,7 +85,7 @@ module.exports = async ({ strapi }) => {
|
|
|
80
85
|
}
|
|
81
86
|
}, cleanupInterval);
|
|
82
87
|
|
|
83
|
-
strapi.log.info('[magic-sessionmanager]
|
|
88
|
+
strapi.log.info('[magic-sessionmanager] [TIME] Periodic cleanup scheduled (every 30 minutes)');
|
|
84
89
|
|
|
85
90
|
// Store interval handle for cleanup on shutdown
|
|
86
91
|
if (!strapi.sessionManagerIntervals) {
|
|
@@ -104,7 +109,7 @@ module.exports = async ({ strapi }) => {
|
|
|
104
109
|
|
|
105
110
|
// Find session by decrypting tokens and matching
|
|
106
111
|
// Since tokens are encrypted, we need to get all active sessions and check each one
|
|
107
|
-
const allSessions = await strapi.
|
|
112
|
+
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
108
113
|
filters: {
|
|
109
114
|
isActive: true,
|
|
110
115
|
},
|
|
@@ -122,8 +127,8 @@ module.exports = async ({ strapi }) => {
|
|
|
122
127
|
});
|
|
123
128
|
|
|
124
129
|
if (matchingSession) {
|
|
125
|
-
await sessionService.terminateSession({ sessionId: matchingSession.
|
|
126
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
130
|
+
await sessionService.terminateSession({ sessionId: matchingSession.documentId });
|
|
131
|
+
strapi.log.info(`[magic-sessionmanager] [LOGOUT] Logout via /api/auth/logout - Session ${matchingSession.documentId} terminated`);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
ctx.status = 200;
|
|
@@ -139,7 +144,7 @@ module.exports = async ({ strapi }) => {
|
|
|
139
144
|
},
|
|
140
145
|
}]);
|
|
141
146
|
|
|
142
|
-
strapi.log.info('[magic-sessionmanager]
|
|
147
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] /api/auth/logout route registered');
|
|
143
148
|
|
|
144
149
|
// Middleware to intercept logins
|
|
145
150
|
strapi.server.use(async (ctx, next) => {
|
|
@@ -158,7 +163,8 @@ module.exports = async ({ strapi }) => {
|
|
|
158
163
|
const ip = getClientIp(ctx);
|
|
159
164
|
const userAgent = ctx.request.headers?.['user-agent'] || ctx.request.header?.['user-agent'] || 'unknown';
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
// Strapi v5: Use documentId for session creation
|
|
167
|
+
strapi.log.info(`[magic-sessionmanager] [CHECK] Login detected! User: ${user.documentId || user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
162
168
|
|
|
163
169
|
// Get config
|
|
164
170
|
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
@@ -216,7 +222,7 @@ module.exports = async ({ strapi }) => {
|
|
|
216
222
|
|
|
217
223
|
// Block if needed
|
|
218
224
|
if (shouldBlock) {
|
|
219
|
-
strapi.log.warn(`[magic-sessionmanager]
|
|
225
|
+
strapi.log.warn(`[magic-sessionmanager] [BLOCKED] Blocking login: ${blockReason}`);
|
|
220
226
|
|
|
221
227
|
// Don't create session, return error
|
|
222
228
|
ctx.status = 403;
|
|
@@ -230,16 +236,23 @@ module.exports = async ({ strapi }) => {
|
|
|
230
236
|
return; // Stop here
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
// Create a new session
|
|
239
|
+
// Create a new session (Strapi v5: Use documentId instead of numeric id)
|
|
240
|
+
// If login response doesn't include documentId, fetch it from DB
|
|
241
|
+
let userDocId = user.documentId;
|
|
242
|
+
if (!userDocId && user.id) {
|
|
243
|
+
const fullUser = await strapi.entityService.findOne(USER_UID, user.id);
|
|
244
|
+
userDocId = fullUser?.documentId || user.id;
|
|
245
|
+
}
|
|
246
|
+
|
|
234
247
|
const newSession = await sessionService.createSession({
|
|
235
|
-
userId:
|
|
248
|
+
userId: userDocId,
|
|
236
249
|
ip,
|
|
237
250
|
userAgent,
|
|
238
251
|
token: ctx.body.jwt, // Store Access Token (encrypted)
|
|
239
252
|
refreshToken: ctx.body.refreshToken, // Store Refresh Token (encrypted) if exists
|
|
240
253
|
});
|
|
241
254
|
|
|
242
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
255
|
+
strapi.log.info(`[magic-sessionmanager] [SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
|
|
243
256
|
|
|
244
257
|
// Advanced: Send notifications
|
|
245
258
|
if (geoData && (config.enableEmailAlerts || config.enableWebhooks)) {
|
|
@@ -286,13 +299,13 @@ module.exports = async ({ strapi }) => {
|
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
} catch (err) {
|
|
289
|
-
strapi.log.error('[magic-sessionmanager]
|
|
302
|
+
strapi.log.error('[magic-sessionmanager] [ERROR] Error creating session:', err);
|
|
290
303
|
// Don't throw - login should still succeed even if session creation fails
|
|
291
304
|
}
|
|
292
305
|
}
|
|
293
306
|
});
|
|
294
307
|
|
|
295
|
-
strapi.log.info('[magic-sessionmanager]
|
|
308
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Login/Logout interceptor middleware mounted');
|
|
296
309
|
|
|
297
310
|
// Middleware to block refresh token requests for terminated sessions
|
|
298
311
|
strapi.server.use(async (ctx, next) => {
|
|
@@ -305,7 +318,7 @@ module.exports = async ({ strapi }) => {
|
|
|
305
318
|
|
|
306
319
|
if (refreshToken) {
|
|
307
320
|
// Find session with this refresh token
|
|
308
|
-
const allSessions = await strapi.
|
|
321
|
+
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
309
322
|
filters: {
|
|
310
323
|
isActive: true,
|
|
311
324
|
},
|
|
@@ -323,8 +336,8 @@ module.exports = async ({ strapi }) => {
|
|
|
323
336
|
});
|
|
324
337
|
|
|
325
338
|
if (!matchingSession) {
|
|
326
|
-
// No active session with this refresh token
|
|
327
|
-
strapi.log.warn('[magic-sessionmanager]
|
|
339
|
+
// No active session with this refresh token - Block!
|
|
340
|
+
strapi.log.warn('[magic-sessionmanager] [BLOCKED] Blocked refresh token request - no active session');
|
|
328
341
|
ctx.status = 401;
|
|
329
342
|
ctx.body = {
|
|
330
343
|
error: {
|
|
@@ -336,7 +349,7 @@ module.exports = async ({ strapi }) => {
|
|
|
336
349
|
return; // Don't continue
|
|
337
350
|
}
|
|
338
351
|
|
|
339
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
352
|
+
strapi.log.info(`[magic-sessionmanager] [SUCCESS] Refresh token allowed for session ${matchingSession.documentId}`);
|
|
340
353
|
}
|
|
341
354
|
} catch (err) {
|
|
342
355
|
strapi.log.error('[magic-sessionmanager] Error checking refresh token:', err);
|
|
@@ -356,7 +369,7 @@ module.exports = async ({ strapi }) => {
|
|
|
356
369
|
|
|
357
370
|
if (oldRefreshToken) {
|
|
358
371
|
// Find session and update with new tokens
|
|
359
|
-
const allSessions = await strapi.
|
|
372
|
+
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
360
373
|
filters: {
|
|
361
374
|
isActive: true,
|
|
362
375
|
},
|
|
@@ -376,7 +389,8 @@ module.exports = async ({ strapi }) => {
|
|
|
376
389
|
const encryptedToken = newAccessToken ? encryptToken(newAccessToken) : matchingSession.token;
|
|
377
390
|
const encryptedRefreshToken = newRefreshToken ? encryptToken(newRefreshToken) : matchingSession.refreshToken;
|
|
378
391
|
|
|
379
|
-
await strapi.
|
|
392
|
+
await strapi.documents(SESSION_UID).update({
|
|
393
|
+
documentId: matchingSession.documentId,
|
|
380
394
|
data: {
|
|
381
395
|
token: encryptedToken,
|
|
382
396
|
refreshToken: encryptedRefreshToken,
|
|
@@ -384,7 +398,7 @@ module.exports = async ({ strapi }) => {
|
|
|
384
398
|
},
|
|
385
399
|
});
|
|
386
400
|
|
|
387
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
401
|
+
strapi.log.info(`[magic-sessionmanager] [REFRESH] Tokens refreshed for session ${matchingSession.documentId}`);
|
|
388
402
|
}
|
|
389
403
|
}
|
|
390
404
|
} catch (err) {
|
|
@@ -393,18 +407,82 @@ module.exports = async ({ strapi }) => {
|
|
|
393
407
|
}
|
|
394
408
|
});
|
|
395
409
|
|
|
396
|
-
strapi.log.info('[magic-sessionmanager]
|
|
410
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Refresh Token interceptor middleware mounted');
|
|
397
411
|
|
|
398
412
|
// Mount lastSeen update middleware
|
|
399
413
|
strapi.server.use(
|
|
400
414
|
require('./middlewares/last-seen')({ strapi, sessionService })
|
|
401
415
|
);
|
|
402
416
|
|
|
403
|
-
strapi.log.info('[magic-sessionmanager]
|
|
404
|
-
|
|
405
|
-
|
|
417
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] LastSeen middleware mounted');
|
|
418
|
+
|
|
419
|
+
// Auto-enable Content-API permissions for authenticated users
|
|
420
|
+
await ensureContentApiPermissions(strapi);
|
|
421
|
+
|
|
422
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Bootstrap complete');
|
|
423
|
+
strapi.log.info('[magic-sessionmanager] [READY] Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
|
|
406
424
|
|
|
407
425
|
} catch (err) {
|
|
408
|
-
strapi.log.error('[magic-sessionmanager]
|
|
426
|
+
strapi.log.error('[magic-sessionmanager] [ERROR] Bootstrap error:', err);
|
|
409
427
|
}
|
|
410
428
|
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Auto-enable Content-API permissions for authenticated users
|
|
432
|
+
* This ensures plugin endpoints are accessible after installation
|
|
433
|
+
* @param {object} strapi - Strapi instance
|
|
434
|
+
*/
|
|
435
|
+
async function ensureContentApiPermissions(strapi) {
|
|
436
|
+
try {
|
|
437
|
+
// Get the authenticated role
|
|
438
|
+
const authenticatedRole = await strapi.query('plugin::users-permissions.role').findOne({
|
|
439
|
+
where: { type: 'authenticated' },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!authenticatedRole) {
|
|
443
|
+
strapi.log.warn('[magic-sessionmanager] Authenticated role not found - skipping permission setup');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Content-API actions that should be enabled for authenticated users
|
|
448
|
+
const requiredActions = [
|
|
449
|
+
'plugin::magic-sessionmanager.session.logout',
|
|
450
|
+
'plugin::magic-sessionmanager.session.logoutAll',
|
|
451
|
+
'plugin::magic-sessionmanager.session.getOwnSessions',
|
|
452
|
+
'plugin::magic-sessionmanager.session.getUserSessions',
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
// Get existing permissions for this role
|
|
456
|
+
const existingPermissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
|
457
|
+
where: {
|
|
458
|
+
role: authenticatedRole.id,
|
|
459
|
+
action: { $in: requiredActions },
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Find which actions are missing
|
|
464
|
+
const existingActions = existingPermissions.map(p => p.action);
|
|
465
|
+
const missingActions = requiredActions.filter(action => !existingActions.includes(action));
|
|
466
|
+
|
|
467
|
+
if (missingActions.length === 0) {
|
|
468
|
+
strapi.log.debug('[magic-sessionmanager] Content-API permissions already configured');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Create missing permissions
|
|
473
|
+
for (const action of missingActions) {
|
|
474
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
475
|
+
data: {
|
|
476
|
+
action,
|
|
477
|
+
role: authenticatedRole.id,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
strapi.log.info(`[magic-sessionmanager] [PERMISSION] Enabled ${action} for authenticated users`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Content-API permissions configured for authenticated users');
|
|
484
|
+
} catch (err) {
|
|
485
|
+
strapi.log.warn('[magic-sessionmanager] Could not auto-configure permissions:', err.message);
|
|
486
|
+
strapi.log.warn('[magic-sessionmanager] Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -219,7 +219,7 @@ module.exports = ({ strapi }) => ({
|
|
|
219
219
|
const verification = await licenseGuard.verifyLicense(trimmedKey);
|
|
220
220
|
|
|
221
221
|
if (!verification.valid) {
|
|
222
|
-
strapi.log.warn(`[magic-sessionmanager]
|
|
222
|
+
strapi.log.warn(`[magic-sessionmanager] [WARNING] Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
|
|
223
223
|
return ctx.badRequest('Invalid or expired license key');
|
|
224
224
|
}
|
|
225
225
|
|
|
@@ -227,13 +227,13 @@ module.exports = ({ strapi }) => ({
|
|
|
227
227
|
const license = await licenseGuard.getLicenseByKey(trimmedKey);
|
|
228
228
|
|
|
229
229
|
if (!license) {
|
|
230
|
-
strapi.log.warn(`[magic-sessionmanager]
|
|
230
|
+
strapi.log.warn(`[magic-sessionmanager] [WARNING] License not found in database: ${trimmedKey.substring(0, 8)}...`);
|
|
231
231
|
return ctx.badRequest('License not found');
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// Verify email matches
|
|
235
235
|
if (license.email.toLowerCase() !== trimmedEmail) {
|
|
236
|
-
strapi.log.warn(`[magic-sessionmanager]
|
|
236
|
+
strapi.log.warn(`[magic-sessionmanager] [WARNING] Email mismatch for license key: ${trimmedKey.substring(0, 8)}... (Attempted: ${trimmedEmail})`);
|
|
237
237
|
return ctx.badRequest('Email address does not match this license key');
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -250,7 +250,7 @@ module.exports = ({ strapi }) => ({
|
|
|
250
250
|
data: verification.data,
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
253
|
+
strapi.log.info(`[magic-sessionmanager] [SUCCESS] Existing license key validated and stored: ${trimmedKey.substring(0, 8)}... (Email: ${trimmedEmail})`);
|
|
254
254
|
|
|
255
255
|
return ctx.send({
|
|
256
256
|
success: true,
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { decryptToken } = require('../utils/encryption');
|
|
4
4
|
|
|
5
|
+
const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
6
|
+
const USER_UID = 'plugin::users-permissions.user';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Session Controller
|
|
7
10
|
* Handles HTTP requests for session management
|
|
@@ -55,6 +58,38 @@ module.exports = {
|
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Get own sessions (authenticated user)
|
|
63
|
+
* GET /api/magic-sessionmanager/my-sessions
|
|
64
|
+
* Automatically uses the authenticated user's documentId
|
|
65
|
+
*/
|
|
66
|
+
async getOwnSessions(ctx) {
|
|
67
|
+
try {
|
|
68
|
+
// Strapi v5: Use documentId from authenticated user
|
|
69
|
+
const userId = ctx.state.user?.documentId;
|
|
70
|
+
|
|
71
|
+
if (!userId) {
|
|
72
|
+
return ctx.throw(401, 'Unauthorized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sessionService = strapi
|
|
76
|
+
.plugin('magic-sessionmanager')
|
|
77
|
+
.service('session');
|
|
78
|
+
|
|
79
|
+
const sessions = await sessionService.getUserSessions(userId);
|
|
80
|
+
|
|
81
|
+
ctx.body = {
|
|
82
|
+
data: sessions,
|
|
83
|
+
meta: {
|
|
84
|
+
count: sessions.length,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
strapi.log.error('[magic-sessionmanager] Error fetching own sessions:', err);
|
|
89
|
+
ctx.throw(500, 'Error fetching sessions');
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
58
93
|
/**
|
|
59
94
|
* Get user's sessions
|
|
60
95
|
* GET /magic-sessionmanager/user/:userId/sessions (Admin API)
|
|
@@ -67,12 +102,13 @@ module.exports = {
|
|
|
67
102
|
|
|
68
103
|
// Check if this is an admin request
|
|
69
104
|
const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
|
|
70
|
-
|
|
105
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
106
|
+
const requestingUserDocId = ctx.state.user?.documentId;
|
|
71
107
|
|
|
72
108
|
// SECURITY CHECK: Content API users can only see their own sessions
|
|
73
109
|
// Admins can see any user's sessions
|
|
74
|
-
if (!isAdminRequest &&
|
|
75
|
-
strapi.log.warn(`[magic-sessionmanager] Security: User ${
|
|
110
|
+
if (!isAdminRequest && requestingUserDocId && String(requestingUserDocId) !== String(userId)) {
|
|
111
|
+
strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
|
|
76
112
|
return ctx.forbidden('You can only access your own sessions');
|
|
77
113
|
}
|
|
78
114
|
|
|
@@ -99,7 +135,8 @@ module.exports = {
|
|
|
99
135
|
*/
|
|
100
136
|
async logout(ctx) {
|
|
101
137
|
try {
|
|
102
|
-
|
|
138
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
139
|
+
const userId = ctx.state.user?.documentId;
|
|
103
140
|
const token = ctx.request.headers.authorization?.replace('Bearer ', '');
|
|
104
141
|
|
|
105
142
|
if (!userId) {
|
|
@@ -111,9 +148,9 @@ module.exports = {
|
|
|
111
148
|
.service('session');
|
|
112
149
|
|
|
113
150
|
// Find current session by decrypting and comparing tokens
|
|
114
|
-
const sessions = await strapi.
|
|
151
|
+
const sessions = await strapi.documents(SESSION_UID).findMany({
|
|
115
152
|
filters: {
|
|
116
|
-
user: {
|
|
153
|
+
user: { documentId: userId },
|
|
117
154
|
isActive: true,
|
|
118
155
|
},
|
|
119
156
|
});
|
|
@@ -131,8 +168,8 @@ module.exports = {
|
|
|
131
168
|
|
|
132
169
|
if (matchingSession) {
|
|
133
170
|
// Terminate only the current session
|
|
134
|
-
await sessionService.terminateSession({ sessionId: matchingSession.
|
|
135
|
-
strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.
|
|
171
|
+
await sessionService.terminateSession({ sessionId: matchingSession.documentId });
|
|
172
|
+
strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.documentId})`);
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
ctx.body = {
|
|
@@ -150,7 +187,8 @@ module.exports = {
|
|
|
150
187
|
*/
|
|
151
188
|
async logoutAll(ctx) {
|
|
152
189
|
try {
|
|
153
|
-
|
|
190
|
+
// Strapi v5: Use documentId instead of numeric id
|
|
191
|
+
const userId = ctx.state.user?.documentId;
|
|
154
192
|
|
|
155
193
|
if (!userId) {
|
|
156
194
|
return ctx.throw(401, 'Unauthorized');
|
|
@@ -337,13 +375,28 @@ module.exports = {
|
|
|
337
375
|
/**
|
|
338
376
|
* Toggle user blocked status
|
|
339
377
|
* POST /magic-sessionmanager/user/:userId/toggle-block
|
|
378
|
+
* Supports both numeric id (from Content Manager) and documentId
|
|
340
379
|
*/
|
|
341
380
|
async toggleUserBlock(ctx) {
|
|
342
381
|
try {
|
|
343
382
|
const { userId } = ctx.params;
|
|
344
383
|
|
|
345
|
-
//
|
|
346
|
-
|
|
384
|
+
// Strapi v5: userId from params could be numeric id or documentId
|
|
385
|
+
// If numeric, look up the documentId first using entityService (fallback)
|
|
386
|
+
let userDocumentId = userId;
|
|
387
|
+
let user = null;
|
|
388
|
+
|
|
389
|
+
// Try to find by documentId first (preferred)
|
|
390
|
+
user = await strapi.documents(USER_UID).findOne({ documentId: userId });
|
|
391
|
+
|
|
392
|
+
// If not found, try numeric id lookup via entityService (fallback for Content Manager)
|
|
393
|
+
if (!user && !isNaN(userId)) {
|
|
394
|
+
const numericUser = await strapi.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
395
|
+
if (numericUser) {
|
|
396
|
+
userDocumentId = numericUser.documentId;
|
|
397
|
+
user = numericUser;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
347
400
|
|
|
348
401
|
if (!user) {
|
|
349
402
|
return ctx.throw(404, 'User not found');
|
|
@@ -352,7 +405,8 @@ module.exports = {
|
|
|
352
405
|
// Toggle blocked status
|
|
353
406
|
const newBlockedStatus = !user.blocked;
|
|
354
407
|
|
|
355
|
-
await strapi.
|
|
408
|
+
await strapi.documents(USER_UID).update({
|
|
409
|
+
documentId: userDocumentId,
|
|
356
410
|
data: {
|
|
357
411
|
blocked: newBlockedStatus,
|
|
358
412
|
},
|
|
@@ -363,7 +417,7 @@ module.exports = {
|
|
|
363
417
|
const sessionService = strapi
|
|
364
418
|
.plugin('magic-sessionmanager')
|
|
365
419
|
.service('session');
|
|
366
|
-
await sessionService.terminateSession({ userId });
|
|
420
|
+
await sessionService.terminateSession({ userId: userDocumentId });
|
|
367
421
|
}
|
|
368
422
|
|
|
369
423
|
ctx.body = {
|
package/server/src/destroy.js
CHANGED
|
@@ -13,6 +13,6 @@ module.exports = async ({ strapi }) => {
|
|
|
13
13
|
strapi.log.info('[magic-sessionmanager] 🛑 Session cleanup interval stopped');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
strapi.log.info('[magic-sessionmanager]
|
|
16
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Plugin cleanup completed');
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
* lastSeen Middleware
|
|
5
5
|
* Updates user lastSeen and session lastActive on each authenticated request
|
|
6
6
|
* Rate-limited to prevent DB write noise (default: 30 seconds)
|
|
7
|
+
*
|
|
8
|
+
* [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
|
|
7
9
|
*/
|
|
10
|
+
|
|
11
|
+
const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
12
|
+
|
|
8
13
|
module.exports = ({ strapi, sessionService }) => {
|
|
9
14
|
return async (ctx, next) => {
|
|
10
15
|
// BEFORE processing request: Check if user's sessions are active
|
|
11
|
-
|
|
16
|
+
// Strapi v5: Use documentId instead of numeric id for Document Service API
|
|
17
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
12
18
|
try {
|
|
13
|
-
const userId = ctx.state.user.
|
|
19
|
+
const userId = ctx.state.user.documentId;
|
|
14
20
|
|
|
15
21
|
// Check if user has ANY active sessions
|
|
16
|
-
const activeSessions = await strapi.
|
|
22
|
+
const activeSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
17
23
|
filters: {
|
|
18
|
-
user: {
|
|
24
|
+
user: { documentId: userId },
|
|
19
25
|
isActive: true,
|
|
20
26
|
},
|
|
21
27
|
limit: 1,
|
|
@@ -23,7 +29,7 @@ module.exports = ({ strapi, sessionService }) => {
|
|
|
23
29
|
|
|
24
30
|
// If user has NO active sessions, reject the request
|
|
25
31
|
if (!activeSessions || activeSessions.length === 0) {
|
|
26
|
-
strapi.log.info(`[magic-sessionmanager]
|
|
32
|
+
strapi.log.info(`[magic-sessionmanager] [BLOCKED] Blocked request - User ${userId} has no active sessions`);
|
|
27
33
|
return ctx.unauthorized('All sessions have been terminated. Please login again.');
|
|
28
34
|
}
|
|
29
35
|
} catch (err) {
|
|
@@ -36,9 +42,9 @@ module.exports = ({ strapi, sessionService }) => {
|
|
|
36
42
|
await next();
|
|
37
43
|
|
|
38
44
|
// AFTER response: Update activity timestamps if user is authenticated
|
|
39
|
-
if (ctx.state.user && ctx.state.user.
|
|
45
|
+
if (ctx.state.user && ctx.state.user.documentId) {
|
|
40
46
|
try {
|
|
41
|
-
const userId = ctx.state.user.
|
|
47
|
+
const userId = ctx.state.user.documentId;
|
|
42
48
|
|
|
43
49
|
// Try to find or extract sessionId from context
|
|
44
50
|
const sessionId = ctx.state.sessionId;
|
package/server/src/register.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Sessions are accessed via the Session Manager plugin UI components
|
|
7
7
|
*/
|
|
8
8
|
module.exports = async ({ strapi }) => {
|
|
9
|
-
strapi.log.info('[magic-sessionmanager]
|
|
9
|
+
strapi.log.info('[magic-sessionmanager] [START] Plugin registration starting...');
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
12
|
// Get the user content type
|
|
@@ -21,12 +21,12 @@ module.exports = async ({ strapi }) => {
|
|
|
21
21
|
// Sessions are managed through SessionInfoPanel sidebar instead
|
|
22
22
|
if (userCT.attributes && userCT.attributes.sessions) {
|
|
23
23
|
delete userCT.attributes.sessions;
|
|
24
|
-
strapi.log.info('[magic-sessionmanager]
|
|
24
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Removed sessions field from User content type');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
strapi.log.info('[magic-sessionmanager]
|
|
27
|
+
strapi.log.info('[magic-sessionmanager] [SUCCESS] Plugin registered successfully');
|
|
28
28
|
|
|
29
29
|
} catch (err) {
|
|
30
|
-
strapi.log.error('[magic-sessionmanager]
|
|
30
|
+
strapi.log.error('[magic-sessionmanager] [ERROR] Registration error:', err);
|
|
31
31
|
}
|
|
32
32
|
};
|
|
@@ -38,14 +38,23 @@ module.exports = {
|
|
|
38
38
|
// SESSION QUERIES
|
|
39
39
|
// ============================================================
|
|
40
40
|
|
|
41
|
+
{
|
|
42
|
+
method: 'GET',
|
|
43
|
+
path: '/my-sessions',
|
|
44
|
+
handler: 'session.getOwnSessions',
|
|
45
|
+
config: {
|
|
46
|
+
auth: { strategies: ['users-permissions'] },
|
|
47
|
+
description: 'Get own sessions (automatically uses authenticated user)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
41
50
|
{
|
|
42
51
|
method: 'GET',
|
|
43
52
|
path: '/user/:userId/sessions',
|
|
44
53
|
handler: 'session.getUserSessions',
|
|
45
54
|
config: {
|
|
46
55
|
auth: { strategies: ['users-permissions'] },
|
|
47
|
-
description: 'Get
|
|
56
|
+
description: 'Get sessions by userId (validates user can only see own sessions)',
|
|
57
|
+
},
|
|
48
58
|
},
|
|
49
|
-
},
|
|
50
59
|
],
|
|
51
60
|
};
|
|
@@ -132,9 +132,11 @@ module.exports = ({ strapi }) => ({
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Get country flag emoji
|
|
135
|
+
* @param {string} countryCode - ISO 2-letter country code
|
|
136
|
+
* @returns {string} Flag emoji or empty string
|
|
135
137
|
*/
|
|
136
138
|
getCountryFlag(countryCode) {
|
|
137
|
-
if (!countryCode) return '
|
|
139
|
+
if (!countryCode) return '';
|
|
138
140
|
|
|
139
141
|
// Convert country code to flag emoji
|
|
140
142
|
const codePoints = countryCode
|
|
@@ -153,7 +155,7 @@ module.exports = ({ strapi }) => ({
|
|
|
153
155
|
ip: ipAddress,
|
|
154
156
|
country: 'Unknown',
|
|
155
157
|
country_code: 'XX',
|
|
156
|
-
country_flag: '
|
|
158
|
+
country_flag: '[GEO]',
|
|
157
159
|
city: 'Unknown',
|
|
158
160
|
region: 'Unknown',
|
|
159
161
|
timezone: 'Unknown',
|