strapi-plugin-magic-sessionmanager 4.2.3 → 4.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/Initializer.jsx +0 -11
- package/admin/src/components/LicenseGuard.jsx +0 -591
- package/admin/src/components/OnlineUsersWidget.jsx +0 -212
- package/admin/src/components/PluginIcon.jsx +0 -8
- package/admin/src/components/SessionDetailModal.jsx +0 -449
- package/admin/src/components/SessionInfoCard.jsx +0 -151
- package/admin/src/components/SessionInfoPanel.jsx +0 -385
- package/admin/src/components/index.jsx +0 -5
- package/admin/src/hooks/useLicense.js +0 -103
- package/admin/src/index.js +0 -149
- package/admin/src/pages/ActiveSessions.jsx +0 -12
- package/admin/src/pages/Analytics.jsx +0 -735
- package/admin/src/pages/App.jsx +0 -12
- package/admin/src/pages/HomePage.jsx +0 -1212
- package/admin/src/pages/License.jsx +0 -603
- package/admin/src/pages/Settings.jsx +0 -1646
- package/admin/src/pages/SettingsNew.jsx +0 -1204
- package/admin/src/pages/UpgradePage.jsx +0 -448
- package/admin/src/pages/index.jsx +0 -3
- package/admin/src/pluginId.js +0 -4
- package/admin/src/translations/de.json +0 -299
- package/admin/src/translations/en.json +0 -299
- package/admin/src/translations/es.json +0 -287
- package/admin/src/translations/fr.json +0 -287
- package/admin/src/translations/pt.json +0 -287
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/index.js +0 -2
- package/admin/src/utils/parseUserAgent.js +0 -79
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -492
- package/server/src/config/index.js +0 -23
- package/server/src/content-types/index.js +0 -9
- package/server/src/content-types/session/schema.json +0 -84
- package/server/src/controllers/controller.js +0 -11
- package/server/src/controllers/index.js +0 -11
- package/server/src/controllers/license.js +0 -266
- package/server/src/controllers/session.js +0 -433
- package/server/src/controllers/settings.js +0 -122
- package/server/src/destroy.js +0 -22
- package/server/src/index.js +0 -23
- package/server/src/middlewares/index.js +0 -5
- package/server/src/middlewares/last-seen.js +0 -62
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -36
- package/server/src/routes/admin.js +0 -149
- package/server/src/routes/content-api.js +0 -60
- package/server/src/routes/index.js +0 -9
- package/server/src/services/geolocation.js +0 -182
- package/server/src/services/index.js +0 -13
- package/server/src/services/license-guard.js +0 -316
- package/server/src/services/notifications.js +0 -319
- package/server/src/services/service.js +0 -7
- package/server/src/services/session.js +0 -393
- package/server/src/utils/encryption.js +0 -121
- package/server/src/utils/getClientIp.js +0 -118
- package/server/src/utils/logger.js +0 -84
|
@@ -1,1204 +0,0 @@
|
|
|
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 ? theme.colors.success[50] : theme.colors.neutral[50]};
|
|
70
|
-
border-radius: ${theme.borderRadius.md};
|
|
71
|
-
padding: 20px;
|
|
72
|
-
transition: all 0.3s;
|
|
73
|
-
border: 2px solid ${props => props.$active ? theme.colors.success[600] : theme.colors.neutral[200]};
|
|
74
|
-
box-shadow: ${props => props.$active ? '0 4px 12px rgba(34, 197, 94, 0.2)' : '0 1px 3px rgba(0, 0, 0, 0.1)'};
|
|
75
|
-
position: relative;
|
|
76
|
-
cursor: pointer;
|
|
77
|
-
|
|
78
|
-
&:hover {
|
|
79
|
-
transform: translateY(-2px);
|
|
80
|
-
box-shadow: ${props => props.$active ? '0 6px 16px rgba(34, 197, 94, 0.3)' : '0 3px 8px rgba(0, 0, 0, 0.15)'};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
${props => props.$active && `
|
|
84
|
-
&::before {
|
|
85
|
-
content: 'ACTIVE';
|
|
86
|
-
position: absolute;
|
|
87
|
-
top: 8px;
|
|
88
|
-
right: 8px;
|
|
89
|
-
background: ${theme.colors.success[600]};
|
|
90
|
-
color: white;
|
|
91
|
-
padding: 2px 8px;
|
|
92
|
-
border-radius: 4px;
|
|
93
|
-
font-size: 10px;
|
|
94
|
-
font-weight: bold;
|
|
95
|
-
}
|
|
96
|
-
`}
|
|
97
|
-
|
|
98
|
-
${props => !props.$active && `
|
|
99
|
-
&::before {
|
|
100
|
-
content: 'INACTIVE';
|
|
101
|
-
position: absolute;
|
|
102
|
-
top: 8px;
|
|
103
|
-
right: 8px;
|
|
104
|
-
background: ${theme.colors.neutral[400]};
|
|
105
|
-
color: white;
|
|
106
|
-
padding: 2px 8px;
|
|
107
|
-
border-radius: 4px;
|
|
108
|
-
font-size: 10px;
|
|
109
|
-
font-weight: bold;
|
|
110
|
-
}
|
|
111
|
-
`}
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
const GreenToggle = styled.div`
|
|
115
|
-
${props => props.$isActive && `
|
|
116
|
-
button[role="switch"] {
|
|
117
|
-
background-color: #16A34A !important;
|
|
118
|
-
border-color: #16A34A !important;
|
|
119
|
-
|
|
120
|
-
&:hover {
|
|
121
|
-
background-color: #15803D !important;
|
|
122
|
-
border-color: #15803D !important;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
&:focus {
|
|
126
|
-
background-color: #16A34A !important;
|
|
127
|
-
border-color: #16A34A !important;
|
|
128
|
-
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.2) !important;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/* Toggle handle */
|
|
133
|
-
button[role="switch"] > span {
|
|
134
|
-
background-color: white !important;
|
|
135
|
-
}
|
|
136
|
-
`}
|
|
137
|
-
|
|
138
|
-
${props => !props.$isActive && `
|
|
139
|
-
button[role="switch"] {
|
|
140
|
-
background-color: #E5E7EB;
|
|
141
|
-
|
|
142
|
-
&:hover {
|
|
143
|
-
background-color: #D1D5DB;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
`}
|
|
147
|
-
`;
|
|
148
|
-
|
|
149
|
-
// Template variable definitions
|
|
150
|
-
const TEMPLATE_VARIABLES = {
|
|
151
|
-
suspiciousLogin: [
|
|
152
|
-
{ var: '{{user.email}}', desc: 'User email address' },
|
|
153
|
-
{ var: '{{user.username}}', desc: 'Username' },
|
|
154
|
-
{ var: '{{session.loginTime}}', desc: 'Login timestamp' },
|
|
155
|
-
{ var: '{{session.ipAddress}}', desc: 'IP address' },
|
|
156
|
-
{ var: '{{geo.city}}', desc: 'City (if available)' },
|
|
157
|
-
{ var: '{{geo.country}}', desc: 'Country (if available)' },
|
|
158
|
-
{ var: '{{geo.timezone}}', desc: 'Timezone (if available)' },
|
|
159
|
-
{ var: '{{session.userAgent}}', desc: 'Browser/Device info' },
|
|
160
|
-
{ var: '{{reason.isVpn}}', desc: 'VPN detected (true/false)' },
|
|
161
|
-
{ var: '{{reason.isProxy}}', desc: 'Proxy detected (true/false)' },
|
|
162
|
-
{ var: '{{reason.isThreat}}', desc: 'Threat detected (true/false)' },
|
|
163
|
-
{ var: '{{reason.securityScore}}', desc: 'Security score (0-100)' },
|
|
164
|
-
],
|
|
165
|
-
newLocation: [
|
|
166
|
-
{ var: '{{user.email}}', desc: 'User email address' },
|
|
167
|
-
{ var: '{{user.username}}', desc: 'Username' },
|
|
168
|
-
{ var: '{{session.loginTime}}', desc: 'Login timestamp' },
|
|
169
|
-
{ var: '{{session.ipAddress}}', desc: 'IP address' },
|
|
170
|
-
{ var: '{{geo.city}}', desc: 'City' },
|
|
171
|
-
{ var: '{{geo.country}}', desc: 'Country' },
|
|
172
|
-
{ var: '{{geo.timezone}}', desc: 'Timezone' },
|
|
173
|
-
{ var: '{{session.userAgent}}', desc: 'Browser/Device info' },
|
|
174
|
-
],
|
|
175
|
-
vpnProxy: [
|
|
176
|
-
{ var: '{{user.email}}', desc: 'User email address' },
|
|
177
|
-
{ var: '{{user.username}}', desc: 'Username' },
|
|
178
|
-
{ var: '{{session.loginTime}}', desc: 'Login timestamp' },
|
|
179
|
-
{ var: '{{session.ipAddress}}', desc: 'IP address' },
|
|
180
|
-
{ var: '{{geo.city}}', desc: 'City (if available)' },
|
|
181
|
-
{ var: '{{geo.country}}', desc: 'Country (if available)' },
|
|
182
|
-
{ var: '{{session.userAgent}}', desc: 'Browser/Device info' },
|
|
183
|
-
{ var: '{{reason.isVpn}}', desc: 'VPN detected (true/false)' },
|
|
184
|
-
{ var: '{{reason.isProxy}}', desc: 'Proxy detected (true/false)' },
|
|
185
|
-
],
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Validate template variables
|
|
189
|
-
const validateTemplate = (template, templateType) => {
|
|
190
|
-
const requiredVars = TEMPLATE_VARIABLES[templateType];
|
|
191
|
-
const foundVars = [];
|
|
192
|
-
|
|
193
|
-
requiredVars.forEach(({ var: variable }) => {
|
|
194
|
-
if (template.includes(variable)) {
|
|
195
|
-
foundVars.push(variable);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
isValid: foundVars.length > 0,
|
|
201
|
-
foundVars,
|
|
202
|
-
totalAvailable: requiredVars.length,
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Get default email templates
|
|
207
|
-
const getDefaultTemplates = () => ({
|
|
208
|
-
suspiciousLogin: {
|
|
209
|
-
subject: '[ALERT] Suspicious Login Alert - Session Manager',
|
|
210
|
-
html: `
|
|
211
|
-
<html>
|
|
212
|
-
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
213
|
-
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb; border-radius: 10px;">
|
|
214
|
-
<h2 style="color: #dc2626;">[ALERT] Suspicious Login Detected</h2>
|
|
215
|
-
<p>A potentially suspicious login was detected for your account.</p>
|
|
216
|
-
|
|
217
|
-
<div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
218
|
-
<h3 style="margin-top: 0;">Account Information:</h3>
|
|
219
|
-
<ul>
|
|
220
|
-
<li><strong>Email:</strong> {{user.email}}</li>
|
|
221
|
-
<li><strong>Username:</strong> {{user.username}}</li>
|
|
222
|
-
</ul>
|
|
223
|
-
|
|
224
|
-
<h3>Login Details:</h3>
|
|
225
|
-
<ul>
|
|
226
|
-
<li><strong>Time:</strong> {{session.loginTime}}</li>
|
|
227
|
-
<li><strong>IP Address:</strong> {{session.ipAddress}}</li>
|
|
228
|
-
<li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
|
|
229
|
-
<li><strong>Timezone:</strong> {{geo.timezone}}</li>
|
|
230
|
-
<li><strong>Device:</strong> {{session.userAgent}}</li>
|
|
231
|
-
</ul>
|
|
232
|
-
|
|
233
|
-
<h3 style="color: #dc2626;">Security Alert:</h3>
|
|
234
|
-
<ul>
|
|
235
|
-
<li>VPN Detected: {{reason.isVpn}}</li>
|
|
236
|
-
<li>Proxy Detected: {{reason.isProxy}}</li>
|
|
237
|
-
<li>Threat Detected: {{reason.isThreat}}</li>
|
|
238
|
-
<li>Security Score: {{reason.securityScore}}/100</li>
|
|
239
|
-
</ul>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<p>If this was you, you can safely ignore this email. If you don't recognize this activity, please secure your account immediately.</p>
|
|
243
|
-
|
|
244
|
-
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
|
|
245
|
-
<p style="color: #666; font-size: 12px;">This is an automated security notification from Magic Session Manager.</p>
|
|
246
|
-
</div>
|
|
247
|
-
</body>
|
|
248
|
-
</html>`,
|
|
249
|
-
text: `[ALERT] Suspicious Login Detected
|
|
250
|
-
|
|
251
|
-
A potentially suspicious login was detected for your account.
|
|
252
|
-
|
|
253
|
-
Account: {{user.email}}
|
|
254
|
-
Username: {{user.username}}
|
|
255
|
-
|
|
256
|
-
Login Details:
|
|
257
|
-
- Time: {{session.loginTime}}
|
|
258
|
-
- IP: {{session.ipAddress}}
|
|
259
|
-
- Location: {{geo.city}}, {{geo.country}}
|
|
260
|
-
|
|
261
|
-
Security: VPN={{reason.isVpn}}, Proxy={{reason.isProxy}}, Threat={{reason.isThreat}}, Score={{reason.securityScore}}/100`,
|
|
262
|
-
},
|
|
263
|
-
newLocation: {
|
|
264
|
-
subject: '[LOCATION] New Location Login Detected',
|
|
265
|
-
html: `
|
|
266
|
-
<html>
|
|
267
|
-
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
268
|
-
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f0f9ff; border-radius: 10px;">
|
|
269
|
-
<h2 style="color: #0284c7;">[LOCATION] Login from New Location</h2>
|
|
270
|
-
<p>Your account was accessed from a new location.</p>
|
|
271
|
-
|
|
272
|
-
<div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
273
|
-
<h3 style="margin-top: 0;">Account:</h3>
|
|
274
|
-
<p><strong>{{user.email}}</strong></p>
|
|
275
|
-
|
|
276
|
-
<h3>New Location Details:</h3>
|
|
277
|
-
<ul>
|
|
278
|
-
<li><strong>Time:</strong> {{session.loginTime}}</li>
|
|
279
|
-
<li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
|
|
280
|
-
<li><strong>IP Address:</strong> {{session.ipAddress}}</li>
|
|
281
|
-
<li><strong>Device:</strong> {{session.userAgent}}</li>
|
|
282
|
-
</ul>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
<p>If this was you, no action is needed. If you don't recognize this login, please secure your account.</p>
|
|
286
|
-
|
|
287
|
-
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
|
|
288
|
-
<p style="color: #666; font-size: 12px;">Magic Session Manager notification</p>
|
|
289
|
-
</div>
|
|
290
|
-
</body>
|
|
291
|
-
</html>`,
|
|
292
|
-
text: `[LOCATION] Login from New Location
|
|
293
|
-
|
|
294
|
-
Your account was accessed from a new location.
|
|
295
|
-
|
|
296
|
-
Account: {{user.email}}
|
|
297
|
-
|
|
298
|
-
New Location Details:
|
|
299
|
-
- Time: {{session.loginTime}}
|
|
300
|
-
- Location: {{geo.city}}, {{geo.country}}
|
|
301
|
-
- IP Address: {{session.ipAddress}}
|
|
302
|
-
- Device: {{session.userAgent}}
|
|
303
|
-
|
|
304
|
-
If this was you, no action is needed.`,
|
|
305
|
-
},
|
|
306
|
-
vpnProxy: {
|
|
307
|
-
subject: '[WARNING] VPN/Proxy Login Detected',
|
|
308
|
-
html: `
|
|
309
|
-
<html>
|
|
310
|
-
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
311
|
-
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #fffbeb; border-radius: 10px;">
|
|
312
|
-
<h2 style="color: #d97706;">[WARNING] VPN/Proxy Detected</h2>
|
|
313
|
-
<p>A login from a VPN or proxy service was detected on your account.</p>
|
|
314
|
-
|
|
315
|
-
<div style="background: white; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
316
|
-
<h3 style="margin-top: 0;">Account:</h3>
|
|
317
|
-
<p><strong>{{user.email}}</strong></p>
|
|
318
|
-
|
|
319
|
-
<h3>Login Details:</h3>
|
|
320
|
-
<ul>
|
|
321
|
-
<li><strong>Time:</strong> {{session.loginTime}}</li>
|
|
322
|
-
<li><strong>IP Address:</strong> {{session.ipAddress}}</li>
|
|
323
|
-
<li><strong>Location:</strong> {{geo.city}}, {{geo.country}}</li>
|
|
324
|
-
<li><strong>Device:</strong> {{session.userAgent}}</li>
|
|
325
|
-
<li><strong>VPN:</strong> {{reason.isVpn}}</li>
|
|
326
|
-
<li><strong>Proxy:</strong> {{reason.isProxy}}</li>
|
|
327
|
-
</ul>
|
|
328
|
-
</div>
|
|
329
|
-
|
|
330
|
-
<p>VPN/Proxy usage may indicate suspicious activity. If this was you, you can safely ignore this email.</p>
|
|
331
|
-
|
|
332
|
-
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;"/>
|
|
333
|
-
<p style="color: #666; font-size: 12px;">Magic Session Manager notification</p>
|
|
334
|
-
</div>
|
|
335
|
-
</body>
|
|
336
|
-
</html>`,
|
|
337
|
-
text: `[WARNING] VPN/Proxy Detected
|
|
338
|
-
|
|
339
|
-
A login from a VPN or proxy service was detected on your account.
|
|
340
|
-
|
|
341
|
-
Account: {{user.email}}
|
|
342
|
-
|
|
343
|
-
Login Details:
|
|
344
|
-
- Time: {{session.loginTime}}
|
|
345
|
-
- IP Address: {{session.ipAddress}}
|
|
346
|
-
- Location: {{geo.city}}, {{geo.country}}
|
|
347
|
-
- VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`,
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const SettingsPage = () => {
|
|
352
|
-
const { get, post } = useFetchClient();
|
|
353
|
-
const { toggleNotification } = useNotification();
|
|
354
|
-
const { isPremium, isAdvanced, isEnterprise } = useLicense();
|
|
355
|
-
const [loading, setLoading] = useState(true);
|
|
356
|
-
const [saving, setSaving] = useState(false);
|
|
357
|
-
const [hasChanges, setHasChanges] = useState(false);
|
|
358
|
-
const [cleaning, setCleaning] = useState(false);
|
|
359
|
-
const [activeTemplateTab, setActiveTemplateTab] = useState('suspiciousLogin');
|
|
360
|
-
|
|
361
|
-
const [settings, setSettings] = useState({
|
|
362
|
-
inactivityTimeout: 15,
|
|
363
|
-
cleanupInterval: 30,
|
|
364
|
-
lastSeenRateLimit: 30,
|
|
365
|
-
retentionDays: 90,
|
|
366
|
-
enableGeolocation: true,
|
|
367
|
-
enableSecurityScoring: true,
|
|
368
|
-
blockSuspiciousSessions: false,
|
|
369
|
-
maxFailedLogins: 5,
|
|
370
|
-
enableEmailAlerts: false,
|
|
371
|
-
alertOnSuspiciousLogin: true,
|
|
372
|
-
alertOnNewLocation: true,
|
|
373
|
-
alertOnVpnProxy: true,
|
|
374
|
-
enableWebhooks: false,
|
|
375
|
-
discordWebhookUrl: '',
|
|
376
|
-
slackWebhookUrl: '',
|
|
377
|
-
enableGeofencing: false,
|
|
378
|
-
allowedCountries: [],
|
|
379
|
-
blockedCountries: [],
|
|
380
|
-
emailTemplates: {
|
|
381
|
-
suspiciousLogin: { subject: '', html: '', text: '' },
|
|
382
|
-
newLocation: { subject: '', html: '', text: '' },
|
|
383
|
-
vpnProxy: { subject: '', html: '', text: '' },
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
useEffect(() => {
|
|
388
|
-
fetchSettings();
|
|
389
|
-
}, []);
|
|
390
|
-
|
|
391
|
-
const fetchSettings = async () => {
|
|
392
|
-
setLoading(true);
|
|
393
|
-
try {
|
|
394
|
-
const saved = localStorage.getItem(`${pluginId}-settings`);
|
|
395
|
-
if (saved) {
|
|
396
|
-
const loadedSettings = JSON.parse(saved);
|
|
397
|
-
if (!loadedSettings.emailTemplates) {
|
|
398
|
-
loadedSettings.emailTemplates = getDefaultTemplates();
|
|
399
|
-
}
|
|
400
|
-
setSettings(loadedSettings);
|
|
401
|
-
} else {
|
|
402
|
-
setSettings(prev => ({ ...prev, emailTemplates: getDefaultTemplates() }));
|
|
403
|
-
}
|
|
404
|
-
} catch (err) {
|
|
405
|
-
console.error('[Settings] Error loading:', err);
|
|
406
|
-
} finally {
|
|
407
|
-
setLoading(false);
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const handleChange = (key, value) => {
|
|
412
|
-
setSettings(prev => ({ ...prev, [key]: value }));
|
|
413
|
-
setHasChanges(true);
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const updateSetting = (key, value) => {
|
|
417
|
-
handleChange(key, value);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const handleSave = async () => {
|
|
421
|
-
setSaving(true);
|
|
422
|
-
try {
|
|
423
|
-
localStorage.setItem(`${pluginId}-settings`, JSON.stringify(settings));
|
|
424
|
-
|
|
425
|
-
toggleNotification({
|
|
426
|
-
type: 'success',
|
|
427
|
-
message: 'Settings saved successfully!',
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
setHasChanges(false);
|
|
431
|
-
} catch (err) {
|
|
432
|
-
toggleNotification({
|
|
433
|
-
type: 'danger',
|
|
434
|
-
message: 'Failed to save settings',
|
|
435
|
-
});
|
|
436
|
-
} finally {
|
|
437
|
-
setSaving(false);
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const handleReset = () => {
|
|
442
|
-
fetchSettings();
|
|
443
|
-
setHasChanges(false);
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const handleCleanInactive = async () => {
|
|
447
|
-
if (!confirm('[WARNING] This will permanently delete ALL inactive sessions.\n\nContinue?')) {
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
setCleaning(true);
|
|
452
|
-
try {
|
|
453
|
-
const { data } = await post(`/${pluginId}/sessions/clean-inactive`);
|
|
454
|
-
|
|
455
|
-
toggleNotification({
|
|
456
|
-
type: 'success',
|
|
457
|
-
message: `Successfully deleted ${data.deletedCount} inactive sessions!`,
|
|
458
|
-
});
|
|
459
|
-
} catch (err) {
|
|
460
|
-
toggleNotification({
|
|
461
|
-
type: 'danger',
|
|
462
|
-
message: 'Failed to delete inactive sessions',
|
|
463
|
-
});
|
|
464
|
-
} finally {
|
|
465
|
-
setCleaning(false);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
if (loading) {
|
|
470
|
-
return (
|
|
471
|
-
<Flex justifyContent="center" padding={8}>
|
|
472
|
-
<Loader>Loading settings...</Loader>
|
|
473
|
-
</Flex>
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return (
|
|
478
|
-
<Container>
|
|
479
|
-
{/* Sticky Header */}
|
|
480
|
-
<StickySaveBar paddingTop={5} paddingBottom={5} paddingLeft={6} paddingRight={6}>
|
|
481
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
482
|
-
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
483
|
-
<Typography variant="alpha" fontWeight="bold" style={{ fontSize: '24px' }}>
|
|
484
|
-
⚙️ Session Manager Settings
|
|
485
|
-
</Typography>
|
|
486
|
-
<Typography variant="epsilon" textColor="neutral600">
|
|
487
|
-
Configure session tracking, security, and email notifications
|
|
488
|
-
</Typography>
|
|
489
|
-
</Flex>
|
|
490
|
-
<Flex gap={2}>
|
|
491
|
-
{hasChanges && (
|
|
492
|
-
<Button onClick={handleReset} variant="tertiary" size="L">
|
|
493
|
-
Reset
|
|
494
|
-
</Button>
|
|
495
|
-
)}
|
|
496
|
-
<Button
|
|
497
|
-
onClick={handleSave}
|
|
498
|
-
loading={saving}
|
|
499
|
-
startIcon={<Check />}
|
|
500
|
-
size="L"
|
|
501
|
-
disabled={!hasChanges || saving}
|
|
502
|
-
style={{
|
|
503
|
-
background: hasChanges && !saving
|
|
504
|
-
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
505
|
-
: '#e5e7eb',
|
|
506
|
-
color: hasChanges && !saving ? 'white' : '#9ca3af',
|
|
507
|
-
fontWeight: '600',
|
|
508
|
-
padding: '12px 24px',
|
|
509
|
-
border: 'none',
|
|
510
|
-
boxShadow: hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none',
|
|
511
|
-
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
512
|
-
}}
|
|
513
|
-
onMouseEnter={(e) => {
|
|
514
|
-
if (hasChanges && !saving) {
|
|
515
|
-
e.currentTarget.style.transform = 'translateY(-2px)';
|
|
516
|
-
e.currentTarget.style.boxShadow = '0 8px 20px rgba(102, 126, 234, 0.5)';
|
|
517
|
-
}
|
|
518
|
-
}}
|
|
519
|
-
onMouseLeave={(e) => {
|
|
520
|
-
e.currentTarget.style.transform = 'translateY(0)';
|
|
521
|
-
e.currentTarget.style.boxShadow = hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none';
|
|
522
|
-
}}
|
|
523
|
-
>
|
|
524
|
-
{saving ? 'Saving...' : hasChanges ? 'Save Changes' : 'No Changes'}
|
|
525
|
-
</Button>
|
|
526
|
-
</Flex>
|
|
527
|
-
</Flex>
|
|
528
|
-
</StickySaveBar>
|
|
529
|
-
|
|
530
|
-
{/* Content */}
|
|
531
|
-
<Box paddingTop={6} paddingLeft={6} paddingRight={6} paddingBottom={10}>
|
|
532
|
-
|
|
533
|
-
{/* Info Alert */}
|
|
534
|
-
<Alert variant="default" title="Configuration Note" closeLabel="Close" style={{ marginBottom: '24px' }}>
|
|
535
|
-
Changes require a server restart. Update config/plugins.ts for permanent changes.
|
|
536
|
-
</Alert>
|
|
537
|
-
|
|
538
|
-
{/* Accordion Layout */}
|
|
539
|
-
<Accordion.Root type="multiple" defaultValue={['general', 'security', 'email']}>
|
|
540
|
-
|
|
541
|
-
{/* General Settings */}
|
|
542
|
-
<Accordion.Item value="general">
|
|
543
|
-
<Accordion.Header>
|
|
544
|
-
<Accordion.Trigger
|
|
545
|
-
icon={Cog}
|
|
546
|
-
description="Basic session tracking configuration"
|
|
547
|
-
>
|
|
548
|
-
General Settings
|
|
549
|
-
</Accordion.Trigger>
|
|
550
|
-
</Accordion.Header>
|
|
551
|
-
<Accordion.Content>
|
|
552
|
-
<Box padding={6}>
|
|
553
|
-
|
|
554
|
-
{/* Session Timeout */}
|
|
555
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
556
|
-
SESSION TIMEOUT
|
|
557
|
-
</Typography>
|
|
558
|
-
<Grid.Root gap={6} style={{ marginBottom: '32px' }}>
|
|
559
|
-
<Grid.Item col={6} s={12}>
|
|
560
|
-
<Box>
|
|
561
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
562
|
-
Inactivity Timeout
|
|
563
|
-
</Typography>
|
|
564
|
-
<SingleSelect
|
|
565
|
-
value={String(settings.inactivityTimeout)}
|
|
566
|
-
onChange={(value) => handleChange('inactivityTimeout', parseInt(value))}
|
|
567
|
-
>
|
|
568
|
-
<SingleSelectOption value="5">5 minutes (Very Strict)</SingleSelectOption>
|
|
569
|
-
<SingleSelectOption value="10">10 minutes (Strict)</SingleSelectOption>
|
|
570
|
-
<SingleSelectOption value="15">15 minutes (Recommended)</SingleSelectOption>
|
|
571
|
-
<SingleSelectOption value="30">30 minutes (Moderate)</SingleSelectOption>
|
|
572
|
-
<SingleSelectOption value="60">1 hour (Relaxed)</SingleSelectOption>
|
|
573
|
-
<SingleSelectOption value="120">2 hours (Very Relaxed)</SingleSelectOption>
|
|
574
|
-
</SingleSelect>
|
|
575
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
576
|
-
Sessions inactive for more than {settings.inactivityTimeout} minutes will be marked as offline
|
|
577
|
-
</Typography>
|
|
578
|
-
</Box>
|
|
579
|
-
</Grid.Item>
|
|
580
|
-
|
|
581
|
-
<Grid.Item col={6} s={12}>
|
|
582
|
-
<Box>
|
|
583
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
584
|
-
Last Seen Rate Limit
|
|
585
|
-
</Typography>
|
|
586
|
-
<SingleSelect
|
|
587
|
-
value={String(settings.lastSeenRateLimit)}
|
|
588
|
-
onChange={(value) => handleChange('lastSeenRateLimit', parseInt(value))}
|
|
589
|
-
>
|
|
590
|
-
<SingleSelectOption value="10">10 seconds</SingleSelectOption>
|
|
591
|
-
<SingleSelectOption value="30">30 seconds (Recommended)</SingleSelectOption>
|
|
592
|
-
<SingleSelectOption value="60">1 minute</SingleSelectOption>
|
|
593
|
-
<SingleSelectOption value="120">2 minutes</SingleSelectOption>
|
|
594
|
-
<SingleSelectOption value="300">5 minutes</SingleSelectOption>
|
|
595
|
-
</SingleSelect>
|
|
596
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
597
|
-
Prevents excessive database writes. Updates throttled to once every {settings.lastSeenRateLimit} seconds
|
|
598
|
-
</Typography>
|
|
599
|
-
</Box>
|
|
600
|
-
</Grid.Item>
|
|
601
|
-
</Grid.Root>
|
|
602
|
-
|
|
603
|
-
{/* Cleanup & Retention */}
|
|
604
|
-
<Divider style={{ marginBottom: '24px' }} />
|
|
605
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
606
|
-
🧹 AUTO-CLEANUP & RETENTION
|
|
607
|
-
</Typography>
|
|
608
|
-
<Grid.Root gap={6}>
|
|
609
|
-
<Grid.Item col={6} s={12}>
|
|
610
|
-
<Box>
|
|
611
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
612
|
-
Cleanup Interval
|
|
613
|
-
</Typography>
|
|
614
|
-
<SingleSelect
|
|
615
|
-
value={String(settings.cleanupInterval)}
|
|
616
|
-
onChange={(value) => handleChange('cleanupInterval', parseInt(value))}
|
|
617
|
-
>
|
|
618
|
-
<SingleSelectOption value="15">15 minutes</SingleSelectOption>
|
|
619
|
-
<SingleSelectOption value="30">30 minutes (Recommended)</SingleSelectOption>
|
|
620
|
-
<SingleSelectOption value="60">1 hour</SingleSelectOption>
|
|
621
|
-
<SingleSelectOption value="120">2 hours</SingleSelectOption>
|
|
622
|
-
</SingleSelect>
|
|
623
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
624
|
-
Inactive sessions are automatically cleaned every {settings.cleanupInterval} minutes
|
|
625
|
-
</Typography>
|
|
626
|
-
</Box>
|
|
627
|
-
</Grid.Item>
|
|
628
|
-
|
|
629
|
-
<Grid.Item col={6} s={12}>
|
|
630
|
-
<Box>
|
|
631
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
632
|
-
Retention Period
|
|
633
|
-
</Typography>
|
|
634
|
-
<SingleSelect
|
|
635
|
-
value={String(settings.retentionDays)}
|
|
636
|
-
onChange={(value) => handleChange('retentionDays', parseInt(value))}
|
|
637
|
-
>
|
|
638
|
-
<SingleSelectOption value="7">7 days</SingleSelectOption>
|
|
639
|
-
<SingleSelectOption value="30">30 days</SingleSelectOption>
|
|
640
|
-
<SingleSelectOption value="60">60 days</SingleSelectOption>
|
|
641
|
-
<SingleSelectOption value="90">90 days (Recommended)</SingleSelectOption>
|
|
642
|
-
<SingleSelectOption value="180">180 days</SingleSelectOption>
|
|
643
|
-
<SingleSelectOption value="365">1 year</SingleSelectOption>
|
|
644
|
-
<SingleSelectOption value="-1">Forever</SingleSelectOption>
|
|
645
|
-
</SingleSelect>
|
|
646
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
647
|
-
Old sessions deleted after {settings.retentionDays === -1 ? 'never' : `${settings.retentionDays} days`}
|
|
648
|
-
</Typography>
|
|
649
|
-
</Box>
|
|
650
|
-
</Grid.Item>
|
|
651
|
-
|
|
652
|
-
<Grid.Item col={12}>
|
|
653
|
-
<Box padding={4} background="danger100" style={{ borderRadius: theme.borderRadius.md, border: `2px solid ${theme.colors.danger[200]}` }}>
|
|
654
|
-
<Flex gap={3} alignItems="flex-start">
|
|
655
|
-
<Trash style={{ width: '18px', height: '18px', color: theme.colors.danger[600], flexShrink: 0, marginTop: '2px' }} />
|
|
656
|
-
<Box style={{ flex: 1 }}>
|
|
657
|
-
<Typography variant="omega" fontWeight="bold" textColor="danger700" style={{ marginBottom: '8px', display: 'block' }}>
|
|
658
|
-
Danger Zone
|
|
659
|
-
</Typography>
|
|
660
|
-
<Typography variant="pi" textColor="danger600" style={{ fontSize: '13px', lineHeight: '1.7' }}>
|
|
661
|
-
<strong>Clean All Inactive:</strong> Permanently deletes all inactive sessions. This cannot be undone.
|
|
662
|
-
</Typography>
|
|
663
|
-
</Box>
|
|
664
|
-
<Button
|
|
665
|
-
onClick={handleCleanInactive}
|
|
666
|
-
loading={cleaning}
|
|
667
|
-
startIcon={<Trash />}
|
|
668
|
-
variant="danger"
|
|
669
|
-
size="S"
|
|
670
|
-
style={{ flexShrink: 0 }}
|
|
671
|
-
>
|
|
672
|
-
Clean Now
|
|
673
|
-
</Button>
|
|
674
|
-
</Flex>
|
|
675
|
-
</Box>
|
|
676
|
-
</Grid.Item>
|
|
677
|
-
</Grid.Root>
|
|
678
|
-
|
|
679
|
-
</Box>
|
|
680
|
-
</Accordion.Content>
|
|
681
|
-
</Accordion.Item>
|
|
682
|
-
|
|
683
|
-
{/* Security Settings */}
|
|
684
|
-
<Accordion.Item value="security">
|
|
685
|
-
<Accordion.Header>
|
|
686
|
-
<Accordion.Trigger
|
|
687
|
-
icon={Shield}
|
|
688
|
-
description="Security policies and threat protection"
|
|
689
|
-
>
|
|
690
|
-
Security Settings
|
|
691
|
-
</Accordion.Trigger>
|
|
692
|
-
</Accordion.Header>
|
|
693
|
-
<Accordion.Content>
|
|
694
|
-
<Box padding={6}>
|
|
695
|
-
|
|
696
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
697
|
-
SECURITY OPTIONS
|
|
698
|
-
</Typography>
|
|
699
|
-
|
|
700
|
-
{/* Feature Toggles */}
|
|
701
|
-
<Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
|
|
702
|
-
<Grid.Root gap={4}>
|
|
703
|
-
<Grid.Item col={6} s={12}>
|
|
704
|
-
<ToggleCard
|
|
705
|
-
$active={settings.blockSuspiciousSessions}
|
|
706
|
-
onClick={() => handleChange('blockSuspiciousSessions', !settings.blockSuspiciousSessions)}
|
|
707
|
-
>
|
|
708
|
-
<Flex direction="column" gap={2}>
|
|
709
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
710
|
-
<Typography variant="omega" fontWeight="bold" textColor={settings.blockSuspiciousSessions ? 'success700' : 'neutral700'}>
|
|
711
|
-
Block Suspicious Sessions
|
|
712
|
-
</Typography>
|
|
713
|
-
<GreenToggle $isActive={settings.blockSuspiciousSessions}>
|
|
714
|
-
<Toggle
|
|
715
|
-
checked={settings.blockSuspiciousSessions}
|
|
716
|
-
onChange={() => handleChange('blockSuspiciousSessions', !settings.blockSuspiciousSessions)}
|
|
717
|
-
/>
|
|
718
|
-
</GreenToggle>
|
|
719
|
-
</Flex>
|
|
720
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
721
|
-
Automatically block sessions from VPNs, proxies, or threat IPs
|
|
722
|
-
</Typography>
|
|
723
|
-
</Flex>
|
|
724
|
-
</ToggleCard>
|
|
725
|
-
</Grid.Item>
|
|
726
|
-
|
|
727
|
-
{isPremium && (
|
|
728
|
-
<>
|
|
729
|
-
<Grid.Item col={6} s={12}>
|
|
730
|
-
<ToggleCard
|
|
731
|
-
$active={settings.enableGeolocation}
|
|
732
|
-
onClick={() => handleChange('enableGeolocation', !settings.enableGeolocation)}
|
|
733
|
-
>
|
|
734
|
-
<Flex direction="column" gap={2}>
|
|
735
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
736
|
-
<Typography variant="omega" fontWeight="bold" textColor={settings.enableGeolocation ? 'success700' : 'neutral700'}>
|
|
737
|
-
IP Geolocation
|
|
738
|
-
</Typography>
|
|
739
|
-
<GreenToggle $isActive={settings.enableGeolocation}>
|
|
740
|
-
<Toggle
|
|
741
|
-
checked={settings.enableGeolocation}
|
|
742
|
-
onChange={() => handleChange('enableGeolocation', !settings.enableGeolocation)}
|
|
743
|
-
/>
|
|
744
|
-
</GreenToggle>
|
|
745
|
-
</Flex>
|
|
746
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
747
|
-
Fetch location data for each session (Premium)
|
|
748
|
-
</Typography>
|
|
749
|
-
</Flex>
|
|
750
|
-
</ToggleCard>
|
|
751
|
-
</Grid.Item>
|
|
752
|
-
|
|
753
|
-
<Grid.Item col={6} s={12}>
|
|
754
|
-
<ToggleCard
|
|
755
|
-
$active={settings.enableSecurityScoring}
|
|
756
|
-
onClick={() => handleChange('enableSecurityScoring', !settings.enableSecurityScoring)}
|
|
757
|
-
>
|
|
758
|
-
<Flex direction="column" gap={2}>
|
|
759
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
760
|
-
<Typography variant="omega" fontWeight="bold" textColor={settings.enableSecurityScoring ? 'success700' : 'neutral700'}>
|
|
761
|
-
Security Scoring
|
|
762
|
-
</Typography>
|
|
763
|
-
<GreenToggle $isActive={settings.enableSecurityScoring}>
|
|
764
|
-
<Toggle
|
|
765
|
-
checked={settings.enableSecurityScoring}
|
|
766
|
-
onChange={() => handleChange('enableSecurityScoring', !settings.enableSecurityScoring)}
|
|
767
|
-
/>
|
|
768
|
-
</GreenToggle>
|
|
769
|
-
</Flex>
|
|
770
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
771
|
-
Calculate security scores and detect threats (Premium)
|
|
772
|
-
</Typography>
|
|
773
|
-
</Flex>
|
|
774
|
-
</ToggleCard>
|
|
775
|
-
</Grid.Item>
|
|
776
|
-
</>
|
|
777
|
-
)}
|
|
778
|
-
</Grid.Root>
|
|
779
|
-
</Box>
|
|
780
|
-
|
|
781
|
-
{/* Max Failed Logins */}
|
|
782
|
-
<Grid.Root gap={6}>
|
|
783
|
-
<Grid.Item col={6} s={12}>
|
|
784
|
-
<Box>
|
|
785
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
786
|
-
🚫 Max Failed Login Attempts
|
|
787
|
-
</Typography>
|
|
788
|
-
<NumberInput
|
|
789
|
-
value={settings.maxFailedLogins}
|
|
790
|
-
onValueChange={(val) => handleChange('maxFailedLogins', val)}
|
|
791
|
-
min={1}
|
|
792
|
-
max={20}
|
|
793
|
-
/>
|
|
794
|
-
<Box padding={2} background="warning50" style={{ borderRadius: '4px', marginTop: '8px' }}>
|
|
795
|
-
<Typography variant="pi" textColor="warning700" style={{ fontSize: '11px' }}>
|
|
796
|
-
User will be blocked after {settings.maxFailedLogins} failed attempts
|
|
797
|
-
</Typography>
|
|
798
|
-
</Box>
|
|
799
|
-
</Box>
|
|
800
|
-
</Grid.Item>
|
|
801
|
-
</Grid.Root>
|
|
802
|
-
|
|
803
|
-
</Box>
|
|
804
|
-
</Accordion.Content>
|
|
805
|
-
</Accordion.Item>
|
|
806
|
-
|
|
807
|
-
{/* Email Notifications - Advanced Only */}
|
|
808
|
-
{isAdvanced && (
|
|
809
|
-
<Accordion.Item value="email">
|
|
810
|
-
<Accordion.Header>
|
|
811
|
-
<Accordion.Trigger
|
|
812
|
-
icon={Mail}
|
|
813
|
-
description="Email alerts for security events"
|
|
814
|
-
>
|
|
815
|
-
Email Notifications (Advanced)
|
|
816
|
-
</Accordion.Trigger>
|
|
817
|
-
</Accordion.Header>
|
|
818
|
-
<Accordion.Content>
|
|
819
|
-
<Box padding={6}>
|
|
820
|
-
|
|
821
|
-
{/* Email Alerts Toggle */}
|
|
822
|
-
<Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
|
|
823
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', textAlign: 'center', color: theme.colors.neutral[700] }}>
|
|
824
|
-
📧 EMAIL ALERTS
|
|
825
|
-
</Typography>
|
|
826
|
-
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', textAlign: 'center', fontSize: '12px' }}>
|
|
827
|
-
Send security alerts to users via email
|
|
828
|
-
</Typography>
|
|
829
|
-
<Grid.Root gap={4}>
|
|
830
|
-
<Grid.Item col={12}>
|
|
831
|
-
<ToggleCard
|
|
832
|
-
$active={settings.enableEmailAlerts}
|
|
833
|
-
onClick={() => handleChange('enableEmailAlerts', !settings.enableEmailAlerts)}
|
|
834
|
-
>
|
|
835
|
-
<Flex direction="column" gap={2}>
|
|
836
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
837
|
-
<Typography variant="omega" fontWeight="bold" textColor={settings.enableEmailAlerts ? 'success700' : 'neutral700'}>
|
|
838
|
-
Enable Email Alerts
|
|
839
|
-
</Typography>
|
|
840
|
-
<GreenToggle $isActive={settings.enableEmailAlerts}>
|
|
841
|
-
<Toggle
|
|
842
|
-
checked={settings.enableEmailAlerts}
|
|
843
|
-
onChange={() => handleChange('enableEmailAlerts', !settings.enableEmailAlerts)}
|
|
844
|
-
/>
|
|
845
|
-
</GreenToggle>
|
|
846
|
-
</Flex>
|
|
847
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
848
|
-
Send security alerts for suspicious logins, new locations, and VPN/Proxy usage
|
|
849
|
-
</Typography>
|
|
850
|
-
</Flex>
|
|
851
|
-
</ToggleCard>
|
|
852
|
-
</Grid.Item>
|
|
853
|
-
</Grid.Root>
|
|
854
|
-
</Box>
|
|
855
|
-
|
|
856
|
-
{/* Alert Type Checkboxes */}
|
|
857
|
-
{settings.enableEmailAlerts && (
|
|
858
|
-
<>
|
|
859
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
860
|
-
⚙️ ALERT TYPES
|
|
861
|
-
</Typography>
|
|
862
|
-
<Grid.Root gap={4} style={{ marginBottom: '32px' }}>
|
|
863
|
-
<Grid.Item col={4} s={12}>
|
|
864
|
-
<Box padding={3} background="neutral50" style={{ borderRadius: theme.borderRadius.md, border: '1px solid #E5E7EB' }}>
|
|
865
|
-
<Checkbox
|
|
866
|
-
checked={settings.alertOnSuspiciousLogin}
|
|
867
|
-
onChange={() => handleChange('alertOnSuspiciousLogin', !settings.alertOnSuspiciousLogin)}
|
|
868
|
-
>
|
|
869
|
-
Suspicious Login
|
|
870
|
-
</Checkbox>
|
|
871
|
-
</Box>
|
|
872
|
-
</Grid.Item>
|
|
873
|
-
<Grid.Item col={4} s={12}>
|
|
874
|
-
<Box padding={3} background="neutral50" style={{ borderRadius: theme.borderRadius.md, border: '1px solid #E5E7EB' }}>
|
|
875
|
-
<Checkbox
|
|
876
|
-
checked={settings.alertOnNewLocation}
|
|
877
|
-
onChange={() => handleChange('alertOnNewLocation', !settings.alertOnNewLocation)}
|
|
878
|
-
>
|
|
879
|
-
New Location
|
|
880
|
-
</Checkbox>
|
|
881
|
-
</Box>
|
|
882
|
-
</Grid.Item>
|
|
883
|
-
<Grid.Item col={4} s={12}>
|
|
884
|
-
<Box padding={3} background="neutral50" style={{ borderRadius: theme.borderRadius.md, border: '1px solid #E5E7EB' }}>
|
|
885
|
-
<Checkbox
|
|
886
|
-
checked={settings.alertOnVpnProxy}
|
|
887
|
-
onChange={() => handleChange('alertOnVpnProxy', !settings.alertOnVpnProxy)}
|
|
888
|
-
>
|
|
889
|
-
VPN/Proxy
|
|
890
|
-
</Checkbox>
|
|
891
|
-
</Box>
|
|
892
|
-
</Grid.Item>
|
|
893
|
-
</Grid.Root>
|
|
894
|
-
|
|
895
|
-
{/* Email Templates */}
|
|
896
|
-
<Divider style={{ marginBottom: '24px' }} />
|
|
897
|
-
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
898
|
-
EMAIL TEMPLATES
|
|
899
|
-
</Typography>
|
|
900
|
-
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', fontSize: '12px' }}>
|
|
901
|
-
Customize email notification templates with dynamic variables
|
|
902
|
-
</Typography>
|
|
903
|
-
|
|
904
|
-
{/* Template Tabs */}
|
|
905
|
-
<Tabs.Root value={activeTemplateTab} onValueChange={setActiveTemplateTab}>
|
|
906
|
-
<Tabs.List aria-label="Email Templates">
|
|
907
|
-
<Tabs.Trigger value="suspiciousLogin">Suspicious Login</Tabs.Trigger>
|
|
908
|
-
<Tabs.Trigger value="newLocation">New Location</Tabs.Trigger>
|
|
909
|
-
<Tabs.Trigger value="vpnProxy">VPN/Proxy</Tabs.Trigger>
|
|
910
|
-
</Tabs.List>
|
|
911
|
-
|
|
912
|
-
{Object.keys(settings.emailTemplates).map((templateKey) => (
|
|
913
|
-
<Tabs.Content key={templateKey} value={templateKey}>
|
|
914
|
-
<Box paddingTop={4}>
|
|
915
|
-
{/* Subject */}
|
|
916
|
-
<Box style={{ marginBottom: '24px' }}>
|
|
917
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
918
|
-
Email Subject
|
|
919
|
-
</Typography>
|
|
920
|
-
<TextInput
|
|
921
|
-
value={settings.emailTemplates[templateKey].subject}
|
|
922
|
-
onChange={(e) => {
|
|
923
|
-
const newTemplates = { ...settings.emailTemplates };
|
|
924
|
-
newTemplates[templateKey].subject = e.target.value;
|
|
925
|
-
handleChange('emailTemplates', newTemplates);
|
|
926
|
-
}}
|
|
927
|
-
placeholder="Enter email subject..."
|
|
928
|
-
/>
|
|
929
|
-
</Box>
|
|
930
|
-
|
|
931
|
-
{/* Available Variables */}
|
|
932
|
-
<Box
|
|
933
|
-
padding={3}
|
|
934
|
-
background="primary100"
|
|
935
|
-
style={{ borderRadius: theme.borderRadius.md, marginBottom: '20px', border: '2px solid #BAE6FD' }}
|
|
936
|
-
>
|
|
937
|
-
<Flex direction="column" gap={2}>
|
|
938
|
-
<Flex alignItems="center" gap={2}>
|
|
939
|
-
<Code style={{ width: '16px', height: '16px', color: theme.colors.primary[600] }} />
|
|
940
|
-
<Typography variant="omega" fontWeight="bold" textColor="primary600">
|
|
941
|
-
Available Variables (click to copy)
|
|
942
|
-
</Typography>
|
|
943
|
-
</Flex>
|
|
944
|
-
<Flex gap={2} wrap="wrap">
|
|
945
|
-
{TEMPLATE_VARIABLES[templateKey].map(({ var: variable, desc }) => (
|
|
946
|
-
<Button
|
|
947
|
-
key={variable}
|
|
948
|
-
size="S"
|
|
949
|
-
variant="tertiary"
|
|
950
|
-
onClick={() => {
|
|
951
|
-
navigator.clipboard.writeText(variable);
|
|
952
|
-
toggleNotification({ type: 'success', message: `${variable} copied!` });
|
|
953
|
-
}}
|
|
954
|
-
style={{
|
|
955
|
-
fontFamily: 'monospace',
|
|
956
|
-
fontSize: '11px',
|
|
957
|
-
padding: '4px 8px',
|
|
958
|
-
}}
|
|
959
|
-
title={desc}
|
|
960
|
-
>
|
|
961
|
-
{variable}
|
|
962
|
-
</Button>
|
|
963
|
-
))}
|
|
964
|
-
</Flex>
|
|
965
|
-
</Flex>
|
|
966
|
-
</Box>
|
|
967
|
-
|
|
968
|
-
{/* HTML Template */}
|
|
969
|
-
<Box style={{ marginBottom: '24px' }}>
|
|
970
|
-
<Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '10px' }}>
|
|
971
|
-
<Typography variant="pi" fontWeight="bold">
|
|
972
|
-
🎨 HTML Template
|
|
973
|
-
</Typography>
|
|
974
|
-
<Button
|
|
975
|
-
size="S"
|
|
976
|
-
variant="secondary"
|
|
977
|
-
onClick={() => {
|
|
978
|
-
const validation = validateTemplate(settings.emailTemplates[templateKey].html, templateKey);
|
|
979
|
-
toggleNotification({
|
|
980
|
-
type: validation.isValid ? 'success' : 'warning',
|
|
981
|
-
message: validation.isValid
|
|
982
|
-
? `Template valid! Found ${validation.foundVars.length} variables.`
|
|
983
|
-
: 'No variables found in template. Add at least one variable.',
|
|
984
|
-
});
|
|
985
|
-
}}
|
|
986
|
-
>
|
|
987
|
-
✓ Validate
|
|
988
|
-
</Button>
|
|
989
|
-
</Flex>
|
|
990
|
-
<Textarea
|
|
991
|
-
value={settings.emailTemplates[templateKey].html}
|
|
992
|
-
onChange={(e) => {
|
|
993
|
-
const newTemplates = { ...settings.emailTemplates };
|
|
994
|
-
newTemplates[templateKey].html = e.target.value;
|
|
995
|
-
handleChange('emailTemplates', newTemplates);
|
|
996
|
-
}}
|
|
997
|
-
style={{
|
|
998
|
-
fontFamily: 'Monaco, Consolas, monospace',
|
|
999
|
-
fontSize: '12px',
|
|
1000
|
-
minHeight: '250px',
|
|
1001
|
-
lineHeight: '1.8',
|
|
1002
|
-
}}
|
|
1003
|
-
placeholder="Enter HTML template with variables like {{user.email}}..."
|
|
1004
|
-
/>
|
|
1005
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
1006
|
-
Use HTML formatting and insert variables from the list above
|
|
1007
|
-
</Typography>
|
|
1008
|
-
</Box>
|
|
1009
|
-
|
|
1010
|
-
{/* Text Template */}
|
|
1011
|
-
<Box style={{ marginBottom: '24px' }}>
|
|
1012
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1013
|
-
📄 Text Template (Fallback)
|
|
1014
|
-
</Typography>
|
|
1015
|
-
<Textarea
|
|
1016
|
-
value={settings.emailTemplates[templateKey].text}
|
|
1017
|
-
onChange={(e) => {
|
|
1018
|
-
const newTemplates = { ...settings.emailTemplates };
|
|
1019
|
-
newTemplates[templateKey].text = e.target.value;
|
|
1020
|
-
handleChange('emailTemplates', newTemplates);
|
|
1021
|
-
}}
|
|
1022
|
-
style={{
|
|
1023
|
-
fontFamily: 'Monaco, Consolas, monospace',
|
|
1024
|
-
fontSize: '12px',
|
|
1025
|
-
minHeight: '150px',
|
|
1026
|
-
lineHeight: '1.8',
|
|
1027
|
-
}}
|
|
1028
|
-
placeholder="Plain text version (no HTML)..."
|
|
1029
|
-
/>
|
|
1030
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
1031
|
-
Plain text version for email clients that don't support HTML
|
|
1032
|
-
</Typography>
|
|
1033
|
-
</Box>
|
|
1034
|
-
|
|
1035
|
-
{/* Load Default Template Button */}
|
|
1036
|
-
<Button
|
|
1037
|
-
size="S"
|
|
1038
|
-
variant="secondary"
|
|
1039
|
-
onClick={() => {
|
|
1040
|
-
const defaultTemplates = getDefaultTemplates();
|
|
1041
|
-
const newTemplates = { ...settings.emailTemplates };
|
|
1042
|
-
newTemplates[templateKey] = defaultTemplates[templateKey];
|
|
1043
|
-
handleChange('emailTemplates', newTemplates);
|
|
1044
|
-
toggleNotification({ type: 'success', message: 'Default template loaded!' });
|
|
1045
|
-
}}
|
|
1046
|
-
>
|
|
1047
|
-
📋 Load Default Template
|
|
1048
|
-
</Button>
|
|
1049
|
-
</Box>
|
|
1050
|
-
</Tabs.Content>
|
|
1051
|
-
))}
|
|
1052
|
-
</Tabs.Root>
|
|
1053
|
-
</>
|
|
1054
|
-
)}
|
|
1055
|
-
|
|
1056
|
-
</Box>
|
|
1057
|
-
</Accordion.Content>
|
|
1058
|
-
</Accordion.Item>
|
|
1059
|
-
)}
|
|
1060
|
-
|
|
1061
|
-
{/* Webhooks - Advanced Only */}
|
|
1062
|
-
{isAdvanced && (
|
|
1063
|
-
<Accordion.Item value="webhooks">
|
|
1064
|
-
<Accordion.Header>
|
|
1065
|
-
<Accordion.Trigger
|
|
1066
|
-
icon={Code}
|
|
1067
|
-
description="Discord & Slack integration"
|
|
1068
|
-
>
|
|
1069
|
-
Webhook Integration (Advanced)
|
|
1070
|
-
</Accordion.Trigger>
|
|
1071
|
-
</Accordion.Header>
|
|
1072
|
-
<Accordion.Content>
|
|
1073
|
-
<Box padding={6}>
|
|
1074
|
-
|
|
1075
|
-
{/* Enable Webhooks Toggle */}
|
|
1076
|
-
<Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
|
|
1077
|
-
<Grid.Root gap={4}>
|
|
1078
|
-
<Grid.Item col={12}>
|
|
1079
|
-
<ToggleCard
|
|
1080
|
-
$active={settings.enableWebhooks}
|
|
1081
|
-
onClick={() => handleChange('enableWebhooks', !settings.enableWebhooks)}
|
|
1082
|
-
>
|
|
1083
|
-
<Flex direction="column" gap={2}>
|
|
1084
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
1085
|
-
<Typography variant="omega" fontWeight="bold" textColor={settings.enableWebhooks ? 'success700' : 'neutral700'}>
|
|
1086
|
-
Enable Webhooks
|
|
1087
|
-
</Typography>
|
|
1088
|
-
<GreenToggle $isActive={settings.enableWebhooks}>
|
|
1089
|
-
<Toggle
|
|
1090
|
-
checked={settings.enableWebhooks}
|
|
1091
|
-
onChange={() => handleChange('enableWebhooks', !settings.enableWebhooks)}
|
|
1092
|
-
/>
|
|
1093
|
-
</GreenToggle>
|
|
1094
|
-
</Flex>
|
|
1095
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
1096
|
-
Send session events to Discord, Slack, or custom endpoints
|
|
1097
|
-
</Typography>
|
|
1098
|
-
</Flex>
|
|
1099
|
-
</ToggleCard>
|
|
1100
|
-
</Grid.Item>
|
|
1101
|
-
</Grid.Root>
|
|
1102
|
-
</Box>
|
|
1103
|
-
|
|
1104
|
-
{/* Webhook URLs */}
|
|
1105
|
-
{settings.enableWebhooks && (
|
|
1106
|
-
<Grid.Root gap={6}>
|
|
1107
|
-
<Grid.Item col={12}>
|
|
1108
|
-
<Box>
|
|
1109
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1110
|
-
Discord Webhook URL
|
|
1111
|
-
</Typography>
|
|
1112
|
-
<TextInput
|
|
1113
|
-
placeholder="https://discord.com/api/webhooks/..."
|
|
1114
|
-
value={settings.discordWebhookUrl}
|
|
1115
|
-
onChange={(e) => handleChange('discordWebhookUrl', e.target.value)}
|
|
1116
|
-
/>
|
|
1117
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
1118
|
-
Optional: Post session alerts to Discord
|
|
1119
|
-
</Typography>
|
|
1120
|
-
</Box>
|
|
1121
|
-
</Grid.Item>
|
|
1122
|
-
|
|
1123
|
-
<Grid.Item col={12}>
|
|
1124
|
-
<Box>
|
|
1125
|
-
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1126
|
-
Slack Webhook URL
|
|
1127
|
-
</Typography>
|
|
1128
|
-
<TextInput
|
|
1129
|
-
placeholder="https://hooks.slack.com/services/..."
|
|
1130
|
-
value={settings.slackWebhookUrl}
|
|
1131
|
-
onChange={(e) => handleChange('slackWebhookUrl', e.target.value)}
|
|
1132
|
-
/>
|
|
1133
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
1134
|
-
Optional: Post session alerts to Slack
|
|
1135
|
-
</Typography>
|
|
1136
|
-
</Box>
|
|
1137
|
-
</Grid.Item>
|
|
1138
|
-
</Grid.Root>
|
|
1139
|
-
)}
|
|
1140
|
-
|
|
1141
|
-
</Box>
|
|
1142
|
-
</Accordion.Content>
|
|
1143
|
-
</Accordion.Item>
|
|
1144
|
-
)}
|
|
1145
|
-
|
|
1146
|
-
</Accordion.Root>
|
|
1147
|
-
|
|
1148
|
-
{/* Footer Info */}
|
|
1149
|
-
<Box padding={5} background="neutral100" style={{ borderRadius: theme.borderRadius.md, marginTop: '32px' }}>
|
|
1150
|
-
<Flex gap={3} alignItems="flex-start">
|
|
1151
|
-
<Information style={{ width: '20px', height: '20px', color: theme.colors.neutral[600], flexShrink: 0, marginTop: '2px' }} />
|
|
1152
|
-
<Box style={{ flex: 1 }}>
|
|
1153
|
-
<Typography variant="omega" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1154
|
-
How to Apply These Settings
|
|
1155
|
-
</Typography>
|
|
1156
|
-
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.8' }}>
|
|
1157
|
-
Settings are saved in your browser. To apply permanently, copy the config below and paste it into{' '}
|
|
1158
|
-
<code style={{ background: '#e5e7eb', padding: '2px 6px', borderRadius: '4px' }}>config/plugins.ts</code>, then restart.
|
|
1159
|
-
</Typography>
|
|
1160
|
-
<Button
|
|
1161
|
-
onClick={async () => {
|
|
1162
|
-
const configCode = `'magic-sessionmanager': {
|
|
1163
|
-
config: {
|
|
1164
|
-
inactivityTimeout: ${settings.inactivityTimeout * 60 * 1000}, // ${settings.inactivityTimeout} min
|
|
1165
|
-
cleanupInterval: ${settings.cleanupInterval * 60 * 1000}, // ${settings.cleanupInterval} min
|
|
1166
|
-
lastSeenRateLimit: ${settings.lastSeenRateLimit * 1000}, // ${settings.lastSeenRateLimit} sec
|
|
1167
|
-
retentionDays: ${settings.retentionDays},
|
|
1168
|
-
enableGeolocation: ${settings.enableGeolocation},
|
|
1169
|
-
enableSecurityScoring: ${settings.enableSecurityScoring},
|
|
1170
|
-
blockSuspiciousSessions: ${settings.blockSuspiciousSessions},
|
|
1171
|
-
maxFailedLogins: ${settings.maxFailedLogins},
|
|
1172
|
-
enableEmailAlerts: ${settings.enableEmailAlerts},
|
|
1173
|
-
alertOnSuspiciousLogin: ${settings.alertOnSuspiciousLogin},
|
|
1174
|
-
alertOnNewLocation: ${settings.alertOnNewLocation},
|
|
1175
|
-
alertOnVpnProxy: ${settings.alertOnVpnProxy},
|
|
1176
|
-
enableWebhooks: ${settings.enableWebhooks},
|
|
1177
|
-
}
|
|
1178
|
-
}`;
|
|
1179
|
-
|
|
1180
|
-
try {
|
|
1181
|
-
await navigator.clipboard.writeText(configCode);
|
|
1182
|
-
toggleNotification({ type: 'success', message: 'Config copied to clipboard!' });
|
|
1183
|
-
} catch (err) {
|
|
1184
|
-
toggleNotification({ type: 'danger', message: 'Failed to copy' });
|
|
1185
|
-
}
|
|
1186
|
-
}}
|
|
1187
|
-
startIcon={<Duplicate />}
|
|
1188
|
-
size="S"
|
|
1189
|
-
variant="secondary"
|
|
1190
|
-
style={{ marginTop: '16px' }}
|
|
1191
|
-
>
|
|
1192
|
-
Copy Config
|
|
1193
|
-
</Button>
|
|
1194
|
-
</Box>
|
|
1195
|
-
</Flex>
|
|
1196
|
-
</Box>
|
|
1197
|
-
|
|
1198
|
-
</Box>
|
|
1199
|
-
</Container>
|
|
1200
|
-
);
|
|
1201
|
-
};
|
|
1202
|
-
|
|
1203
|
-
export default SettingsPage;
|
|
1204
|
-
|