strapi-plugin-magic-sessionmanager 4.2.4 → 4.2.5
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/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/Initializer.jsx +0 -11
- package/admin/src/components/LicenseGuard.jsx +0 -591
- package/admin/src/components/OnlineUsersWidget.jsx +0 -212
- package/admin/src/components/PluginIcon.jsx +0 -8
- package/admin/src/components/SessionDetailModal.jsx +0 -449
- package/admin/src/components/SessionInfoCard.jsx +0 -151
- package/admin/src/components/SessionInfoPanel.jsx +0 -385
- package/admin/src/components/index.jsx +0 -5
- package/admin/src/hooks/useLicense.js +0 -103
- package/admin/src/index.js +0 -149
- package/admin/src/pages/ActiveSessions.jsx +0 -12
- package/admin/src/pages/Analytics.jsx +0 -735
- package/admin/src/pages/App.jsx +0 -12
- package/admin/src/pages/HomePage.jsx +0 -1212
- package/admin/src/pages/License.jsx +0 -603
- package/admin/src/pages/Settings.jsx +0 -1646
- package/admin/src/pages/SettingsNew.jsx +0 -1204
- package/admin/src/pages/UpgradePage.jsx +0 -448
- package/admin/src/pages/index.jsx +0 -3
- package/admin/src/pluginId.js +0 -4
- package/admin/src/translations/de.json +0 -299
- package/admin/src/translations/en.json +0 -299
- package/admin/src/translations/es.json +0 -287
- package/admin/src/translations/fr.json +0 -287
- package/admin/src/translations/pt.json +0 -287
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/index.js +0 -2
- package/admin/src/utils/parseUserAgent.js +0 -79
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -492
- package/server/src/config/index.js +0 -23
- package/server/src/content-types/index.js +0 -9
- package/server/src/content-types/session/schema.json +0 -84
- package/server/src/controllers/controller.js +0 -11
- package/server/src/controllers/index.js +0 -11
- package/server/src/controllers/license.js +0 -266
- package/server/src/controllers/session.js +0 -433
- package/server/src/controllers/settings.js +0 -122
- package/server/src/destroy.js +0 -22
- package/server/src/index.js +0 -23
- package/server/src/middlewares/index.js +0 -5
- package/server/src/middlewares/last-seen.js +0 -62
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -36
- package/server/src/routes/admin.js +0 -149
- package/server/src/routes/content-api.js +0 -60
- package/server/src/routes/index.js +0 -9
- package/server/src/services/geolocation.js +0 -182
- package/server/src/services/index.js +0 -13
- package/server/src/services/license-guard.js +0 -316
- package/server/src/services/notifications.js +0 -319
- package/server/src/services/service.js +0 -7
- package/server/src/services/session.js +0 -393
- package/server/src/utils/encryption.js +0 -121
- package/server/src/utils/getClientIp.js +0 -118
- package/server/src/utils/logger.js +0 -84
package/server/src/bootstrap.js
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Bootstrap: Mount middleware for session tracking
|
|
5
|
-
* Sessions are managed via plugin::magic-sessionmanager.session content type
|
|
6
|
-
*
|
|
7
|
-
* [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
|
|
8
|
-
*
|
|
9
|
-
* NOTE: For multi-instance deployments, consider Redis locks or session store
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const getClientIp = require('./utils/getClientIp');
|
|
13
|
-
const { encryptToken, decryptToken } = require('./utils/encryption');
|
|
14
|
-
const { createLogger } = require('./utils/logger');
|
|
15
|
-
|
|
16
|
-
const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
17
|
-
const USER_UID = 'plugin::users-permissions.user';
|
|
18
|
-
|
|
19
|
-
module.exports = async ({ strapi }) => {
|
|
20
|
-
const log = createLogger(strapi);
|
|
21
|
-
|
|
22
|
-
log.info('[START] Bootstrap starting...');
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
// Initialize License Guard
|
|
26
|
-
const licenseGuardService = strapi.plugin('magic-sessionmanager').service('license-guard');
|
|
27
|
-
|
|
28
|
-
// Wait a bit for all services to be ready
|
|
29
|
-
setTimeout(async () => {
|
|
30
|
-
const licenseStatus = await licenseGuardService.initialize();
|
|
31
|
-
|
|
32
|
-
if (!licenseStatus.valid) {
|
|
33
|
-
log.error('╔════════════════════════════════════════════════════════════════╗');
|
|
34
|
-
log.error('║ [ERROR] SESSION MANAGER - NO VALID LICENSE ║');
|
|
35
|
-
log.error('║ ║');
|
|
36
|
-
log.error('║ This plugin requires a valid license to operate. ║');
|
|
37
|
-
log.error('║ Please activate your license via Admin UI: ║');
|
|
38
|
-
log.error('║ Go to Settings → Sessions → License ║');
|
|
39
|
-
log.error('║ ║');
|
|
40
|
-
log.error('║ The plugin will run with limited functionality until ║');
|
|
41
|
-
log.error('║ a valid license is activated. ║');
|
|
42
|
-
log.error('╚════════════════════════════════════════════════════════════════╝');
|
|
43
|
-
} else if (licenseStatus.valid) {
|
|
44
|
-
const pluginStore = strapi.store({
|
|
45
|
-
type: 'plugin',
|
|
46
|
-
name: 'magic-sessionmanager',
|
|
47
|
-
});
|
|
48
|
-
const storedKey = await pluginStore.get({ key: 'licenseKey' });
|
|
49
|
-
|
|
50
|
-
log.info('╔════════════════════════════════════════════════════════════════╗');
|
|
51
|
-
log.info('║ [SUCCESS] SESSION MANAGER LICENSE ACTIVE ║');
|
|
52
|
-
log.info('║ ║');
|
|
53
|
-
|
|
54
|
-
if (licenseStatus.data) {
|
|
55
|
-
log.info(`║ License: ${licenseStatus.data.licenseKey}`.padEnd(66) + '║');
|
|
56
|
-
log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + '║');
|
|
57
|
-
log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + '║');
|
|
58
|
-
} else if (storedKey) {
|
|
59
|
-
log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + '║');
|
|
60
|
-
log.info(`║ Status: Grace Period Active`.padEnd(66) + '║');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
log.info('║ ║');
|
|
64
|
-
log.info('║ [RELOAD] Auto-pinging every 15 minutes ║');
|
|
65
|
-
log.info('╚════════════════════════════════════════════════════════════════╝');
|
|
66
|
-
}
|
|
67
|
-
}, 3000); // Wait 3 seconds for API to be ready
|
|
68
|
-
|
|
69
|
-
// Get session service
|
|
70
|
-
const sessionService = strapi
|
|
71
|
-
.plugin('magic-sessionmanager')
|
|
72
|
-
.service('session');
|
|
73
|
-
|
|
74
|
-
// Cleanup inactive sessions on startup
|
|
75
|
-
log.info('Running initial session cleanup...');
|
|
76
|
-
await sessionService.cleanupInactiveSessions();
|
|
77
|
-
|
|
78
|
-
// Schedule periodic cleanup every 30 minutes
|
|
79
|
-
const cleanupInterval = 30 * 60 * 1000; // 30 minutes
|
|
80
|
-
|
|
81
|
-
const cleanupIntervalHandle = setInterval(async () => {
|
|
82
|
-
try {
|
|
83
|
-
// Get fresh reference to service to avoid scope issues
|
|
84
|
-
const service = strapi.plugin('magic-sessionmanager').service('session');
|
|
85
|
-
await service.cleanupInactiveSessions();
|
|
86
|
-
} catch (err) {
|
|
87
|
-
log.error('Periodic cleanup error:', err);
|
|
88
|
-
}
|
|
89
|
-
}, cleanupInterval);
|
|
90
|
-
|
|
91
|
-
log.info('[TIME] Periodic cleanup scheduled (every 30 minutes)');
|
|
92
|
-
|
|
93
|
-
// Store interval handle for cleanup on shutdown
|
|
94
|
-
if (!strapi.sessionManagerIntervals) {
|
|
95
|
-
strapi.sessionManagerIntervals = {};
|
|
96
|
-
}
|
|
97
|
-
strapi.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
|
|
98
|
-
|
|
99
|
-
// HIGH PRIORITY: Register /api/auth/logout route BEFORE other plugins
|
|
100
|
-
strapi.server.routes([{
|
|
101
|
-
method: 'POST',
|
|
102
|
-
path: '/api/auth/logout',
|
|
103
|
-
handler: async (ctx) => {
|
|
104
|
-
try {
|
|
105
|
-
const token = ctx.request.headers?.authorization?.replace('Bearer ', '');
|
|
106
|
-
|
|
107
|
-
if (!token) {
|
|
108
|
-
ctx.status = 200;
|
|
109
|
-
ctx.body = { message: 'Logged out successfully' };
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Find session by decrypting tokens and matching
|
|
114
|
-
// Since tokens are encrypted, we need to get all active sessions and check each one
|
|
115
|
-
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
116
|
-
filters: {
|
|
117
|
-
isActive: true,
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Find matching session by decrypting and comparing tokens
|
|
122
|
-
const matchingSession = allSessions.find(session => {
|
|
123
|
-
if (!session.token) return false;
|
|
124
|
-
try {
|
|
125
|
-
const decrypted = decryptToken(session.token);
|
|
126
|
-
return decrypted === token;
|
|
127
|
-
} catch (err) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (matchingSession) {
|
|
133
|
-
await sessionService.terminateSession({ sessionId: matchingSession.documentId });
|
|
134
|
-
log.info(`[LOGOUT] Logout via /api/auth/logout - Session ${matchingSession.documentId} terminated`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
ctx.status = 200;
|
|
138
|
-
ctx.body = { message: 'Logged out successfully' };
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log.error('Logout error:', err);
|
|
141
|
-
ctx.status = 200;
|
|
142
|
-
ctx.body = { message: 'Logged out successfully' };
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
config: {
|
|
146
|
-
auth: false,
|
|
147
|
-
},
|
|
148
|
-
}]);
|
|
149
|
-
|
|
150
|
-
log.info('[SUCCESS] /api/auth/logout route registered');
|
|
151
|
-
|
|
152
|
-
// Middleware to intercept logins
|
|
153
|
-
strapi.server.use(async (ctx, next) => {
|
|
154
|
-
// Execute the actual request
|
|
155
|
-
await next();
|
|
156
|
-
|
|
157
|
-
// Check if this was a successful login request
|
|
158
|
-
const isAuthLocal = ctx.path === '/api/auth/local' && ctx.method === 'POST';
|
|
159
|
-
const isMagicLink = ctx.path.includes('/magic-link/login') && ctx.method === 'POST';
|
|
160
|
-
|
|
161
|
-
if ((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.user) {
|
|
162
|
-
try {
|
|
163
|
-
const user = ctx.body.user;
|
|
164
|
-
|
|
165
|
-
// Extract REAL client IP (handles proxies, load balancers, Cloudflare, etc.)
|
|
166
|
-
const ip = getClientIp(ctx);
|
|
167
|
-
const userAgent = ctx.request.headers?.['user-agent'] || ctx.request.header?.['user-agent'] || 'unknown';
|
|
168
|
-
|
|
169
|
-
// Strapi v5: Use documentId for session creation
|
|
170
|
-
log.info(`[CHECK] Login detected! User: ${user.documentId || user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
171
|
-
|
|
172
|
-
// Get config
|
|
173
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
174
|
-
|
|
175
|
-
// Check if we should analyze this session (Premium/Advanced features)
|
|
176
|
-
let shouldBlock = false;
|
|
177
|
-
let blockReason = '';
|
|
178
|
-
let geoData = null;
|
|
179
|
-
|
|
180
|
-
// Premium: Get geolocation data
|
|
181
|
-
if (config.enableGeolocation || config.enableSecurityScoring) {
|
|
182
|
-
try {
|
|
183
|
-
const geolocationService = strapi.plugin('magic-sessionmanager').service('geolocation');
|
|
184
|
-
geoData = await geolocationService.getIpInfo(ip);
|
|
185
|
-
|
|
186
|
-
// Advanced: Auto-blocking
|
|
187
|
-
if (config.blockSuspiciousSessions && geoData) {
|
|
188
|
-
if (geoData.isThreat) {
|
|
189
|
-
shouldBlock = true;
|
|
190
|
-
blockReason = 'Known threat IP detected';
|
|
191
|
-
} else if (geoData.isVpn && config.alertOnVpnProxy) {
|
|
192
|
-
shouldBlock = true;
|
|
193
|
-
blockReason = 'VPN detected';
|
|
194
|
-
} else if (geoData.isProxy && config.alertOnVpnProxy) {
|
|
195
|
-
shouldBlock = true;
|
|
196
|
-
blockReason = 'Proxy detected';
|
|
197
|
-
} else if (geoData.securityScore < 50) {
|
|
198
|
-
shouldBlock = true;
|
|
199
|
-
blockReason = `Low security score: ${geoData.securityScore}/100`;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Advanced: Geo-fencing
|
|
204
|
-
if (config.enableGeofencing && geoData && geoData.country_code) {
|
|
205
|
-
const countryCode = geoData.country_code;
|
|
206
|
-
|
|
207
|
-
// Check blocked countries
|
|
208
|
-
if (config.blockedCountries && config.blockedCountries.includes(countryCode)) {
|
|
209
|
-
shouldBlock = true;
|
|
210
|
-
blockReason = `Country ${countryCode} is blocked`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check allowed countries (whitelist)
|
|
214
|
-
if (config.allowedCountries && config.allowedCountries.length > 0) {
|
|
215
|
-
if (!config.allowedCountries.includes(countryCode)) {
|
|
216
|
-
shouldBlock = true;
|
|
217
|
-
blockReason = `Country ${countryCode} is not in allowlist`;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch (geoErr) {
|
|
222
|
-
log.warn('Geolocation check failed:', geoErr.message);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Block if needed
|
|
227
|
-
if (shouldBlock) {
|
|
228
|
-
log.warn(`[BLOCKED] Blocking login: ${blockReason}`);
|
|
229
|
-
|
|
230
|
-
// Don't create session, return error
|
|
231
|
-
ctx.status = 403;
|
|
232
|
-
ctx.body = {
|
|
233
|
-
error: {
|
|
234
|
-
status: 403,
|
|
235
|
-
message: 'Login blocked for security reasons',
|
|
236
|
-
details: { reason: blockReason }
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
return; // Stop here
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Create a new session (Strapi v5: Use documentId instead of numeric id)
|
|
243
|
-
// If login response doesn't include documentId, fetch it from DB
|
|
244
|
-
let userDocId = user.documentId;
|
|
245
|
-
if (!userDocId && user.id) {
|
|
246
|
-
const fullUser = await strapi.entityService.findOne(USER_UID, user.id);
|
|
247
|
-
userDocId = fullUser?.documentId || user.id;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const newSession = await sessionService.createSession({
|
|
251
|
-
userId: userDocId,
|
|
252
|
-
ip,
|
|
253
|
-
userAgent,
|
|
254
|
-
token: ctx.body.jwt, // Store Access Token (encrypted)
|
|
255
|
-
refreshToken: ctx.body.refreshToken, // Store Refresh Token (encrypted) if exists
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
log.info(`[SUCCESS] Session created for user ${userDocId} (IP: ${ip})`);
|
|
259
|
-
|
|
260
|
-
// Advanced: Send notifications
|
|
261
|
-
if (geoData && (config.enableEmailAlerts || config.enableWebhooks)) {
|
|
262
|
-
try {
|
|
263
|
-
const notificationService = strapi.plugin('magic-sessionmanager').service('notifications');
|
|
264
|
-
|
|
265
|
-
// Determine if suspicious
|
|
266
|
-
const isSuspicious = geoData.isVpn || geoData.isProxy || geoData.isThreat || geoData.securityScore < 70;
|
|
267
|
-
|
|
268
|
-
// Email alerts
|
|
269
|
-
if (config.enableEmailAlerts && config.alertOnSuspiciousLogin && isSuspicious) {
|
|
270
|
-
await notificationService.sendSuspiciousLoginAlert({
|
|
271
|
-
user,
|
|
272
|
-
session: newSession,
|
|
273
|
-
reason: {
|
|
274
|
-
isVpn: geoData.isVpn,
|
|
275
|
-
isProxy: geoData.isProxy,
|
|
276
|
-
isThreat: geoData.isThreat,
|
|
277
|
-
securityScore: geoData.securityScore,
|
|
278
|
-
},
|
|
279
|
-
geoData,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Webhook notifications (Discord/Slack)
|
|
284
|
-
if (config.enableWebhooks) {
|
|
285
|
-
const webhookData = notificationService.formatDiscordWebhook({
|
|
286
|
-
event: isSuspicious ? 'login.suspicious' : 'login.success',
|
|
287
|
-
session: newSession,
|
|
288
|
-
user,
|
|
289
|
-
geoData,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (config.discordWebhookUrl) {
|
|
293
|
-
await notificationService.sendWebhook({
|
|
294
|
-
event: 'session.login',
|
|
295
|
-
data: webhookData,
|
|
296
|
-
webhookUrl: config.discordWebhookUrl,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
} catch (notifErr) {
|
|
301
|
-
log.warn('Notification failed:', notifErr.message);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
} catch (err) {
|
|
305
|
-
log.error('[ERROR] Error creating session:', err);
|
|
306
|
-
// Don't throw - login should still succeed even if session creation fails
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
log.info('[SUCCESS] Login/Logout interceptor middleware mounted');
|
|
312
|
-
|
|
313
|
-
// Middleware to block refresh token requests for terminated sessions
|
|
314
|
-
strapi.server.use(async (ctx, next) => {
|
|
315
|
-
// Check if this is a refresh token request (Strapi v5: /api/auth/refresh)
|
|
316
|
-
const isRefreshToken = ctx.path === '/api/auth/refresh' && ctx.method === 'POST';
|
|
317
|
-
|
|
318
|
-
if (isRefreshToken) {
|
|
319
|
-
try {
|
|
320
|
-
const refreshToken = ctx.request.body?.refreshToken;
|
|
321
|
-
|
|
322
|
-
if (refreshToken) {
|
|
323
|
-
// Find session with this refresh token
|
|
324
|
-
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
325
|
-
filters: {
|
|
326
|
-
isActive: true,
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Find matching session by decrypting and comparing refresh tokens
|
|
331
|
-
const matchingSession = allSessions.find(session => {
|
|
332
|
-
if (!session.refreshToken) return false;
|
|
333
|
-
try {
|
|
334
|
-
const decrypted = decryptToken(session.refreshToken);
|
|
335
|
-
return decrypted === refreshToken;
|
|
336
|
-
} catch (err) {
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (!matchingSession) {
|
|
342
|
-
// No active session with this refresh token - Block!
|
|
343
|
-
log.warn('[BLOCKED] Blocked refresh token request - no active session');
|
|
344
|
-
ctx.status = 401;
|
|
345
|
-
ctx.body = {
|
|
346
|
-
error: {
|
|
347
|
-
status: 401,
|
|
348
|
-
message: 'Session terminated. Please login again.',
|
|
349
|
-
name: 'UnauthorizedError'
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
return; // Don't continue
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
log.info(`[SUCCESS] Refresh token allowed for session ${matchingSession.documentId}`);
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
log.error('Error checking refresh token:', err);
|
|
359
|
-
// On error, allow request to continue (fail-open for availability)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Continue with request
|
|
364
|
-
await next();
|
|
365
|
-
|
|
366
|
-
// AFTER: If refresh token response was successful, update session with new tokens
|
|
367
|
-
if (isRefreshToken && ctx.status === 200 && ctx.body && ctx.body.jwt) {
|
|
368
|
-
try {
|
|
369
|
-
const oldRefreshToken = ctx.request.body?.refreshToken;
|
|
370
|
-
const newAccessToken = ctx.body.jwt;
|
|
371
|
-
const newRefreshToken = ctx.body.refreshToken;
|
|
372
|
-
|
|
373
|
-
if (oldRefreshToken) {
|
|
374
|
-
// Find session and update with new tokens
|
|
375
|
-
const allSessions = await strapi.documents(SESSION_UID).findMany( {
|
|
376
|
-
filters: {
|
|
377
|
-
isActive: true,
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
const matchingSession = allSessions.find(session => {
|
|
382
|
-
if (!session.refreshToken) return false;
|
|
383
|
-
try {
|
|
384
|
-
const decrypted = decryptToken(session.refreshToken);
|
|
385
|
-
return decrypted === oldRefreshToken;
|
|
386
|
-
} catch (err) {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
if (matchingSession) {
|
|
392
|
-
const encryptedToken = newAccessToken ? encryptToken(newAccessToken) : matchingSession.token;
|
|
393
|
-
const encryptedRefreshToken = newRefreshToken ? encryptToken(newRefreshToken) : matchingSession.refreshToken;
|
|
394
|
-
|
|
395
|
-
await strapi.documents(SESSION_UID).update({
|
|
396
|
-
documentId: matchingSession.documentId,
|
|
397
|
-
data: {
|
|
398
|
-
token: encryptedToken,
|
|
399
|
-
refreshToken: encryptedRefreshToken,
|
|
400
|
-
lastActive: new Date(),
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
log.info(`[REFRESH] Tokens refreshed for session ${matchingSession.documentId}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
} catch (err) {
|
|
408
|
-
log.error('Error updating refreshed tokens:', err);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
log.info('[SUCCESS] Refresh Token interceptor middleware mounted');
|
|
414
|
-
|
|
415
|
-
// Mount lastSeen update middleware
|
|
416
|
-
strapi.server.use(
|
|
417
|
-
require('./middlewares/last-seen')({ strapi, sessionService })
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
log.info('[SUCCESS] LastSeen middleware mounted');
|
|
421
|
-
|
|
422
|
-
// Auto-enable Content-API permissions for authenticated users
|
|
423
|
-
await ensureContentApiPermissions(strapi, log);
|
|
424
|
-
|
|
425
|
-
log.info('[SUCCESS] Bootstrap complete');
|
|
426
|
-
log.info('[READY] Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
|
|
427
|
-
|
|
428
|
-
} catch (err) {
|
|
429
|
-
log.error('[ERROR] Bootstrap error:', err);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Auto-enable Content-API permissions for authenticated users
|
|
435
|
-
* This ensures plugin endpoints are accessible after installation
|
|
436
|
-
* @param {object} strapi - Strapi instance
|
|
437
|
-
* @param {object} log - Logger instance
|
|
438
|
-
*/
|
|
439
|
-
async function ensureContentApiPermissions(strapi, log) {
|
|
440
|
-
try {
|
|
441
|
-
// Get the authenticated role
|
|
442
|
-
const authenticatedRole = await strapi.query('plugin::users-permissions.role').findOne({
|
|
443
|
-
where: { type: 'authenticated' },
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
if (!authenticatedRole) {
|
|
447
|
-
log.warn('Authenticated role not found - skipping permission setup');
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Content-API actions that should be enabled for authenticated users
|
|
452
|
-
const requiredActions = [
|
|
453
|
-
'plugin::magic-sessionmanager.session.logout',
|
|
454
|
-
'plugin::magic-sessionmanager.session.logoutAll',
|
|
455
|
-
'plugin::magic-sessionmanager.session.getOwnSessions',
|
|
456
|
-
'plugin::magic-sessionmanager.session.getUserSessions',
|
|
457
|
-
];
|
|
458
|
-
|
|
459
|
-
// Get existing permissions for this role
|
|
460
|
-
const existingPermissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
|
461
|
-
where: {
|
|
462
|
-
role: authenticatedRole.id,
|
|
463
|
-
action: { $in: requiredActions },
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// Find which actions are missing
|
|
468
|
-
const existingActions = existingPermissions.map(p => p.action);
|
|
469
|
-
const missingActions = requiredActions.filter(action => !existingActions.includes(action));
|
|
470
|
-
|
|
471
|
-
if (missingActions.length === 0) {
|
|
472
|
-
log.debug('Content-API permissions already configured');
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Create missing permissions
|
|
477
|
-
for (const action of missingActions) {
|
|
478
|
-
await strapi.query('plugin::users-permissions.permission').create({
|
|
479
|
-
data: {
|
|
480
|
-
action,
|
|
481
|
-
role: authenticatedRole.id,
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
log.info(`[PERMISSION] Enabled ${action} for authenticated users`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
log.info('[SUCCESS] Content-API permissions configured for authenticated users');
|
|
488
|
-
} catch (err) {
|
|
489
|
-
log.warn('Could not auto-configure permissions:', err.message);
|
|
490
|
-
log.warn('Please manually enable plugin permissions in Settings > Users & Permissions > Roles > Authenticated');
|
|
491
|
-
}
|
|
492
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
default: {
|
|
5
|
-
// Enable debug logging (set to true to see all plugin logs)
|
|
6
|
-
debug: false,
|
|
7
|
-
|
|
8
|
-
// Rate limit for lastSeen updates (in milliseconds)
|
|
9
|
-
lastSeenRateLimit: 30000, // 30 seconds
|
|
10
|
-
|
|
11
|
-
// Session inactivity timeout (in milliseconds)
|
|
12
|
-
// After this time without activity, a session is considered inactive
|
|
13
|
-
inactivityTimeout: 15 * 60 * 1000, // 15 minutes
|
|
14
|
-
},
|
|
15
|
-
validator: (config) => {
|
|
16
|
-
if (config.lastSeenRateLimit && typeof config.lastSeenRateLimit !== 'number') {
|
|
17
|
-
throw new Error('lastSeenRateLimit must be a number (milliseconds)');
|
|
18
|
-
}
|
|
19
|
-
if (config.inactivityTimeout && typeof config.inactivityTimeout !== 'number') {
|
|
20
|
-
throw new Error('inactivityTimeout must be a number (milliseconds)');
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"kind": "collectionType",
|
|
3
|
-
"collectionName": "magic_sessions",
|
|
4
|
-
"info": {
|
|
5
|
-
"singularName": "session",
|
|
6
|
-
"pluralName": "sessions",
|
|
7
|
-
"displayName": "Session",
|
|
8
|
-
"description": "User session tracking with IP, device, and activity information"
|
|
9
|
-
},
|
|
10
|
-
"options": {
|
|
11
|
-
"draftAndPublish": false,
|
|
12
|
-
"comment": ""
|
|
13
|
-
},
|
|
14
|
-
"pluginOptions": {
|
|
15
|
-
"content-manager": {
|
|
16
|
-
"visible": false
|
|
17
|
-
},
|
|
18
|
-
"content-type-builder": {
|
|
19
|
-
"visible": false
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"attributes": {
|
|
23
|
-
"sessionId": {
|
|
24
|
-
"type": "string",
|
|
25
|
-
"unique": true,
|
|
26
|
-
"required": true
|
|
27
|
-
},
|
|
28
|
-
"user": {
|
|
29
|
-
"type": "relation",
|
|
30
|
-
"relation": "manyToOne",
|
|
31
|
-
"target": "plugin::users-permissions.user"
|
|
32
|
-
},
|
|
33
|
-
"ipAddress": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"maxLength": 45,
|
|
36
|
-
"required": true
|
|
37
|
-
},
|
|
38
|
-
"userAgent": {
|
|
39
|
-
"type": "text",
|
|
40
|
-
"maxLength": 500
|
|
41
|
-
},
|
|
42
|
-
"token": {
|
|
43
|
-
"type": "text",
|
|
44
|
-
"private": true
|
|
45
|
-
},
|
|
46
|
-
"refreshToken": {
|
|
47
|
-
"type": "text",
|
|
48
|
-
"private": true
|
|
49
|
-
},
|
|
50
|
-
"loginTime": {
|
|
51
|
-
"type": "datetime",
|
|
52
|
-
"required": true
|
|
53
|
-
},
|
|
54
|
-
"logoutTime": {
|
|
55
|
-
"type": "datetime"
|
|
56
|
-
},
|
|
57
|
-
"lastActive": {
|
|
58
|
-
"type": "datetime"
|
|
59
|
-
},
|
|
60
|
-
"isActive": {
|
|
61
|
-
"type": "boolean",
|
|
62
|
-
"default": true,
|
|
63
|
-
"required": true
|
|
64
|
-
},
|
|
65
|
-
"geoLocation": {
|
|
66
|
-
"type": "json"
|
|
67
|
-
},
|
|
68
|
-
"securityScore": {
|
|
69
|
-
"type": "integer",
|
|
70
|
-
"min": 0,
|
|
71
|
-
"max": 100
|
|
72
|
-
},
|
|
73
|
-
"deviceType": {
|
|
74
|
-
"type": "string"
|
|
75
|
-
},
|
|
76
|
-
"browserName": {
|
|
77
|
-
"type": "string"
|
|
78
|
-
},
|
|
79
|
-
"osName": {
|
|
80
|
-
"type": "string"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|