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