strapi-plugin-magic-sessionmanager 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer.jsx +11 -0
- package/admin/src/components/LicenseGuard.jsx +591 -0
- package/admin/src/components/OnlineUsersWidget.jsx +208 -0
- package/admin/src/components/PluginIcon.jsx +8 -0
- package/admin/src/components/SessionDetailModal.jsx +445 -0
- package/admin/src/components/SessionInfoCard.jsx +151 -0
- package/admin/src/components/SessionInfoPanel.jsx +375 -0
- package/admin/src/components/index.jsx +5 -0
- package/admin/src/hooks/useLicense.js +103 -0
- package/admin/src/index.js +137 -0
- package/admin/src/pages/ActiveSessions.jsx +12 -0
- package/admin/src/pages/Analytics.jsx +735 -0
- package/admin/src/pages/App.jsx +12 -0
- package/admin/src/pages/HomePage.jsx +1248 -0
- package/admin/src/pages/License.jsx +603 -0
- package/admin/src/pages/Settings.jsx +1497 -0
- package/admin/src/pages/SettingsNew.jsx +1204 -0
- package/admin/src/pages/index.jsx +3 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +20 -0
- package/admin/src/translations/en.json +20 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/parseUserAgent.js +79 -0
- package/dist/server/index.js +91 -2
- package/dist/server/index.mjs +91 -2
- package/package.json +3 -1
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +297 -0
- package/server/src/config/index.js +20 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/session/schema.json +76 -0
- package/server/src/controllers/controller.js +11 -0
- package/server/src/controllers/index.js +11 -0
- package/server/src/controllers/license.js +266 -0
- package/server/src/controllers/session.js +362 -0
- package/server/src/controllers/settings.js +122 -0
- package/server/src/destroy.js +18 -0
- package/server/src/index.js +23 -0
- package/server/src/middlewares/index.js +5 -0
- package/server/src/middlewares/last-seen.js +56 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +32 -0
- package/server/src/routes/admin.js +149 -0
- package/server/src/routes/content-api.js +51 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/geolocation.js +180 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/license-guard.js +308 -0
- package/server/src/services/notifications.js +319 -0
- package/server/src/services/service.js +7 -0
- package/server/src/services/session.js +345 -0
- package/server/src/utils/getClientIp.js +118 -0
|
@@ -0,0 +1,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,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,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
|
+
};
|