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.
Files changed (52) hide show
  1. package/admin/jsconfig.json +10 -0
  2. package/admin/src/components/Initializer.jsx +11 -0
  3. package/admin/src/components/LicenseGuard.jsx +591 -0
  4. package/admin/src/components/OnlineUsersWidget.jsx +208 -0
  5. package/admin/src/components/PluginIcon.jsx +8 -0
  6. package/admin/src/components/SessionDetailModal.jsx +445 -0
  7. package/admin/src/components/SessionInfoCard.jsx +151 -0
  8. package/admin/src/components/SessionInfoPanel.jsx +375 -0
  9. package/admin/src/components/index.jsx +5 -0
  10. package/admin/src/hooks/useLicense.js +103 -0
  11. package/admin/src/index.js +137 -0
  12. package/admin/src/pages/ActiveSessions.jsx +12 -0
  13. package/admin/src/pages/Analytics.jsx +735 -0
  14. package/admin/src/pages/App.jsx +12 -0
  15. package/admin/src/pages/HomePage.jsx +1248 -0
  16. package/admin/src/pages/License.jsx +603 -0
  17. package/admin/src/pages/Settings.jsx +1497 -0
  18. package/admin/src/pages/SettingsNew.jsx +1204 -0
  19. package/admin/src/pages/index.jsx +3 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +20 -0
  22. package/admin/src/translations/en.json +20 -0
  23. package/admin/src/utils/getTranslation.js +5 -0
  24. package/admin/src/utils/index.js +2 -0
  25. package/admin/src/utils/parseUserAgent.js +79 -0
  26. package/package.json +3 -1
  27. package/server/jsconfig.json +10 -0
  28. package/server/src/bootstrap.js +297 -0
  29. package/server/src/config/index.js +20 -0
  30. package/server/src/content-types/index.js +9 -0
  31. package/server/src/content-types/session/schema.json +76 -0
  32. package/server/src/controllers/controller.js +11 -0
  33. package/server/src/controllers/index.js +11 -0
  34. package/server/src/controllers/license.js +266 -0
  35. package/server/src/controllers/session.js +362 -0
  36. package/server/src/controllers/settings.js +122 -0
  37. package/server/src/destroy.js +18 -0
  38. package/server/src/index.js +21 -0
  39. package/server/src/middlewares/index.js +5 -0
  40. package/server/src/middlewares/last-seen.js +56 -0
  41. package/server/src/policies/index.js +3 -0
  42. package/server/src/register.js +32 -0
  43. package/server/src/routes/admin.js +149 -0
  44. package/server/src/routes/content-api.js +51 -0
  45. package/server/src/routes/index.js +9 -0
  46. package/server/src/services/geolocation.js +180 -0
  47. package/server/src/services/index.js +13 -0
  48. package/server/src/services/license-guard.js +308 -0
  49. package/server/src/services/notifications.js +319 -0
  50. package/server/src/services/service.js +7 -0
  51. package/server/src/services/session.js +345 -0
  52. package/server/src/utils/getClientIp.js +118 -0
@@ -0,0 +1,1497 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Typography,
5
+ Flex,
6
+ Button,
7
+ Loader,
8
+ SingleSelect,
9
+ SingleSelectOption,
10
+ Checkbox,
11
+ Alert,
12
+ TextInput,
13
+ Textarea,
14
+ Tabs,
15
+ Divider,
16
+ Badge,
17
+ Accordion,
18
+ Grid,
19
+ Toggle,
20
+ NumberInput,
21
+ } from '@strapi/design-system';
22
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
23
+ import { Check, Information, Duplicate, Trash, Mail, Code, Cog, Shield, Clock } from '@strapi/icons';
24
+ import styled, { keyframes, css } from 'styled-components';
25
+ import pluginId from '../pluginId';
26
+ import { useLicense } from '../hooks/useLicense';
27
+
28
+ // ================ THEME ================
29
+ const theme = {
30
+ colors: {
31
+ primary: { 600: '#0284C7', 700: '#075985', 100: '#E0F2FE', 50: '#F0F9FF' },
32
+ success: { 600: '#16A34A', 700: '#15803D', 100: '#DCFCE7', 50: '#F0FDF4' },
33
+ danger: { 600: '#DC2626', 700: '#B91C1C', 100: '#FEE2E2', 50: '#FEF2F2' },
34
+ warning: { 600: '#D97706', 700: '#A16207', 100: '#FEF3C7', 50: '#FFFBEB' },
35
+ neutral: { 0: '#FFFFFF', 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 400: '#9CA3AF', 600: '#4B5563', 700: '#374151', 800: '#1F2937' }
36
+ },
37
+ shadows: { sm: '0 1px 3px rgba(0,0,0,0.1)', md: '0 4px 6px rgba(0,0,0,0.1)', xl: '0 20px 25px rgba(0,0,0,0.1)' },
38
+ borderRadius: { md: '8px', lg: '12px', xl: '16px' }
39
+ };
40
+
41
+ // ================ ANIMATIONS ================
42
+ const fadeIn = keyframes`
43
+ from { opacity: 0; transform: translateY(10px); }
44
+ to { opacity: 1; transform: translateY(0); }
45
+ `;
46
+
47
+ const shimmer = keyframes`
48
+ 0% { background-position: -200% 0; }
49
+ 100% { background-position: 200% 0; }
50
+ `;
51
+
52
+ // ================ STYLED COMPONENTS ================
53
+ const Container = styled(Box)`
54
+ ${css`animation: ${fadeIn} 0.5s;`}
55
+ max-width: 1400px;
56
+ margin: 0 auto;
57
+ `;
58
+
59
+ const StickySaveBar = styled(Box)`
60
+ position: sticky;
61
+ top: 0;
62
+ z-index: 10;
63
+ background: white;
64
+ border-bottom: 1px solid ${theme.colors.neutral[200]};
65
+ box-shadow: ${theme.shadows.sm};
66
+ `;
67
+
68
+ const ToggleCard = styled(Box)`
69
+ background: ${props => props.$active
70
+ ? 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)'
71
+ : 'linear-gradient(135deg, #fafafa 0%, #f3f4f6 100%)'};
72
+ border-radius: ${theme.borderRadius.lg};
73
+ padding: 24px;
74
+ min-height: 120px;
75
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
76
+ border: 2px solid ${props => props.$active ? theme.colors.success[600] : theme.colors.neutral[200]};
77
+ box-shadow: ${props => props.$active
78
+ ? '0 4px 20px rgba(34, 197, 94, 0.15)'
79
+ : '0 2px 8px rgba(0, 0, 0, 0.06)'};
80
+ position: relative;
81
+ cursor: pointer;
82
+ display: flex;
83
+ align-items: center;
84
+
85
+ &:hover {
86
+ transform: translateY(-4px);
87
+ box-shadow: ${props => props.$active
88
+ ? '0 8px 30px rgba(34, 197, 94, 0.25)'
89
+ : '0 6px 16px rgba(0, 0, 0, 0.12)'};
90
+ border-color: ${props => props.$active ? theme.colors.success[700] : theme.colors.neutral[300]};
91
+ }
92
+
93
+ &:active {
94
+ transform: translateY(-2px);
95
+ }
96
+
97
+ ${props => props.$active && `
98
+ &::before {
99
+ content: 'ACTIVE';
100
+ position: absolute;
101
+ top: 12px;
102
+ right: 12px;
103
+ background: ${theme.colors.success[600]};
104
+ color: white;
105
+ padding: 4px 10px;
106
+ border-radius: 6px;
107
+ font-size: 10px;
108
+ font-weight: 700;
109
+ letter-spacing: 0.5px;
110
+ box-shadow: 0 2px 6px rgba(22, 163, 74, 0.3);
111
+ }
112
+ `}
113
+
114
+ ${props => !props.$active && `
115
+ &::before {
116
+ content: 'INACTIVE';
117
+ position: absolute;
118
+ top: 12px;
119
+ right: 12px;
120
+ background: ${theme.colors.neutral[400]};
121
+ color: white;
122
+ padding: 4px 10px;
123
+ border-radius: 6px;
124
+ font-size: 10px;
125
+ font-weight: 700;
126
+ letter-spacing: 0.5px;
127
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
128
+ }
129
+ `}
130
+ `;
131
+
132
+ const GreenToggle = styled.div`
133
+ ${props => props.$isActive && `
134
+ button[role="switch"] {
135
+ background-color: #16A34A !important;
136
+ border-color: #16A34A !important;
137
+
138
+ &:hover {
139
+ background-color: #15803D !important;
140
+ border-color: #15803D !important;
141
+ }
142
+
143
+ &:focus {
144
+ background-color: #16A34A !important;
145
+ border-color: #16A34A !important;
146
+ box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.2) !important;
147
+ }
148
+ }
149
+
150
+ /* Toggle handle */
151
+ button[role="switch"] > span {
152
+ background-color: white !important;
153
+ }
154
+ `}
155
+
156
+ ${props => !props.$isActive && `
157
+ button[role="switch"] {
158
+ background-color: #E5E7EB;
159
+
160
+ &:hover {
161
+ background-color: #D1D5DB;
162
+ }
163
+ }
164
+ `}
165
+ `;
166
+
167
+ // Template variable definitions
168
+ const TEMPLATE_VARIABLES = {
169
+ suspiciousLogin: [
170
+ { var: '{{user.email}}', desc: 'User email address' },
171
+ { var: '{{user.username}}', desc: 'Username' },
172
+ { var: '{{session.loginTime}}', desc: 'Login timestamp' },
173
+ { var: '{{session.ipAddress}}', desc: 'IP address' },
174
+ { var: '{{geo.city}}', desc: 'City (if available)' },
175
+ { var: '{{geo.country}}', desc: 'Country (if available)' },
176
+ { var: '{{geo.timezone}}', desc: 'Timezone (if available)' },
177
+ { var: '{{session.userAgent}}', desc: 'Browser/Device info' },
178
+ { var: '{{reason.isVpn}}', desc: 'VPN detected (true/false)' },
179
+ { var: '{{reason.isProxy}}', desc: 'Proxy detected (true/false)' },
180
+ { var: '{{reason.isThreat}}', desc: 'Threat detected (true/false)' },
181
+ { var: '{{reason.securityScore}}', desc: 'Security score (0-100)' },
182
+ ],
183
+ newLocation: [
184
+ { var: '{{user.email}}', desc: 'User email address' },
185
+ { var: '{{user.username}}', desc: 'Username' },
186
+ { var: '{{session.loginTime}}', desc: 'Login timestamp' },
187
+ { var: '{{session.ipAddress}}', desc: 'IP address' },
188
+ { var: '{{geo.city}}', desc: 'City' },
189
+ { var: '{{geo.country}}', desc: 'Country' },
190
+ { var: '{{geo.timezone}}', desc: 'Timezone' },
191
+ { var: '{{session.userAgent}}', desc: 'Browser/Device info' },
192
+ ],
193
+ vpnProxy: [
194
+ { var: '{{user.email}}', desc: 'User email address' },
195
+ { var: '{{user.username}}', desc: 'Username' },
196
+ { var: '{{session.loginTime}}', desc: 'Login timestamp' },
197
+ { var: '{{session.ipAddress}}', desc: 'IP address' },
198
+ { var: '{{geo.city}}', desc: 'City (if available)' },
199
+ { var: '{{geo.country}}', desc: 'Country (if available)' },
200
+ { var: '{{session.userAgent}}', desc: 'Browser/Device info' },
201
+ { var: '{{reason.isVpn}}', desc: 'VPN detected (true/false)' },
202
+ { var: '{{reason.isProxy}}', desc: 'Proxy detected (true/false)' },
203
+ ],
204
+ };
205
+
206
+ // Validate template variables
207
+ const validateTemplate = (template, templateType) => {
208
+ const requiredVars = TEMPLATE_VARIABLES[templateType];
209
+ const foundVars = [];
210
+
211
+ requiredVars.forEach(({ var: variable }) => {
212
+ if (template.includes(variable)) {
213
+ foundVars.push(variable);
214
+ }
215
+ });
216
+
217
+ return {
218
+ isValid: foundVars.length > 0,
219
+ foundVars,
220
+ totalAvailable: requiredVars.length,
221
+ };
222
+ };
223
+
224
+ // Get default email templates
225
+ const getDefaultTemplates = () => ({
226
+ suspiciousLogin: {
227
+ subject: '🚨 Suspicious Login Alert - Session Manager',
228
+ html: `
229
+ <html>
230
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
231
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb; border-radius: 10px;">
232
+ <h2 style="color: #dc2626;">🚨 Suspicious Login Detected</h2>
233
+ <p>A potentially suspicious login was detected for your account.</p>
234
+
235
+ <div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
236
+ <h3 style="margin-top: 0;">Account Information:</h3>
237
+ <ul>
238
+ <li><strong>Email:</strong> {{user.email}}</li>
239
+ <li><strong>Username:</strong> {{user.username}}</li>
240
+ </ul>
241
+
242
+ <h3>Login Details:</h3>
243
+ <ul>
244
+ <li><strong>Time:</strong> {{session.loginTime}}</li>
245
+ <li><strong>IP Address:</strong> {{session.ipAddress}}</li>
246
+ <li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
247
+ <li><strong>Timezone:</strong> {{geo.timezone}}</li>
248
+ <li><strong>Device:</strong> {{session.userAgent}}</li>
249
+ </ul>
250
+
251
+ <h3 style="color: #dc2626;">Security Alert:</h3>
252
+ <ul>
253
+ <li>VPN Detected: {{reason.isVpn}}</li>
254
+ <li>Proxy Detected: {{reason.isProxy}}</li>
255
+ <li>Threat Detected: {{reason.isThreat}}</li>
256
+ <li>Security Score: {{reason.securityScore}}/100</li>
257
+ </ul>
258
+ </div>
259
+
260
+ <p>If this was you, you can safely ignore this email. If you don't recognize this activity, please secure your account immediately.</p>
261
+
262
+ <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
263
+ <p style="color: #666; font-size: 12px;">This is an automated security notification from Magic Session Manager.</p>
264
+ </div>
265
+ </body>
266
+ </html>`,
267
+ text: `🚨 Suspicious Login Detected
268
+
269
+ A potentially suspicious login was detected for your account.
270
+
271
+ Account: {{user.email}}
272
+ Username: {{user.username}}
273
+
274
+ Login Details:
275
+ - Time: {{session.loginTime}}
276
+ - IP: {{session.ipAddress}}
277
+ - Location: {{geo.city}}, {{geo.country}}
278
+
279
+ Security: VPN={{reason.isVpn}}, Proxy={{reason.isProxy}}, Threat={{reason.isThreat}}, Score={{reason.securityScore}}/100`,
280
+ },
281
+ newLocation: {
282
+ subject: '📍 New Location Login Detected',
283
+ html: `
284
+ <html>
285
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
286
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f0f9ff; border-radius: 10px;">
287
+ <h2 style="color: #0284c7;">📍 Login from New Location</h2>
288
+ <p>Your account was accessed from a new location.</p>
289
+
290
+ <div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
291
+ <h3 style="margin-top: 0;">Account:</h3>
292
+ <p><strong>{{user.email}}</strong></p>
293
+
294
+ <h3>New Location Details:</h3>
295
+ <ul>
296
+ <li><strong>Time:</strong> {{session.loginTime}}</li>
297
+ <li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
298
+ <li><strong>IP Address:</strong> {{session.ipAddress}}</li>
299
+ <li><strong>Device:</strong> {{session.userAgent}}</li>
300
+ </ul>
301
+ </div>
302
+
303
+ <p>If this was you, no action is needed. If you don't recognize this login, please secure your account.</p>
304
+
305
+ <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
306
+ <p style="color: #666; font-size: 12px;">Magic Session Manager notification</p>
307
+ </div>
308
+ </body>
309
+ </html>`,
310
+ text: `📍 Login from New Location
311
+
312
+ Your account was accessed from a new location.
313
+
314
+ Account: {{user.email}}
315
+
316
+ New Location Details:
317
+ - Time: {{session.loginTime}}
318
+ - Location: {{geo.city}}, {{geo.country}}
319
+ - IP Address: {{session.ipAddress}}
320
+ - Device: {{session.userAgent}}
321
+
322
+ If this was you, no action is needed.`,
323
+ },
324
+ vpnProxy: {
325
+ subject: '⚠️ VPN/Proxy Login Detected',
326
+ html: `
327
+ <html>
328
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
329
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #fffbeb; border-radius: 10px;">
330
+ <h2 style="color: #d97706;">⚠️ VPN/Proxy Detected</h2>
331
+ <p>A login from a VPN or proxy service was detected on your account.</p>
332
+
333
+ <div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
334
+ <h3 style="margin-top: 0;">Account:</h3>
335
+ <p><strong>{{user.email}}</strong></p>
336
+
337
+ <h3>Login Details:</h3>
338
+ <ul>
339
+ <li><strong>Time:</strong> {{session.loginTime}}</li>
340
+ <li><strong>IP Address:</strong> {{session.ipAddress}}</li>
341
+ <li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
342
+ <li><strong>Device:</strong> {{session.userAgent}}</li>
343
+ <li><strong>VPN:</strong> {{reason.isVpn}}</li>
344
+ <li><strong>Proxy:</strong> {{reason.isProxy}}</li>
345
+ </ul>
346
+ </div>
347
+
348
+ <p>VPN/Proxy usage may indicate suspicious activity. If this was you, you can safely ignore this email.</p>
349
+
350
+ <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
351
+ <p style="color: #666; font-size: 12px;">Magic Session Manager notification</p>
352
+ </div>
353
+ </body>
354
+ </html>`,
355
+ text: `⚠️ VPN/Proxy Detected
356
+
357
+ A login from a VPN or proxy service was detected on your account.
358
+
359
+ Account: {{user.email}}
360
+
361
+ Login Details:
362
+ - Time: {{session.loginTime}}
363
+ - IP Address: {{session.ipAddress}}
364
+ - Location: {{geo.city}}, {{geo.country}}
365
+ - VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`,
366
+ },
367
+ });
368
+
369
+ const SettingsPage = () => {
370
+ const { get, post, put } = useFetchClient();
371
+ const { toggleNotification } = useNotification();
372
+ const { isPremium, isAdvanced, isEnterprise } = useLicense();
373
+ const [loading, setLoading] = useState(true);
374
+ const [saving, setSaving] = useState(false);
375
+ const [hasChanges, setHasChanges] = useState(false);
376
+ const [cleaning, setCleaning] = useState(false);
377
+ const [activeTemplateTab, setActiveTemplateTab] = useState('suspiciousLogin');
378
+
379
+ const [settings, setSettings] = useState({
380
+ inactivityTimeout: 15,
381
+ cleanupInterval: 30,
382
+ lastSeenRateLimit: 30,
383
+ retentionDays: 90,
384
+ enableGeolocation: true,
385
+ enableSecurityScoring: true,
386
+ blockSuspiciousSessions: false,
387
+ maxFailedLogins: 5,
388
+ enableEmailAlerts: false,
389
+ alertOnSuspiciousLogin: true,
390
+ alertOnNewLocation: true,
391
+ alertOnVpnProxy: true,
392
+ enableWebhooks: false,
393
+ discordWebhookUrl: '',
394
+ slackWebhookUrl: '',
395
+ enableGeofencing: false,
396
+ allowedCountries: [],
397
+ blockedCountries: [],
398
+ emailTemplates: {
399
+ suspiciousLogin: { subject: '', html: '', text: '' },
400
+ newLocation: { subject: '', html: '', text: '' },
401
+ vpnProxy: { subject: '', html: '', text: '' },
402
+ },
403
+ });
404
+
405
+ useEffect(() => {
406
+ fetchSettings();
407
+ }, []);
408
+
409
+ const fetchSettings = async () => {
410
+ setLoading(true);
411
+ try {
412
+ // Load settings from backend API
413
+ const response = await get(`/${pluginId}/settings`);
414
+
415
+ if (response?.data?.settings) {
416
+ const loadedSettings = response.data.settings;
417
+
418
+ // Ensure email templates exist with defaults
419
+ if (!loadedSettings.emailTemplates || Object.keys(loadedSettings.emailTemplates).length === 0) {
420
+ loadedSettings.emailTemplates = getDefaultTemplates();
421
+ } else {
422
+ // Ensure each template has all required fields
423
+ const defaultTemplates = getDefaultTemplates();
424
+ Object.keys(defaultTemplates).forEach(key => {
425
+ if (!loadedSettings.emailTemplates[key]) {
426
+ loadedSettings.emailTemplates[key] = defaultTemplates[key];
427
+ } else {
428
+ // Fill missing fields with defaults
429
+ loadedSettings.emailTemplates[key] = {
430
+ subject: loadedSettings.emailTemplates[key].subject || defaultTemplates[key].subject,
431
+ html: loadedSettings.emailTemplates[key].html || defaultTemplates[key].html,
432
+ text: loadedSettings.emailTemplates[key].text || defaultTemplates[key].text,
433
+ };
434
+ }
435
+ });
436
+ }
437
+
438
+ setSettings(loadedSettings);
439
+ } else {
440
+ // Use defaults if no settings in DB
441
+ setSettings(prev => ({ ...prev, emailTemplates: getDefaultTemplates() }));
442
+ }
443
+ } catch (err) {
444
+ console.error('[Settings] Error loading from backend:', err);
445
+ toggleNotification({
446
+ type: 'warning',
447
+ message: 'Could not load settings from server. Using defaults.',
448
+ });
449
+ // Fallback to default settings
450
+ setSettings(prev => ({ ...prev, emailTemplates: getDefaultTemplates() }));
451
+ } finally {
452
+ setLoading(false);
453
+ }
454
+ };
455
+
456
+ const handleChange = (key, value) => {
457
+ setSettings(prev => ({ ...prev, [key]: value }));
458
+ setHasChanges(true);
459
+ };
460
+
461
+ const updateSetting = (key, value) => {
462
+ handleChange(key, value);
463
+ };
464
+
465
+ const handleSave = async () => {
466
+ setSaving(true);
467
+ try {
468
+ // Save to backend API using PUT
469
+ const response = await put(`/${pluginId}/settings`, settings);
470
+
471
+ if (response?.data?.success) {
472
+ toggleNotification({
473
+ type: 'success',
474
+ message: 'Settings saved successfully to database!',
475
+ });
476
+
477
+ setHasChanges(false);
478
+
479
+ // Optional: Also save to localStorage as backup
480
+ try {
481
+ localStorage.setItem(`${pluginId}-settings`, JSON.stringify(settings));
482
+ } catch (localErr) {
483
+ console.warn('[Settings] Could not save to localStorage:', localErr);
484
+ }
485
+ } else {
486
+ throw new Error('Save failed');
487
+ }
488
+ } catch (err) {
489
+ console.error('[Settings] Error saving:', err);
490
+ toggleNotification({
491
+ type: 'danger',
492
+ message: 'Failed to save settings to server',
493
+ });
494
+ } finally {
495
+ setSaving(false);
496
+ }
497
+ };
498
+
499
+ const handleReset = () => {
500
+ fetchSettings();
501
+ setHasChanges(false);
502
+ };
503
+
504
+ const handleCleanInactive = async () => {
505
+ if (!confirm('⚠️ WARNING: This will permanently delete ALL inactive sessions.\n\nContinue?')) {
506
+ return;
507
+ }
508
+
509
+ setCleaning(true);
510
+ try {
511
+ const { data } = await post(`/${pluginId}/sessions/clean-inactive`);
512
+
513
+ toggleNotification({
514
+ type: 'success',
515
+ message: `Successfully deleted ${data.deletedCount} inactive sessions!`,
516
+ });
517
+ } catch (err) {
518
+ toggleNotification({
519
+ type: 'danger',
520
+ message: 'Failed to delete inactive sessions',
521
+ });
522
+ } finally {
523
+ setCleaning(false);
524
+ }
525
+ };
526
+
527
+ if (loading) {
528
+ return (
529
+ <Flex justifyContent="center" padding={8}>
530
+ <Loader>Loading settings...</Loader>
531
+ </Flex>
532
+ );
533
+ }
534
+
535
+ return (
536
+ <Container>
537
+ {/* Sticky Header */}
538
+ <StickySaveBar paddingTop={5} paddingBottom={5} paddingLeft={6} paddingRight={6}>
539
+ <Flex justifyContent="space-between" alignItems="center">
540
+ <Flex direction="column" gap={1} alignItems="flex-start">
541
+ <Typography variant="alpha" fontWeight="bold" style={{ fontSize: '24px' }}>
542
+ ⚙️ Session Manager Settings
543
+ </Typography>
544
+ <Typography variant="epsilon" textColor="neutral600">
545
+ Configure session tracking, security, and email notifications
546
+ </Typography>
547
+ </Flex>
548
+ <Flex gap={2}>
549
+ {hasChanges && (
550
+ <Button onClick={handleReset} variant="tertiary" size="L">
551
+ Reset
552
+ </Button>
553
+ )}
554
+ <Button
555
+ onClick={handleSave}
556
+ loading={saving}
557
+ startIcon={<Check />}
558
+ size="L"
559
+ disabled={!hasChanges || saving}
560
+ style={{
561
+ background: hasChanges && !saving
562
+ ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
563
+ : '#e5e7eb',
564
+ color: hasChanges && !saving ? 'white' : '#9ca3af',
565
+ fontWeight: '600',
566
+ padding: '12px 24px',
567
+ border: 'none',
568
+ boxShadow: hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none',
569
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
570
+ }}
571
+ onMouseEnter={(e) => {
572
+ if (hasChanges && !saving) {
573
+ e.currentTarget.style.transform = 'translateY(-2px)';
574
+ e.currentTarget.style.boxShadow = '0 8px 20px rgba(102, 126, 234, 0.5)';
575
+ }
576
+ }}
577
+ onMouseLeave={(e) => {
578
+ e.currentTarget.style.transform = 'translateY(0)';
579
+ e.currentTarget.style.boxShadow = hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none';
580
+ }}
581
+ >
582
+ {saving ? 'Saving...' : hasChanges ? 'Save Changes' : 'No Changes'}
583
+ </Button>
584
+ </Flex>
585
+ </Flex>
586
+ </StickySaveBar>
587
+
588
+ {/* Content */}
589
+ <Box paddingTop={6} paddingLeft={6} paddingRight={6} paddingBottom={10}>
590
+
591
+ {/* License Status Debug */}
592
+ <Box padding={4} background="primary50" hasRadius style={{ marginBottom: '24px', border: '1px solid #bae6fd' }}>
593
+ <Flex gap={3} alignItems="center">
594
+ <Information style={{ width: '20px', height: '20px', color: '#0284C7' }} />
595
+ <Box>
596
+ <Typography variant="omega" fontWeight="bold" textColor="primary700" style={{ marginBottom: '4px' }}>
597
+ Current License Status
598
+ </Typography>
599
+ <Flex gap={3}>
600
+ <Badge backgroundColor={isPremium ? "success100" : "neutral100"} textColor={isPremium ? "success700" : "neutral600"}>
601
+ {isPremium ? '✓' : '✗'} Premium
602
+ </Badge>
603
+ <Badge backgroundColor={isAdvanced ? "primary100" : "neutral100"} textColor={isAdvanced ? "primary700" : "neutral600"}>
604
+ {isAdvanced ? '✓' : '✗'} Advanced
605
+ </Badge>
606
+ <Badge backgroundColor={isEnterprise ? "secondary100" : "neutral100"} textColor={isEnterprise ? "secondary700" : "neutral600"}>
607
+ {isEnterprise ? '✓' : '✗'} Enterprise
608
+ </Badge>
609
+ </Flex>
610
+ </Box>
611
+ </Flex>
612
+ </Box>
613
+
614
+ {/* Accordion Layout */}
615
+ <Accordion.Root type="multiple" defaultValue={['general', 'security', 'email']}>
616
+
617
+ {/* General Settings */}
618
+ <Accordion.Item value="general">
619
+ <Accordion.Header>
620
+ <Accordion.Trigger
621
+ icon={Cog}
622
+ description="Basic session tracking configuration"
623
+ >
624
+ General Settings
625
+ </Accordion.Trigger>
626
+ </Accordion.Header>
627
+ <Accordion.Content>
628
+ <Box padding={6}>
629
+
630
+ {/* Session Timeout */}
631
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
632
+ ⏱️ SESSION TIMEOUT
633
+ </Typography>
634
+ <Grid.Root gap={6} style={{ marginBottom: '32px' }}>
635
+ <Grid.Item col={6} s={12}>
636
+ <Box>
637
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
638
+ Inactivity Timeout
639
+ </Typography>
640
+ <SingleSelect
641
+ value={String(settings.inactivityTimeout)}
642
+ onChange={(value) => handleChange('inactivityTimeout', parseInt(value))}
643
+ >
644
+ <SingleSelectOption value="5">5 minutes (Very Strict)</SingleSelectOption>
645
+ <SingleSelectOption value="10">10 minutes (Strict)</SingleSelectOption>
646
+ <SingleSelectOption value="15">15 minutes (Recommended)</SingleSelectOption>
647
+ <SingleSelectOption value="30">30 minutes (Moderate)</SingleSelectOption>
648
+ <SingleSelectOption value="60">1 hour (Relaxed)</SingleSelectOption>
649
+ <SingleSelectOption value="120">2 hours (Very Relaxed)</SingleSelectOption>
650
+ </SingleSelect>
651
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
652
+ Sessions inactive for more than {settings.inactivityTimeout} minutes will be marked as offline
653
+ </Typography>
654
+ </Box>
655
+ </Grid.Item>
656
+
657
+ <Grid.Item col={6} s={12}>
658
+ <Box>
659
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
660
+ Last Seen Rate Limit
661
+ </Typography>
662
+ <SingleSelect
663
+ value={String(settings.lastSeenRateLimit)}
664
+ onChange={(value) => handleChange('lastSeenRateLimit', parseInt(value))}
665
+ >
666
+ <SingleSelectOption value="10">10 seconds</SingleSelectOption>
667
+ <SingleSelectOption value="30">30 seconds (Recommended)</SingleSelectOption>
668
+ <SingleSelectOption value="60">1 minute</SingleSelectOption>
669
+ <SingleSelectOption value="120">2 minutes</SingleSelectOption>
670
+ <SingleSelectOption value="300">5 minutes</SingleSelectOption>
671
+ </SingleSelect>
672
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
673
+ Prevents excessive database writes. Updates throttled to once every {settings.lastSeenRateLimit} seconds
674
+ </Typography>
675
+ </Box>
676
+ </Grid.Item>
677
+ </Grid.Root>
678
+
679
+ {/* Cleanup & Retention */}
680
+ <Divider style={{ marginBottom: '24px' }} />
681
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
682
+ 🧹 AUTO-CLEANUP & RETENTION
683
+ </Typography>
684
+ <Grid.Root gap={6}>
685
+ <Grid.Item col={6} s={12}>
686
+ <Box>
687
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
688
+ Cleanup Interval
689
+ </Typography>
690
+ <SingleSelect
691
+ value={String(settings.cleanupInterval)}
692
+ onChange={(value) => handleChange('cleanupInterval', parseInt(value))}
693
+ >
694
+ <SingleSelectOption value="15">15 minutes</SingleSelectOption>
695
+ <SingleSelectOption value="30">30 minutes (Recommended)</SingleSelectOption>
696
+ <SingleSelectOption value="60">1 hour</SingleSelectOption>
697
+ <SingleSelectOption value="120">2 hours</SingleSelectOption>
698
+ </SingleSelect>
699
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
700
+ Inactive sessions are automatically cleaned every {settings.cleanupInterval} minutes
701
+ </Typography>
702
+ </Box>
703
+ </Grid.Item>
704
+
705
+ <Grid.Item col={6} s={12}>
706
+ <Box>
707
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
708
+ Retention Period
709
+ </Typography>
710
+ <SingleSelect
711
+ value={String(settings.retentionDays)}
712
+ onChange={(value) => handleChange('retentionDays', parseInt(value))}
713
+ >
714
+ <SingleSelectOption value="7">7 days</SingleSelectOption>
715
+ <SingleSelectOption value="30">30 days</SingleSelectOption>
716
+ <SingleSelectOption value="60">60 days</SingleSelectOption>
717
+ <SingleSelectOption value="90">90 days (Recommended)</SingleSelectOption>
718
+ <SingleSelectOption value="180">180 days</SingleSelectOption>
719
+ <SingleSelectOption value="365">1 year</SingleSelectOption>
720
+ <SingleSelectOption value="-1">Forever</SingleSelectOption>
721
+ </SingleSelect>
722
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
723
+ Old sessions deleted after {settings.retentionDays === -1 ? 'never' : `${settings.retentionDays} days`}
724
+ </Typography>
725
+ </Box>
726
+ </Grid.Item>
727
+
728
+ <Grid.Item col={12}>
729
+ <Box padding={4} background="danger100" style={{ borderRadius: theme.borderRadius.md, border: `2px solid ${theme.colors.danger[200]}` }}>
730
+ <Flex gap={3} alignItems="flex-start">
731
+ <Trash style={{ width: '18px', height: '18px', color: theme.colors.danger[600], flexShrink: 0, marginTop: '2px' }} />
732
+ <Box style={{ flex: 1 }}>
733
+ <Typography variant="omega" fontWeight="bold" textColor="danger700" style={{ marginBottom: '8px', display: 'block' }}>
734
+ Danger Zone
735
+ </Typography>
736
+ <Typography variant="pi" textColor="danger600" style={{ fontSize: '13px', lineHeight: '1.7' }}>
737
+ <strong>Clean All Inactive:</strong> Permanently deletes all inactive sessions. This cannot be undone.
738
+ </Typography>
739
+ </Box>
740
+ <Button
741
+ onClick={handleCleanInactive}
742
+ loading={cleaning}
743
+ startIcon={<Trash />}
744
+ variant="danger"
745
+ size="S"
746
+ style={{ flexShrink: 0 }}
747
+ >
748
+ Clean Now
749
+ </Button>
750
+ </Flex>
751
+ </Box>
752
+ </Grid.Item>
753
+ </Grid.Root>
754
+
755
+ </Box>
756
+ </Accordion.Content>
757
+ </Accordion.Item>
758
+
759
+ {/* Security Settings */}
760
+ <Accordion.Item value="security">
761
+ <Accordion.Header>
762
+ <Accordion.Trigger
763
+ icon={Shield}
764
+ description="Security policies and threat protection"
765
+ >
766
+ Security Settings
767
+ </Accordion.Trigger>
768
+ </Accordion.Header>
769
+ <Accordion.Content>
770
+ <Box padding={6}>
771
+
772
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
773
+ 🔒 SECURITY OPTIONS
774
+ </Typography>
775
+
776
+ {/* Feature Toggles */}
777
+ <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
778
+ <Grid.Root gap={4}>
779
+ <Grid.Item col={6} s={12}>
780
+ <ToggleCard
781
+ $active={settings.blockSuspiciousSessions}
782
+ onClick={() => handleChange('blockSuspiciousSessions', !settings.blockSuspiciousSessions)}
783
+ >
784
+ <Flex direction="column" gap={3} style={{ width: '100%' }} alignItems="center">
785
+ <GreenToggle $isActive={settings.blockSuspiciousSessions}>
786
+ <Toggle
787
+ checked={settings.blockSuspiciousSessions}
788
+ onChange={() => handleChange('blockSuspiciousSessions', !settings.blockSuspiciousSessions)}
789
+ />
790
+ </GreenToggle>
791
+ <Flex direction="column" gap={2} alignItems="center" style={{ textAlign: 'center' }}>
792
+ <Typography
793
+ variant="delta"
794
+ fontWeight="bold"
795
+ textColor={settings.blockSuspiciousSessions ? 'success700' : 'neutral800'}
796
+ style={{ fontSize: '16px' }}
797
+ >
798
+ Block Suspicious Sessions
799
+ </Typography>
800
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
801
+ Automatically block sessions from VPNs, proxies, or threat IPs
802
+ </Typography>
803
+ </Flex>
804
+ </Flex>
805
+ </ToggleCard>
806
+ </Grid.Item>
807
+
808
+ {isPremium && (
809
+ <>
810
+ <Grid.Item col={6} s={12}>
811
+ <ToggleCard
812
+ $active={settings.enableGeolocation}
813
+ onClick={() => handleChange('enableGeolocation', !settings.enableGeolocation)}
814
+ >
815
+ <Flex direction="column" gap={3} style={{ width: '100%' }} alignItems="center">
816
+ <GreenToggle $isActive={settings.enableGeolocation}>
817
+ <Toggle
818
+ checked={settings.enableGeolocation}
819
+ onChange={() => handleChange('enableGeolocation', !settings.enableGeolocation)}
820
+ />
821
+ </GreenToggle>
822
+ <Flex direction="column" gap={2} alignItems="center" style={{ textAlign: 'center' }}>
823
+ <Typography
824
+ variant="delta"
825
+ fontWeight="bold"
826
+ textColor={settings.enableGeolocation ? 'success700' : 'neutral800'}
827
+ style={{ fontSize: '16px' }}
828
+ >
829
+ IP Geolocation
830
+ </Typography>
831
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
832
+ Fetch location data for each session (Premium)
833
+ </Typography>
834
+ </Flex>
835
+ </Flex>
836
+ </ToggleCard>
837
+ </Grid.Item>
838
+
839
+ <Grid.Item col={6} s={12}>
840
+ <ToggleCard
841
+ $active={settings.enableSecurityScoring}
842
+ onClick={() => handleChange('enableSecurityScoring', !settings.enableSecurityScoring)}
843
+ >
844
+ <Flex direction="column" gap={3} style={{ width: '100%' }} alignItems="center">
845
+ <GreenToggle $isActive={settings.enableSecurityScoring}>
846
+ <Toggle
847
+ checked={settings.enableSecurityScoring}
848
+ onChange={() => handleChange('enableSecurityScoring', !settings.enableSecurityScoring)}
849
+ />
850
+ </GreenToggle>
851
+ <Flex direction="column" gap={2} alignItems="center" style={{ textAlign: 'center' }}>
852
+ <Typography
853
+ variant="delta"
854
+ fontWeight="bold"
855
+ textColor={settings.enableSecurityScoring ? 'success700' : 'neutral800'}
856
+ style={{ fontSize: '16px' }}
857
+ >
858
+ Security Scoring
859
+ </Typography>
860
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
861
+ Calculate security scores and detect threats (Premium)
862
+ </Typography>
863
+ </Flex>
864
+ </Flex>
865
+ </ToggleCard>
866
+ </Grid.Item>
867
+ </>
868
+ )}
869
+ </Grid.Root>
870
+ </Box>
871
+
872
+ {/* Max Failed Logins */}
873
+ <Grid.Root gap={6}>
874
+ <Grid.Item col={6} s={12}>
875
+ <Box>
876
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
877
+ 🚫 Max Failed Login Attempts
878
+ </Typography>
879
+ <NumberInput
880
+ value={settings.maxFailedLogins}
881
+ onValueChange={(val) => handleChange('maxFailedLogins', val)}
882
+ min={1}
883
+ max={20}
884
+ />
885
+ <Box padding={2} background="warning50" style={{ borderRadius: '4px', marginTop: '8px' }}>
886
+ <Typography variant="pi" textColor="warning700" style={{ fontSize: '11px' }}>
887
+ User will be blocked after {settings.maxFailedLogins} failed attempts
888
+ </Typography>
889
+ </Box>
890
+ </Box>
891
+ </Grid.Item>
892
+ </Grid.Root>
893
+
894
+ </Box>
895
+ </Accordion.Content>
896
+ </Accordion.Item>
897
+
898
+ {/* Email Notifications - Advanced Only */}
899
+ {isAdvanced && (
900
+ <Accordion.Item value="email">
901
+ <Accordion.Header>
902
+ <Accordion.Trigger
903
+ icon={Mail}
904
+ description="Email alerts for security events"
905
+ >
906
+ Email Notifications (Advanced)
907
+ </Accordion.Trigger>
908
+ </Accordion.Header>
909
+ <Accordion.Content>
910
+ <Box padding={6}>
911
+
912
+ {/* Email Alerts Toggle */}
913
+ <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
914
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', textAlign: 'center', color: theme.colors.neutral[700] }}>
915
+ 📧 EMAIL ALERTS
916
+ </Typography>
917
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', textAlign: 'center', fontSize: '12px' }}>
918
+ Send security alerts to users via email
919
+ </Typography>
920
+ <Grid.Root gap={4}>
921
+ <Grid.Item col={12}>
922
+ <ToggleCard
923
+ $active={settings.enableEmailAlerts}
924
+ onClick={() => handleChange('enableEmailAlerts', !settings.enableEmailAlerts)}
925
+ >
926
+ <Flex direction="column" gap={3} style={{ width: '100%' }} alignItems="center">
927
+ <GreenToggle $isActive={settings.enableEmailAlerts}>
928
+ <Toggle
929
+ checked={settings.enableEmailAlerts}
930
+ onChange={() => handleChange('enableEmailAlerts', !settings.enableEmailAlerts)}
931
+ />
932
+ </GreenToggle>
933
+ <Flex direction="column" gap={2} alignItems="center" style={{ textAlign: 'center' }}>
934
+ <Typography
935
+ variant="delta"
936
+ fontWeight="bold"
937
+ textColor={settings.enableEmailAlerts ? 'success700' : 'neutral800'}
938
+ style={{ fontSize: '16px' }}
939
+ >
940
+ Enable Email Alerts
941
+ </Typography>
942
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
943
+ Send security alerts for suspicious logins, new locations, and VPN/Proxy usage
944
+ </Typography>
945
+ </Flex>
946
+ </Flex>
947
+ </ToggleCard>
948
+ </Grid.Item>
949
+ </Grid.Root>
950
+ </Box>
951
+
952
+ {/* Alert Type Checkboxes */}
953
+ {settings.enableEmailAlerts && (
954
+ <>
955
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
956
+ ⚙️ ALERT TYPES
957
+ </Typography>
958
+ <Grid.Root gap={4} style={{ marginBottom: '32px' }}>
959
+ <Grid.Item col={4} s={12}>
960
+ <Box
961
+ padding={4}
962
+ background={settings.alertOnSuspiciousLogin ? 'danger50' : 'neutral50'}
963
+ style={{
964
+ borderRadius: theme.borderRadius.md,
965
+ border: `2px solid ${settings.alertOnSuspiciousLogin ? '#fecaca' : '#E5E7EB'}`,
966
+ transition: 'all 0.2s',
967
+ cursor: 'pointer'
968
+ }}
969
+ onClick={() => handleChange('alertOnSuspiciousLogin', !settings.alertOnSuspiciousLogin)}
970
+ >
971
+ <Checkbox
972
+ checked={settings.alertOnSuspiciousLogin}
973
+ onChange={() => handleChange('alertOnSuspiciousLogin', !settings.alertOnSuspiciousLogin)}
974
+ >
975
+ <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
976
+ 🚨 Suspicious Login
977
+ </Typography>
978
+ </Checkbox>
979
+ </Box>
980
+ </Grid.Item>
981
+ <Grid.Item col={4} s={12}>
982
+ <Box
983
+ padding={4}
984
+ background={settings.alertOnNewLocation ? 'primary50' : 'neutral50'}
985
+ style={{
986
+ borderRadius: theme.borderRadius.md,
987
+ border: `2px solid ${settings.alertOnNewLocation ? '#bae6fd' : '#E5E7EB'}`,
988
+ transition: 'all 0.2s',
989
+ cursor: 'pointer'
990
+ }}
991
+ onClick={() => handleChange('alertOnNewLocation', !settings.alertOnNewLocation)}
992
+ >
993
+ <Checkbox
994
+ checked={settings.alertOnNewLocation}
995
+ onChange={() => handleChange('alertOnNewLocation', !settings.alertOnNewLocation)}
996
+ >
997
+ <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
998
+ 📍 New Location
999
+ </Typography>
1000
+ </Checkbox>
1001
+ </Box>
1002
+ </Grid.Item>
1003
+ <Grid.Item col={4} s={12}>
1004
+ <Box
1005
+ padding={4}
1006
+ background={settings.alertOnVpnProxy ? 'warning50' : 'neutral50'}
1007
+ style={{
1008
+ borderRadius: theme.borderRadius.md,
1009
+ border: `2px solid ${settings.alertOnVpnProxy ? '#fde68a' : '#E5E7EB'}`,
1010
+ transition: 'all 0.2s',
1011
+ cursor: 'pointer'
1012
+ }}
1013
+ onClick={() => handleChange('alertOnVpnProxy', !settings.alertOnVpnProxy)}
1014
+ >
1015
+ <Checkbox
1016
+ checked={settings.alertOnVpnProxy}
1017
+ onChange={() => handleChange('alertOnVpnProxy', !settings.alertOnVpnProxy)}
1018
+ >
1019
+ <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
1020
+ ⚠️ VPN/Proxy
1021
+ </Typography>
1022
+ </Checkbox>
1023
+ </Box>
1024
+ </Grid.Item>
1025
+ </Grid.Root>
1026
+
1027
+ {/* Email Templates */}
1028
+ <Divider style={{ marginBottom: '24px' }} />
1029
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.neutral[700] }}>
1030
+ 📝 EMAIL TEMPLATES
1031
+ </Typography>
1032
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', fontSize: '12px' }}>
1033
+ Customize email notification templates with dynamic variables
1034
+ </Typography>
1035
+
1036
+ {/* Template Tabs */}
1037
+ <Tabs.Root value={activeTemplateTab} onValueChange={setActiveTemplateTab}>
1038
+ <Tabs.List aria-label="Email Templates">
1039
+ <Tabs.Trigger value="suspiciousLogin">🚨 Suspicious Login</Tabs.Trigger>
1040
+ <Tabs.Trigger value="newLocation">📍 New Location</Tabs.Trigger>
1041
+ <Tabs.Trigger value="vpnProxy">⚠️ VPN/Proxy</Tabs.Trigger>
1042
+ </Tabs.List>
1043
+
1044
+ {Object.keys(settings.emailTemplates).map((templateKey) => (
1045
+ <Tabs.Content key={templateKey} value={templateKey}>
1046
+ <Box paddingTop={4}>
1047
+ {/* Subject */}
1048
+ <Box style={{ marginBottom: '24px' }}>
1049
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1050
+ ✉️ Email Subject
1051
+ </Typography>
1052
+ <TextInput
1053
+ value={settings.emailTemplates[templateKey].subject}
1054
+ onChange={(e) => {
1055
+ const newTemplates = { ...settings.emailTemplates };
1056
+ newTemplates[templateKey].subject = e.target.value;
1057
+ handleChange('emailTemplates', newTemplates);
1058
+ }}
1059
+ placeholder="Enter email subject..."
1060
+ />
1061
+ </Box>
1062
+
1063
+ {/* Available Variables */}
1064
+ <Box
1065
+ padding={3}
1066
+ background="primary100"
1067
+ style={{ borderRadius: theme.borderRadius.md, marginBottom: '20px', border: '2px solid #BAE6FD' }}
1068
+ >
1069
+ <Flex direction="column" gap={2}>
1070
+ <Flex alignItems="center" gap={2}>
1071
+ <Code style={{ width: '16px', height: '16px', color: theme.colors.primary[600] }} />
1072
+ <Typography variant="omega" fontWeight="bold" textColor="primary600">
1073
+ Available Variables (click to copy)
1074
+ </Typography>
1075
+ </Flex>
1076
+ <Flex gap={2} wrap="wrap">
1077
+ {TEMPLATE_VARIABLES[templateKey].map(({ var: variable, desc }) => (
1078
+ <Button
1079
+ key={variable}
1080
+ size="S"
1081
+ variant="tertiary"
1082
+ onClick={() => {
1083
+ navigator.clipboard.writeText(variable);
1084
+ toggleNotification({ type: 'success', message: `${variable} copied!` });
1085
+ }}
1086
+ style={{
1087
+ fontFamily: 'monospace',
1088
+ fontSize: '11px',
1089
+ padding: '4px 8px',
1090
+ }}
1091
+ title={desc}
1092
+ >
1093
+ {variable}
1094
+ </Button>
1095
+ ))}
1096
+ </Flex>
1097
+ </Flex>
1098
+ </Box>
1099
+
1100
+ {/* HTML Template - VS Code Style Editor */}
1101
+ <Box
1102
+ background="neutral0"
1103
+ padding={6}
1104
+ style={{ borderRadius: theme.borderRadius.lg, border: '2px solid #E5E7EB', width: '100%', marginBottom: '24px' }}
1105
+ >
1106
+ <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
1107
+ <Flex alignItems="center" gap={2}>
1108
+ <Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
1109
+ 🎨 HTML Template
1110
+ </Typography>
1111
+ <Badge variant="success">Main Template</Badge>
1112
+ </Flex>
1113
+ <Button
1114
+ variant="tertiary"
1115
+ size="S"
1116
+ onClick={() => {
1117
+ const defaultTemplates = getDefaultTemplates();
1118
+ const newTemplates = { ...settings.emailTemplates };
1119
+ newTemplates[templateKey].html = defaultTemplates[templateKey].html;
1120
+ handleChange('emailTemplates', newTemplates);
1121
+ toggleNotification({ type: 'success', message: 'Default HTML template loaded!' });
1122
+ }}
1123
+ >
1124
+ 📋 Load Default
1125
+ </Button>
1126
+ </Flex>
1127
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
1128
+ HTML template for email notifications. Use variables like <code>{'{{user.email}}'}</code> for dynamic content.
1129
+ </Typography>
1130
+ <Box
1131
+ style={{
1132
+ border: '2px solid #E5E7EB',
1133
+ borderRadius: '6px',
1134
+ overflow: 'hidden',
1135
+ background: '#1e1e1e',
1136
+ height: '500px',
1137
+ display: 'flex',
1138
+ flexDirection: 'column'
1139
+ }}
1140
+ >
1141
+ <Box
1142
+ padding={2}
1143
+ background="neutral700"
1144
+ style={{ borderBottom: '1px solid #333', flexShrink: 0 }}
1145
+ >
1146
+ <Typography variant="omega" style={{ color: '#888', fontSize: '11px', fontFamily: 'monospace' }}>
1147
+ template.html
1148
+ </Typography>
1149
+ </Box>
1150
+ <textarea
1151
+ value={settings.emailTemplates[templateKey].html}
1152
+ onChange={(e) => {
1153
+ const newTemplates = { ...settings.emailTemplates };
1154
+ newTemplates[templateKey].html = e.target.value;
1155
+ handleChange('emailTemplates', newTemplates);
1156
+ }}
1157
+ style={{
1158
+ fontFamily: 'Monaco, Consolas, "Courier New", monospace',
1159
+ height: '100%',
1160
+ fontSize: '14px',
1161
+ lineHeight: '1.8',
1162
+ background: '#1e1e1e',
1163
+ color: '#d4d4d4',
1164
+ border: 'none',
1165
+ padding: '20px',
1166
+ resize: 'none',
1167
+ width: '100%',
1168
+ boxSizing: 'border-box',
1169
+ outline: 'none',
1170
+ margin: 0,
1171
+ display: 'block',
1172
+ overflow: 'auto'
1173
+ }}
1174
+ placeholder="Enter HTML template with variables like {{user.email}}..."
1175
+ />
1176
+ </Box>
1177
+ <Flex gap={2} style={{ marginTop: '12px' }} wrap="wrap">
1178
+ <Button
1179
+ variant="secondary"
1180
+ size="S"
1181
+ onClick={() => {
1182
+ navigator.clipboard.writeText(settings.emailTemplates[templateKey].html);
1183
+ toggleNotification({ type: 'success', message: 'HTML template copied!' });
1184
+ }}
1185
+ >
1186
+ 📋 Copy Template
1187
+ </Button>
1188
+ <Button
1189
+ variant="tertiary"
1190
+ size="S"
1191
+ onClick={() => {
1192
+ const validation = validateTemplate(settings.emailTemplates[templateKey].html, templateKey);
1193
+ toggleNotification({
1194
+ type: validation.isValid ? 'success' : 'warning',
1195
+ message: validation.isValid
1196
+ ? `✓ Template valid! Found ${validation.foundVars.length}/${validation.totalAvailable} variables.`
1197
+ : '⚠️ No variables found. Add at least one variable.',
1198
+ });
1199
+ }}
1200
+ >
1201
+ ✓ Validate
1202
+ </Button>
1203
+ <Button
1204
+ variant="tertiary"
1205
+ size="S"
1206
+ onClick={() => {
1207
+ const lines = settings.emailTemplates[templateKey].html.split('\n').length;
1208
+ const chars = settings.emailTemplates[templateKey].html.length;
1209
+ toggleNotification({
1210
+ type: 'info',
1211
+ message: `Template has ${lines} lines and ${chars} characters`
1212
+ });
1213
+ }}
1214
+ >
1215
+ ℹ️ Template Info
1216
+ </Button>
1217
+ </Flex>
1218
+ </Box>
1219
+
1220
+ {/* Text Template - VS Code Style Editor */}
1221
+ <Box
1222
+ background="neutral0"
1223
+ padding={6}
1224
+ style={{ borderRadius: theme.borderRadius.lg, border: '2px solid #E5E7EB', width: '100%', marginBottom: '24px' }}
1225
+ >
1226
+ <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
1227
+ <Flex alignItems="center" gap={2}>
1228
+ <Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
1229
+ 📄 Text Template
1230
+ </Typography>
1231
+ <Badge variant="secondary">Fallback</Badge>
1232
+ </Flex>
1233
+ <Button
1234
+ variant="tertiary"
1235
+ size="S"
1236
+ onClick={() => {
1237
+ const defaultTemplates = getDefaultTemplates();
1238
+ const newTemplates = { ...settings.emailTemplates };
1239
+ newTemplates[templateKey].text = defaultTemplates[templateKey].text;
1240
+ handleChange('emailTemplates', newTemplates);
1241
+ toggleNotification({ type: 'success', message: 'Default text template loaded!' });
1242
+ }}
1243
+ >
1244
+ 📋 Load Default
1245
+ </Button>
1246
+ </Flex>
1247
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
1248
+ Plain text version (no HTML) as fallback for older email clients
1249
+ </Typography>
1250
+ <Box
1251
+ style={{
1252
+ border: '2px solid #E5E7EB',
1253
+ borderRadius: '6px',
1254
+ overflow: 'hidden',
1255
+ background: '#1e1e1e',
1256
+ height: '300px',
1257
+ display: 'flex',
1258
+ flexDirection: 'column'
1259
+ }}
1260
+ >
1261
+ <Box
1262
+ padding={2}
1263
+ background="neutral700"
1264
+ style={{ borderBottom: '1px solid #333', flexShrink: 0 }}
1265
+ >
1266
+ <Typography variant="omega" style={{ color: '#888', fontSize: '11px', fontFamily: 'monospace' }}>
1267
+ template.txt
1268
+ </Typography>
1269
+ </Box>
1270
+ <textarea
1271
+ value={settings.emailTemplates[templateKey].text}
1272
+ onChange={(e) => {
1273
+ const newTemplates = { ...settings.emailTemplates };
1274
+ newTemplates[templateKey].text = e.target.value;
1275
+ handleChange('emailTemplates', newTemplates);
1276
+ }}
1277
+ style={{
1278
+ fontFamily: 'Monaco, Consolas, "Courier New", monospace',
1279
+ height: '100%',
1280
+ fontSize: '14px',
1281
+ lineHeight: '1.8',
1282
+ background: '#1e1e1e',
1283
+ color: '#d4d4d4',
1284
+ border: 'none',
1285
+ padding: '20px',
1286
+ resize: 'none',
1287
+ width: '100%',
1288
+ boxSizing: 'border-box',
1289
+ outline: 'none',
1290
+ margin: 0,
1291
+ display: 'block',
1292
+ overflow: 'auto'
1293
+ }}
1294
+ placeholder="Plain text version (no HTML)..."
1295
+ />
1296
+ </Box>
1297
+ <Flex gap={2} style={{ marginTop: '12px' }} wrap="wrap">
1298
+ <Button
1299
+ variant="secondary"
1300
+ size="S"
1301
+ onClick={() => {
1302
+ navigator.clipboard.writeText(settings.emailTemplates[templateKey].text);
1303
+ toggleNotification({ type: 'success', message: 'Text template copied!' });
1304
+ }}
1305
+ >
1306
+ 📋 Copy Template
1307
+ </Button>
1308
+ </Flex>
1309
+ </Box>
1310
+ </Box>
1311
+ </Tabs.Content>
1312
+ ))}
1313
+ </Tabs.Root>
1314
+ </>
1315
+ )}
1316
+
1317
+ </Box>
1318
+ </Accordion.Content>
1319
+ </Accordion.Item>
1320
+ )}
1321
+
1322
+ {/* Webhooks - Advanced Only */}
1323
+ {isAdvanced && (
1324
+ <Accordion.Item value="webhooks">
1325
+ <Accordion.Header>
1326
+ <Accordion.Trigger
1327
+ icon={Code}
1328
+ description="Discord & Slack integration"
1329
+ >
1330
+ Webhook Integration (Advanced)
1331
+ </Accordion.Trigger>
1332
+ </Accordion.Header>
1333
+ <Accordion.Content>
1334
+ <Box padding={6}>
1335
+
1336
+ {/* Enable Webhooks Toggle */}
1337
+ <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
1338
+ <Grid.Root gap={4}>
1339
+ <Grid.Item col={12}>
1340
+ <ToggleCard
1341
+ $active={settings.enableWebhooks}
1342
+ onClick={() => handleChange('enableWebhooks', !settings.enableWebhooks)}
1343
+ >
1344
+ <Flex direction="column" gap={3} style={{ width: '100%' }} alignItems="center">
1345
+ <GreenToggle $isActive={settings.enableWebhooks}>
1346
+ <Toggle
1347
+ checked={settings.enableWebhooks}
1348
+ onChange={() => handleChange('enableWebhooks', !settings.enableWebhooks)}
1349
+ />
1350
+ </GreenToggle>
1351
+ <Flex direction="column" gap={2} alignItems="center" style={{ textAlign: 'center' }}>
1352
+ <Typography
1353
+ variant="delta"
1354
+ fontWeight="bold"
1355
+ textColor={settings.enableWebhooks ? 'success700' : 'neutral800'}
1356
+ style={{ fontSize: '16px' }}
1357
+ >
1358
+ Enable Webhooks
1359
+ </Typography>
1360
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
1361
+ Send session events to Discord, Slack, or custom endpoints
1362
+ </Typography>
1363
+ </Flex>
1364
+ </Flex>
1365
+ </ToggleCard>
1366
+ </Grid.Item>
1367
+ </Grid.Root>
1368
+ </Box>
1369
+
1370
+ {/* Webhook URLs */}
1371
+ {settings.enableWebhooks && (
1372
+ <Grid.Root gap={6}>
1373
+ <Grid.Item col={12}>
1374
+ <Box>
1375
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
1376
+ 🔗 Discord Webhook URL
1377
+ </Typography>
1378
+ <Box
1379
+ style={{
1380
+ border: '2px solid #E5E7EB',
1381
+ borderRadius: theme.borderRadius.md,
1382
+ overflow: 'hidden',
1383
+ background: '#fafafa'
1384
+ }}
1385
+ >
1386
+ <textarea
1387
+ placeholder="https://discord.com/api/webhooks/123456789/abcdefghijklmnopqrstuvwxyz..."
1388
+ value={settings.discordWebhookUrl}
1389
+ onChange={(e) => handleChange('discordWebhookUrl', e.target.value)}
1390
+ rows={3}
1391
+ style={{
1392
+ width: '100%',
1393
+ padding: '14px 18px',
1394
+ border: 'none',
1395
+ outline: 'none',
1396
+ fontFamily: 'Monaco, Consolas, monospace',
1397
+ fontSize: '14px',
1398
+ lineHeight: '1.8',
1399
+ color: '#374151',
1400
+ background: 'white',
1401
+ resize: 'vertical',
1402
+ minHeight: '80px',
1403
+ }}
1404
+ />
1405
+ </Box>
1406
+ <Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
1407
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
1408
+ 💡 Optional: Post session alerts to your Discord channel
1409
+ </Typography>
1410
+ {settings.discordWebhookUrl && (
1411
+ <Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
1412
+ {settings.discordWebhookUrl.length} characters
1413
+ </Typography>
1414
+ )}
1415
+ </Flex>
1416
+ </Box>
1417
+ </Grid.Item>
1418
+
1419
+ <Grid.Item col={12}>
1420
+ <Box>
1421
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
1422
+ 💬 Slack Webhook URL
1423
+ </Typography>
1424
+ <Box
1425
+ style={{
1426
+ border: '2px solid #E5E7EB',
1427
+ borderRadius: theme.borderRadius.md,
1428
+ overflow: 'hidden',
1429
+ background: '#fafafa'
1430
+ }}
1431
+ >
1432
+ <textarea
1433
+ placeholder="https://hooks.slack.com/services/XXXX/XXXX/XXXX"
1434
+ value={settings.slackWebhookUrl}
1435
+ onChange={(e) => handleChange('slackWebhookUrl', e.target.value)}
1436
+ rows={3}
1437
+ style={{
1438
+ width: '100%',
1439
+ padding: '14px 18px',
1440
+ border: 'none',
1441
+ outline: 'none',
1442
+ fontFamily: 'Monaco, Consolas, monospace',
1443
+ fontSize: '14px',
1444
+ lineHeight: '1.8',
1445
+ color: '#374151',
1446
+ background: 'white',
1447
+ resize: 'vertical',
1448
+ minHeight: '80px',
1449
+ }}
1450
+ />
1451
+ </Box>
1452
+ <Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
1453
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
1454
+ 💡 Optional: Post session alerts to your Slack workspace
1455
+ </Typography>
1456
+ {settings.slackWebhookUrl && (
1457
+ <Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
1458
+ {settings.slackWebhookUrl.length} characters
1459
+ </Typography>
1460
+ )}
1461
+ </Flex>
1462
+ </Box>
1463
+ </Grid.Item>
1464
+ </Grid.Root>
1465
+ )}
1466
+
1467
+ </Box>
1468
+ </Accordion.Content>
1469
+ </Accordion.Item>
1470
+ )}
1471
+
1472
+ </Accordion.Root>
1473
+
1474
+ {/* Footer Info */}
1475
+ <Box padding={5} background="primary100" style={{ borderRadius: theme.borderRadius.md, marginTop: '32px', border: '2px solid #BAE6FD' }}>
1476
+ <Flex gap={3} alignItems="flex-start">
1477
+ <Check style={{ width: '20px', height: '20px', color: theme.colors.success[600], flexShrink: 0, marginTop: '2px' }} />
1478
+ <Box style={{ flex: 1 }}>
1479
+ <Typography variant="omega" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.primary[700] }}>
1480
+ ✨ Database-Backed Settings
1481
+ </Typography>
1482
+ <Typography variant="pi" textColor="primary700" style={{ fontSize: '13px', lineHeight: '1.8' }}>
1483
+ All settings are stored in your Strapi database and shared across all admin users.
1484
+ Changes take effect immediately - no server restart required!
1485
+ Email templates, webhooks, and security options are all managed from this interface.
1486
+ </Typography>
1487
+ </Box>
1488
+ </Flex>
1489
+ </Box>
1490
+
1491
+ </Box>
1492
+ </Container>
1493
+ );
1494
+ };
1495
+
1496
+ export default SettingsPage;
1497
+