strapi-plugin-magic-sessionmanager 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/admin/jsconfig.json +10 -0
  2. package/admin/src/components/Initializer.jsx +11 -0
  3. package/admin/src/components/LicenseGuard.jsx +591 -0
  4. package/admin/src/components/OnlineUsersWidget.jsx +208 -0
  5. package/admin/src/components/PluginIcon.jsx +8 -0
  6. package/admin/src/components/SessionDetailModal.jsx +445 -0
  7. package/admin/src/components/SessionInfoCard.jsx +151 -0
  8. package/admin/src/components/SessionInfoPanel.jsx +375 -0
  9. package/admin/src/components/index.jsx +5 -0
  10. package/admin/src/hooks/useLicense.js +103 -0
  11. package/admin/src/index.js +137 -0
  12. package/admin/src/pages/ActiveSessions.jsx +12 -0
  13. package/admin/src/pages/Analytics.jsx +735 -0
  14. package/admin/src/pages/App.jsx +12 -0
  15. package/admin/src/pages/HomePage.jsx +1248 -0
  16. package/admin/src/pages/License.jsx +603 -0
  17. package/admin/src/pages/Settings.jsx +1497 -0
  18. package/admin/src/pages/SettingsNew.jsx +1204 -0
  19. package/admin/src/pages/index.jsx +3 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +20 -0
  22. package/admin/src/translations/en.json +20 -0
  23. package/admin/src/utils/getTranslation.js +5 -0
  24. package/admin/src/utils/index.js +2 -0
  25. package/admin/src/utils/parseUserAgent.js +79 -0
  26. package/dist/server/index.js +91 -2
  27. package/dist/server/index.mjs +91 -2
  28. package/package.json +3 -1
  29. package/server/jsconfig.json +10 -0
  30. package/server/src/bootstrap.js +297 -0
  31. package/server/src/config/index.js +20 -0
  32. package/server/src/content-types/index.js +9 -0
  33. package/server/src/content-types/session/schema.json +76 -0
  34. package/server/src/controllers/controller.js +11 -0
  35. package/server/src/controllers/index.js +11 -0
  36. package/server/src/controllers/license.js +266 -0
  37. package/server/src/controllers/session.js +362 -0
  38. package/server/src/controllers/settings.js +122 -0
  39. package/server/src/destroy.js +18 -0
  40. package/server/src/index.js +23 -0
  41. package/server/src/middlewares/index.js +5 -0
  42. package/server/src/middlewares/last-seen.js +56 -0
  43. package/server/src/policies/index.js +3 -0
  44. package/server/src/register.js +32 -0
  45. package/server/src/routes/admin.js +149 -0
  46. package/server/src/routes/content-api.js +51 -0
  47. package/server/src/routes/index.js +9 -0
  48. package/server/src/services/geolocation.js +180 -0
  49. package/server/src/services/index.js +13 -0
  50. package/server/src/services/license-guard.js +308 -0
  51. package/server/src/services/notifications.js +319 -0
  52. package/server/src/services/service.js +7 -0
  53. package/server/src/services/session.js +345 -0
  54. package/server/src/utils/getClientIp.js +118 -0
@@ -0,0 +1,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
+ };
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Settings controller
5
+ * Manages plugin settings stored in Strapi database
6
+ */
7
+
8
+ module.exports = {
9
+ /**
10
+ * Get plugin settings
11
+ */
12
+ async getSettings(ctx) {
13
+ try {
14
+ const pluginStore = strapi.store({
15
+ type: 'plugin',
16
+ name: 'magic-sessionmanager',
17
+ });
18
+
19
+ let settings = await pluginStore.get({ key: 'settings' });
20
+
21
+ // If no settings exist, return defaults
22
+ if (!settings) {
23
+ settings = {
24
+ inactivityTimeout: 15,
25
+ cleanupInterval: 30,
26
+ lastSeenRateLimit: 30,
27
+ retentionDays: 90,
28
+ enableGeolocation: true,
29
+ enableSecurityScoring: true,
30
+ blockSuspiciousSessions: false,
31
+ maxFailedLogins: 5,
32
+ enableEmailAlerts: false,
33
+ alertOnSuspiciousLogin: true,
34
+ alertOnNewLocation: true,
35
+ alertOnVpnProxy: true,
36
+ enableWebhooks: false,
37
+ discordWebhookUrl: '',
38
+ slackWebhookUrl: '',
39
+ enableGeofencing: false,
40
+ allowedCountries: [],
41
+ blockedCountries: [],
42
+ emailTemplates: {
43
+ suspiciousLogin: { subject: '', html: '', text: '' },
44
+ newLocation: { subject: '', html: '', text: '' },
45
+ vpnProxy: { subject: '', html: '', text: '' },
46
+ },
47
+ };
48
+ }
49
+
50
+ ctx.send({
51
+ settings,
52
+ success: true
53
+ });
54
+ } catch (error) {
55
+ strapi.log.error('[magic-sessionmanager/settings] Error getting settings:', error);
56
+ ctx.badRequest('Error loading settings');
57
+ }
58
+ },
59
+
60
+ /**
61
+ * Update plugin settings
62
+ */
63
+ async updateSettings(ctx) {
64
+ try {
65
+ const { body } = ctx.request;
66
+
67
+ if (!body) {
68
+ return ctx.badRequest('Settings data is required');
69
+ }
70
+
71
+ const pluginStore = strapi.store({
72
+ type: 'plugin',
73
+ name: 'magic-sessionmanager',
74
+ });
75
+
76
+ // Validate and sanitize settings
77
+ const sanitizedSettings = {
78
+ inactivityTimeout: parseInt(body.inactivityTimeout) || 15,
79
+ cleanupInterval: parseInt(body.cleanupInterval) || 30,
80
+ lastSeenRateLimit: parseInt(body.lastSeenRateLimit) || 30,
81
+ retentionDays: parseInt(body.retentionDays) || 90,
82
+ enableGeolocation: !!body.enableGeolocation,
83
+ enableSecurityScoring: !!body.enableSecurityScoring,
84
+ blockSuspiciousSessions: !!body.blockSuspiciousSessions,
85
+ maxFailedLogins: parseInt(body.maxFailedLogins) || 5,
86
+ enableEmailAlerts: !!body.enableEmailAlerts,
87
+ alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
88
+ alertOnNewLocation: !!body.alertOnNewLocation,
89
+ alertOnVpnProxy: !!body.alertOnVpnProxy,
90
+ enableWebhooks: !!body.enableWebhooks,
91
+ discordWebhookUrl: String(body.discordWebhookUrl || ''),
92
+ slackWebhookUrl: String(body.slackWebhookUrl || ''),
93
+ enableGeofencing: !!body.enableGeofencing,
94
+ allowedCountries: Array.isArray(body.allowedCountries) ? body.allowedCountries : [],
95
+ blockedCountries: Array.isArray(body.blockedCountries) ? body.blockedCountries : [],
96
+ emailTemplates: body.emailTemplates || {
97
+ suspiciousLogin: { subject: '', html: '', text: '' },
98
+ newLocation: { subject: '', html: '', text: '' },
99
+ vpnProxy: { subject: '', html: '', text: '' },
100
+ },
101
+ };
102
+
103
+ // Save to database
104
+ await pluginStore.set({
105
+ key: 'settings',
106
+ value: sanitizedSettings
107
+ });
108
+
109
+ strapi.log.info('[magic-sessionmanager/settings] Settings updated successfully');
110
+
111
+ ctx.send({
112
+ settings: sanitizedSettings,
113
+ success: true,
114
+ message: 'Settings saved successfully!'
115
+ });
116
+ } catch (error) {
117
+ strapi.log.error('[magic-sessionmanager/settings] Error updating settings:', error);
118
+ ctx.badRequest('Error saving settings');
119
+ }
120
+ },
121
+ };
122
+
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ module.exports = async ({ strapi }) => {
4
+ // Stop license pinging
5
+ if (strapi.licenseGuard && strapi.licenseGuard.pingInterval) {
6
+ clearInterval(strapi.licenseGuard.pingInterval);
7
+ strapi.log.info('[magic-sessionmanager] 🛑 License pinging stopped');
8
+ }
9
+
10
+ // Stop cleanup interval
11
+ if (strapi.sessionManagerIntervals && strapi.sessionManagerIntervals.cleanup) {
12
+ clearInterval(strapi.sessionManagerIntervals.cleanup);
13
+ strapi.log.info('[magic-sessionmanager] 🛑 Session cleanup interval stopped');
14
+ }
15
+
16
+ strapi.log.info('[magic-sessionmanager] ✅ Plugin cleanup completed');
17
+ };
18
+
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const register = require('./register');
4
+ const bootstrap = require('./bootstrap');
5
+ const destroy = require('./destroy');
6
+ const config = require('./config');
7
+ const contentTypes = require('./content-types');
8
+ const routes = require('./routes');
9
+ const controllers = require('./controllers');
10
+ const services = require('./services');
11
+ const middlewares = require('./middlewares');
12
+
13
+ module.exports = {
14
+ register,
15
+ bootstrap,
16
+ destroy,
17
+ config,
18
+ contentTypes,
19
+ routes,
20
+ controllers,
21
+ services,
22
+ middlewares,
23
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ 'last-seen': require('./last-seen'),
5
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * lastSeen Middleware
5
+ * Updates user lastSeen and session lastActive on each authenticated request
6
+ * Rate-limited to prevent DB write noise (default: 30 seconds)
7
+ */
8
+ module.exports = ({ strapi, sessionService }) => {
9
+ return async (ctx, next) => {
10
+ // BEFORE processing request: Check if user's sessions are active
11
+ if (ctx.state.user && ctx.state.user.id) {
12
+ try {
13
+ const userId = ctx.state.user.id;
14
+
15
+ // Check if user has ANY active sessions
16
+ const activeSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
17
+ filters: {
18
+ user: { id: userId },
19
+ isActive: true,
20
+ },
21
+ limit: 1,
22
+ });
23
+
24
+ // If user has NO active sessions, reject the request
25
+ if (!activeSessions || activeSessions.length === 0) {
26
+ strapi.log.info(`[magic-sessionmanager] 🚫 Blocked request - User ${userId} has no active sessions`);
27
+ return ctx.unauthorized('All sessions have been terminated. Please login again.');
28
+ }
29
+ } catch (err) {
30
+ strapi.log.debug('[magic-sessionmanager] Error checking active sessions:', err.message);
31
+ // On error, allow request to continue (fail-open for availability)
32
+ }
33
+ }
34
+
35
+ // Process request
36
+ await next();
37
+
38
+ // AFTER response: Update activity timestamps if user is authenticated
39
+ if (ctx.state.user && ctx.state.user.id) {
40
+ try {
41
+ const userId = ctx.state.user.id;
42
+
43
+ // Try to find or extract sessionId from context
44
+ const sessionId = ctx.state.sessionId;
45
+
46
+ // Call touch with rate limiting
47
+ await sessionService.touch({
48
+ userId,
49
+ sessionId,
50
+ });
51
+ } catch (err) {
52
+ strapi.log.debug('[magic-sessionmanager] Error updating lastSeen:', err.message);
53
+ }
54
+ }
55
+ };
56
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = {};
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Register hook
5
+ * Sessions relation is hidden from UI to keep User interface clean
6
+ * Sessions are accessed via the Session Manager plugin UI components
7
+ */
8
+ module.exports = async ({ strapi }) => {
9
+ strapi.log.info('[magic-sessionmanager] 🚀 Plugin registration starting...');
10
+
11
+ try {
12
+ // Get the user content type
13
+ const userCT = strapi.contentType('plugin::users-permissions.user');
14
+
15
+ if (!userCT) {
16
+ strapi.log.error('[magic-sessionmanager] User content type not found');
17
+ return;
18
+ }
19
+
20
+ // REMOVE sessions relation from User content type to keep UI clean
21
+ // Sessions are managed through SessionInfoPanel sidebar instead
22
+ if (userCT.attributes && userCT.attributes.sessions) {
23
+ delete userCT.attributes.sessions;
24
+ strapi.log.info('[magic-sessionmanager] ✅ Removed sessions field from User content type');
25
+ }
26
+
27
+ strapi.log.info('[magic-sessionmanager] ✅ Plugin registered successfully');
28
+
29
+ } catch (err) {
30
+ strapi.log.error('[magic-sessionmanager] ❌ Registration error:', err);
31
+ }
32
+ };