strapi-plugin-magic-sessionmanager 2.0.0 → 2.0.2

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 (52) 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/package.json +3 -1
  27. package/server/jsconfig.json +10 -0
  28. package/server/src/bootstrap.js +297 -0
  29. package/server/src/config/index.js +20 -0
  30. package/server/src/content-types/index.js +9 -0
  31. package/server/src/content-types/session/schema.json +76 -0
  32. package/server/src/controllers/controller.js +11 -0
  33. package/server/src/controllers/index.js +11 -0
  34. package/server/src/controllers/license.js +266 -0
  35. package/server/src/controllers/session.js +362 -0
  36. package/server/src/controllers/settings.js +122 -0
  37. package/server/src/destroy.js +18 -0
  38. package/server/src/index.js +21 -0
  39. package/server/src/middlewares/index.js +5 -0
  40. package/server/src/middlewares/last-seen.js +56 -0
  41. package/server/src/policies/index.js +3 -0
  42. package/server/src/register.js +32 -0
  43. package/server/src/routes/admin.js +149 -0
  44. package/server/src/routes/content-api.js +51 -0
  45. package/server/src/routes/index.js +9 -0
  46. package/server/src/services/geolocation.js +180 -0
  47. package/server/src/services/index.js +13 -0
  48. package/server/src/services/license-guard.js +308 -0
  49. package/server/src/services/notifications.js +319 -0
  50. package/server/src/services/service.js +7 -0
  51. package/server/src/services/session.js +345 -0
  52. package/server/src/utils/getClientIp.js +118 -0
@@ -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
+
@@ -0,0 +1,362 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Session Controller
5
+ * Handles HTTP requests for session management
6
+ */
7
+ module.exports = {
8
+ /**
9
+ * Get ALL sessions (active + inactive) - Admin only
10
+ * GET /magic-sessionmanager/sessions
11
+ */
12
+ async getAllSessionsAdmin(ctx) {
13
+ try {
14
+ const sessionService = strapi
15
+ .plugin('magic-sessionmanager')
16
+ .service('session');
17
+
18
+ const sessions = await sessionService.getAllSessions();
19
+
20
+ ctx.body = {
21
+ data: sessions,
22
+ meta: {
23
+ count: sessions.length,
24
+ active: sessions.filter(s => s.isTrulyActive).length,
25
+ inactive: sessions.filter(s => !s.isTrulyActive).length,
26
+ },
27
+ };
28
+ } catch (err) {
29
+ ctx.throw(500, 'Error fetching sessions');
30
+ }
31
+ },
32
+
33
+ /**
34
+ * Get active sessions only
35
+ * GET /magic-sessionmanager/sessions/active
36
+ */
37
+ async getActiveSessions(ctx) {
38
+ try {
39
+ const sessionService = strapi
40
+ .plugin('magic-sessionmanager')
41
+ .service('session');
42
+
43
+ const sessions = await sessionService.getActiveSessions();
44
+
45
+ ctx.body = {
46
+ data: sessions,
47
+ meta: {
48
+ count: sessions.length,
49
+ },
50
+ };
51
+ } catch (err) {
52
+ ctx.throw(500, 'Error fetching active sessions');
53
+ }
54
+ },
55
+
56
+ /**
57
+ * Get user's sessions
58
+ * GET /magic-sessionmanager/user/:userId/sessions
59
+ * SECURITY: User can only access their own sessions
60
+ */
61
+ async getUserSessions(ctx) {
62
+ try {
63
+ const { userId } = ctx.params;
64
+ const requestingUserId = ctx.state.user?.id;
65
+
66
+ // SECURITY CHECK: User can only see their own sessions
67
+ if (requestingUserId && String(requestingUserId) !== String(userId)) {
68
+ strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserId} tried to access sessions of user ${userId}`);
69
+ return ctx.forbidden('You can only access your own sessions');
70
+ }
71
+
72
+ const sessionService = strapi
73
+ .plugin('magic-sessionmanager')
74
+ .service('session');
75
+
76
+ const sessions = await sessionService.getUserSessions(userId);
77
+
78
+ ctx.body = {
79
+ data: sessions,
80
+ meta: {
81
+ count: sessions.length,
82
+ },
83
+ };
84
+ } catch (err) {
85
+ ctx.throw(500, 'Error fetching user sessions');
86
+ }
87
+ },
88
+
89
+ /**
90
+ * Logout handler - terminates current session
91
+ * POST /api/magic-sessionmanager/logout
92
+ */
93
+ async logout(ctx) {
94
+ try {
95
+ const userId = ctx.state.user?.id;
96
+ const token = ctx.request.headers.authorization?.replace('Bearer ', '');
97
+
98
+ if (!userId) {
99
+ return ctx.throw(401, 'Unauthorized');
100
+ }
101
+
102
+ const sessionService = strapi
103
+ .plugin('magic-sessionmanager')
104
+ .service('session');
105
+
106
+ // Find current session by token
107
+ const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
108
+ filters: {
109
+ user: { id: userId },
110
+ token: token,
111
+ isActive: true,
112
+ },
113
+ });
114
+
115
+ if (sessions.length > 0) {
116
+ // Terminate only the current session
117
+ await sessionService.terminateSession({ sessionId: sessions[0].id });
118
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out (session ${sessions[0].id})`);
119
+ }
120
+
121
+ ctx.body = {
122
+ message: 'Logged out successfully',
123
+ };
124
+ } catch (err) {
125
+ strapi.log.error('[magic-sessionmanager] Logout error:', err);
126
+ ctx.throw(500, 'Error during logout');
127
+ }
128
+ },
129
+
130
+ /**
131
+ * Logout from all devices - terminates all sessions for current user
132
+ * POST /api/magic-sessionmanager/logout-all
133
+ */
134
+ async logoutAll(ctx) {
135
+ try {
136
+ const userId = ctx.state.user?.id;
137
+
138
+ if (!userId) {
139
+ return ctx.throw(401, 'Unauthorized');
140
+ }
141
+
142
+ const sessionService = strapi
143
+ .plugin('magic-sessionmanager')
144
+ .service('session');
145
+
146
+ // Terminate all sessions for this user
147
+ await sessionService.terminateSession({ userId });
148
+
149
+ strapi.log.info(`[magic-sessionmanager] User ${userId} logged out from all devices`);
150
+
151
+ ctx.body = {
152
+ message: 'Logged out from all devices successfully',
153
+ };
154
+ } catch (err) {
155
+ strapi.log.error('[magic-sessionmanager] Logout-all error:', err);
156
+ ctx.throw(500, 'Error during logout');
157
+ }
158
+ },
159
+
160
+ /**
161
+ * Terminate specific session
162
+ * DELETE /magic-sessionmanager/sessions/:sessionId
163
+ */
164
+ async terminateSession(ctx) {
165
+ try {
166
+ const { sessionId } = ctx.params;
167
+ const sessionService = strapi
168
+ .plugin('magic-sessionmanager')
169
+ .service('session');
170
+
171
+ await sessionService.terminateSession({ sessionId });
172
+
173
+ ctx.body = {
174
+ message: `Session ${sessionId} terminated`,
175
+ };
176
+ } catch (err) {
177
+ ctx.throw(500, 'Error terminating session');
178
+ }
179
+ },
180
+
181
+ /**
182
+ * Terminate a single session (Admin action)
183
+ * POST /magic-sessionmanager/sessions/:sessionId/terminate
184
+ */
185
+ async terminateSingleSession(ctx) {
186
+ try {
187
+ const { sessionId } = ctx.params;
188
+
189
+ const sessionService = strapi
190
+ .plugin('magic-sessionmanager')
191
+ .service('session');
192
+
193
+ await sessionService.terminateSession({ sessionId });
194
+
195
+ ctx.body = {
196
+ message: `Session ${sessionId} terminated`,
197
+ success: true,
198
+ };
199
+ } catch (err) {
200
+ strapi.log.error('[magic-sessionmanager] Error terminating session:', err);
201
+ ctx.throw(500, 'Error terminating session');
202
+ }
203
+ },
204
+
205
+ /**
206
+ * Terminate ALL sessions for a specific user (Admin action)
207
+ * POST /magic-sessionmanager/user/:userId/terminate-all
208
+ */
209
+ async terminateAllUserSessions(ctx) {
210
+ try {
211
+ const { userId } = ctx.params;
212
+
213
+ const sessionService = strapi
214
+ .plugin('magic-sessionmanager')
215
+ .service('session');
216
+
217
+ await sessionService.terminateSession({ userId });
218
+
219
+ ctx.body = {
220
+ message: `All sessions terminated for user ${userId}`,
221
+ success: true,
222
+ };
223
+ } catch (err) {
224
+ strapi.log.error('[magic-sessionmanager] Error terminating all user sessions:', err);
225
+ ctx.throw(500, 'Error terminating all user sessions');
226
+ }
227
+ },
228
+
229
+ /**
230
+ * Get IP Geolocation data (Premium feature)
231
+ * GET /magic-sessionmanager/geolocation/:ipAddress
232
+ */
233
+ async getIpGeolocation(ctx) {
234
+ try {
235
+ const { ipAddress } = ctx.params;
236
+
237
+ if (!ipAddress) {
238
+ return ctx.badRequest('IP address is required');
239
+ }
240
+
241
+ // Check if user has premium license
242
+ const licenseGuard = strapi.plugin('magic-sessionmanager').service('license-guard');
243
+ const pluginStore = strapi.store({
244
+ type: 'plugin',
245
+ name: 'magic-sessionmanager'
246
+ });
247
+ const licenseKey = await pluginStore.get({ key: 'licenseKey' });
248
+
249
+ if (!licenseKey) {
250
+ return ctx.forbidden('Premium license required for geolocation features');
251
+ }
252
+
253
+ const license = await licenseGuard.getLicenseByKey(licenseKey);
254
+
255
+ if (!license || !license.featurePremium) {
256
+ return ctx.forbidden('Premium license required for geolocation features');
257
+ }
258
+
259
+ // Get geolocation data
260
+ const geolocationService = strapi.plugin('magic-sessionmanager').service('geolocation');
261
+ const ipData = await geolocationService.getIpInfo(ipAddress);
262
+
263
+ ctx.body = {
264
+ success: true,
265
+ data: ipData,
266
+ };
267
+ } catch (err) {
268
+ strapi.log.error('[magic-sessionmanager] Error getting IP geolocation:', err);
269
+ ctx.throw(500, 'Error fetching IP geolocation data');
270
+ }
271
+ },
272
+
273
+ /**
274
+ * Delete a single session permanently (Admin action)
275
+ * DELETE /magic-sessionmanager/sessions/:sessionId
276
+ */
277
+ async deleteSession(ctx) {
278
+ try {
279
+ const { sessionId } = ctx.params;
280
+
281
+ const sessionService = strapi
282
+ .plugin('magic-sessionmanager')
283
+ .service('session');
284
+
285
+ await sessionService.deleteSession(sessionId);
286
+
287
+ ctx.body = {
288
+ message: `Session ${sessionId} permanently deleted`,
289
+ success: true,
290
+ };
291
+ } catch (err) {
292
+ strapi.log.error('[magic-sessionmanager] Error deleting session:', err);
293
+ ctx.throw(500, 'Error deleting session');
294
+ }
295
+ },
296
+
297
+ /**
298
+ * Delete all inactive sessions (Admin action)
299
+ * POST /magic-sessionmanager/sessions/clean-inactive
300
+ */
301
+ async cleanInactiveSessions(ctx) {
302
+ try {
303
+ const sessionService = strapi
304
+ .plugin('magic-sessionmanager')
305
+ .service('session');
306
+
307
+ const deletedCount = await sessionService.deleteInactiveSessions();
308
+
309
+ ctx.body = {
310
+ message: `Successfully deleted ${deletedCount} inactive sessions`,
311
+ success: true,
312
+ deletedCount,
313
+ };
314
+ } catch (err) {
315
+ strapi.log.error('[magic-sessionmanager] Error cleaning inactive sessions:', err);
316
+ ctx.throw(500, 'Error deleting inactive sessions');
317
+ }
318
+ },
319
+
320
+ /**
321
+ * Toggle user blocked status
322
+ * POST /magic-sessionmanager/user/:userId/toggle-block
323
+ */
324
+ async toggleUserBlock(ctx) {
325
+ try {
326
+ const { userId } = ctx.params;
327
+
328
+ // Get current user status
329
+ const user = await strapi.entityService.findOne('plugin::users-permissions.user', userId);
330
+
331
+ if (!user) {
332
+ return ctx.throw(404, 'User not found');
333
+ }
334
+
335
+ // Toggle blocked status
336
+ const newBlockedStatus = !user.blocked;
337
+
338
+ await strapi.entityService.update('plugin::users-permissions.user', userId, {
339
+ data: {
340
+ blocked: newBlockedStatus,
341
+ },
342
+ });
343
+
344
+ // If blocking user, terminate all their sessions
345
+ if (newBlockedStatus) {
346
+ const sessionService = strapi
347
+ .plugin('magic-sessionmanager')
348
+ .service('session');
349
+ await sessionService.terminateSession({ userId });
350
+ }
351
+
352
+ ctx.body = {
353
+ message: `User ${newBlockedStatus ? 'blocked' : 'unblocked'} successfully`,
354
+ blocked: newBlockedStatus,
355
+ success: true,
356
+ };
357
+ } catch (err) {
358
+ strapi.log.error('[magic-sessionmanager] Error toggling user block:', err);
359
+ ctx.throw(500, 'Error toggling user block status');
360
+ }
361
+ },
362
+ };