strapi-plugin-magic-sessionmanager 4.2.4 → 4.2.6

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