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.
Files changed (43) hide show
  1. package/README.md +90 -0
  2. package/admin/src/components/LicenseGuard.jsx +6 -6
  3. package/admin/src/components/SessionDetailModal.jsx +12 -12
  4. package/admin/src/components/SessionInfoCard.jsx +3 -3
  5. package/admin/src/components/SessionInfoPanel.jsx +3 -2
  6. package/admin/src/hooks/useLicense.js +1 -1
  7. package/admin/src/index.js +2 -2
  8. package/admin/src/pages/Analytics.jsx +2 -2
  9. package/admin/src/pages/HomePage.jsx +11 -14
  10. package/admin/src/pages/License.jsx +2 -2
  11. package/admin/src/pages/Settings.jsx +24 -25
  12. package/admin/src/pages/SettingsNew.jsx +21 -21
  13. package/admin/src/utils/parseUserAgent.js +7 -7
  14. package/dist/_chunks/{Analytics-Bi-vcT63.js → Analytics-BBdv1I5y.js} +4 -4
  15. package/dist/_chunks/{Analytics-BM9i88xu.mjs → Analytics-Dv9f_0eZ.mjs} +4 -4
  16. package/dist/_chunks/{App-DcnJOCL9.mjs → App-CIQ-7sa7.mjs} +26 -31
  17. package/dist/_chunks/{App-BbiNy_cT.js → App-CJaZPNjt.js} +26 -31
  18. package/dist/_chunks/{License-kYo8j2yl.js → License-D24rgaZQ.js} +3 -3
  19. package/dist/_chunks/{License-DsxP-MAL.mjs → License-nrmFxoBm.mjs} +3 -3
  20. package/dist/_chunks/{Settings-jW0TOE_d.js → Settings-CqxgjU0y.js} +26 -26
  21. package/dist/_chunks/{Settings-C3sW9eBD.mjs → Settings-D5dLEGc_.mjs} +26 -26
  22. package/dist/_chunks/{index-DG9XeVSg.mjs → index-Duk1_Wrz.mjs} +15 -15
  23. package/dist/_chunks/{index-Dr2HT-Dd.js → index-WH04CS1c.js} +15 -15
  24. package/dist/_chunks/{useLicense-BL_3bX9O.js → useLicense-BwOlCyhc.js} +2 -2
  25. package/dist/_chunks/{useLicense-DOkJX-tk.mjs → useLicense-Ce8GaxB0.mjs} +2 -2
  26. package/dist/admin/index.js +1 -1
  27. package/dist/admin/index.mjs +1 -1
  28. package/dist/server/index.js +250 -119
  29. package/dist/server/index.mjs +250 -119
  30. package/package.json +1 -1
  31. package/server/src/bootstrap.js +106 -28
  32. package/server/src/controllers/license.js +4 -4
  33. package/server/src/controllers/session.js +67 -13
  34. package/server/src/destroy.js +1 -1
  35. package/server/src/middlewares/last-seen.js +13 -7
  36. package/server/src/register.js +4 -4
  37. package/server/src/routes/content-api.js +11 -2
  38. package/server/src/services/geolocation.js +4 -2
  39. package/server/src/services/license-guard.js +13 -10
  40. package/server/src/services/notifications.js +20 -20
  41. package/server/src/services/service.js +1 -1
  42. package/server/src/services/session.js +63 -33
  43. package/server/src/utils/encryption.js +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.7.0",
2
+ "version": "4.0.1",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",
@@ -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] 🚀 Bootstrap starting...');
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('║ SESSION MANAGER - NO VALID LICENSE ║');
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('║ SESSION MANAGER LICENSE ACTIVE ║');
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('║ 🔄 Auto-pinging every 15 minutes ║');
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] Periodic cleanup scheduled (every 30 minutes)');
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.entityService.findMany('plugin::magic-sessionmanager.session', {
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.id });
126
- strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${matchingSession.id} terminated`);
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] /api/auth/logout route registered');
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
- strapi.log.info(`[magic-sessionmanager] 🔍 Login detected! User: ${user.id} (${user.email || user.username}) from IP: ${ip}`);
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] 🚫 Blocking login: ${blockReason}`);
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: user.id,
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] Session created for user ${user.id} (IP: ${ip})`);
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] Error creating session:', err);
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] Login/Logout interceptor middleware mounted');
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.entityService.findMany('plugin::magic-sessionmanager.session', {
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 Block!
327
- strapi.log.warn('[magic-sessionmanager] 🚫 Blocked refresh token request - no active session');
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] Refresh token allowed for session ${matchingSession.id}`);
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.entityService.findMany('plugin::magic-sessionmanager.session', {
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.entityService.update('plugin::magic-sessionmanager.session', matchingSession.id, {
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] 🔄 Tokens refreshed for session ${matchingSession.id}`);
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] Refresh Token interceptor middleware mounted');
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] LastSeen middleware mounted');
404
- strapi.log.info('[magic-sessionmanager] ✅ Bootstrap complete');
405
- strapi.log.info('[magic-sessionmanager] 🎉 Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
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] Bootstrap error:', err);
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] ⚠️ Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
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] ⚠️ License not found in database: ${trimmedKey.substring(0, 8)}...`);
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] ⚠️ Email mismatch for license key: ${trimmedKey.substring(0, 8)}... (Attempted: ${trimmedEmail})`);
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] Existing license key validated and stored: ${trimmedKey.substring(0, 8)}... (Email: ${trimmedEmail})`);
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
- const requestingUserId = ctx.state.user?.id;
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 && requestingUserId && String(requestingUserId) !== String(userId)) {
75
- strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
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
- const userId = ctx.state.user?.id;
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.entityService.findMany('plugin::magic-sessionmanager.session', {
151
+ const sessions = await strapi.documents(SESSION_UID).findMany({
115
152
  filters: {
116
- user: { id: userId },
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.id });
135
- strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${matchingSession.id})`);
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
- const userId = ctx.state.user?.id;
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
- // Get current user status
346
- const user = await strapi.entityService.findOne('plugin::users-permissions.user', userId);
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.entityService.update('plugin::users-permissions.user', userId, {
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 = {
@@ -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] Plugin cleanup completed');
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
- if (ctx.state.user && ctx.state.user.id) {
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.id;
19
+ const userId = ctx.state.user.documentId;
14
20
 
15
21
  // Check if user has ANY active sessions
16
- const activeSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
22
+ const activeSessions = await strapi.documents(SESSION_UID).findMany( {
17
23
  filters: {
18
- user: { id: userId },
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] 🚫 Blocked request - User ${userId} has no active sessions`);
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.id) {
45
+ if (ctx.state.user && ctx.state.user.documentId) {
40
46
  try {
41
- const userId = ctx.state.user.id;
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;
@@ -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] 🚀 Plugin registration starting...');
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] Removed sessions field from User content type');
24
+ strapi.log.info('[magic-sessionmanager] [SUCCESS] Removed sessions field from User content type');
25
25
  }
26
26
 
27
- strapi.log.info('[magic-sessionmanager] Plugin registered successfully');
27
+ strapi.log.info('[magic-sessionmanager] [SUCCESS] Plugin registered successfully');
28
28
 
29
29
  } catch (err) {
30
- strapi.log.error('[magic-sessionmanager] Registration error:', err);
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 own sessions (controller validates user can only see own sessions)',
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',