strapi-plugin-magic-sessionmanager 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +21 -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,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin.name": "Session Manager",
|
|
3
|
+
"plugin.description": "Verfolgung von Benutzer-Login/Logout und Sitzungsaktivität",
|
|
4
|
+
"settings.section": "Session Manager",
|
|
5
|
+
"settings.sessions": "Aktive Sitzungen",
|
|
6
|
+
"settings.emailTemplates.title": "E-Mail Templates",
|
|
7
|
+
"settings.emailTemplates.description": "Passe E-Mail-Benachrichtigungen mit dynamischen Variablen an",
|
|
8
|
+
"settings.emailTemplates.validate": "Validieren",
|
|
9
|
+
"settings.emailTemplates.loadDefault": "Standard-Template laden",
|
|
10
|
+
"settings.emailTemplates.subject": "E-Mail Betreff",
|
|
11
|
+
"settings.emailTemplates.htmlTemplate": "HTML Template",
|
|
12
|
+
"settings.emailTemplates.textTemplate": "Text Template (Fallback)",
|
|
13
|
+
"settings.emailTemplates.variables": "Verfügbare Variablen (klicken zum Kopieren)",
|
|
14
|
+
"settings.emailTemplates.validation.success": "Template gültig! {count} Variablen gefunden.",
|
|
15
|
+
"settings.emailTemplates.validation.warning": "Keine Variablen im Template gefunden. Füge mindestens eine Variable hinzu.",
|
|
16
|
+
"settings.emailTemplates.defaultLoaded": "Standard-Template geladen!",
|
|
17
|
+
"settings.emailTemplates.suspiciousLogin": "Verdächtiger Login",
|
|
18
|
+
"settings.emailTemplates.newLocation": "Neuer Standort",
|
|
19
|
+
"settings.emailTemplates.vpnProxy": "VPN/Proxy"
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin.name": "Session Manager",
|
|
3
|
+
"plugin.description": "Track user login/logout and session activity",
|
|
4
|
+
"settings.section": "Session Manager",
|
|
5
|
+
"settings.sessions": "Active Sessions",
|
|
6
|
+
"settings.emailTemplates.title": "Email Templates",
|
|
7
|
+
"settings.emailTemplates.description": "Customize email notification templates with dynamic variables",
|
|
8
|
+
"settings.emailTemplates.validate": "Validate",
|
|
9
|
+
"settings.emailTemplates.loadDefault": "Load Default Template",
|
|
10
|
+
"settings.emailTemplates.subject": "Email Subject",
|
|
11
|
+
"settings.emailTemplates.htmlTemplate": "HTML Template",
|
|
12
|
+
"settings.emailTemplates.textTemplate": "Text Template (Fallback)",
|
|
13
|
+
"settings.emailTemplates.variables": "Available Variables (click to copy)",
|
|
14
|
+
"settings.emailTemplates.validation.success": "Template valid! Found {count} variables.",
|
|
15
|
+
"settings.emailTemplates.validation.warning": "No variables found in template. Add at least one variable.",
|
|
16
|
+
"settings.emailTemplates.defaultLoaded": "Default template loaded!",
|
|
17
|
+
"settings.emailTemplates.suspiciousLogin": "Suspicious Login",
|
|
18
|
+
"settings.emailTemplates.newLocation": "New Location",
|
|
19
|
+
"settings.emailTemplates.vpnProxy": "VPN/Proxy"
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse User Agent to extract device and browser info
|
|
3
|
+
* Returns human-readable device type and browser name
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const parseUserAgent = (userAgent) => {
|
|
7
|
+
if (!userAgent) {
|
|
8
|
+
return {
|
|
9
|
+
device: 'Unknown',
|
|
10
|
+
deviceIcon: '❓',
|
|
11
|
+
browser: 'Unknown',
|
|
12
|
+
os: 'Unknown',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ua = userAgent.toLowerCase();
|
|
17
|
+
|
|
18
|
+
// Device detection
|
|
19
|
+
let device = 'Desktop';
|
|
20
|
+
let deviceIcon = '💻';
|
|
21
|
+
|
|
22
|
+
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(userAgent)) {
|
|
23
|
+
device = 'Tablet';
|
|
24
|
+
deviceIcon = '📱';
|
|
25
|
+
} else if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(userAgent)) {
|
|
26
|
+
device = 'Mobile';
|
|
27
|
+
deviceIcon = '📱';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Browser detection
|
|
31
|
+
let browser = 'Unknown';
|
|
32
|
+
if (ua.includes('edg/')) {
|
|
33
|
+
browser = 'Edge';
|
|
34
|
+
} else if (ua.includes('chrome/') && !ua.includes('edg/')) {
|
|
35
|
+
browser = 'Chrome';
|
|
36
|
+
} else if (ua.includes('firefox/')) {
|
|
37
|
+
browser = 'Firefox';
|
|
38
|
+
} else if (ua.includes('safari/') && !ua.includes('chrome/')) {
|
|
39
|
+
browser = 'Safari';
|
|
40
|
+
} else if (ua.includes('opera/') || ua.includes('opr/')) {
|
|
41
|
+
browser = 'Opera';
|
|
42
|
+
} else if (ua.includes('curl/')) {
|
|
43
|
+
browser = 'cURL';
|
|
44
|
+
deviceIcon = '⚙️';
|
|
45
|
+
device = 'API Client';
|
|
46
|
+
} else if (ua.includes('postman')) {
|
|
47
|
+
browser = 'Postman';
|
|
48
|
+
deviceIcon = '📮';
|
|
49
|
+
device = 'API Client';
|
|
50
|
+
} else if (ua.includes('insomnia')) {
|
|
51
|
+
browser = 'Insomnia';
|
|
52
|
+
deviceIcon = '🌙';
|
|
53
|
+
device = 'API Client';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// OS detection
|
|
57
|
+
let os = 'Unknown';
|
|
58
|
+
if (ua.includes('windows')) {
|
|
59
|
+
os = 'Windows';
|
|
60
|
+
} else if (ua.includes('mac os x') || ua.includes('macintosh')) {
|
|
61
|
+
os = 'macOS';
|
|
62
|
+
} else if (ua.includes('linux')) {
|
|
63
|
+
os = 'Linux';
|
|
64
|
+
} else if (ua.includes('android')) {
|
|
65
|
+
os = 'Android';
|
|
66
|
+
} else if (ua.includes('iphone') || ua.includes('ipad')) {
|
|
67
|
+
os = 'iOS';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
device,
|
|
72
|
+
deviceIcon,
|
|
73
|
+
browser,
|
|
74
|
+
os,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default parseUserAgent;
|
|
79
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
* NOTE: For multi-instance deployments, consider Redis locks or session store
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const getClientIp = require('./utils/getClientIp');
|
|
11
|
+
|
|
12
|
+
module.exports = async ({ strapi }) => {
|
|
13
|
+
strapi.log.info('[magic-sessionmanager] 🚀 Bootstrap starting...');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Initialize License Guard
|
|
17
|
+
const licenseGuardService = strapi.plugin('magic-sessionmanager').service('license-guard');
|
|
18
|
+
|
|
19
|
+
// Wait a bit for all services to be ready
|
|
20
|
+
setTimeout(async () => {
|
|
21
|
+
const licenseStatus = await licenseGuardService.initialize();
|
|
22
|
+
|
|
23
|
+
if (!licenseStatus.valid) {
|
|
24
|
+
strapi.log.error('╔════════════════════════════════════════════════════════════════╗');
|
|
25
|
+
strapi.log.error('║ ❌ SESSION MANAGER - NO VALID LICENSE ║');
|
|
26
|
+
strapi.log.error('║ ║');
|
|
27
|
+
strapi.log.error('║ This plugin requires a valid license to operate. ║');
|
|
28
|
+
strapi.log.error('║ Please activate your license via Admin UI: ║');
|
|
29
|
+
strapi.log.error('║ Go to Settings → Sessions → License ║');
|
|
30
|
+
strapi.log.error('║ ║');
|
|
31
|
+
strapi.log.error('║ The plugin will run with limited functionality until ║');
|
|
32
|
+
strapi.log.error('║ a valid license is activated. ║');
|
|
33
|
+
strapi.log.error('╚════════════════════════════════════════════════════════════════╝');
|
|
34
|
+
} else if (licenseStatus.valid) {
|
|
35
|
+
const pluginStore = strapi.store({
|
|
36
|
+
type: 'plugin',
|
|
37
|
+
name: 'magic-sessionmanager',
|
|
38
|
+
});
|
|
39
|
+
const storedKey = await pluginStore.get({ key: 'licenseKey' });
|
|
40
|
+
|
|
41
|
+
strapi.log.info('╔════════════════════════════════════════════════════════════════╗');
|
|
42
|
+
strapi.log.info('║ ✅ SESSION MANAGER LICENSE ACTIVE ║');
|
|
43
|
+
strapi.log.info('║ ║');
|
|
44
|
+
|
|
45
|
+
if (licenseStatus.data) {
|
|
46
|
+
strapi.log.info(`║ License: ${licenseStatus.data.licenseKey}`.padEnd(66) + '║');
|
|
47
|
+
strapi.log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + '║');
|
|
48
|
+
strapi.log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + '║');
|
|
49
|
+
} else if (storedKey) {
|
|
50
|
+
strapi.log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + '║');
|
|
51
|
+
strapi.log.info(`║ Status: Grace Period Active`.padEnd(66) + '║');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
strapi.log.info('║ ║');
|
|
55
|
+
strapi.log.info('║ 🔄 Auto-pinging every 15 minutes ║');
|
|
56
|
+
strapi.log.info('╚════════════════════════════════════════════════════════════════╝');
|
|
57
|
+
}
|
|
58
|
+
}, 3000); // Wait 3 seconds for API to be ready
|
|
59
|
+
|
|
60
|
+
// Get session service
|
|
61
|
+
const sessionService = strapi
|
|
62
|
+
.plugin('magic-sessionmanager')
|
|
63
|
+
.service('session');
|
|
64
|
+
|
|
65
|
+
// Cleanup inactive sessions on startup
|
|
66
|
+
strapi.log.info('[magic-sessionmanager] Running initial session cleanup...');
|
|
67
|
+
await sessionService.cleanupInactiveSessions();
|
|
68
|
+
|
|
69
|
+
// Schedule periodic cleanup every 30 minutes
|
|
70
|
+
const cleanupInterval = 30 * 60 * 1000; // 30 minutes
|
|
71
|
+
|
|
72
|
+
const cleanupIntervalHandle = setInterval(async () => {
|
|
73
|
+
try {
|
|
74
|
+
// Get fresh reference to service to avoid scope issues
|
|
75
|
+
const service = strapi.plugin('magic-sessionmanager').service('session');
|
|
76
|
+
await service.cleanupInactiveSessions();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
strapi.log.error('[magic-sessionmanager] Periodic cleanup error:', err);
|
|
79
|
+
}
|
|
80
|
+
}, cleanupInterval);
|
|
81
|
+
|
|
82
|
+
strapi.log.info('[magic-sessionmanager] ⏰ Periodic cleanup scheduled (every 30 minutes)');
|
|
83
|
+
|
|
84
|
+
// Store interval handle for cleanup on shutdown
|
|
85
|
+
if (!strapi.sessionManagerIntervals) {
|
|
86
|
+
strapi.sessionManagerIntervals = {};
|
|
87
|
+
}
|
|
88
|
+
strapi.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
|
|
89
|
+
|
|
90
|
+
// HIGH PRIORITY: Register /api/auth/logout route BEFORE other plugins
|
|
91
|
+
strapi.server.routes([{
|
|
92
|
+
method: 'POST',
|
|
93
|
+
path: '/api/auth/logout',
|
|
94
|
+
handler: async (ctx) => {
|
|
95
|
+
try {
|
|
96
|
+
const token = ctx.request.headers?.authorization?.replace('Bearer ', '');
|
|
97
|
+
|
|
98
|
+
if (!token) {
|
|
99
|
+
ctx.status = 200;
|
|
100
|
+
ctx.body = { message: 'Logged out successfully' };
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Find and terminate session by token
|
|
105
|
+
const sessions = await strapi.entityService.findMany('plugin::magic-sessionmanager.session', {
|
|
106
|
+
filters: {
|
|
107
|
+
token: token,
|
|
108
|
+
isActive: true,
|
|
109
|
+
},
|
|
110
|
+
limit: 1,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (sessions.length > 0) {
|
|
114
|
+
await sessionService.terminateSession({ sessionId: sessions[0].id });
|
|
115
|
+
strapi.log.info(`[magic-sessionmanager] 🚪 Logout via /api/auth/logout - Session ${sessions[0].id} terminated`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ctx.status = 200;
|
|
119
|
+
ctx.body = { message: 'Logged out successfully' };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
strapi.log.error('[magic-sessionmanager] Logout error:', err);
|
|
122
|
+
ctx.status = 200;
|
|
123
|
+
ctx.body = { message: 'Logged out successfully' };
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
config: {
|
|
127
|
+
auth: false,
|
|
128
|
+
},
|
|
129
|
+
}]);
|
|
130
|
+
|
|
131
|
+
strapi.log.info('[magic-sessionmanager] ✅ /api/auth/logout route registered');
|
|
132
|
+
|
|
133
|
+
// Middleware to intercept logins
|
|
134
|
+
strapi.server.use(async (ctx, next) => {
|
|
135
|
+
// Execute the actual request
|
|
136
|
+
await next();
|
|
137
|
+
|
|
138
|
+
// Check if this was a successful login request
|
|
139
|
+
const isAuthLocal = ctx.path === '/api/auth/local' && ctx.method === 'POST';
|
|
140
|
+
const isMagicLink = ctx.path.includes('/magic-link/login') && ctx.method === 'POST';
|
|
141
|
+
|
|
142
|
+
if ((isAuthLocal || isMagicLink) && ctx.status === 200 && ctx.body && ctx.body.user) {
|
|
143
|
+
try {
|
|
144
|
+
const user = ctx.body.user;
|
|
145
|
+
|
|
146
|
+
// Extract REAL client IP (handles proxies, load balancers, Cloudflare, etc.)
|
|
147
|
+
const ip = getClientIp(ctx);
|
|
148
|
+
const userAgent = ctx.request.headers?.['user-agent'] || ctx.request.header?.['user-agent'] || 'unknown';
|
|
149
|
+
|
|
150
|
+
strapi.log.info(`[magic-sessionmanager] 🔍 Login detected! User: ${user.id} (${user.email || user.username}) from IP: ${ip}`);
|
|
151
|
+
|
|
152
|
+
// Get config
|
|
153
|
+
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
154
|
+
|
|
155
|
+
// Check if we should analyze this session (Premium/Advanced features)
|
|
156
|
+
let shouldBlock = false;
|
|
157
|
+
let blockReason = '';
|
|
158
|
+
let geoData = null;
|
|
159
|
+
|
|
160
|
+
// Premium: Get geolocation data
|
|
161
|
+
if (config.enableGeolocation || config.enableSecurityScoring) {
|
|
162
|
+
try {
|
|
163
|
+
const geolocationService = strapi.plugin('magic-sessionmanager').service('geolocation');
|
|
164
|
+
geoData = await geolocationService.getIpInfo(ip);
|
|
165
|
+
|
|
166
|
+
// Advanced: Auto-blocking
|
|
167
|
+
if (config.blockSuspiciousSessions && geoData) {
|
|
168
|
+
if (geoData.isThreat) {
|
|
169
|
+
shouldBlock = true;
|
|
170
|
+
blockReason = 'Known threat IP detected';
|
|
171
|
+
} else if (geoData.isVpn && config.alertOnVpnProxy) {
|
|
172
|
+
shouldBlock = true;
|
|
173
|
+
blockReason = 'VPN detected';
|
|
174
|
+
} else if (geoData.isProxy && config.alertOnVpnProxy) {
|
|
175
|
+
shouldBlock = true;
|
|
176
|
+
blockReason = 'Proxy detected';
|
|
177
|
+
} else if (geoData.securityScore < 50) {
|
|
178
|
+
shouldBlock = true;
|
|
179
|
+
blockReason = `Low security score: ${geoData.securityScore}/100`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Advanced: Geo-fencing
|
|
184
|
+
if (config.enableGeofencing && geoData && geoData.country_code) {
|
|
185
|
+
const countryCode = geoData.country_code;
|
|
186
|
+
|
|
187
|
+
// Check blocked countries
|
|
188
|
+
if (config.blockedCountries && config.blockedCountries.includes(countryCode)) {
|
|
189
|
+
shouldBlock = true;
|
|
190
|
+
blockReason = `Country ${countryCode} is blocked`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check allowed countries (whitelist)
|
|
194
|
+
if (config.allowedCountries && config.allowedCountries.length > 0) {
|
|
195
|
+
if (!config.allowedCountries.includes(countryCode)) {
|
|
196
|
+
shouldBlock = true;
|
|
197
|
+
blockReason = `Country ${countryCode} is not in allowlist`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (geoErr) {
|
|
202
|
+
strapi.log.warn('[magic-sessionmanager] Geolocation check failed:', geoErr.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Block if needed
|
|
207
|
+
if (shouldBlock) {
|
|
208
|
+
strapi.log.warn(`[magic-sessionmanager] 🚫 Blocking login: ${blockReason}`);
|
|
209
|
+
|
|
210
|
+
// Don't create session, return error
|
|
211
|
+
ctx.status = 403;
|
|
212
|
+
ctx.body = {
|
|
213
|
+
error: {
|
|
214
|
+
status: 403,
|
|
215
|
+
message: 'Login blocked for security reasons',
|
|
216
|
+
details: { reason: blockReason }
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return; // Stop here
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create a new session
|
|
223
|
+
const newSession = await sessionService.createSession({
|
|
224
|
+
userId: user.id,
|
|
225
|
+
ip,
|
|
226
|
+
userAgent,
|
|
227
|
+
token: ctx.body.jwt, // Store JWT token reference
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
strapi.log.info(`[magic-sessionmanager] ✅ Session created for user ${user.id} (IP: ${ip})`);
|
|
231
|
+
|
|
232
|
+
// Advanced: Send notifications
|
|
233
|
+
if (geoData && (config.enableEmailAlerts || config.enableWebhooks)) {
|
|
234
|
+
try {
|
|
235
|
+
const notificationService = strapi.plugin('magic-sessionmanager').service('notifications');
|
|
236
|
+
|
|
237
|
+
// Determine if suspicious
|
|
238
|
+
const isSuspicious = geoData.isVpn || geoData.isProxy || geoData.isThreat || geoData.securityScore < 70;
|
|
239
|
+
|
|
240
|
+
// Email alerts
|
|
241
|
+
if (config.enableEmailAlerts && config.alertOnSuspiciousLogin && isSuspicious) {
|
|
242
|
+
await notificationService.sendSuspiciousLoginAlert({
|
|
243
|
+
user,
|
|
244
|
+
session: newSession,
|
|
245
|
+
reason: {
|
|
246
|
+
isVpn: geoData.isVpn,
|
|
247
|
+
isProxy: geoData.isProxy,
|
|
248
|
+
isThreat: geoData.isThreat,
|
|
249
|
+
securityScore: geoData.securityScore,
|
|
250
|
+
},
|
|
251
|
+
geoData,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Webhook notifications (Discord/Slack)
|
|
256
|
+
if (config.enableWebhooks) {
|
|
257
|
+
const webhookData = notificationService.formatDiscordWebhook({
|
|
258
|
+
event: isSuspicious ? 'login.suspicious' : 'login.success',
|
|
259
|
+
session: newSession,
|
|
260
|
+
user,
|
|
261
|
+
geoData,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (config.discordWebhookUrl) {
|
|
265
|
+
await notificationService.sendWebhook({
|
|
266
|
+
event: 'session.login',
|
|
267
|
+
data: webhookData,
|
|
268
|
+
webhookUrl: config.discordWebhookUrl,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (notifErr) {
|
|
273
|
+
strapi.log.warn('[magic-sessionmanager] Notification failed:', notifErr.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
strapi.log.error('[magic-sessionmanager] ❌ Error creating session:', err);
|
|
278
|
+
// Don't throw - login should still succeed even if session creation fails
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
strapi.log.info('[magic-sessionmanager] ✅ Login/Logout interceptor middleware mounted');
|
|
284
|
+
|
|
285
|
+
// Mount lastSeen update middleware
|
|
286
|
+
strapi.server.use(
|
|
287
|
+
require('./middlewares/last-seen')({ strapi, sessionService })
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
strapi.log.info('[magic-sessionmanager] ✅ LastSeen middleware mounted');
|
|
291
|
+
strapi.log.info('[magic-sessionmanager] ✅ Bootstrap complete');
|
|
292
|
+
strapi.log.info('[magic-sessionmanager] 🎉 Session Manager ready! Sessions stored in plugin::magic-sessionmanager.session');
|
|
293
|
+
|
|
294
|
+
} catch (err) {
|
|
295
|
+
strapi.log.error('[magic-sessionmanager] ❌ Bootstrap error:', err);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
default: {
|
|
5
|
+
// Rate limit for lastSeen updates (in milliseconds)
|
|
6
|
+
lastSeenRateLimit: 30000, // 30 seconds
|
|
7
|
+
|
|
8
|
+
// Session inactivity timeout (in milliseconds)
|
|
9
|
+
// After this time without activity, a session is considered inactive
|
|
10
|
+
inactivityTimeout: 15 * 60 * 1000, // 15 minutes
|
|
11
|
+
},
|
|
12
|
+
validator: (config) => {
|
|
13
|
+
if (config.lastSeenRateLimit && typeof config.lastSeenRateLimit !== 'number') {
|
|
14
|
+
throw new Error('lastSeenRateLimit must be a number (milliseconds)');
|
|
15
|
+
}
|
|
16
|
+
if (config.inactivityTimeout && typeof config.inactivityTimeout !== 'number') {
|
|
17
|
+
throw new Error('inactivityTimeout must be a number (milliseconds)');
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "collectionType",
|
|
3
|
+
"collectionName": "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": true
|
|
17
|
+
},
|
|
18
|
+
"content-type-builder": {
|
|
19
|
+
"visible": false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"attributes": {
|
|
23
|
+
"user": {
|
|
24
|
+
"type": "relation",
|
|
25
|
+
"relation": "manyToOne",
|
|
26
|
+
"target": "plugin::users-permissions.user",
|
|
27
|
+
"inversedBy": "sessions"
|
|
28
|
+
},
|
|
29
|
+
"ipAddress": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"maxLength": 45,
|
|
32
|
+
"required": true
|
|
33
|
+
},
|
|
34
|
+
"userAgent": {
|
|
35
|
+
"type": "text",
|
|
36
|
+
"maxLength": 500
|
|
37
|
+
},
|
|
38
|
+
"token": {
|
|
39
|
+
"type": "text",
|
|
40
|
+
"private": true
|
|
41
|
+
},
|
|
42
|
+
"loginTime": {
|
|
43
|
+
"type": "datetime",
|
|
44
|
+
"required": true
|
|
45
|
+
},
|
|
46
|
+
"logoutTime": {
|
|
47
|
+
"type": "datetime"
|
|
48
|
+
},
|
|
49
|
+
"lastActive": {
|
|
50
|
+
"type": "datetime"
|
|
51
|
+
},
|
|
52
|
+
"isActive": {
|
|
53
|
+
"type": "boolean",
|
|
54
|
+
"default": true,
|
|
55
|
+
"required": true
|
|
56
|
+
},
|
|
57
|
+
"geoLocation": {
|
|
58
|
+
"type": "json"
|
|
59
|
+
},
|
|
60
|
+
"securityScore": {
|
|
61
|
+
"type": "integer",
|
|
62
|
+
"min": 0,
|
|
63
|
+
"max": 100
|
|
64
|
+
},
|
|
65
|
+
"deviceType": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"browserName": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
},
|
|
71
|
+
"osName": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|