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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notifications Service (ADVANCED Feature)
|
|
3
|
+
* Send email alerts for session events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = ({ strapi }) => ({
|
|
7
|
+
/**
|
|
8
|
+
* Get email templates from database settings
|
|
9
|
+
* Falls back to default hardcoded templates if not found
|
|
10
|
+
*/
|
|
11
|
+
async getEmailTemplates() {
|
|
12
|
+
try {
|
|
13
|
+
// Try to load templates from database
|
|
14
|
+
const pluginStore = strapi.store({
|
|
15
|
+
type: 'plugin',
|
|
16
|
+
name: 'magic-sessionmanager',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const settings = await pluginStore.get({ key: 'settings' });
|
|
20
|
+
|
|
21
|
+
if (settings?.emailTemplates && Object.keys(settings.emailTemplates).length > 0) {
|
|
22
|
+
// Check if templates have content
|
|
23
|
+
const hasContent = Object.values(settings.emailTemplates).some(
|
|
24
|
+
template => template.html || template.text
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (hasContent) {
|
|
28
|
+
strapi.log.debug('[magic-sessionmanager/notifications] Using templates from database');
|
|
29
|
+
return settings.emailTemplates;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
strapi.log.warn('[magic-sessionmanager/notifications] Could not load templates from DB, using defaults:', err.message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Default fallback templates
|
|
37
|
+
strapi.log.debug('[magic-sessionmanager/notifications] Using default fallback templates');
|
|
38
|
+
return {
|
|
39
|
+
suspiciousLogin: {
|
|
40
|
+
subject: '๐จ Suspicious Login Alert - Session Manager',
|
|
41
|
+
html: `
|
|
42
|
+
<html>
|
|
43
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
44
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb; border-radius: 10px;">
|
|
45
|
+
<h2 style="color: #dc2626;">๐จ Suspicious Login Detected</h2>
|
|
46
|
+
<p>A potentially suspicious login was detected for your account.</p>
|
|
47
|
+
|
|
48
|
+
<div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
49
|
+
<h3 style="margin-top: 0;">Account Information:</h3>
|
|
50
|
+
<ul>
|
|
51
|
+
<li><strong>Email:</strong> {{user.email}}</li>
|
|
52
|
+
<li><strong>Username:</strong> {{user.username}}</li>
|
|
53
|
+
</ul>
|
|
54
|
+
|
|
55
|
+
<h3>Login Details:</h3>
|
|
56
|
+
<ul>
|
|
57
|
+
<li><strong>Time:</strong> {{session.loginTime}}</li>
|
|
58
|
+
<li><strong>IP Address:</strong> {{session.ipAddress}}</li>
|
|
59
|
+
<li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
|
|
60
|
+
<li><strong>Timezone:</strong> {{geo.timezone}}</li>
|
|
61
|
+
<li><strong>Device:</strong> {{session.userAgent}}</li>
|
|
62
|
+
</ul>
|
|
63
|
+
|
|
64
|
+
<h3 style="color: #dc2626;">Security Alert:</h3>
|
|
65
|
+
<ul>
|
|
66
|
+
<li>VPN Detected: {{reason.isVpn}}</li>
|
|
67
|
+
<li>Proxy Detected: {{reason.isProxy}}</li>
|
|
68
|
+
<li>Threat Detected: {{reason.isThreat}}</li>
|
|
69
|
+
<li>Security Score: {{reason.securityScore}}/100</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<p>If this was you, you can safely ignore this email. If you don't recognize this activity, please secure your account immediately.</p>
|
|
74
|
+
|
|
75
|
+
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
|
|
76
|
+
<p style="color: #666; font-size: 12px;">This is an automated security notification from Magic Session Manager.</p>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>`,
|
|
80
|
+
text: `๐จ Suspicious Login Detected\n\nA potentially suspicious login was detected for your account.\n\nAccount: {{user.email}}\nUsername: {{user.username}}\n\nLogin Details:\n- Time: {{session.loginTime}}\n- IP: {{session.ipAddress}}\n- Location: {{geo.city}}, {{geo.country}}\n\nSecurity: VPN={{reason.isVpn}}, Proxy={{reason.isProxy}}, Threat={{reason.isThreat}}, Score={{reason.securityScore}}/100`,
|
|
81
|
+
},
|
|
82
|
+
newLocation: {
|
|
83
|
+
subject: '๐ New Location Login Detected',
|
|
84
|
+
html: `<h2>๐ New Location Login</h2><p>Account: {{user.email}}</p><p>Time: {{session.loginTime}}</p><p>Location: {{geo.city}}, {{geo.country}}</p><p>IP: {{session.ipAddress}}</p>`,
|
|
85
|
+
text: `๐ New Location Login\n\nAccount: {{user.email}}\nTime: {{session.loginTime}}\nLocation: {{geo.city}}, {{geo.country}}\nIP: {{session.ipAddress}}`,
|
|
86
|
+
},
|
|
87
|
+
vpnProxy: {
|
|
88
|
+
subject: 'โ ๏ธ VPN/Proxy Login Detected',
|
|
89
|
+
html: `<h2>โ ๏ธ VPN/Proxy Detected</h2><p>Account: {{user.email}}</p><p>Time: {{session.loginTime}}</p><p>IP: {{session.ipAddress}}</p><p>VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}</p>`,
|
|
90
|
+
text: `โ ๏ธ VPN/Proxy Detected\n\nAccount: {{user.email}}\nTime: {{session.loginTime}}\nIP: {{session.ipAddress}}\nVPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Replace template variables with actual values
|
|
97
|
+
*/
|
|
98
|
+
replaceVariables(template, data) {
|
|
99
|
+
let result = template;
|
|
100
|
+
|
|
101
|
+
// User variables
|
|
102
|
+
result = result.replace(/\{\{user\.email\}\}/g, data.user?.email || 'N/A');
|
|
103
|
+
result = result.replace(/\{\{user\.username\}\}/g, data.user?.username || 'N/A');
|
|
104
|
+
|
|
105
|
+
// Session variables
|
|
106
|
+
result = result.replace(/\{\{session\.loginTime\}\}/g,
|
|
107
|
+
data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : 'N/A');
|
|
108
|
+
result = result.replace(/\{\{session\.ipAddress\}\}/g, data.session?.ipAddress || 'N/A');
|
|
109
|
+
result = result.replace(/\{\{session\.userAgent\}\}/g, data.session?.userAgent || 'N/A');
|
|
110
|
+
|
|
111
|
+
// Geo variables
|
|
112
|
+
result = result.replace(/\{\{geo\.city\}\}/g, data.geoData?.city || 'Unknown');
|
|
113
|
+
result = result.replace(/\{\{geo\.country\}\}/g, data.geoData?.country || 'Unknown');
|
|
114
|
+
result = result.replace(/\{\{geo\.timezone\}\}/g, data.geoData?.timezone || 'Unknown');
|
|
115
|
+
|
|
116
|
+
// Reason variables
|
|
117
|
+
result = result.replace(/\{\{reason\.isVpn\}\}/g, data.reason?.isVpn ? 'Yes' : 'No');
|
|
118
|
+
result = result.replace(/\{\{reason\.isProxy\}\}/g, data.reason?.isProxy ? 'Yes' : 'No');
|
|
119
|
+
result = result.replace(/\{\{reason\.isThreat\}\}/g, data.reason?.isThreat ? 'Yes' : 'No');
|
|
120
|
+
result = result.replace(/\{\{reason\.securityScore\}\}/g, data.reason?.securityScore || '0');
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Send suspicious login alert
|
|
127
|
+
* @param {Object} params - { user, session, reason, geoData }
|
|
128
|
+
*/
|
|
129
|
+
async sendSuspiciousLoginAlert({ user, session, reason, geoData }) {
|
|
130
|
+
try {
|
|
131
|
+
// Get templates from database (or defaults)
|
|
132
|
+
const templates = await this.getEmailTemplates();
|
|
133
|
+
const template = templates.suspiciousLogin;
|
|
134
|
+
|
|
135
|
+
// Prepare data for variable replacement
|
|
136
|
+
const data = { user, session, reason, geoData };
|
|
137
|
+
|
|
138
|
+
// Replace variables in template
|
|
139
|
+
const htmlContent = this.replaceVariables(template.html, data);
|
|
140
|
+
const textContent = this.replaceVariables(template.text, data);
|
|
141
|
+
|
|
142
|
+
await strapi.plugins['email'].services.email.send({
|
|
143
|
+
to: user.email,
|
|
144
|
+
subject: template.subject,
|
|
145
|
+
html: htmlContent,
|
|
146
|
+
text: textContent,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
strapi.log.info(`[magic-sessionmanager/notifications] Suspicious login alert sent to ${user.email}`);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
strapi.log.error('[magic-sessionmanager/notifications] Error sending email:', err);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Send new location login alert
|
|
159
|
+
* @param {Object} params - { user, session, geoData }
|
|
160
|
+
*/
|
|
161
|
+
async sendNewLocationAlert({ user, session, geoData }) {
|
|
162
|
+
try {
|
|
163
|
+
// Get templates from database (or defaults)
|
|
164
|
+
const templates = await this.getEmailTemplates();
|
|
165
|
+
const template = templates.newLocation;
|
|
166
|
+
|
|
167
|
+
// Prepare data for variable replacement
|
|
168
|
+
const data = { user, session, geoData, reason: {} };
|
|
169
|
+
|
|
170
|
+
// Replace variables in template
|
|
171
|
+
const htmlContent = this.replaceVariables(template.html, data);
|
|
172
|
+
const textContent = this.replaceVariables(template.text, data);
|
|
173
|
+
|
|
174
|
+
await strapi.plugins['email'].services.email.send({
|
|
175
|
+
to: user.email,
|
|
176
|
+
subject: template.subject,
|
|
177
|
+
html: htmlContent,
|
|
178
|
+
text: textContent,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
strapi.log.info(`[magic-sessionmanager/notifications] New location alert sent to ${user.email}`);
|
|
182
|
+
return true;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
strapi.log.error('[magic-sessionmanager/notifications] Error sending new location email:', err);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Send VPN/Proxy login alert
|
|
191
|
+
* @param {Object} params - { user, session, reason, geoData }
|
|
192
|
+
*/
|
|
193
|
+
async sendVpnProxyAlert({ user, session, reason, geoData }) {
|
|
194
|
+
try {
|
|
195
|
+
// Get templates from database (or defaults)
|
|
196
|
+
const templates = await this.getEmailTemplates();
|
|
197
|
+
const template = templates.vpnProxy;
|
|
198
|
+
|
|
199
|
+
// Prepare data for variable replacement
|
|
200
|
+
const data = { user, session, reason, geoData };
|
|
201
|
+
|
|
202
|
+
// Replace variables in template
|
|
203
|
+
const htmlContent = this.replaceVariables(template.html, data);
|
|
204
|
+
const textContent = this.replaceVariables(template.text, data);
|
|
205
|
+
|
|
206
|
+
await strapi.plugins['email'].services.email.send({
|
|
207
|
+
to: user.email,
|
|
208
|
+
subject: template.subject,
|
|
209
|
+
html: htmlContent,
|
|
210
|
+
text: textContent,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
strapi.log.info(`[magic-sessionmanager/notifications] VPN/Proxy alert sent to ${user.email}`);
|
|
214
|
+
return true;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
strapi.log.error('[magic-sessionmanager/notifications] Error sending VPN/Proxy email:', err);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Send webhook notification
|
|
223
|
+
* @param {Object} params - { event, data, webhookUrl }
|
|
224
|
+
*/
|
|
225
|
+
async sendWebhook({ event, data, webhookUrl }) {
|
|
226
|
+
try {
|
|
227
|
+
const payload = {
|
|
228
|
+
event,
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
data,
|
|
231
|
+
source: 'magic-sessionmanager',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const response = await fetch(webhookUrl, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
'User-Agent': 'Strapi-Magic-SessionManager-Webhook/1.0',
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(payload),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (response.ok) {
|
|
244
|
+
strapi.log.info(`[magic-sessionmanager/notifications] Webhook sent: ${event}`);
|
|
245
|
+
return true;
|
|
246
|
+
} else {
|
|
247
|
+
strapi.log.warn(`[magic-sessionmanager/notifications] Webhook failed: ${response.status}`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
strapi.log.error('[magic-sessionmanager/notifications] Webhook error:', err);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format webhook for Discord
|
|
258
|
+
* @param {Object} params - { event, session, user, geoData }
|
|
259
|
+
*/
|
|
260
|
+
formatDiscordWebhook({ event, session, user, geoData }) {
|
|
261
|
+
const embed = {
|
|
262
|
+
title: this.getEventTitle(event),
|
|
263
|
+
color: this.getEventColor(event),
|
|
264
|
+
fields: [
|
|
265
|
+
{ name: '๐ค User', value: `${user.email}\n${user.username || 'N/A'}`, inline: true },
|
|
266
|
+
{ name: '๐ IP', value: session.ipAddress, inline: true },
|
|
267
|
+
{ name: '๐
Time', value: new Date(session.loginTime).toLocaleString(), inline: false },
|
|
268
|
+
],
|
|
269
|
+
timestamp: new Date().toISOString(),
|
|
270
|
+
footer: { text: 'Magic Session Manager' },
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (geoData) {
|
|
274
|
+
embed.fields.push({
|
|
275
|
+
name: '๐ Location',
|
|
276
|
+
value: `${geoData.country_flag} ${geoData.city}, ${geoData.country}`,
|
|
277
|
+
inline: true,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (geoData.isVpn || geoData.isProxy || geoData.isThreat) {
|
|
281
|
+
const warnings = [];
|
|
282
|
+
if (geoData.isVpn) warnings.push('VPN');
|
|
283
|
+
if (geoData.isProxy) warnings.push('Proxy');
|
|
284
|
+
if (geoData.isThreat) warnings.push('Threat');
|
|
285
|
+
|
|
286
|
+
embed.fields.push({
|
|
287
|
+
name: 'โ ๏ธ Security',
|
|
288
|
+
value: `${warnings.join(', ')} detected\nScore: ${geoData.securityScore}/100`,
|
|
289
|
+
inline: true,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { embeds: [embed] };
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
getEventTitle(event) {
|
|
298
|
+
const titles = {
|
|
299
|
+
'login.suspicious': '๐จ Suspicious Login',
|
|
300
|
+
'login.new_location': '๐ New Location Login',
|
|
301
|
+
'login.vpn': '๐ด VPN Login Detected',
|
|
302
|
+
'login.threat': 'โ Threat IP Login',
|
|
303
|
+
'session.terminated': '๐ด Session Terminated',
|
|
304
|
+
};
|
|
305
|
+
return titles[event] || '๐ Session Event';
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
getEventColor(event) {
|
|
309
|
+
const colors = {
|
|
310
|
+
'login.suspicious': 0xFF0000, // Red
|
|
311
|
+
'login.new_location': 0xFFA500, // Orange
|
|
312
|
+
'login.vpn': 0xFF6B6B, // Light Red
|
|
313
|
+
'login.threat': 0x8B0000, // Dark Red
|
|
314
|
+
'session.terminated': 0x808080, // Gray
|
|
315
|
+
};
|
|
316
|
+
return colors[event] || 0x5865F2; // Discord Blue
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Service
|
|
5
|
+
* Uses plugin::magic-sessionmanager.session content type with relation to users
|
|
6
|
+
* All session tracking happens in the Session collection
|
|
7
|
+
*
|
|
8
|
+
* TODO: For production multi-instance deployments, use Redis for:
|
|
9
|
+
* - Session store instead of DB
|
|
10
|
+
* - Rate limiting locks
|
|
11
|
+
* - Distributed session state
|
|
12
|
+
*/
|
|
13
|
+
module.exports = ({ strapi }) => ({
|
|
14
|
+
/**
|
|
15
|
+
* Create a new session record
|
|
16
|
+
* @param {Object} params - { userId, ip, userAgent, token }
|
|
17
|
+
* @returns {Promise<Object>} Created session
|
|
18
|
+
*/
|
|
19
|
+
async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token }) {
|
|
20
|
+
try {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
|
|
23
|
+
const session = await strapi.entityService.create('plugin::magic-sessionmanager.session', {
|
|
24
|
+
data: {
|
|
25
|
+
user: userId,
|
|
26
|
+
ipAddress: ip.substring(0, 45),
|
|
27
|
+
userAgent: userAgent.substring(0, 500),
|
|
28
|
+
loginTime: now,
|
|
29
|
+
lastActive: now,
|
|
30
|
+
isActive: true,
|
|
31
|
+
token: token, // Store JWT for logout matching
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
strapi.log.info(`[magic-sessionmanager] โ
Session ${session.id} created for user ${userId}`);
|
|
36
|
+
|
|
37
|
+
return session;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
strapi.log.error('[magic-sessionmanager] Error creating session:', err);
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Terminate a session or all sessions for a user
|
|
46
|
+
* @param {Object} params - { sessionId | userId }
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
async terminateSession({ sessionId, userId }) {
|
|
50
|
+
try {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
|
|
53
|
+
if (sessionId) {
|
|
54
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', sessionId, {
|
|
55
|
+
data: {
|
|
56
|
+
isActive: false,
|
|
57
|
+
logoutTime: now,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
strapi.log.info(`[magic-sessionmanager] Session ${sessionId} terminated`);
|
|
62
|
+
} else if (userId) {
|
|
63
|
+
// Find all active sessions for user
|
|
64
|
+
const activeSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
65
|
+
filters: {
|
|
66
|
+
user: { id: userId },
|
|
67
|
+
isActive: true,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Terminate all active sessions
|
|
72
|
+
for (const session of activeSessions) {
|
|
73
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', session.id, {
|
|
74
|
+
data: {
|
|
75
|
+
isActive: false,
|
|
76
|
+
logoutTime: now,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
strapi.log.info(`[magic-sessionmanager] All sessions terminated for user ${userId}`);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
strapi.log.error('[magic-sessionmanager] Error terminating session:', err);
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get ALL sessions (active + inactive) with accurate online status
|
|
91
|
+
* @returns {Promise<Array>} All sessions with enhanced data
|
|
92
|
+
*/
|
|
93
|
+
async getAllSessions() {
|
|
94
|
+
try {
|
|
95
|
+
const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
96
|
+
populate: { user: { fields: ['id', 'email', 'username'] } },
|
|
97
|
+
sort: { loginTime: 'desc' },
|
|
98
|
+
limit: 1000, // Reasonable limit
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Get inactivity timeout from config (default: 15 minutes)
|
|
102
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
103
|
+
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
104
|
+
|
|
105
|
+
// Enhance sessions with accurate online status
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const enhancedSessions = sessions.map(session => {
|
|
108
|
+
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
109
|
+
const timeSinceActive = now - lastActiveTime;
|
|
110
|
+
|
|
111
|
+
// Session is "truly active" if within timeout window AND isActive is true
|
|
112
|
+
const isTrulyActive = session.isActive && (timeSinceActive < inactivityTimeout);
|
|
113
|
+
|
|
114
|
+
// Remove sensitive token field for security
|
|
115
|
+
const { token, ...sessionWithoutToken } = session;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...sessionWithoutToken,
|
|
119
|
+
isTrulyActive,
|
|
120
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return enhancedSessions;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
strapi.log.error('[magic-sessionmanager] Error getting all sessions:', err);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get all active sessions with accurate online status
|
|
133
|
+
* @returns {Promise<Array>} Active sessions with user data and online status
|
|
134
|
+
*/
|
|
135
|
+
async getActiveSessions() {
|
|
136
|
+
try {
|
|
137
|
+
const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
138
|
+
filters: { isActive: true },
|
|
139
|
+
populate: { user: { fields: ['id', 'email', 'username'] } },
|
|
140
|
+
sort: { loginTime: 'desc' },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Get inactivity timeout from config (default: 15 minutes)
|
|
144
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
145
|
+
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
146
|
+
|
|
147
|
+
// Enhance sessions with accurate online status
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const enhancedSessions = sessions.map(session => {
|
|
150
|
+
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
151
|
+
const timeSinceActive = now - lastActiveTime;
|
|
152
|
+
|
|
153
|
+
// Session is "truly active" if within timeout window
|
|
154
|
+
const isTrulyActive = timeSinceActive < inactivityTimeout;
|
|
155
|
+
|
|
156
|
+
// Remove sensitive token field for security
|
|
157
|
+
const { token, ...sessionWithoutToken } = session;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...sessionWithoutToken,
|
|
161
|
+
isTrulyActive,
|
|
162
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Only return truly active sessions
|
|
167
|
+
return enhancedSessions.filter(s => s.isTrulyActive);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
strapi.log.error('[magic-sessionmanager] Error getting active sessions:', err);
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get all sessions for a specific user
|
|
176
|
+
* @param {number} userId
|
|
177
|
+
* @returns {Promise<Array>} User's sessions with accurate online status
|
|
178
|
+
*/
|
|
179
|
+
async getUserSessions(userId) {
|
|
180
|
+
try {
|
|
181
|
+
const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
182
|
+
filters: { user: { id: userId } },
|
|
183
|
+
sort: { loginTime: 'desc' },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Get inactivity timeout from config (default: 15 minutes)
|
|
187
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
188
|
+
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
189
|
+
|
|
190
|
+
// Enhance sessions with accurate online status
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const enhancedSessions = sessions.map(session => {
|
|
193
|
+
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
194
|
+
const timeSinceActive = now - lastActiveTime;
|
|
195
|
+
|
|
196
|
+
// Session is "truly active" if:
|
|
197
|
+
// 1. isActive = true AND
|
|
198
|
+
// 2. lastActive is within timeout window
|
|
199
|
+
const isTrulyActive = session.isActive && (timeSinceActive < inactivityTimeout);
|
|
200
|
+
|
|
201
|
+
// Remove sensitive token field for security
|
|
202
|
+
const { token, ...sessionWithoutToken } = session;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...sessionWithoutToken,
|
|
206
|
+
isTrulyActive,
|
|
207
|
+
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return enhancedSessions;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
strapi.log.error('[magic-sessionmanager] Error getting user sessions:', err);
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update lastActive timestamp on session (rate-limited to avoid DB noise)
|
|
220
|
+
* @param {Object} params - { userId, sessionId }
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
async touch({ userId, sessionId }) {
|
|
224
|
+
try {
|
|
225
|
+
const now = new Date();
|
|
226
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
227
|
+
const rateLimit = config.lastSeenRateLimit || 30000;
|
|
228
|
+
|
|
229
|
+
// Update session lastActive only
|
|
230
|
+
if (sessionId) {
|
|
231
|
+
const session = await strapi.entityService.findOne('plugin::magic-sessionmanager.session', sessionId);
|
|
232
|
+
|
|
233
|
+
if (session && session.lastActive) {
|
|
234
|
+
const lastActiveTime = new Date(session.lastActive).getTime();
|
|
235
|
+
const currentTime = now.getTime();
|
|
236
|
+
|
|
237
|
+
if (currentTime - lastActiveTime > rateLimit) {
|
|
238
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', sessionId, {
|
|
239
|
+
data: { lastActive: now },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} else if (session) {
|
|
243
|
+
// First time or null
|
|
244
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', sessionId, {
|
|
245
|
+
data: { lastActive: now },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
strapi.log.debug('[magic-sessionmanager] Error touching session:', err.message);
|
|
251
|
+
// Don't throw - this is a non-critical operation
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Cleanup inactive sessions - set isActive to false for sessions older than inactivityTimeout
|
|
257
|
+
* Should be called on bootstrap to clean up stale sessions
|
|
258
|
+
*/
|
|
259
|
+
async cleanupInactiveSessions() {
|
|
260
|
+
try {
|
|
261
|
+
// Get inactivity timeout from config (default: 15 minutes)
|
|
262
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
263
|
+
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
264
|
+
|
|
265
|
+
// Calculate cutoff time
|
|
266
|
+
const now = new Date();
|
|
267
|
+
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
268
|
+
|
|
269
|
+
strapi.log.info(`[magic-sessionmanager] ๐งน Cleaning up sessions inactive since before ${cutoffTime.toISOString()}`);
|
|
270
|
+
|
|
271
|
+
// Find all active sessions
|
|
272
|
+
const activeSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
273
|
+
filters: { isActive: true },
|
|
274
|
+
fields: ['id', 'lastActive', 'loginTime'],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Deactivate old sessions
|
|
278
|
+
let deactivatedCount = 0;
|
|
279
|
+
for (const session of activeSessions) {
|
|
280
|
+
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
281
|
+
|
|
282
|
+
if (lastActiveTime < cutoffTime) {
|
|
283
|
+
await strapi.entityService.update('plugin::magic-sessionmanager.session', session.id, {
|
|
284
|
+
data: { isActive: false },
|
|
285
|
+
});
|
|
286
|
+
deactivatedCount++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
strapi.log.info(`[magic-sessionmanager] โ
Cleanup complete: ${deactivatedCount} sessions deactivated`);
|
|
291
|
+
return deactivatedCount;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
strapi.log.error('[magic-sessionmanager] Error cleaning up inactive sessions:', err);
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Delete a single session from database
|
|
300
|
+
* WARNING: This permanently deletes the record!
|
|
301
|
+
* @param {number} sessionId - Session ID to delete
|
|
302
|
+
* @returns {Promise<boolean>} Success status
|
|
303
|
+
*/
|
|
304
|
+
async deleteSession(sessionId) {
|
|
305
|
+
try {
|
|
306
|
+
await strapi.entityService.delete('plugin::magic-sessionmanager.session', sessionId);
|
|
307
|
+
strapi.log.info(`[magic-sessionmanager] ๐๏ธ Session ${sessionId} permanently deleted`);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
strapi.log.error('[magic-sessionmanager] Error deleting session:', err);
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Delete all inactive sessions from database
|
|
317
|
+
* WARNING: This permanently deletes records!
|
|
318
|
+
* @returns {Promise<number>} Number of deleted sessions
|
|
319
|
+
*/
|
|
320
|
+
async deleteInactiveSessions() {
|
|
321
|
+
try {
|
|
322
|
+
strapi.log.info('[magic-sessionmanager] ๐๏ธ Deleting all inactive sessions...');
|
|
323
|
+
|
|
324
|
+
// Find all inactive sessions
|
|
325
|
+
const inactiveSessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
326
|
+
filters: { isActive: false },
|
|
327
|
+
fields: ['id'],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
let deletedCount = 0;
|
|
331
|
+
|
|
332
|
+
// Delete each inactive session
|
|
333
|
+
for (const session of inactiveSessions) {
|
|
334
|
+
await strapi.entityService.delete('plugin::magic-sessionmanager.session', session.id);
|
|
335
|
+
deletedCount++;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
strapi.log.info(`[magic-sessionmanager] โ
Deleted ${deletedCount} inactive sessions`);
|
|
339
|
+
return deletedCount;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
strapi.log.error('[magic-sessionmanager] Error deleting inactive sessions:', err);
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
});
|